package com.sksamuel.jqm4gwt.plugins.datatables; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayInteger; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.ButtonElement; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.InputElement; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.logical.shared.ResizeEvent; import com.google.gwt.event.logical.shared.ResizeHandler; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent.Type; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.uibinder.client.UiChild; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.ComplexPanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; import com.sksamuel.jqm4gwt.Empty; import com.sksamuel.jqm4gwt.JQMCommon; import com.sksamuel.jqm4gwt.JsUtils; import com.sksamuel.jqm4gwt.StrUtils; import com.sksamuel.jqm4gwt.events.JQMComponentEvents; import com.sksamuel.jqm4gwt.events.JQMEvent; import com.sksamuel.jqm4gwt.events.JQMEventFactory; import com.sksamuel.jqm4gwt.events.JQMHandlerRegistration; import com.sksamuel.jqm4gwt.events.JQMHandlerRegistration.WidgetHandlerCounter; import com.sksamuel.jqm4gwt.events.JQMOrientationChangeHandler; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.AjaxHandler; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.CellClickHandler; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.CellRender; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.DrawHandler; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsAjax; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsCallback; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsColumn; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsColumns; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsEnhanceParams; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsLanguage; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsLengthMenu; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsRowCallback; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsRowDetails; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsRowSelect; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsSortItem; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.JsSortItems; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.RowData; import com.sksamuel.jqm4gwt.plugins.datatables.JsDataTable.RowDetailsRenderer; import com.sksamuel.jqm4gwt.plugins.datatables.events.JQMDataTableEnhancedEvent; import com.sksamuel.jqm4gwt.plugins.datatables.events.JQMDataTableRowSelChangedEvent; import com.sksamuel.jqm4gwt.plugins.datatables.events.JQMDataTableRowSelChangedEvent.RowSelChangedData; import com.sksamuel.jqm4gwt.table.ColumnDef; import com.sksamuel.jqm4gwt.table.JQMTableGrid; /** * Wrapper for jQuery DataTables. * <br> See <a href="https://www.datatables.net/examples/index">DataTables examples</a> * * @author slavap * */ public class JQMDataTable extends JQMTableGrid { /** Add an ID to the TR element, see <a href="https://datatables.net/examples/server_side/ids.html"> * Row ID attributes</a> **/ public static final String DT_ROWID = "DT_RowId"; public static final String SELECTED_ROW = "selected"; /** Space separated classes for adding to head groups panel. */ public static String headGroupsClasses; /** * For constructing individual column search input placeholders. * <br> null - language.search will be used, empty - means no prefix, just footer column title as placeholder. **/ public static String individualColSearchPrefix = null; public static interface AjaxPrepare { /** Raised right before ajax call to the server. */ void prepare(JsAjax ajax); } private AjaxPrepare ajaxPrepare; private AjaxHandler ajaxHandler; private RowData rowData; private CellRender cellRender; private boolean enhanced; private boolean manualEnhance; private boolean paging = true; private boolean lengthChange = true; private boolean info = true; private boolean ordering = true; private boolean searching = true; private final List<ColumnDefEx> datacols = new ArrayList<>(); // expected: 0, 2=desc, 3 // then it should be translated to: "order": [[0, "asc"], [2, "desc"], [3, "asc"]] private String colSorts; public static enum ColSortDir { DESC("desc"), ASC("asc"); private final String jsName; ColSortDir(String jsName) { this.jsName = jsName; } public String getJsName() { return jsName; } public static ColSortDir fromJsName(String jsName) { if (Empty.is(jsName)) return null; for (ColSortDir v : values()) { if (jsName.equalsIgnoreCase(v.getJsName())) return v; } return null; } } public static class ColSort { public final int num; public final ColSortDir sortDir; public ColSort(int colNum, ColSortDir sortDir) { this.num = colNum; this.sortDir = sortDir; } /** Expects something like: 1=asc or 2=desc or 3 */ public static ColSort create(String s) { if (Empty.is(s)) return null; int j = s.lastIndexOf('='); ColSortDir k = ColSortDir.ASC; if (j >= 0) { String ss = s.substring(j + 1).trim(); s = s.substring(0, j).trim(); if (s.isEmpty()) return null; k = ColSortDir.fromJsName(ss); if (k == null) k = ColSortDir.ASC; } return new ColSort(Integer.parseInt(s), k); } @Override public String toString() { if (sortDir == null || ColSortDir.ASC.equals(sortDir)) return String.valueOf(num); else return String.valueOf(num) + "=" + sortDir.getJsName(); } } private List<ColSort> sorts; private boolean scrollX; private String scrollXcss; private String scrollY; private int scrollYnum; private boolean scrollCollapse; private boolean useParentHeight; private DrawHandler useParentHeightDrawHandler; private HandlerRegistration windowResizeInitialized; private HandlerRegistration orientationChangeInitialized; private Language language; private String languageJSON; public static enum PagingType { /** 'Previous' and 'Next' buttons only */ SIMPLE("simple"), /** 'Previous' and 'Next' buttons, plus page numbers */ SIMPLE_NUMBERS("simple_numbers"), /** 'First', 'Previous', 'Next' and 'Last' buttons */ FULL("full"), /** 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers */ FULL_NUMBERS("full_numbers"); private final String jsName; PagingType(String jsName) { this.jsName = jsName; } public String getJsName() { return jsName; } public static PagingType fromJsName(String jsName) { if (Empty.is(jsName)) return null; for (PagingType v : values()) { if (jsName.equalsIgnoreCase(v.getJsName())) return v; } return null; } } private PagingType pagingType; private String lengthMenu; private boolean serverSide; private String ajax; private String dataSrc; /** * Defines how to make ajax call to server side processing. * <br> GET - pass as query params, POST and PUT - pass as form params. **/ public static enum HttpMethod { GET, POST, PUT } private HttpMethod httpMethod; private boolean deferRender; private boolean processing; private boolean stateSave; private int stateDuration = 7200; private boolean autoWidth; public static enum RowSelectMode { SINGLE, MULTI } private RowSelectMode rowSelectMode; private Set<String> serverRowSelected; private Set<String> serverRowDetails; public static interface RowIdHelper { String calcRowId(JQMDataTable table, JavaScriptObject rowData); } private RowIdHelper rowIdHelper; private boolean individualColSearches; private boolean visible = true; private final EnumSet<DataTableStyleClass> dfltTableStyles = EnumSet.of(DataTableStyleClass.STD_DISPLAY); public JQMDataTable() { } @Override protected String getHeadGroupsClasses() { return headGroupsClasses; } @Override protected void onLoad() { super.onLoad(); populateHeadAndBody(); if (!manualEnhance) enhance(); else JsDataTable.setDataRoleNone(getElement()); // we don't need jQuery Mobile enhancement for DataTable parts! if (isAdjustSizesNeeded()) { initWindowResize(); initOrientationChange(); } } @Override protected void onUnload() { if (windowResizeInitialized != null) { windowResizeInitialized.removeHandler(); windowResizeInitialized = null; } if (orientationChangeInitialized != null) { orientationChangeInitialized.removeHandler(); orientationChangeInitialized = null; } super.onUnload(); } private JsEnhanceParams prepareJsEnhanceParams() { JsEnhanceParams p = JsEnhanceParams.create(); if (!paging) p.setPaging(false); if (!lengthChange) p.setLengthChange(false); if (!Empty.is(lengthMenu)) { List<String> lst = StrUtils.commaSplit(lengthMenu); Map<String, String> keyVal = StrUtils.splitKeyValue(lst); if (!Empty.is(keyVal)) { boolean allNulls = true; for (String v : keyVal.values()) { if (v != null) { allNulls = false; break; } } if (allNulls) { JsArrayInteger values = JavaScriptObject.createArray(keyVal.size()).cast(); int j = 0; for (Entry<String, String> i : keyVal.entrySet()) { values.set(j, Integer.parseInt(i.getKey())); j++; } p.setLengtMenu(JsLengthMenu.create(values)); } else { JsArrayInteger values = JavaScriptObject.createArray(keyVal.size()).cast(); JsArrayString names = JavaScriptObject.createArray(keyVal.size()).cast(); int j = 0; for (Entry<String, String> i : keyVal.entrySet()) { values.set(j, Integer.parseInt(i.getKey())); names.set(j, i.getValue() != null ? i.getValue() : i.getKey()); j++; } p.setLengtMenu(JsLengthMenu.create(values, names)); } } } if (pagingType != null) p.setPagingType(pagingType.getJsName()); if (!info) p.setInfo(false); if (!ordering) p.setOrdering(false); if (!searching) p.setSearching(false); JsSortItems order = prepareJsOrder(sorts); if (order == null) { // No initial order: https://datatables.net/reference/option/order order = JsSortItems.create(null); } p.setOrder(order); JsColumns cols = prepareJsColumns(); if (cols != null) p.setColumns(cols); if (!Empty.is(scrollXcss)) p.setScrollXcss(scrollXcss); else if (scrollX) p.setScrollX(true); if (!Empty.is(scrollY)) p.setScrollY(scrollY); if (scrollCollapse) p.setScrollCollapse(true); JsLanguage l = null; if (!Empty.is(languageJSON)) { l = JsLanguage.create(languageJSON); } if (language != null) { if (l == null) l = JsLanguage.create(); Language.Builder.copy(language, l, true/*nonEmpty*/); } if (l != null) p.setLanguage(l); if (ajaxHandler != null) { p.setAjaxHandler(getElement(), ajaxHandler); } else if (!Empty.is(ajax)) { if (dataSrc == null && httpMethod == null && ajaxPrepare == null) { p.setAjax(ajax); } else { JsAjax aj = JsAjax.create(); aj.setUrl(ajax); if (dataSrc != null) aj.setDataSrc(dataSrc); if (httpMethod != null) aj.setMethod(httpMethod.name()); if (ajaxPrepare != null) ajaxPrepare.prepare(aj); p.setAjaxObj(aj); } } if (serverSide) { p.setServerSide(true); p.setRowCallback(new JsRowCallback() { @Override public void onRow(Element row, JavaScriptObject rowData) { if (!Empty.is(serverRowSelected)) { String rowId = getRowId(rowData); if (!Empty.is(rowId) && serverRowSelected.contains(rowId)) { JsDataTable.initRow(row, true); } } }}); JsDataTable.setRowSelChanged(getElement(), new JsRowSelect() { @Override public void onRowSelChanged(Element row, boolean selected, JavaScriptObject rowData) { String rowId = getRowId(rowData); if (!Empty.is(rowId)) { if (selected) { if (serverRowSelected == null) serverRowSelected = new HashSet<>(); serverRowSelected.add(rowId); } else { if (!Empty.is(serverRowSelected)) serverRowSelected.remove(rowId); } } fireRowSelChanged(row, selected, rowData); }}); } else { JsDataTable.setRowSelChanged(getElement(), new JsRowSelect() { @Override public void onRowSelChanged(Element row, boolean selected, JavaScriptObject rowData) { fireRowSelChanged(row, selected, rowData); }}); } if (deferRender) p.setDeferRender(true); if (processing) p.setProcessing(true); if (stateSave) { p.setStateSave(true); p.setStateDuration(stateDuration); } if (!autoWidth) p.setAutoWidth(false); return p; } public HandlerRegistration addRowSelChangedHandler(JQMDataTableRowSelChangedEvent.Handler handler) { if (handler == null) return null; return addHandler(handler, JQMDataTableRowSelChangedEvent.getType()); } private void fireRowSelChanged(Element row, boolean selected, JavaScriptObject rowData) { int cnt = getHandlerCount(JQMDataTableRowSelChangedEvent.getType()); if (cnt == 0) return; JQMDataTableRowSelChangedEvent.fire(this, new RowSelChangedData(row, selected, rowData)); } public HandlerRegistration addEnhancedHandler(JQMDataTableEnhancedEvent.Handler handler) { if (handler == null) return null; return addHandler(handler, JQMDataTableEnhancedEvent.getType()); } private void fireEnhanced() { int cnt = getHandlerCount(JQMDataTableEnhancedEvent.getType()); if (cnt == 0) return; JQMDataTableEnhancedEvent.fire(this, false/*before*/); } private void fireBeforeEnhance() { int cnt = getHandlerCount(JQMDataTableEnhancedEvent.getType()); if (cnt == 0) return; JQMDataTableEnhancedEvent.fire(this, true/*before*/); } private String getRowId(JavaScriptObject rowData) { String rowId = JsUtils.getObjValue(rowData, DT_ROWID); if (Empty.is(rowId) && rowIdHelper != null) { rowId = rowIdHelper.calcRowId(this, rowData); } return rowId; } public AjaxPrepare getAjaxPrepare() { return ajaxPrepare; } /** * Additional options could be specified before making ajax call to the server. * <br> See <a href="http://api.jquery.com/jQuery.ajax/">jQuery.ajax()</a> **/ public void setAjaxPrepare(AjaxPrepare value) { ajaxPrepare = value; } public AjaxHandler getAjaxHandler() { return ajaxHandler; } /** * Custom handler for getting data from the server, could be useful in case of client side * caching, specific data exchange protocol, ... **/ public void setAjaxHandler(AjaxHandler handler) { ajaxHandler = handler; } private static JsSortItems prepareJsOrder(List<ColSort> sorts) { if (Empty.is(sorts)) return null; JsSortItems rslt = null; for (ColSort sort : sorts) { JsSortItem jsSort = JsSortItem.create(sort.num, sort.sortDir.getJsName()); if (rslt == null) rslt = JsSortItems.create(jsSort); else rslt.push(jsSort); } return rslt; } private JsColumn processColClassNames(int colIdx, JsColumn jsCol) { String classes = getColClassNames(colIdx); if (classes != null && !classes.isEmpty()) { String s = jsCol != null ? jsCol.getClassName() : null; if (s != null && !s.isEmpty()) s += " " + classes; else s = classes; if (jsCol == null) jsCol = JsColumn.create(); jsCol.setClassName(classes); } return jsCol; } private JsColumns prepareJsColumns() { if (Empty.is(datacols)) { if (Empty.is(columns)) return null; boolean nothing = true; JsColumns rslt = JsColumns.create(null); int idx = 0; for (ColumnDef col : columns) { if (col.isGroup()) continue; JsColumn jsCol = null; if (!Empty.is(col.getName())) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setName(col.getName()); } if (rowData != null) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setDataFunc(getElement(), rowData); } if (cellRender != null) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setRenderFunc(getElement(), col, cellRender); } jsCol = processColClassNames(idx, jsCol); if (jsCol != null) nothing = false; rslt.push(jsCol); idx++; } if (nothing) return null; return rslt; } boolean nothing = true; JsColumns rslt = JsColumns.create(null); int idx = 0; for (ColumnDefEx col : datacols) { if (col.isGroup()) continue; JsColumn jsCol = null; if (!Empty.is(col.getName())) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setName(col.getName()); } if (!col.isVisible()) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setVisible(false); } if (!col.isSearchable()) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setSearchable(false); } if (!col.isOrderable()) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setOrderable(false); } if (!Empty.is(col.getClassNames())) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setClassName(col.getClassNames()); } jsCol = processColClassNames(idx, jsCol); if (col.isCellTypeTh()) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setCellType("th"); } if (col.calcDefaultContent() != null) { // empty is valid value for content if (jsCol == null) jsCol = JsColumn.create(); jsCol.setDefaultContent(col.calcDefaultContent()); } if (!Empty.is(col.getWidth())) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setWidth(col.getWidth()); } if (rowData != null) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setDataFunc(getElement(), rowData); } else if (col.getDataIdx() != null) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setDataIdx(col.getDataIdx()); } else if (col.getData() != null) { // empty is valid value and means that data is null if (jsCol == null) jsCol = JsColumn.create(); String s = col.getData(); jsCol.setData(s.isEmpty() || "null".equals(s) ? null : s); } if (col.isCustomCellRender() && cellRender != null) { if (jsCol == null) jsCol = JsColumn.create(); jsCol.setRenderFunc(getElement(), col, cellRender); } if (jsCol != null) nothing = false; rslt.push(jsCol); idx++; } if (nothing) return null; return rslt; } public boolean isManualEnhance() { return manualEnhance; } public void setManualEnhance(boolean manualEnhance) { this.manualEnhance = manualEnhance; } public DataTableStyleClass[] getDfltTableStyles() { return (DataTableStyleClass[]) dfltTableStyles.toArray(); } public void setDfltTableStyles(DataTableStyleClass... classes) { dfltTableStyles.clear(); if (classes != null) { for (DataTableStyleClass i : classes) dfltTableStyles.add(i); } } /** * Expected DataTableStyleClass enum values as space separated string. */ public void setDfltTableStylesStr(String classes) { dfltTableStyles.clear(); if (classes == null || classes.isEmpty()) return; List<String> lst = StrUtils.split(classes, " "); for (int k = 0; k < lst.size(); k++) { String s = lst.get(k).trim(); DataTableStyleClass cls; try { cls = DataTableStyleClass.valueOf(s); dfltTableStyles.add(cls); } catch (Exception ex) { continue; } } } public void enhance() { if (enhanced) return; enhanced = true; final Element elt = getElement(); for (DataTableStyleClass cls : dfltTableStyles) elt.addClassName(cls.getJqmValue()); elt.setAttribute("width", "100%"); elt.setAttribute("cellspacing", "0"); // obsolete in HTML5, but used in DataTables examples fireBeforeEnhance(); // some properties can be preset for using in prepareJsEnhanceParams() JsEnhanceParams jsParams = prepareJsEnhanceParams(); jsParams.setInitComplete(new JsCallback() { @Override public void onSuccess() { String wrapId = elt.getId() + "_wrapper"; Element p = elt.getParentElement(); while (p != null) { if (wrapId.equals(p.getId())) { /* slow! working only when mobileinit sets $.mobile.ignoreContentEnabled = true p.setAttribute("data-enhance", "false"); */ JsDataTable.setDataRoleNone(p); // we don't need jQuery Mobile enhancement for DataTable parts! break; } p = p.getParentElement(); } afterEnhance(); onInitComplete(); fireEnhanced(); } }); JsDataTable.enhance(elt, jsParams); } public void unEnhance() { if (!enhanced) return; JsDataTable.destroy(getElement()); enhanced = false; } private void afterEnhance() { initRowSelectMode(); initIndividualColSearches(); processVisible(); } /** Called when DataTable's asynchronous initialization is complete/finished. */ protected void onInitComplete() { } public boolean isPaging() { return paging; } public void setPaging(boolean paging) { this.paging = paging; } public PagingType getPagingType() { return pagingType; } public void setPagingType(PagingType pagingType) { this.pagingType = pagingType; } public boolean isInfo() { return info; } /** For paging and searching, like: Showing 1 to 10 of 51 entries (filtered from 57 total entries) */ public void setInfo(boolean info) { this.info = info; } public boolean isOrdering() { return ordering; } public void setOrdering(boolean ordering) { this.ordering = ordering; } public boolean isSearching() { return searching; } public void setSearching(boolean searching) { this.searching = searching; } public boolean isScrollX() { return scrollX; } /** See <a href="https://datatables.net/reference/option/scrollX">scrollX</a> */ public void setScrollX(boolean scrollX) { this.scrollX = scrollX; } public String getScrollXcss() { return scrollXcss; } /** Value in CSS units. * <br> See <a href="https://datatables.net/reference/option/scrollX">scrollX</a> **/ public void setScrollXcss(String scrollXcss) { this.scrollXcss = scrollXcss; } public String getScrollY() { return scrollY; } /** * Max table's scrolling area vertical height, so actual table's height will be higher: * header + scrolling area + footer. * <br> Any CSS measurement is acceptable, or just a number which is treated as pixels. **/ public void setScrollY(String scrollY) { this.scrollY = scrollY; if (Empty.is(this.scrollY)) this.scrollYnum = 0; else { String s = StrUtils.getDigitsOnly(this.scrollY); if (!Empty.is(s)) this.scrollYnum = Integer.parseInt(s); else this.scrollYnum = 0; } } public boolean isScrollCollapse() { return scrollCollapse; } /** * @param scrollCollapse - if true then table will match the height of the rows shown * if that height is smaller than that given height by the scrollY. */ public void setScrollCollapse(boolean scrollCollapse) { this.scrollCollapse = scrollCollapse; } public boolean isUseParentHeight() { return useParentHeight; } /** Takes all parent height. Only works when scrollY is set to some value, for example: 1px * <br> And scrollY value will be used as min-height for scrolling area. **/ public void setUseParentHeight(boolean useParentHeight) { this.useParentHeight = useParentHeight; if (this.useParentHeight) { if (useParentHeightDrawHandler == null) { useParentHeightDrawHandler = new DrawHandler() { @Override public void afterDraw(Element tableElt, JavaScriptObject settings) { if (JQMDataTable.this.useParentHeight) adjustToParentHeight(); } @Override public boolean beforeDraw(Element tableElt, JavaScriptObject settings) { return true; }}; JsDataTable.addDrawHandler(getElement(), useParentHeightDrawHandler); } if (loaded) { initWindowResize(); initOrientationChange(); } } } private void initWindowResize() { if (windowResizeInitialized != null) return; windowResizeInitialized = Window.addResizeHandler(new ResizeHandler() { @Override public void onResize(ResizeEvent event) { if (isAdjustSizesNeeded()) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { adjustAllSizes(); }}); } }}); } private void initOrientationChange() { if (orientationChangeInitialized != null) return; orientationChangeInitialized = addJQMEventHandler(JQMComponentEvents.ORIENTATIONCHANGE, new JQMOrientationChangeHandler() { @Override public void onEvent(JQMEvent<?> event) { if (isAdjustSizesNeeded()) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { adjustAllSizes(); }}); } }}); } /** Some properties (for example: scrollX, useParentHeight) may require to call final adjustments * when page is completely shown, i.e. it cannot be done automatically by JQMDataTable and * should be resolved manually. */ public boolean isAdjustSizesNeeded() { return isUseParentHeight() || isScrollX() || !Empty.is(scrollXcss); } public void adjustAllSizes(boolean forceAdjustColumns) { boolean isScrollX = isScrollX() || !Empty.is(scrollXcss); boolean isParentHeight = isUseParentHeight(); if (isParentHeight) adjustToParentHeight(); if (isParentHeight || isScrollX || forceAdjustColumns) adjustColumnSizing(); } public void adjustAllSizes() { adjustAllSizes(false/*forceAdjustColumns*/); } private HandlerRegistration addJQMEventHandler(String jqmEventName, EventHandler handler) { Type<EventHandler> t = JQMEventFactory.getType(jqmEventName, EventHandler.class); return JQMHandlerRegistration.registerJQueryHandler(new WidgetHandlerCounter() { @Override public int getHandlerCountForWidget(Type<?> type) { return getHandlerCount(type); } }, this, handler, jqmEventName, t); } private static final String SCROLL_BODY = "dataTables_scrollBody"; private static final String WRAPPER = "dataTables_wrapper"; /** * Adjusts height to parent's height. * <br> It's one time action, and works regardless of useParentHeight current value. * <br> Only works when scrollY is set to some value, for example: 1px * <br> And scrollY value will be used as min-height for scrolling area. **/ public void adjustToParentHeight() { Element tableElt = getElement(); Element wrapper = null; Element scrollBody = null; Element elt = tableElt.getParentElement(); while (elt != null) { if (scrollBody == null) { if (JQMCommon.hasStyle(elt, SCROLL_BODY)) { scrollBody = elt; } } else if (wrapper == null) { if (JQMCommon.hasStyle(elt, WRAPPER)) { wrapper = elt; break; } } elt = elt.getParentElement(); } if (wrapper != null && scrollBody != null) { Element wrapParent = wrapper.getParentElement(); if (wrapParent != null) { int h = wrapParent.getClientHeight(); int wrapH = wrapper.getOffsetHeight(); String s = scrollBody.getStyle().getHeight(); s = StrUtils.getDigitsOnly(s); if (!Empty.is(s)) { int scrollBodyH = Integer.parseInt(s); int newH = (h - wrapH) + scrollBodyH - 1; if (newH < 0) newH = 0; if (scrollYnum > 0 && newH < scrollYnum) newH = scrollYnum; scrollBody.getStyle().setHeight(newH, Unit.PX); } } } } /** Aligns header to match the columns, useful after resize or orientation changes. */ public void adjustColumnSizing() { JsDataTable.adjustColumnSizing(getElement()); } public String getColSorts() { if (enhanced) { JsSortItems jsSort = JsDataTable.getOrder(getElement()); if (jsSort == null || jsSort.length() == 0) { colSorts = null; return colSorts; } StringBuilder sb = new StringBuilder(); for (int i = 0; i < jsSort.length(); i++) { JsSortItem item = jsSort.get(i); if (i > 0) sb.append(", "); sb.append(item.getCol()); ColSortDir sk = ColSortDir.fromJsName(item.getJsSortDir()); if (ColSortDir.DESC.equals(sk)) sb.append('=').append(sk.getJsName()); } colSorts = sb.toString(); return colSorts; } else { return colSorts; } } /** * @param colSorts - expected: 0, 2=desc, 3 **/ public void setColSorts(String colSorts) { String old = getColSorts(); if (old == colSorts || old != null && old.equals(colSorts)) return; this.colSorts = colSorts; if (sorts != null) sorts.clear(); if (Empty.is(this.colSorts)) { if (enhanced) JsDataTable.setOrder(getElement(), null); return; } List<String> lst = StrUtils.commaSplit(this.colSorts); for (String i : lst) { String s = StrUtils.replaceAllBackslashCommas(i.trim()); ColSort colSort = ColSort.create(s); if (colSort != null) { if (sorts == null) sorts = new ArrayList<>(); sorts.add(colSort); } } if (enhanced) JsDataTable.setOrder(getElement(), prepareJsOrder(sorts)); } public void setColSorts(List<ColSort> sortList) { String newColSorts = ""; if (!Empty.is(sortList)) { for (ColSort i : sortList) { if (!newColSorts.isEmpty()) newColSorts += ", "; newColSorts += i.toString(); } } String old = getColSorts(); if (newColSorts.equals(old)) return; this.colSorts = newColSorts; if (sorts != null) sorts.clear(); if (Empty.is(this.colSorts)) { if (enhanced) JsDataTable.setOrder(getElement(), null); return; } if (sorts == null) sorts = new ArrayList<>(); sorts.addAll(sortList); if (enhanced) JsDataTable.setOrder(getElement(), prepareJsOrder(sorts)); } @UiChild(tagname = "language", limit = 1) public void setLanguage(Language value) { language = value; } public Language getLanguage() { return language; } public String getLanguageJSON() { return languageJSON; } /** If defined will be used as starting point for language initialization. * <br> I.e. languageJSON is parsed first, then language applies/overrides on top of it. **/ public void setLanguageJSON(String languageJSON) { this.languageJSON = languageJSON; } @UiChild(tagname = "column") public void addColumn(ColumnDefEx col) { if (col == null) return; clearHead(); datacols.add(col); // head will be created later in onLoad() or by refreshColumns() } /** You have to call refreshColumns() to update head and body (if widget is already loaded). */ public void setColumns(List<ColumnDefEx> cols) { clearHead(); datacols.clear(); if (!Empty.is(cols)) datacols.addAll(cols); // head will be created later in onLoad() or by refreshColumns() } public ColumnDefEx findColumn(String colName) { if (Empty.is(colName) || Empty.is(datacols)) return null; for (ColumnDefEx col : datacols) { if (!col.isGroup()) { if (colName.equals(col.getName())) return col; } } return null; } public int findColumnIndex(ColumnDefEx col) { if (col == null) return -1; if (!Empty.is(datacols)) return datacols.indexOf(col); else return -1; } /** Works dynamically after dataTable is initialized */ public void setColumnVisible(String colName, boolean visible) { ColumnDefEx col = findColumn(colName); if (col == null) return; col.setVisible(visible); JsDataTable.setColVisible(getElement(), colName, visible); } public ColumnDefEx getColumn(int index) { if (index < 0) return null; if (!Empty.is(datacols)) { if (index >= datacols.size()) return null; int i = 0; for (ColumnDefEx col : datacols) { if (!col.isGroup()) { if (i == index) return col; i++; } } } return null; } public int getColumnCount() { if (!Empty.is(datacols)) { int i = 0; for (ColumnDefEx col : datacols) { if (!col.isGroup()) i++; } return i; } return 0; } public String getCellData(int rowIndex, int colIndex) { JavaScriptObject cellData = JsDataTable.getCellData(getElement(), rowIndex, colIndex); return cellData != null ? cellData.toString() : null; } public String getCellData(int rowIndex, String colName) { JavaScriptObject cellData = JsDataTable.getCellData(getElement(), rowIndex, colName); return cellData != null ? cellData.toString() : null; } public String getColumnsAsTableHtml(int rowIndex, String addAttrsToResult) { if (Empty.is(datacols)) return null; int colIdx = -1; String rslt = ""; for (ColumnDefEx col : datacols) { if (col.isGroup()) continue; colIdx++; if (Empty.is(col.getData())) continue; String v = Empty.nonNull(getCellData(rowIndex, colIdx), ""); String k = Empty.nonNull(col.getTitle(), ""); if (Empty.is(k) && Empty.is(v)) continue; rslt += "<tr><td>" + k + "</td><td>" + v + "</td></tr>"; } if (Empty.is(rslt)) return null; String s = Empty.is(addAttrsToResult) ? "<table>" : "<table " + addAttrsToResult + ">"; return s + rslt + "</table>"; } @Override protected int getNumOfCols() { if (loaded && !Empty.is(datacols)) { return getColumnCount(); } return super.getNumOfCols(); } @Override protected boolean isColCellTypeTh(int colIdx) { if (loaded && !Empty.is(datacols)) { int i = 0; for (ColumnDefEx col : datacols) { if (!col.isGroup()) { if (i == colIdx) return col.isCellTypeTh(); i++; } } return false; } return super.isColCellTypeTh(colIdx); } /** Refreshes head and body, needed for example after addColumn(). * <br>Currently it's slow and completely re-enhances DataTable, because there is no support * for dynamic columns in DataTable, see <a href="https://github.com/DataTables/DataTables/issues/273"> * Create columns dynamically</a> **/ public void refreshColumns() { boolean wasEnhanced = enhanced; if (enhanced) unEnhance(); clearHead(); tBody.clear(); populateHeadAndBody(); if (wasEnhanced) enhance(); } private void populateHeadAndBody() { if (Empty.is(datacols)) return; List<ColumnDefEx> row0 = null; List<ColumnDefEx> row1 = null; for (ColumnDefEx col : datacols) { if (col.isRegularInGroupRow() || col.isGroup()) { if (row0 == null) row0 = new ArrayList<>(); row0.add(col); } else { if (row1 == null) row1 = new ArrayList<>(); row1.add(col); } } if (row0 != null && !Empty.is(row0)) { int i = 0; for (ColumnDefEx col : row0) { ComplexPanel pa = addToHeadGroups(col, i++); if (pa != null && col.hasWidgets()) { List<Widget> lst = col.getWidgets(); for (Widget w : lst) pa.add(w); } } } if (row1 != null && !Empty.is(row1)) { int i = 0; for (ColumnDefEx col : row1) { ComplexPanel pa = addToHead(col.getTitle(), col.getPriority(), i++); if (pa != null && col.hasWidgets()) { List<Widget> lst = col.getWidgets(); for (Widget w : lst) pa.add(w); } } } refreshBody(); } public String getAjax() { return ajax; } public void setAjax(String ajax) { this.ajax = ajax; } private void clearServerRowStructs() { if (!Empty.is(serverRowSelected)) serverRowSelected.clear(); if (!Empty.is(serverRowDetails)) serverRowDetails.clear(); } /** * Reload the table data from the ajax data source. * <br> It makes sense when we are receiving all data at once from the server side, * so calling draw() is not enough in such case, because it will just reuse previously * loaded client side data. **/ public void ajaxReload(boolean resetPaging) { clearServerRowStructs(); JsDataTable.ajaxReload(getElement(), resetPaging); } public void ajaxReload(String newUrl, boolean resetPaging) { clearServerRowStructs(); JsDataTable.ajaxReload(getElement(), newUrl, resetPaging); } /** * Softer than {@link JQMDataTable#ajaxReload(boolean) }, uses already loaded client side data. * <br> In case of (serverSide == true) it's practically the same as ajaxReload(). * * @param resetPaging - if <b>true(full-reset)</b> then the ordering and search will be recalculated * and the rows redrawn in their new positions. The paging will be reset back to the first page. * <br> if <b>false(full-hold)</b> then the ordering and search will be recalculated * and the rows redrawn in their new positions. The paging will not be reset - i.e. the current * page will still be shown. **/ public void refreshDraw(boolean resetPaging) { JsDataTable.draw(getElement(), resetPaging); } /** * The ordering and search will not be updated and the paging position held where is was. * This is useful for paging when data has not been changed between draws. * <br> See <a href="https://datatables.net/reference/api/draw()">draw()</a> documentation. */ public void refreshPage() { JsDataTable.drawPage(getElement()); } public String getDataSrc() { return dataSrc; } /** * Defines the property from the data source object (i.e. that returned by the Ajax request) to read. * <br> empty string means read data from a plain array rather than an array in an object. * <br> See <a href="https://datatables.net/reference/option/ajax.dataSrc">ajax dataSrc</a> **/ public void setDataSrc(String dataSrc) { this.dataSrc = dataSrc; } public HttpMethod getHttpMethod() { return httpMethod; } public void setHttpMethod(HttpMethod httpMethod) { this.httpMethod = httpMethod; } public boolean isDeferRender() { return deferRender; } /** * @param deferRender - if true then DataTables will create the nodes (rows and cells * in the table body) only when they are needed for a draw. * <br> See <a href="https://datatables.net/reference/option/deferRender">deferRender</a> */ public void setDeferRender(boolean deferRender) { this.deferRender = deferRender; } public boolean isLengthChange() { return lengthChange; } /** Feature control the end user's ability to change the paging display length of the table. */ public void setLengthChange(boolean lengthChange) { this.lengthChange = lengthChange; } public String getLengthMenu() { return lengthMenu; } /** Customizes the options shown in the length menu. Example: 10, 25, 50, -1=All */ public void setLengthMenu(String lengthMenu) { this.lengthMenu = lengthMenu; } public boolean isProcessing() { return processing; } /** * Enable or disable the display of a 'processing' indicator when the table is being processed * (e.g. a sort). This is particularly useful for tables with large amounts of data * where it can take a noticeable amount of time to sort the entries. */ public void setProcessing(boolean processing) { this.processing = processing; } public boolean isServerSide() { return serverSide; } /** Server-side processing - where filtering, paging and sorting calculations are all performed * by a server. Parameters are passed to server as query params (GET) or form params (POST, PUT). * <br> See <a href="https://datatables.net/manual/server-side">Parameters</a> **/ public void setServerSide(boolean serverSide) { this.serverSide = serverSide; } public boolean isStateSave() { return stateSave; } /** * When enabled a DataTables will storage state information such as pagination position, * display length, filtering and sorting. When the end user reloads the page the table's * state will be altered to match what they had previously set up. * * <br><br> See <a href="https://datatables.net/reference/option/stateSave">stateSave</a> */ public void setStateSave(boolean stateSave) { this.stateSave = stateSave; } public int getStateDuration() { return stateDuration; } /** * Value is given in seconds. The value 0 is a special value as it indicates that the state * can be stored and retrieved indefinitely with no time limit. * <br> When set to -1 sessionStorage will be used, while for 0 or greater localStorage will be used. */ public void setStateDuration(int stateDuration) { this.stateDuration = stateDuration; } public boolean isAutoWidth() { return autoWidth; } /** See <a href="https://datatables.net/reference/option/autoWidth">autoWidth</a> */ public void setAutoWidth(boolean autoWidth) { this.autoWidth = autoWidth; } public void addCellBtnClickHandler(CellClickHandler handler) { if (handler == null) return; JsDataTable.addCellClickHandler(getElement(), ButtonElement.TAG, handler, true); } public void removeCellBtnClickHandlers() { JsDataTable.removeCellClickHandler(getElement(), ButtonElement.TAG); } /** * @param eltSelector - some selector to specific widgets, for example: a.ui-btn.special-btn * @param handler - in custom case callback of handler.onClick() may occur * with undefined rowIndex and colIndex, i.e. -1 values. */ public void addCellCustomClickHandler(CellClickHandler handler, String eltSelector) { if (handler == null || Empty.is(eltSelector)) return; JsDataTable.addCellClickHandler(getElement(), eltSelector, handler, false); } public void removeCellCustomClickHandlers(String eltSelector) { JsDataTable.removeCellClickHandler(getElement(), eltSelector); } public void addCellCheckboxClickHandler(CellClickHandler handler) { if (handler == null) return; JsDataTable.addCellClickHandler(getElement(), InputElement.TAG + "[type='checkbox']", handler, true); } public void removeCellCheckboxClickHandlers() { JsDataTable.removeCellClickHandler(getElement(), InputElement.TAG + "[type='checkbox']"); } public RowSelectMode getRowSelectMode() { return rowSelectMode; } public void setRowSelectMode(RowSelectMode value) { if (rowSelectMode == value) return; doneRowSelectMode(); rowSelectMode = value; initRowSelectMode(); } protected void doneRowSelectMode() { if (!enhanced || rowSelectMode == null) return; if (RowSelectMode.SINGLE.equals(rowSelectMode)) { JsDataTable.switchOffSingleRowSelect(getElement()); } else if (RowSelectMode.MULTI.equals(rowSelectMode)) { JsDataTable.switchOffMultiRowSelect(getElement()); } } protected void initRowSelectMode() { if (!enhanced || rowSelectMode == null) return; if (RowSelectMode.SINGLE.equals(rowSelectMode)) { JsDataTable.switchOnSingleRowSelect(getElement()); } else if (RowSelectMode.MULTI.equals(rowSelectMode)) { JsDataTable.switchOnMultiRowSelect(getElement()); } } public void addRowDetailsRenderer(RowDetailsRenderer renderer) { if (renderer == null) return; JsDataTable.addRowDetailsRenderer(getElement(), renderer); if (serverSide) { JsDataTable.setRowDetailsChanged(getElement(), new JsRowDetails() { @Override public void onChanged(Element row, boolean opened, JavaScriptObject rowData) { String rowId = getRowId(rowData); if (Empty.is(rowId)) return; if (opened) { if (serverRowDetails == null) serverRowDetails = new HashSet<>(); serverRowDetails.add(rowId); } else { if (!Empty.is(serverRowDetails)) serverRowDetails.remove(rowId); } } }); JsDataTable.addDrawHandler(getElement(), new DrawHandler() { @Override public void afterDraw(Element tableElt, JavaScriptObject settings) { if (!Empty.is(serverRowDetails)) { JsArrayString rowIds = JavaScriptObject.createArray(serverRowDetails.size()).cast(); int i = 0; for (String s : serverRowDetails) { rowIds.set(i, s); i++; } JsDataTable.openRowDetails(tableElt, rowIds); } } @Override public boolean beforeDraw(Element tableElt, JavaScriptObject settings) { return true; } }); } } public void closeRowDetails(Element rowDetailElt) { JsDataTable.closeRowDetails(getElement(), rowDetailElt); } /** See <a href="https://datatables.net/reference/event/draw">Draw event</a> */ public void addDrawHandler(DrawHandler handler) { if (handler == null) return; JsDataTable.addDrawHandler(getElement(), handler); } /** * Creates groups bands by specified column. * <br> Could be called from afterDraw() event handler, see addDrawHandler() * * <br> You should define group styling in CSS like this: * <br> .dataTable tr.group, .dataTable tr.group:hover { background-color: #ddd !important; } * <br> OR you can directly process group row elements, which are returned by this method. * * @param additionalSorts - optional, will be sorted by colIdx column + additionalSorts * * @param settings - just taken/passed-through directly from afterDraw() * * @return - array of group rows, can be used for additional adjustments. **/ public static JsArray<Element> doGrouping(JavaScriptObject settings, int colIdx, ColSort... additionalSorts) { if (additionalSorts != null && additionalSorts.length > 0) { List<ColSort> lst = new ArrayList<>(additionalSorts.length); for (int i = 0; i < additionalSorts.length; i++) lst.add(additionalSorts[i]); JsSortItems jsAddnlSorts = prepareJsOrder(lst); return JsDataTable.doGrouping(settings, colIdx, jsAddnlSorts); } else { return JsDataTable.doGrouping(settings, colIdx, null/*additionalSorts*/); } } /** Makes no much sense when serverSide is true, use getSelRowIds() in that case. */ public JsArrayInteger getSelRowIndexes() { return JsDataTable.getSelRowIndexes(getElement()); } /** Makes no much sense when serverSide is true, use getSelRowIds() in that case. */ public JsArray<JavaScriptObject> getSelRowDatas() { return JsDataTable.getSelRowDatas(getElement()); } /** Works when serverSide is true. */ public Set<String> getSelRowIds() { return serverRowSelected; } public void unselectAllRows() { JsDataTable.unselectAllRows(getElement()); } /** @param cellOrRowElt - could be cellElt or rowElt */ public void changeRow(Element cellOrRowElt, boolean selected) { JsDataTable.changeRow(cellOrRowElt, selected); } /** @param cellOrRowElt - could be cellElt or rowElt */ public void selectOneRowOnly(Element cellOrRowElt) { JsDataTable.selectOneRowOnly(cellOrRowElt); } public JavaScriptObject getData() { return JsDataTable.getData(getElement()); } public void clearData() { JsDataTable.clearData(getElement()); } public void clearAll() { clearData(); clearSearch(); } /** * Allows multiple additions, like addRow(); addRow(); ...; refreshDraw(); * <br> refreshDraw() or refreshPage() must be called after it to repaint dataTable. */ public void addRow(JavaScriptObject newRow) { JsDataTable.addRow(getElement(), newRow); } /** * Allows multiple removals, like removeRow(); removeRow(); ...; refreshDraw(); * <br> refreshDraw() or refreshPage() must be called after it to repaint dataTable. */ public void removeRow(int rowIndex) { JsDataTable.removeRow(getElement(), rowIndex); } /** * refreshDraw() or refreshPage() must be called after it to repaint dataTable. */ public boolean removeSelRows() { JsArrayInteger sel = JsDataTable.getSelRowIndexes(getElement()); if (sel.length() == 0) return false; int[] idxs = new int[sel.length()]; for (int i = 0; i < idxs.length; i++) idxs[i] = sel.get(i); Arrays.sort(idxs); for (int i = idxs.length - 1; i >= 0; i--) { removeRow(idxs[i]); } return true; } /** Invalidate all rows. refreshDraw() or refreshPage() must be called after it to repaint dataTable. */ public void rowsInvalidate() { JsDataTable.rowsInvalidate(getElement()); } /** Just a synonym for rowsInvalidate() */ public void invalidateRows() { rowsInvalidate(); } public void invalidateRow(int rowIndex) { JsDataTable.invalidateRow(getElement(), rowIndex); } /** @param cellOrRowElt - could be cellElt or rowElt */ public void invalidateRow(Element cellOrRowElt) { JsDataTable.invalidateRow(getElement(), cellOrRowElt); } /** @param cellElt - cell or one of its children */ public void invalidateCell(Element cellElt) { JsDataTable.invalidateCell(getElement(), cellElt); } public RowIdHelper getRowIdHelper() { return rowIdHelper; } /** Could be useful when we need selection support for server side mode, but server doesn't * provide DT_RowId in data for some reason. **/ public void setRowIdHelper(RowIdHelper rowIdHelper) { this.rowIdHelper = rowIdHelper; } public boolean isIndividualColSearches() { return individualColSearches; } /** You must define footer column titles to get this property working, i.e. for each non-empty * title search input widget will be auto-generated. **/ public void setIndividualColSearches(boolean individualColSearches) { this.individualColSearches = individualColSearches; } protected void initIndividualColSearches() { if (!enhanced || !individualColSearches || tFoot == null) return; JsDataTable.createFooterIndividualColumnSearches(getElement(), individualColSearchPrefix); } public RowData getRowData() { return rowData; } /** Custom data accessor, useful in case non-JavaScriptObject data structure, i.e. DTO/POJO. */ public void setRowData(RowData rowData) { this.rowData = rowData; } public CellRender getCellRender() { return cellRender; } /** * Custom widget can be inserted into any cell. */ public void setCellRender(CellRender cellRender) { this.cellRender = cellRender; } public void clearSearch() { JsDataTable.clearSearch(getElement()); } private void processVisible() { if (!enhanced) return; Element tableElt = getElement(); Element elt = tableElt.getParentElement(); while (elt != null) { if (JQMCommon.hasStyle(elt, WRAPPER)) { UIObject.setVisible(elt, visible); return; } elt = elt.getParentElement(); } } @Override public void setVisible(boolean value) { visible = value; processVisible(); } @Override public boolean isVisible() { return visible; } @Override protected void applyColClassNames(boolean add) { // nothing, we don't need super.applyColClassNames() to be called } }