001/*
002 * #%L
003 * GwtMaterial
004 * %%
005 * Copyright (C) 2015 - 2016 GwtMaterialDesign
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 * 
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 * 
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package gwt.material.design.client.data;
021
022import com.google.gwt.cell.client.Cell.Context;
023import com.google.gwt.core.client.Scheduler;
024import com.google.gwt.dom.client.Element;
025import com.google.gwt.dom.client.Style.Display;
026import com.google.gwt.event.logical.shared.AttachEvent;
027import com.google.gwt.event.shared.GwtEvent;
028import com.google.gwt.event.shared.HandlerRegistration;
029import com.google.gwt.user.client.ui.Widget;
030import com.google.gwt.view.client.ProvidesKey;
031import com.google.gwt.view.client.Range;
032import gwt.material.design.client.base.MaterialWidget;
033import gwt.material.design.client.base.constants.TableCssName;
034import gwt.material.design.client.data.component.CategoryComponent;
035import gwt.material.design.client.data.component.CategoryComponent.OrphanCategoryComponent;
036import gwt.material.design.client.data.component.Component;
037import gwt.material.design.client.data.component.ComponentFactory;
038import gwt.material.design.client.data.component.Components;
039import gwt.material.design.client.data.component.RowComponent;
040import gwt.material.design.client.data.events.CategoryClosedEvent;
041import gwt.material.design.client.data.events.CategoryOpenedEvent;
042import gwt.material.design.client.data.events.ColumnSortEvent;
043import gwt.material.design.client.data.events.ComponentsRenderedEvent;
044import gwt.material.design.client.data.events.DestroyEvent;
045import gwt.material.design.client.data.events.InsertColumnEvent;
046import gwt.material.design.client.data.events.RangeChangeEvent;
047import gwt.material.design.client.data.events.RemoveColumnEvent;
048import gwt.material.design.client.data.events.RenderedEvent;
049import gwt.material.design.client.data.events.RowCollapsedEvent;
050import gwt.material.design.client.data.events.RowCollapsingEvent;
051import gwt.material.design.client.data.events.RowContextMenuEvent;
052import gwt.material.design.client.data.events.RowDoubleClickEvent;
053import gwt.material.design.client.data.events.RowExpandingEvent;
054import gwt.material.design.client.data.events.RowExpandedEvent;
055import gwt.material.design.client.data.events.RowLongPressEvent;
056import gwt.material.design.client.data.events.RowSelectEvent;
057import gwt.material.design.client.data.events.RowShortPressEvent;
058import gwt.material.design.client.data.events.SelectAllEvent;
059import gwt.material.design.client.data.events.SetupEvent;
060import gwt.material.design.client.data.factory.CategoryComponentFactory;
061import gwt.material.design.client.data.factory.RowComponentFactory;
062import gwt.material.design.client.jquery.JQueryExtension;
063import gwt.material.design.client.js.Js;
064import gwt.material.design.client.js.JsTableElement;
065import gwt.material.design.client.js.JsTableSubHeaders;
066import gwt.material.design.client.js.StickyTableOptions;
067import gwt.material.design.client.ui.MaterialCheckBox;
068import gwt.material.design.client.ui.MaterialProgress;
069import gwt.material.design.client.ui.Selectors;
070import gwt.material.design.client.ui.table.*;
071import gwt.material.design.client.ui.table.cell.Column;
072import gwt.material.design.jquery.client.api.Event;
073import gwt.material.design.jquery.client.api.JQueryElement;
074import gwt.material.design.jquery.client.api.MouseEvent;
075
076import java.util.*;
077import java.util.logging.Level;
078import java.util.logging.Logger;
079
080import static gwt.material.design.jquery.client.api.JQuery.$;
081import static gwt.material.design.jquery.client.api.JQuery.window;
082
083/**
084 * Abstract DataView handles the creation, preparation and UI logic for
085 * the table rows and subheaders (if enabled). All of the basic table
086 * rendering is handled.
087 *
088 * @param <T>
089 * @author Ben Dol
090 */
091public abstract class AbstractDataView<T> implements DataView<T> {
092
093    private static final Logger logger = Logger.getLogger(AbstractDataView.class.getName());
094
095    // Main
096    protected final String id;
097    protected DataDisplay<T> display;
098    protected DataSource<T> dataSource;
099    protected Renderer<T> renderer;
100    protected SortContext<T> sortContext;
101    protected Column<T, ?> autoSortColumn;
102    protected RowComponentFactory<T> rowFactory;
103    protected ComponentFactory<? extends CategoryComponent, String> categoryFactory;
104    protected ProvidesKey<T> keyProvider;
105    //protected List<ComponentFactory<?, T>> componentFactories;
106    protected JsTableSubHeaders subheaderLib;
107    protected int categoryHeight = 0;
108    protected String height;
109    protected boolean rendering;
110    protected boolean redraw;
111    protected boolean redrawCategories;
112    private boolean pendingRenderEvent;
113
114    // DOM
115    protected Table table;
116    protected MaterialWidget thead;
117    protected MaterialWidget tbody;
118    protected MaterialProgress progressWidget;
119    protected TableRow headerRow;
120    protected JQueryElement container;
121    protected JsTableElement $table;
122    protected JQueryElement maskElement;
123    protected JQueryElement tableBody;
124    protected JQueryElement topPanel;
125
126    // Configurations
127    protected Range range = new Range(0, 0);
128    protected int totalRows = 20;
129    protected int longPressDuration = 500;
130
131    private int lastSelected;
132    private boolean setup;
133    private boolean loadMask;
134    private boolean shiftDown;
135    private boolean useRowExpansion;
136    private boolean useStickyHeader;
137    private boolean useLoadOverlay;
138    private boolean useCategories;
139    private SelectionType selectionType = SelectionType.NONE;
140
141    // Components
142    protected final Components<RowComponent<T>> rows = new Components<>();
143    protected final Components<RowComponent<T>> pendingRows = new Components<>();
144    protected final Components<CategoryComponent> categories = new Components<>();
145
146    // Rendering
147    protected final List<Column<T, ?>> columns = new ArrayList<>();
148    protected final List<TableHeader> headers = new ArrayList<>();
149    protected HandlerRegistration attachHandler;
150
151    public static final String ORPHAN_PATTERN = "<@orphans@>";
152
153    private static final String expansionHtml = "<tr class='expansion'>" +
154     "<td class='expansion' colspan='100%'>" +
155        "<div>" +
156            "<section class='overlay'>" +
157                "<div class='progress' style='height:4px;top:-1px;'>" +
158                    "<div class='indeterminate'></div>" +
159                "</div>" +
160            "</section>" +
161            "<div class='content'><br/><br/><br/></div>" +
162        "</div>" +
163     "</td></tr>";
164
165    public static final String maskHtml = "<div class='mask'>" +
166        //"<!--i style='left:50%;top:20%;z-index:9999;position:absolute;color:white' class='fa fa-3x fa-spinner fa-spin'></i-->" +
167    "</div>";
168
169    public static final String transitionEvents = "transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd";
170
171    public AbstractDataView() {
172        this("DataView");
173    }
174
175    public AbstractDataView(String id) {
176        this(id, null);
177    }
178
179    public AbstractDataView(ProvidesKey<T> keyProvider) {
180        this("DataView", keyProvider);
181    }
182
183    public AbstractDataView(String id, ProvidesKey<T> keyProvider) {
184        this.id = id;
185        this.keyProvider = keyProvider;
186        this.categoryFactory = new CategoryComponentFactory();
187        this.rowFactory = new RowComponentFactory<>();
188        //this.componentFactories = new ArrayList<>();
189
190        setRenderer(new BaseRenderer<>());
191        onConstructed();
192    }
193
194    /**
195     * Called after the data view is constructed.
196     * Note that this is not when the data view is attached,
197     * see {@link #setup(TableScaffolding)}.
198     */
199    protected void onConstructed() {
200        // Do nothing by default
201    }
202
203    @Override
204    public void render(Components<Component<?>> components) {
205        // Clear the current row components
206        // This does not clear the rows DOM elements
207        this.rows.clearComponents();
208
209        // Render the new components
210        for(Component<?> component : components) {
211            renderComponent(component);
212        }
213
214        redraw = false;
215        prepareRows();
216
217        // Reset category indexes and row counts
218        if(isUseCategories()) {
219            for (CategoryComponent category : categories) {
220                category.setCurrentIndex(-1);
221                category.setRowCount(0);
222            }
223        }
224
225        if(!components.isEmpty()) {
226            // Remove the last attach handler
227            if(attachHandler != null) {
228                attachHandler.removeHandler();
229            }
230            // When the last component has been rendered we
231            // will set the rendering flag to false.
232            // This can be improved later.
233            Component<?> component = components.get(components.size() - 1);
234            Widget componentWidget = component.getWidget();
235            AttachEvent.Handler handler = event -> {
236                if(attachHandler != null) {
237                    attachHandler.removeHandler();
238                }
239
240                // Recheck the row height to ensure
241                // the calculated row height is accurate.
242                getCalculatedRowHeight();
243
244                // Fixes an issue with heights updating too early.
245                // Also ensure the cell widths are updated.
246                subheaderLib.recalculate(true);
247
248                // Fixes an issue with heights updating too early.
249                subheaderLib.updateHeights();
250
251                rendering = false;
252
253                if(attachHandler != null) {
254                    attachHandler.removeHandler();
255                }
256
257                ComponentsRenderedEvent.fire(this);
258
259                if(pendingRenderEvent) {
260                    RenderedEvent.fire(this);
261                    pendingRenderEvent = false;
262                }
263            };
264            if (componentWidget == null || componentWidget.isAttached()) {
265                handler.onAttachOrDetach(null);
266            } else {
267                attachHandler = componentWidget.addAttachHandler(handler);
268            }
269        } else {
270            rendering = false;
271        }
272    }
273
274    /**
275     * Compile list of {@link RowComponent}'s and invoke a render.
276     * Controls the building of category components and custom components.
277     * Which then {@link #render(Components)} is invoked to perform DOM render.
278     *
279     * Rows which are already rendered are reprocessed based on their equals method.
280     * If the data is equal to the row in the same index it use the existing row.
281     *
282     * @param rows list of rows to be rendered against the existing rows.
283     */
284    protected boolean renderRows(Components<RowComponent<T>> rows) {
285        // Make sure we are setup, if we aren't then store the rows
286        // the rows will be attached upon setup.
287        if(!setup) {
288            pendingRows.clear();
289            pendingRows.addAll(rows);
290            return false; // early exit, not setup yet.
291        }
292        rendering = true;
293        Range visibleRange = getVisibleRange();
294
295        // Check if we need to redraw categories
296        if(redrawCategories) {
297            redrawCategories = false;
298
299            // When we perform a category redraw we have
300            // to clear the row elements also.
301            this.rows.clearWidgets();
302
303            if (isUseCategories()) {
304                List<CategoryComponent> openCategories = getOpenCategories();
305                categories.clearWidgets();
306
307                for (CategoryComponent category : categories) {
308                    // Re-render the category component
309                    renderComponent(category);
310
311                    if (openCategories.contains(category) && category.isRendered()) {
312                        subheaderLib.open(category.getWidget().$this());
313                    }
314                }
315            } else {
316                categories.clearWidgets();
317            }
318        }
319
320        // The linear component list.
321        // This component list will be the rendering
322        // blueprint for the current view, so the sequence
323        // is compiled according to the component generation.
324        Components<Component<?>> components = new Components<>(visibleRange.getLength());
325
326        int index = 0;
327        for(RowComponent<T> row : rows) {
328            if (components.isFull()) {
329                break; // Component stack is full, break
330            }
331
332            if (isUseCategories()) {
333                CategoryComponent category = row.getCategory();
334                if(category == null) {
335                    category = buildCategoryComponent(row);
336                    categories.add(category);
337                }
338
339                if (category.isRendered()) {
340                    category.getWidget().setVisible(true);
341                }
342            }
343
344            // Do we have an existing row to use
345            if (index < this.rows.size()) {
346                RowComponent<T> existingRow = this.rows.get(index);
347                if(existingRow != null) {
348                    // Replace the rows element with the
349                    // existing indexes element.
350                    row.setWidget(existingRow.getWidget());
351                    row.setRedraw(true);
352
353                    // Rebuild the rows custom components
354                    //existingRow.destroyChildren();
355                    //buildCustomComponents(existingRow);
356                }
357            }
358            row.setIndex(index++);
359            components.add(row);
360        }
361
362        // Render the component stack
363        render(components);
364        return true;
365    }
366
367    @SuppressWarnings("unchecked")
368    protected void renderComponent(Component<?> component) {
369        if(component != null) {
370            TableRow row;
371            int index = -1;
372
373            if(component instanceof RowComponent) {
374                RowComponent<T> rowComponent = (RowComponent<T>)component;
375
376                // Check if the row has a category
377                // Categories have been rendered before the rows
378                CategoryComponent category = null;
379                if(isUseCategories()){
380                    category = rowComponent.getCategory();
381
382                    // Ensure the category exists and is rendered
383                    if(category != null && !category.isRendered()) {
384                        renderComponent(category);
385                    }
386                }
387                T data = rowComponent.getData();
388
389                // Draw the table row
390                row = renderer.drawRow(this, rowComponent, getValueKey(data), columns, redraw);
391
392                if(row != null) {
393                    if(category != null) {
394                        if(categories.size() > 1) {
395                            int categoryIndex = 0/*category.getCurrentIndex()*/;
396                            //if(categoryIndex == -1) {
397                                categoryIndex = tbody.getWidgetIndex(category.getWidget());
398                                //category.setCurrentIndex(categoryIndex);
399                            //}
400
401                            int categoryCount = category.getRowCount() + 1;
402                            category.setRowCount(categoryCount);
403
404                            // Calculate the rows index
405                            index = (categoryIndex + categoryCount) - 1;
406                        }
407
408                        // Check the display of the row
409                        TableSubHeader subHeader = category.getWidget();
410                        if (subHeader != null && subHeader.isOpen()) {
411                            row.getElement().getStyle().clearDisplay();
412                        }
413                    } else {
414                        // Not using categories
415                        row.getElement().getStyle().clearDisplay();
416                    }
417
418                    rows.add(rowComponent);
419                }
420            } else if(component instanceof CategoryComponent) {
421                CategoryComponent categoryComponent = (CategoryComponent)component;
422                row = bindCategoryEvents(renderer.drawCategory(categoryComponent));
423
424                if(categoryComponent.isOpenByDefault()) {
425                    row.addAttachHandler(event -> openCategory(categoryComponent), true);
426                }
427            } else {
428                row = renderer.drawCustom(component);
429            }
430
431            if(row != null) {
432                if(row.getParent() == null) {
433                    if (index < 0) {
434                        tbody.add(row);
435                    } else {
436                        tbody.insert(row, index + 1);
437                    }
438                } else {
439                    // TODO: calculate row element repositioning.
440                    // This will only apply if its index is different
441                    // to the new index generated based on the category.
442                }
443            } else {
444                logger.warning("Attempted to add a null TableRow to tbody, the row was ignored.");
445            }
446
447            // Render the components children
448            /*for(Component<?> child : component.getChildren()) {
449                renderComponent(child);
450            }*/
451        }
452    }
453
454    protected void renderColumns() {
455        for (Column<T, ?> column : columns) {
456            renderColumn(column);
457        }
458    }
459
460    public void renderColumn(Column<T, ?> column) {
461        int index = columns.indexOf(column) + getColumnOffset();
462
463        TableHeader th = renderer.drawColumnHeader(column, column.getName(), index);
464        if (th != null) {
465            if (column.isSortable()) {
466                th.$this().on("click", e -> {
467                    sort(rows, th, column, index);
468                    return true;
469                });
470                th.addStyleName(TableCssName.SORTABLE);
471            }
472
473            addHeader(index, th);
474        }
475
476        for (RowComponent<T> row : rows) {
477            Context context = new Context(row.getIndex(), index, getValueKey(row.getData()));
478            renderer.drawColumn(row.getWidget(), context, row.getData(), column, index, true);
479        }
480
481        refreshStickyHeaders();
482    }
483
484    @Override
485    public void refresh() {
486        // Recheck the row height to ensure
487        // the calculated row height is accurate.
488        getCalculatedRowHeight();
489
490        if(redraw && setup) {
491            // Render the rows
492            renderRows(rows);
493        }
494    }
495
496    @Override
497    public void setRenderer(Renderer<T> renderer) {
498        if(this.renderer != null) {
499            // Copy existing render properties.
500            renderer.copy(this.renderer);
501        }
502        this.renderer = renderer;
503    }
504
505    public Renderer<T> getRenderer() {
506        return renderer;
507    }
508
509    @Override
510    public void setDataSource(DataSource<T> dataSource) {
511        if(dataSource instanceof HasDataView) {
512            ((HasDataView<T>) dataSource).setDataView(this);
513        }
514        this.dataSource = dataSource;
515    }
516
517    @Override
518    public DataSource<T> getDataSource() {
519        return dataSource;
520    }
521
522    @Override
523    public JsTableSubHeaders getSubheaderLib() {
524        return subheaderLib;
525    }
526
527    @Override
528    public void setup(TableScaffolding scaffolding) throws Exception {
529        try {
530            container = $(getContainer());
531            table = scaffolding.getTable();
532            tableBody = $(scaffolding.getTableBody());
533            topPanel = $(scaffolding.getTopPanel());
534            tbody = table.getBody();
535            thead = table.getHead();
536            $table = table.getJsElement();
537
538            headerRow = new TableRow();
539            thead.add(headerRow);
540
541            // Create progress widget
542            progressWidget = new MaterialProgress();
543            progressWidget.setTop(0);
544            progressWidget.setGwtDisplay(Display.NONE);
545            TableRow progressRow = new TableRow();
546            progressRow.addStyleName(TableCssName.STICKYEXCLUDE);
547            progressRow.setHeight("3px");
548            TableData progressTd = new TableData();
549            progressTd.getElement().setAttribute("colspan", "999");
550            progressTd.setPadding(0);
551            progressTd.setHeight("0px");
552            progressTd.add(progressWidget);
553            progressRow.add(progressTd);
554            thead.add(progressRow);
555
556            if(useRowExpansion) {
557                // Add the expand header
558                TableHeader expandHeader = new TableHeader();
559                expandHeader.setStyleName(TableCssName.COLEX);
560                addHeader(0, expandHeader);
561            }
562
563            if(!selectionType.equals(SelectionType.NONE)) {
564                setupHeaderSelectionBox();
565
566                if(selectionType.equals(SelectionType.MULTIPLE)) {
567                    setupShiftDetection();
568                }
569            }
570
571            // Setup the sticky header bar
572            if (useStickyHeader) {
573                setupStickyHeader();
574            }
575
576            // Setup the subheaders for categories
577            setupSubHeaders();
578
579            // Setup the resize event handlers
580            tableBody.on("resize." + id, e -> {
581                refresh();
582                return true;
583            });
584
585            // We will check the window resize just in case
586            // it has updated the view size of the data view.
587            $(window()).on("resize." + id, e -> {
588                // In the cases where the table is not currently attached.
589                if(getContainer().isAttached()) {
590                    refresh();
591                }
592                return true;
593            });
594
595            setup = true;
596
597            onSetup(scaffolding);
598
599            SetupEvent.fire(this, scaffolding);
600        } catch (Exception ex) {
601            logger.log(Level.SEVERE, "Problem setting up the DataView.", ex);
602            throw ex;
603        }
604    }
605
606    protected void onSetup(TableScaffolding scaffolding) {
607        // We are setup, lets check the render tasks
608        if(height != null) {
609            setHeight(height);
610        }
611
612        setSelectionType(selectionType);
613
614        renderColumns();
615
616        for(CategoryComponent category : categories) {
617            if(!category.isRendered()) {
618                renderCategory(category);
619            }
620        }
621
622        if(!pendingRows.isEmpty()) {
623            Components<RowComponent<T>> sortedRows = null;
624            if(maybeApplyAutoSortColumn()) {
625                // We have an auto sort column, sort the pending rows.
626                Column<T, ?> column = sortContext.getSortColumn();
627                sortedRows = sort(pendingRows, sortContext.getTableHeader(), column, columns.indexOf(column) + getColumnOffset(), false);
628            }
629
630            if(sortedRows == null) {
631                renderRows(pendingRows);
632                pendingRows.clearComponents();
633            } else {
634                renderRows(sortedRows);
635            }
636        }
637    }
638
639    @Override
640    public void destroy() {
641        rows.clear();
642        categories.clear();
643
644        columns.clear();
645        headers.clear();
646        headerRow.clear();
647
648        container.off("." + id);
649        tableBody.off("." + id);
650        $(window()).off("." + id);
651
652        $table.stickyTableHeaders("destroy");
653        subheaderLib.unload();
654
655        setRedraw(true);
656        rendering = false;
657        setup = false;
658
659        DestroyEvent.fire(this);
660    }
661
662    /**
663     * Prepare all row specific functionality.
664     */
665    protected void prepareRows() {
666        JQueryElement rows = $table.find("tr.data-row");
667        rows.off("." + id);
668
669        // Select row click bind
670        // This will also update the check status of check all input.
671        rows.on("tap." + id + " click." + id, (e, o) -> {
672             Element row = $(e.getCurrentTarget()).asElement();
673            int rowIndex = getRowIndexByElement(row);
674            if (selectionType.equals(SelectionType.MULTIPLE) && shiftDown) {
675                if (lastSelected < rowIndex) {
676                    // Increment
677                    for (int i = lastSelected; i <= rowIndex; i++) {
678                        if (i < getVisibleItemCount()) {
679                            RowComponent<T> rowComponent = this.rows.get(i);
680                            if (rowComponent != null && rowComponent.isRendered()) {
681                                selectRow(rowComponent.getWidget().getElement(), true);
682                            }
683                        }
684                    }
685                } else {
686                    // Decrement
687                    for (int i = lastSelected - 1; i >= rowIndex - 1; i--) {
688                        if (i >= 0) {
689                            RowComponent<T> rowComponent = this.rows.get(i);
690                            if (rowComponent != null && rowComponent.isRendered()) {
691                                selectRow(rowComponent.getWidget().getElement(), true);
692                            }
693                        }
694                    }
695                }
696            } else {
697                toggleRowSelect(e, row);
698            }
699            return true;
700        });
701
702        rows.on("contextmenu." + id, (e, o) -> {
703            Element row = $(e.getCurrentTarget()).asElement();
704
705            // Fire row select event
706            RowContextMenuEvent.fire(this, (MouseEvent)e, getModelByRowElement(row), row);
707            return false;
708        });
709
710        rows.on("dblclick." + id, (e, o) -> {
711            Element row = $(e.getCurrentTarget()).asElement();
712
713            // Fire row select event
714            RowDoubleClickEvent.fire(this, e, getModelByRowElement(row), row);
715            return false;
716        });
717
718        JQueryExtension.$(rows).longpress(e -> {
719            Element row = $(e.getCurrentTarget()).asElement();
720
721            // Fire row select event
722            RowLongPressEvent.fire(this, e, getModelByRowElement(row), row);
723            return true;
724        }, e -> {
725            Element row = $(e.getCurrentTarget()).asElement();
726
727            // Fire row select event
728            RowShortPressEvent.fire(this, e, getModelByRowElement(row), row);
729            return true;
730        }, longPressDuration);
731
732        JQueryElement expands = $table.find("i#expand");
733        expands.off("." + id);
734        if(useRowExpansion) {
735            // Expand current row extra information
736            expands.on("tap." + id + " click." + id, e -> {
737                final boolean[] recalculated = {false};
738
739                JQueryElement tr = $(e.getCurrentTarget()).parent().parent();
740                if (!tr.hasClass("disabled") && !tr.is("[disabled]")) {
741                    JQueryElement[] expansion = new JQueryElement[]{tr.next().find("td.expansion div")};
742
743                    if (expansion[0].length() < 1) {
744                        expansion[0] = $(expansionHtml).insertAfter(tr);
745                        expansion[0] = expansion[0].find("td.expansion div");
746                    }
747
748                    final boolean expanding = !expansion[0].hasClass("expanded");
749                    final JQueryElement row = tr.next();
750                    final T model = getModelByRowElement(tr.asElement());
751
752                    RowExpansion<T> rowExpansion = new RowExpansion<>(model, row);
753
754                    expansion[0].one(transitionEvents,
755                        (e1, param1) -> {
756                            if (!recalculated[0]) {
757                                // Recalculate subheaders
758                                subheaderLib.recalculate(true);
759                                recalculated[0] = true;
760
761                                // Apply overlay
762                                JQueryElement overlay = row.find("section.overlay");
763                                overlay.height(row.outerHeight(false));
764
765                                if(expanding) {
766                                    // Fire table expanded event
767                                    RowExpandedEvent.fire(this, rowExpansion);
768                                } else {
769                                    // Fire table collapsed event
770                                    RowCollapsedEvent.fire(this, rowExpansion);
771                                }
772                            }
773                            return true;
774                        });
775
776                    if(expanding) {
777                        // Fire table expand event
778                        RowExpandingEvent.fire(this, rowExpansion);
779                    } else {
780                        RowCollapsingEvent.fire(this, rowExpansion);
781                    }
782
783                    Scheduler.get().scheduleDeferred(() -> {
784                        expansion[0].toggleClass("expanded");
785                    });
786                }
787
788                e.stopPropagation();
789                return true;
790            });
791        }
792
793        subheaderLib.detect();
794        subheaderLib.recalculate(true);
795    }
796
797    protected void setupStickyHeader() {
798        if($table != null && display != null) {
799            $table.stickyTableHeaders(StickyTableOptions.create(
800                $(".table-body", getContainer())));
801        }
802    }
803
804    protected void setupSubHeaders() {
805        if($table != null && display != null) {
806            subheaderLib = JsTableSubHeaders.newInstance(
807                $(".table-body", getContainer()), "tr.subheader");
808
809            final JQueryElement header = $table.find("thead");
810            $(subheaderLib).off("before-recalculate");
811            $(subheaderLib).on("before-recalculate", e -> {
812                boolean updateMargin = header.is(":visible") && isUseStickyHeader();
813                subheaderLib.setMarginTop(updateMargin ? header.outerHeight() : 0);
814                return true;
815            });
816
817            // Load the subheaders after binding.
818            subheaderLib.load();
819        }
820    }
821
822    protected boolean isWithinView(int start, int length) {
823        return isWithinView(start, length, true);
824    }
825
826    protected boolean isWithinView(int start, int length, boolean canOverflow) {
827        int end = start + length;
828        int rangeStart = range.getStart();
829        int rangeEnd = rangeStart + range.getLength();
830        return ((canOverflow ? (end > rangeStart && start < rangeEnd) : (start >= rangeStart && end <= rangeEnd)));
831    }
832
833    @Override
834    public int getRowCount() {
835        return rows.size();
836    }
837
838    @Override
839    public Range getVisibleRange() {
840        return range;
841    }
842
843    @Override
844    public void setVisibleRange(int start, int length) {
845        setVisibleRange(new Range(start, length));
846    }
847
848    @Override
849    public void setVisibleRange(Range range) {
850        setVisibleRange(range, true);
851    }
852
853    protected void setVisibleRange(Range range, boolean forceRangeChangeEvent) {
854        final int start = range.getStart();
855        final int length = range.getLength();
856        if (start < 0) {
857            throw new IllegalArgumentException("Range start cannot be less than 0");
858        }
859        if (length < 0) {
860            throw new IllegalArgumentException("Range length cannot be less than 0");
861        }
862
863        // Update the page start.
864        final int pageStart = this.range.getStart();
865        final int pageSize = this.range.getLength();
866        final boolean pageStartChanged = (pageStart != start);
867        if (pageStartChanged) {
868            // Update the range start
869            this.range = new Range(start, this.range.getLength());
870        }
871
872        // Update the page size
873        final boolean pageSizeChanged = (pageSize != length);
874        if (pageSizeChanged) {
875            this.range = new Range(this.range.getStart(), length);
876        }
877
878        // Clear the rows
879        rows.clear();
880
881        // Update the pager and data source if the range changed
882        if (pageStartChanged || pageSizeChanged || forceRangeChangeEvent) {
883            RangeChangeEvent.fire(this, getVisibleRange());
884        }
885    }
886
887    @Override
888    public boolean isHeaderVisible(int colIndex) {
889        return colIndex < headers.size() && (headers.get(colIndex).$this().is(":visible") || headers.get(colIndex).isVisible());
890    }
891
892    @Override
893    public void addColumn(Column<T, ?> column) {
894        addColumn(column, "");
895    }
896
897    @Override
898    public void addColumn(Column<T, ?> column, String header) {
899        insertColumn(columns.size(), column, header);
900    }
901
902    @Override
903    public void insertColumn(int beforeIndex, Column<T, ?> column, String header) {
904        // Allow insert at the end.
905        if (beforeIndex != getColumnCount()) {
906            checkColumnBounds(beforeIndex);
907        }
908
909        String name = column.getName();
910        if(name == null || name.isEmpty()) {
911            // Set the columns name
912            column.setName(header);
913        }
914
915        if(columns.size() < beforeIndex) {
916            columns.add(column);
917        } else {
918            columns.add(beforeIndex, column);
919        }
920
921        if(setup) {
922            renderColumn(column);
923        }
924
925        InsertColumnEvent.fire(this, beforeIndex, column, header);
926    }
927
928    protected void updateSortContext(TableHeader th, Column<T, ?> column) {
929        updateSortContext(th, column, null);
930    }
931
932    protected void updateSortContext(TableHeader th, Column<T, ?> column, SortDir dir) {
933        if(sortContext == null) {
934            sortContext = new SortContext<>(column, th);
935        } else {
936            Column<T, ?> sortColumn = sortContext.getSortColumn();
937            if(sortColumn != column) {
938                sortContext.setSortColumn(column);
939                sortContext.setTableHeader(th);
940            } else if(dir == null && sortContext.isSorted()) {
941                sortContext.reverse();
942            }
943        }
944        if(dir != null) {
945            sortContext.setSortDir(dir);
946        }
947    }
948
949    @Override
950    public void sort(int columnIndex) {
951        sort(columnIndex, null);
952    }
953
954    @Override
955    public void sort(int columnIndex, SortDir dir) {
956        sort(columns.get(columnIndex), dir);
957    }
958
959    @Override
960    public void sort(Column<T, ?> column) {
961        sort(column, null);
962    }
963
964    @Override
965    public void sort(Column<T, ?> column, SortDir dir) {
966        if(column != null) {
967            int index = columns.indexOf(column) + getColumnOffset();
968            TableHeader th = headers.get(index);
969            sort(rows, th, column, index, dir);
970        } else {
971            throw new RuntimeException("Cannot sort on a null column.");
972        }
973    }
974
975    protected Components<RowComponent<T>> sort(Components<RowComponent<T>> rows, TableHeader th, Column<T, ?> column,
976                                               int index) {
977        return sort(rows, th, column, index, dataSource == null || !dataSource.useRemoteSort());
978    }
979
980    protected Components<RowComponent<T>> sort(Components<RowComponent<T>> rows, TableHeader th, Column<T, ?> column,
981                                               int index, SortDir dir) {
982        return sort(rows, th, column, index, dir, dataSource == null || !dataSource.useRemoteSort());
983    }
984
985    protected Components<RowComponent<T>> sort(Components<RowComponent<T>> rows, TableHeader th, Column<T, ?> column,
986                                               int index, boolean renderRows) {
987        return sort(rows, th, column, index, null, renderRows);
988    }
989
990    protected Components<RowComponent<T>> sort(Components<RowComponent<T>> rows, TableHeader th, Column<T, ?> column,
991                                               int index, SortDir dir, boolean renderRows) {
992        SortContext<T> oldSortContext = new SortContext<>(this.sortContext);
993        updateSortContext(th, column, dir);
994
995        Components<RowComponent<T>> clonedRows = new Components<>(rows, RowComponent::new);
996        if(doSort(sortContext, clonedRows)) {
997            th.addStyleName(TableCssName.SELECTED);
998
999            // Draw and apply the sort icon.
1000            renderer.drawSortIcon(th, sortContext);
1001
1002            // No longer a fresh sort
1003            sortContext.setSorted(true);
1004
1005            if (renderRows) {
1006                // Render the new sort order.
1007                renderRows(clonedRows);
1008            }
1009
1010            ColumnSortEvent.fire(this, sortContext, index);
1011        } else {
1012            // revert the sort context
1013            sortContext = oldSortContext;
1014        }
1015
1016        return clonedRows;
1017    }
1018
1019    /**
1020     * Perform a sort on the a set of {@link RowComponent}'s.
1021     * Sorting will check for each components category and sort per category if found.
1022     * @return true if the data was sorted, false if no sorting was performed.
1023     */
1024    protected boolean doSort(SortContext<T> sortContext, Components<RowComponent<T>> rows) {
1025
1026        if (dataSource != null && dataSource.useRemoteSort()) {
1027            // The sorting should be handled by an external
1028            // data source rather than re-ordered by the
1029            // client comparator.
1030            return true;
1031        }
1032
1033        Comparator<? super RowComponent<T>> comparator = sortContext != null
1034            ? sortContext.getSortColumn().getSortComparator() : null;
1035        if (isUseCategories()) {
1036            // Split row data into categories
1037            Map<String, List<RowComponent<T>>> splitMap = new HashMap<>();
1038            List<RowComponent<T>> orphanRows = new ArrayList<>();
1039
1040            for (RowComponent<T> row : rows) {
1041                if(row != null) {
1042                    String category = row.getCategoryName();
1043                    if(category != null) {
1044                        List<RowComponent<T>> data = splitMap.computeIfAbsent(category, k -> new ArrayList<>());
1045                        data.add(row);
1046                    } else {
1047                        orphanRows.add(row);
1048                    }
1049                }
1050            }
1051
1052            if(!orphanRows.isEmpty()) {
1053                splitMap.put(ORPHAN_PATTERN, orphanRows);
1054            }
1055
1056            rows.clearComponents();
1057            for (Map.Entry<String, List<RowComponent<T>>> entry : splitMap.entrySet()) {
1058                List<RowComponent<T>> list = entry.getValue();
1059                if (comparator != null) {
1060                    list.sort(new DataSort<>(comparator, sortContext.getSortDir()));
1061                }
1062                rows.addAll(list);
1063            }
1064        } else {
1065            if (comparator != null) {
1066                rows.sort(new DataSort<>(comparator, sortContext.getSortDir()));
1067            } else if(sortContext != null) {
1068                rows.sort(new DataSort<>(new Comparator<RowComponent<T>>() {
1069                    @Override
1070                    public int compare(RowComponent<T> o1, RowComponent<T> o2) {
1071                        return o1.getData().toString().compareToIgnoreCase(o2.getData().toString());
1072                    }
1073                }, sortContext.getSortDir()));
1074            } else {
1075                return false;
1076            }
1077        }
1078        return true;
1079    }
1080
1081    @Override
1082    public void removeColumn(int colIndex) {
1083        removeColumn(colIndex, true);
1084    }
1085
1086    public void removeColumn(int colIndex, boolean hardRemove) {
1087        int index = colIndex + getColumnOffset();
1088        headerRow.remove(index);
1089
1090        for(RowComponent<T> row : rows) {
1091            row.getWidget().remove(index);
1092        }
1093
1094        reindexColumns();
1095        refreshStickyHeaders();
1096
1097        if(hardRemove) {
1098            columns.remove(colIndex);
1099
1100            RemoveColumnEvent.fire(this, colIndex);
1101        }
1102    }
1103
1104    @Override
1105    public void removeColumns() {
1106        if(!columns.isEmpty()) {
1107            int size = columns.size() - 1;
1108            for (int i = 0; i < size; i++) {
1109                removeColumn(i, false);
1110            }
1111            columns.clear();
1112
1113            for (int i = 0; i < size; i++) {
1114                RemoveColumnEvent.fire(this, i);
1115            }
1116        }
1117    }
1118
1119    @Override
1120    public List<Column<T, ?>> getColumns() {
1121        return columns;
1122    }
1123
1124    @Override
1125    public int getColumnOffset() {
1126        return selectionType.equals(SelectionType.NONE) ? 0 : 1;
1127    }
1128
1129    /**
1130     * Check that the specified column is within bounds.
1131     *
1132     * @param col the column index
1133     * @throws IndexOutOfBoundsException if the column is out of bounds
1134     */
1135    private void checkColumnBounds(int col) {
1136        if (col < 0 || col >= getColumnCount()) {
1137            throw new IndexOutOfBoundsException("Column index is out of bounds: " + col);
1138        }
1139    }
1140
1141    /**
1142     * Get the number of columns in the table.
1143     *
1144     * @return the column count
1145     */
1146    public int getColumnCount() {
1147        return columns.size();
1148    }
1149
1150    @Override
1151    public SelectionType getSelectionType() {
1152        return selectionType;
1153    }
1154
1155    @Override
1156    public void setSelectionType(SelectionType selectionType) {
1157        boolean hadSelection = !this.selectionType.equals(SelectionType.NONE);
1158        this.selectionType = selectionType;
1159
1160        // Add the selection header
1161        if(setup) {
1162            if (!selectionType.equals(SelectionType.NONE) && !hadSelection) {
1163                setupHeaderSelectionBox();
1164
1165                if(selectionType.equals(SelectionType.MULTIPLE)) {
1166                    setupShiftDetection();
1167                }
1168
1169                // Rebuild the columns
1170                for (RowComponent<T> row : rows) {
1171                    row.getWidget().insert(renderer.drawSelectionCell(), 0);
1172                }
1173                reindexColumns();
1174            } else if (selectionType.equals(SelectionType.NONE) && hadSelection) {
1175                removeHeader(0);
1176                $("td#col0", getContainer()).remove();
1177                reindexColumns();
1178            }
1179        }
1180    }
1181
1182    protected void setupHeaderSelectionBox() {
1183        // Setup select all checkbox
1184        TableHeader th = new TableHeader();
1185        th.setId("col0");
1186        th.setStyleName(TableCssName.SELECTION);
1187        if(selectionType.equals(SelectionType.MULTIPLE)) {
1188            new MaterialCheckBox(th.getElement());
1189
1190            // Select all row click bind
1191            // This will also update the check status of check all input.
1192            JQueryElement selectAll = $(th).find("label");
1193            selectAll.off("." + id);
1194            selectAll.on("tap." + id + " click." + id, (e) -> {
1195                JQueryElement input = $("input", th);
1196
1197                boolean marked = Js.isTrue(input.prop("checked")) ||
1198                                 Js.isTrue(input.prop("indeterminate"));
1199
1200                selectAllRows(!marked || hasDeselectedRows(true));
1201                return false;
1202            });
1203        }
1204        addHeader(0, th);
1205    }
1206
1207    protected void setupShiftDetection() {
1208        tableBody.attr("tabindex", "0");
1209
1210        tableBody.off("keydown");
1211        tableBody.keydown(e -> {
1212            shiftDown = e.isShiftKey();
1213            return true;
1214        });
1215
1216        tableBody.off("keyup");
1217        tableBody.keyup(e -> {
1218            shiftDown = e.isShiftKey();
1219            return true;
1220        });
1221    }
1222
1223    protected void reindexColumns() {
1224        int colMod = getColumnOffset();
1225
1226        for(RowComponent<T> row : rows) {
1227            TableRow tableRow = row.getWidget();
1228            for(int i = colMod; i < tableRow.getWidgetCount(); i++) {
1229                TableData td = tableRow.getColumn(i);
1230                if(!td.getStyleName().contains("colex")) {
1231                    td.setId("col" + i);
1232                }
1233            }
1234        }
1235
1236        for(int i = colMod; i < headerRow.getWidgetCount(); i++) {
1237            TableData td = headerRow.getColumn(i);
1238            if(!td.getStyleName().contains("colex")) {
1239                td.setId("col" + i);
1240            }
1241        }
1242    }
1243
1244    @Override
1245    public boolean isSetup() {
1246        return setup;
1247    }
1248
1249    @Override
1250    public boolean isRendering() {
1251        return rendering;
1252    }
1253
1254    @Override
1255    public void setUseStickyHeader(boolean stickyHeader) {
1256        if (this.useStickyHeader && !stickyHeader) {
1257            // Destroy existing sticky header function
1258            $table.stickyTableHeaders("destroy");
1259        } else if (stickyHeader) {
1260            // Initialize sticky header
1261            setupStickyHeader();
1262        }
1263        this.useStickyHeader = stickyHeader;
1264    }
1265
1266    @Override
1267    public boolean isUseStickyHeader() {
1268        return useStickyHeader;
1269    }
1270
1271    /**
1272     * TODO: This method can be optimized.
1273     */
1274    private void refreshStickyHeaders() {
1275        if($table != null) {
1276            // Destroy existing sticky header function
1277            $table.stickyTableHeaders("destroy");
1278
1279            if(isUseStickyHeader()) {
1280                // Initialize sticky header
1281                setupStickyHeader();
1282            }
1283        }
1284    }
1285
1286    @Override
1287    public void selectAllRows(boolean select) {
1288        selectAllRows(select, true);
1289    }
1290
1291    @Override
1292    public void selectAllRows(boolean select, boolean fireEvent) {
1293        List<Element> rows = new ArrayList<>();
1294
1295        // Select all rows
1296        $table.find("tr.data-row").each((i, e) -> {
1297            JQueryElement row = $(e);
1298
1299            if(row.is(":visible") && !row.hasClass("disabled") && !row.is("[disabled]")) {
1300                JQueryElement input = $("td#col0 input", row);
1301                input.prop("checked", select);
1302
1303                boolean isSelected = row.hasClass("selected");
1304                row.removeClass("selected");
1305                if(select) {
1306                    row.addClass("selected");
1307
1308                    // Only add to row selection if
1309                    // not selected previously.
1310                    if(!isSelected) {
1311                        rows.add(row.asElement());
1312                    }
1313                } else if(isSelected) {
1314                    rows.add(row.asElement());
1315                }
1316            }
1317        });
1318
1319        // Update check all input
1320        updateCheckAllInputState();
1321
1322        if(fireEvent) {
1323            // Fire select all event
1324            SelectAllEvent.fire(this, getModelsByRowElements(rows), rows, select);
1325        }
1326    }
1327
1328    /**
1329     * Select a row by given element.
1330     *
1331     * @param event sourced even (can be null)
1332     * @param row element of the row selection
1333     */
1334    public void toggleRowSelect(Event event, Element row) {
1335        toggleRowSelect(event, row, true);
1336    }
1337
1338    /**
1339     * Select a row by given element.
1340     *
1341     * @param event sourced even (can be null)
1342     * @param row element of the row selection
1343     * @param fireEvent fire the row select event.
1344     */
1345    public void toggleRowSelect(Event event, Element row, boolean fireEvent) {
1346        JQueryElement $row = $(row);
1347        if(!$row.hasClass("disabled") && !$row.is("[disabled]")) {
1348            boolean selected = Js.isTrue($row.hasClass("selected"));
1349            if(selected) {
1350                $("td#col0 input", row).prop("checked", false);
1351                $row.removeClass("selected");
1352            } else {
1353                // deselect all rows when using single selection
1354                if(!selectionType.equals(SelectionType.MULTIPLE)) {
1355                    selectAllRows(false, true);
1356                }
1357
1358                $("td#col0 input", row).prop("checked", true);
1359                $row.addClass("selected");
1360                lastSelected = getRowIndexByElement(row);
1361            }
1362
1363            // Update check all input
1364            updateCheckAllInputState();
1365
1366            if(fireEvent) {
1367                // Fire row select event
1368                RowSelectEvent.fire(this, event, getModelByRowElement(row), row, !selected);
1369            }
1370        }
1371    }
1372
1373    @Override
1374    public void selectRow(Element row, boolean fireEvent) {
1375        JQueryElement $row = $(row);
1376        if(!$row.hasClass("disabled") && !$row.is("[disabled]")) {
1377            if(!Js.isTrue($row.hasClass("selected"))) {
1378                // deselect all rows when using single selection
1379                if(selectionType.equals(SelectionType.SINGLE)) {
1380                    selectAllRows(false, true);
1381                }
1382
1383                $("td#col0 input", row).prop("checked", true);
1384                $row.addClass("selected");
1385                lastSelected = getRowIndexByElement(row);
1386            }
1387
1388            // Update check all input
1389            updateCheckAllInputState();
1390
1391            if(fireEvent) {
1392                // Fire row select event
1393                RowSelectEvent.fire(this, null, getModelByRowElement(row), row, true);
1394            }
1395        }
1396    }
1397
1398    @Override
1399    public void deselectRow(Element row, boolean fireEvent) {
1400        JQueryElement $row = $(row);
1401        if(!$row.hasClass("disabled") && !$row.is("[disabled]")) {
1402            if(Js.isTrue($row.hasClass("selected"))) {
1403                $("td#col0 input", row).prop("checked", false);
1404                $row.removeClass("selected");
1405            }
1406
1407            // Update check all input
1408            updateCheckAllInputState();
1409
1410            if(fireEvent) {
1411                // Fire row select event
1412                RowSelectEvent.fire(this, null, getModelByRowElement(row), row, false);
1413            }
1414        }
1415    }
1416
1417    @Override
1418    public boolean hasDeselectedRows(boolean visibleOnly) {
1419        return $table.find(Selectors.rowInputNotCheckedSelector + (visibleOnly ? ":visible" : "")).length() > 0;
1420    }
1421
1422    @Override
1423    public boolean hasSelectedRows(boolean visibleOnly) {
1424        return $table.find(Selectors.rowInputCheckedSelector + (visibleOnly ? ":visible" : "")).length() > 0;
1425    }
1426
1427    @Override
1428    public List<T> getSelectedRowModels(boolean visibleOnly) {
1429        final List<T> models = new ArrayList<>();
1430        $table.find(Selectors.rowInputCheckedSelector + (visibleOnly ? ":visible" : "")).each((i, e) -> {
1431            T model = getModelByRowElement($(e).parent().parent().asElement());
1432            if(model != null) {
1433                models.add(model);
1434            }
1435        });
1436        return models;
1437    }
1438
1439    @Override
1440    public void setRowData(int start, List<? extends T> values) {
1441        int length = values.size();
1442        int end = start + length;
1443
1444        // Make sure we have a valid range
1445        if(range.getStart() < 0 || range.getLength() < 1) {
1446            setVisibleRange(0, length);
1447        }
1448
1449        // Current range start and end
1450        int rangeStart = range.getStart();
1451        int rangeEnd = rangeStart + range.getLength();
1452
1453        // Calculated boundary scope
1454        int boundedStart = Math.max(start, rangeStart);
1455        int boundedEnd = Math.min(end, rangeEnd);
1456
1457        if (start != rangeStart && boundedStart >= boundedEnd) {
1458            // The data is out of range for the current range.
1459            // Intentionally allow empty lists that start on the range start.
1460            return;
1461        }
1462
1463        // Merge the existing data with the new data
1464        Components<RowComponent<T>> rows = new Components<>(this.rows);
1465        for (int i = boundedStart; i < boundedEnd; i++) {
1466            RowComponent<T> newRow = buildRowComponent(values.get(i - boundedStart));
1467            if(i < rows.size()) {
1468                // Within the existing data set
1469                rows.set(i, newRow);
1470            } else {
1471                // Expanding the data set
1472                rows.add(newRow);
1473            }
1474        }
1475
1476        // Ensure sort order is applied for new rows
1477        doSort(sortContext, rows);
1478
1479        pendingRenderEvent = true;
1480
1481        if(maybeApplyAutoSortColumn()) {
1482            // We have an auto sort column, sort the new rows.
1483            Column<T, ?> column = sortContext.getSortColumn();
1484            rows = sort(rows, sortContext.getTableHeader(), column, columns.indexOf(column) + getColumnOffset(), false);
1485        }
1486
1487        // Render the new rows normally
1488        renderRows(rows);
1489    }
1490
1491    /**
1492     * Check and apply the auto sort column {@link Column#setAutoSort(boolean)}
1493     * if no sort has been invoked.
1494     * @return true if the auto sort column is assigned.
1495     */
1496    protected boolean maybeApplyAutoSortColumn() {
1497        // Check if we already have a sort column
1498        if(sortContext == null || sortContext.getSortColumn() == null) {
1499            Column<T, ?> autoSortColumn = getAutoSortColumn();
1500
1501            if(autoSortColumn != null) {
1502                if(setup) {
1503                    int index = columns.indexOf(autoSortColumn) + getColumnOffset();
1504                    updateSortContext(headers.get(index), autoSortColumn);
1505                    return true;
1506                }
1507            }
1508        }
1509        return false;
1510    }
1511
1512    /**
1513     * Get the auto sorting column, or null if no column is auto sorting.
1514     */
1515    protected Column<T, ?> getAutoSortColumn() {
1516        if(autoSortColumn == null) {
1517            for (Column<T, ?> column : columns) {
1518                if (column.isAutoSort()) {
1519                    autoSortColumn = column;
1520                    return autoSortColumn;
1521                }
1522            }
1523        }
1524        return autoSortColumn;
1525    }
1526
1527    public ComponentFactory<? extends CategoryComponent, String> getCategoryFactory() {
1528        return categoryFactory;
1529    }
1530
1531    protected RowComponent<T> buildRowComponent(T data) {
1532        if(data != null) {
1533            assert rowFactory != null : "The dataview's row factory cannot be null";
1534            return /*buildCustomComponents(*/rowFactory.generate(this, data)/*)*/;
1535        }
1536        return null;
1537    }
1538
1539    protected CategoryComponent buildCategoryComponent(RowComponent<T> row) {
1540        return row != null ? buildCategoryComponent(row.getCategoryName()) : null;
1541    }
1542
1543    protected CategoryComponent buildCategoryComponent(String categoryName) {
1544        if(categoryName != null) {
1545            // Generate the category if not exists
1546            if (categoryFactory != null) {
1547                CategoryComponent category = getCategory(categoryName);
1548                if (category == null) {
1549                    return categoryFactory.generate(this, categoryName);
1550                } else {
1551                    return category;
1552                }
1553            }
1554        }
1555        return null;
1556    }
1557
1558    /*protected RowComponent<T> buildCustomComponents(RowComponent<T> row) {
1559        if(row != null) {
1560            // custom components
1561            for (ComponentFactory<?, T> factory : componentFactories) {
1562                T data = row.getData();
1563                if(data != null) {
1564                    Component<?> component = factory.generate(data);
1565                    if (component != null) {
1566                        row.add(component);
1567                    } else {
1568                        logger.fine("DataView component factory: " + factory.toString() + " returned a null component.");
1569                    }
1570                }
1571            }
1572        }
1573        return row;
1574    }*/
1575
1576    /**
1577     * Get the key for the specified value. If a keyProvider is not specified or the value is null,
1578     * the value is returned. If the key provider is specified, it is used to get the key from
1579     * the value.
1580     *
1581     * @param value the value
1582     * @return the key
1583     */
1584    public Object getValueKey(T value) {
1585        ProvidesKey<T> keyProvider = getKeyProvider();
1586        return (keyProvider == null || value == null) ? value : keyProvider.getKey(value);
1587    }
1588
1589    @Override
1590    public ProvidesKey<T> getKeyProvider() {
1591        return keyProvider;
1592    }
1593
1594    @Override
1595    public int getVisibleItemCount() {
1596        return rows.size();
1597    }
1598
1599    @Override
1600    public int getRowHeight() {
1601        return renderer.getExpectedRowHeight();
1602    }
1603
1604    @Override
1605    public void setRowHeight(int rowHeight) {
1606        renderer.setExpectedRowHeight(rowHeight);
1607    }
1608
1609    protected int getCalculatedRowHeight() {
1610        if(!rows.isEmpty()) {
1611            renderer.calculateRowHeight(rows.get(0));
1612        }
1613        return renderer.getCalculatedRowHeight();
1614    }
1615
1616    @Override
1617    public void addCategory(String category) {
1618        if(category != null) {
1619            addCategory(buildCategoryComponent(category));
1620        }
1621    }
1622
1623    @Override
1624    public void addCategory(final CategoryComponent category) {
1625        if(category != null && !hasCategory(category.getName())) {
1626            categories.add(category);
1627
1628            if(setup && isUseCategories()) {
1629                renderCategory(category);
1630            }
1631        }
1632    }
1633
1634    protected void renderCategory(CategoryComponent category) {
1635        if(category != null) {
1636            // Render the category component
1637            renderComponent(category);
1638
1639            if (subheaderLib != null) {
1640                subheaderLib.detect();
1641                subheaderLib.recalculate(true);
1642            }
1643        }
1644    }
1645
1646    @Override
1647    public boolean hasCategory(String categoryName) {
1648        if(categoryName != null) {
1649            for (CategoryComponent category : categories) {
1650                if (category.getName().equals(categoryName)) {
1651                    return true;
1652                }
1653            }
1654        }
1655        return getOrphansCategory() != null;
1656    }
1657
1658    @Override
1659    public void disableCategory(String categoryName) {
1660        CategoryComponent category = getCategory(categoryName);
1661        if(category != null && category.isRendered()) {
1662            subheaderLib.close(category.getWidget().$this());
1663            category.getWidget().setEnabled(false);
1664        }
1665    }
1666
1667    @Override
1668    public void enableCategory(String categoryName) {
1669        CategoryComponent category = getCategory(categoryName);
1670        if(category != null && category.isRendered()) {
1671            category.getWidget().setEnabled(true);
1672        }
1673    }
1674
1675    @Override
1676    public List<CategoryComponent> getCategories() {
1677        return Collections.unmodifiableList(categories);
1678    }
1679
1680    @Override
1681    public List<CategoryComponent> getOpenCategories() {
1682        List<CategoryComponent> openCategories = null;
1683        if(isUseCategories()) {
1684            openCategories = new ArrayList<>();
1685            for (CategoryComponent category : categories) {
1686                TableSubHeader element = category.getWidget();
1687                if (element != null && element.isOpen()) {
1688                    openCategories.add(category);
1689                }
1690            }
1691        }
1692        return openCategories;
1693    }
1694
1695    @Override
1696    public boolean isCategoryEmpty(CategoryComponent category) {
1697        for(RowComponent<T> row : rows) {
1698            if(row.getCategoryName().equals(category.getName())) {
1699                return false;
1700            }
1701        }
1702        return true;
1703    }
1704
1705    @Override
1706    public SortContext<T> getSortContext() {
1707        return sortContext;
1708    }
1709
1710    @Override
1711    public boolean isRedraw() {
1712        return redraw;
1713    }
1714
1715    @Override
1716    public void setRedraw(boolean redraw) {
1717        setRedrawCategories(redraw);
1718        this.redraw = redraw;
1719    }
1720
1721    @Override
1722    public void updateRow(final T model) {
1723        RowComponent<T> row = getRowByModel(model);
1724        if (row != null) {
1725            row.setRedraw(true);
1726            row.setData(model);
1727            renderComponent(row);
1728        }
1729    }
1730
1731    @Override
1732    public RowComponent<T> getRow(T model) {
1733        for(RowComponent<T> row : rows) {
1734            if(row.getData().equals(model)) {
1735                return row;
1736            }
1737        }
1738        return null;
1739    }
1740
1741    @Override
1742    public RowComponent<T> getRow(int index) {
1743        for(RowComponent<T> row : rows) {
1744            if(row.isRendered() && row.getIndex() == index) {
1745                return row;
1746            }
1747        }
1748        return null;
1749    }
1750
1751    @Override
1752    public RowComponent<T> getRowByModel(T model) {
1753        for (final RowComponent<T> row : rows) {
1754            if (row.getData().equals(model)) {
1755                return row;
1756            }
1757        }
1758        return null;
1759    }
1760
1761    protected int getRowIndexByElement(Element rowElement) {
1762        for(RowComponent<T> row : rows) {
1763            if(row.isRendered() && row.getWidget().getElement().equals(rowElement)) {
1764                return row.getIndex();
1765            }
1766        }
1767        return -1;
1768    }
1769
1770    protected Element getRowElementByModel(T model) {
1771        for(RowComponent<T> row : rows) {
1772            if(row.getData().equals(model)) {
1773                return row.getWidget().getElement();
1774            }
1775        }
1776        return null;
1777    }
1778
1779    protected T getModelByRowElement(Element rowElement) {
1780        for(RowComponent<T> row : rows) {
1781            if(row.isRendered() && row.getWidget().getElement().equals(rowElement)) {
1782                return row.getData();
1783            }
1784        }
1785        return null;
1786    }
1787
1788    protected List<T> getModelsByRowElements(List<Element> rowElements) {
1789        List<T> models = new ArrayList<>();
1790        for(Element element : rowElements) {
1791            models.add(getModelByRowElement(element));
1792        }
1793        return models;
1794    }
1795
1796    protected List<RowComponent<T>> getRowsByCategory(Components<RowComponent<T>> rows, CategoryComponent category) {
1797        List<RowComponent<T>> byCategory = new ArrayList<>();
1798        for(RowComponent<T> row : rows) {
1799            if(row.getCategoryName().equals(category.getName())) {
1800                byCategory.add(row);
1801            }
1802        }
1803        return byCategory;
1804    }
1805
1806    protected List<CategoryComponent> getHiddenCategories() {
1807        List<CategoryComponent> hidden = new ArrayList<>();
1808        for(CategoryComponent category : categories) {
1809            TableSubHeader element = category.getWidget();
1810            if(element != null && !element.isVisible()) {
1811                hidden.add(category);
1812            }
1813        }
1814        return hidden;
1815    }
1816
1817    protected List<CategoryComponent> getVisibleCategories() {
1818        List<CategoryComponent> visible = new ArrayList<>();
1819        for(CategoryComponent category : categories) {
1820            TableSubHeader element = category.getWidget();
1821            if(element != null && element.isVisible()) {
1822                visible.add(category);
1823            }
1824        }
1825        return visible;
1826    }
1827
1828    protected List<CategoryComponent> getPassedCategories() {
1829        List<CategoryComponent> passed = new ArrayList<>();
1830        int scrollTop = tableBody.scrollTop();
1831        for(CategoryComponent category : categories) {
1832            if(isCategoryEmpty(category) && scrollTop > (getRowHeight() + thead.$this().height())) {
1833                passed.add(category);
1834            } else {
1835                // Hit the current category
1836                return passed;
1837            }
1838        }
1839        // No categories are populated.
1840        return new ArrayList<>();
1841    }
1842
1843    @Override
1844    public void setRowFactory(RowComponentFactory<T> rowFactory) {
1845        this.rowFactory = rowFactory;
1846    }
1847
1848    @Override
1849    public RowComponentFactory<T> getRowFactory() {
1850        return rowFactory;
1851    }
1852
1853    @Override
1854    public void setCategoryFactory(ComponentFactory<? extends CategoryComponent, String> categoryFactory) {
1855        this.categoryFactory = categoryFactory;
1856    }
1857
1858    @Override
1859    public void setLoadMask(boolean loadMask) {
1860        if(!isSetup()) {
1861            // The widget isn't ready yet
1862            return;
1863        }
1864        if (!this.loadMask && loadMask) {
1865            if(isUseLoadOverlay()) {
1866                if (maskElement == null) {
1867                    maskElement = $(maskHtml);
1868                }
1869                $table.prepend(maskElement);
1870            }
1871        } else if(!loadMask && maskElement != null) {
1872            maskElement.detach();
1873        }
1874        getProgressWidget().setVisible(loadMask);
1875        this.loadMask = loadMask;
1876    }
1877
1878    @Override
1879    public boolean isLoadMask() {
1880        return loadMask;
1881    }
1882
1883    @Override
1884    public MaterialProgress getProgressWidget() {
1885        return progressWidget;
1886    }
1887
1888    @Override
1889    public int getTotalRows() {
1890        return totalRows;
1891    }
1892
1893    @Override
1894    public void setTotalRows(int totalRows) {
1895        this.totalRows = totalRows;
1896    }
1897
1898    @Override
1899    public boolean isUseCategories() {
1900        return useCategories;
1901    }
1902
1903    @Override
1904    public void setUseCategories(boolean useCategories) {
1905        if(this.useCategories && !useCategories) {
1906            //subheaderLib.unload();
1907            categories.clearWidgets();
1908            setRedrawCategories(true);
1909        }
1910        this.useCategories = useCategories;
1911    }
1912
1913    @Override
1914    public boolean isUseLoadOverlay() {
1915        return useLoadOverlay;
1916    }
1917
1918    @Override
1919    public void setUseLoadOverlay(boolean useLoadOverlay) {
1920        this.useLoadOverlay = useLoadOverlay;
1921    }
1922
1923    @Override
1924    public boolean isUseRowExpansion() {
1925        return useRowExpansion;
1926    }
1927
1928    @Override
1929    public void setUseRowExpansion(boolean useRowExpansion) {
1930        this.useRowExpansion = useRowExpansion;
1931    }
1932
1933    @Override
1934    public int getLongPressDuration() {
1935        return longPressDuration;
1936    }
1937
1938    @Override
1939    public void setLongPressDuration(int longPressDuration) {
1940        this.longPressDuration = longPressDuration;
1941    }
1942
1943    @Override
1944    public void loaded(int startIndex, List<T> data) {
1945        setRowData(startIndex, data);
1946        setLoadMask(false);
1947    }
1948
1949    @Override
1950    public Widget getContainer() {
1951        return display.asWidget();
1952    }
1953
1954    @Override
1955    public String getId() {
1956        return id;
1957    }
1958
1959    @Override
1960    public void setDisplay(DataDisplay<T> display) {
1961        assert display != null : "Display cannot be null";
1962        this.display = display;
1963    }
1964
1965    @Override
1966    public final void fireEvent(GwtEvent<?> event) {
1967        getContainer().fireEvent(event);
1968    }
1969
1970    protected TableSubHeader bindCategoryEvents(TableSubHeader category) {
1971        if(category != null) {
1972            // Attach the category events
1973            category.$this().off("opened");
1974            category.$this().on("opened", (e, categoryElem) -> {
1975                CategoryOpenedEvent.fire(this, category.getName());
1976                return true;
1977            });
1978            category.$this().off("closed");
1979            category.$this().on("closed", (e, categoryElem) -> {
1980                CategoryClosedEvent.fire(this, category.getName());
1981                return true;
1982            });
1983        }
1984        return category;
1985    }
1986
1987    public void updateCheckAllInputState() {
1988        updateCheckAllInputState(null);
1989    }
1990
1991    protected void updateCheckAllInputState(JQueryElement input) {
1992        if(Js.isUndefinedOrNull(input)) {
1993            input = $table.find("th#col0 input");
1994        }
1995        input.prop("indeterminate", false);
1996        input.prop("checked", false);
1997
1998        if($("tr.data-row:visible", getContainer()).length() > 0) {
1999            boolean fullSelection = !hasDeselectedRows(false);
2000
2001            if (!fullSelection && hasSelectedRows(true)) {
2002                input.prop("indeterminate", true);
2003            } else if (fullSelection) {
2004                input.prop("checked", true);
2005            }
2006        }
2007    }
2008
2009    public List<RowComponent<T>> getRows() {
2010        return Collections.unmodifiableList(rows);
2011    }
2012
2013    protected List<T> getData() {
2014        return RowComponent.extractData(rows);
2015    }
2016
2017    /**
2018     * Get a stored data category by name.
2019     */
2020    @Override
2021    public CategoryComponent getCategory(String name) {
2022        if(name != null) {
2023            for (CategoryComponent category : categories) {
2024                if (category.getName().equals(name)) {
2025                    return category;
2026                }
2027            }
2028        } else {
2029            return getOrphansCategory();
2030        }
2031        return null;
2032    }
2033
2034    /**
2035     * Get the {@link OrphanCategoryComponent} for orphan rows.
2036     */
2037    protected OrphanCategoryComponent getOrphansCategory() {
2038        for(CategoryComponent category : categories) {
2039            if(category instanceof OrphanCategoryComponent) {
2040                return (OrphanCategoryComponent)category;
2041            }
2042        }
2043        return null;
2044    }
2045
2046    public int getCategoryHeight() {
2047        if (isUseCategories() && categoryHeight == 0) {
2048            try {
2049                CategoryComponent categoryComponent = categories.get(0);
2050                if (categoryComponent != null && categoryComponent.isRendered()) {
2051                    categoryHeight = categoryComponent.getWidget().getOffsetHeight();
2052                }
2053            } catch (IndexOutOfBoundsException ex) {
2054                logger.log(Level.FINE, "Couldn't get the first category.", ex);
2055            }
2056        }
2057        return categoryHeight;
2058    }
2059
2060    @Override
2061    public void openCategory(String categoryName) {
2062        openCategory(getCategory(categoryName));
2063    }
2064
2065    @Override
2066    public void openCategory(CategoryComponent category) {
2067        if(category != null && category.isRendered()) {
2068            subheaderLib.open(category.getWidget().$this());
2069        }
2070    }
2071
2072    @Override
2073    public void closeCategory(String categoryName) {
2074        closeCategory(getCategory(categoryName));
2075    }
2076
2077    @Override
2078    public void closeCategory(CategoryComponent category) {
2079        if(category != null && category.isRendered()) {
2080            subheaderLib.close(category.getWidget().$this());
2081        }
2082    }
2083
2084    /**
2085     * Get a stored data categories subheader by name.
2086     */
2087    protected TableSubHeader getTableSubHeader(String name) {
2088        CategoryComponent category  = getCategory(name);
2089        return category != null ? category.getWidget() : null;
2090    }
2091
2092    /**
2093     * Get a stored data categories subheader by jquery element.
2094     */
2095    protected TableSubHeader getTableSubHeader(JQueryElement elem) {
2096        for(CategoryComponent category : categories) {
2097            TableSubHeader subheader = category.getWidget();
2098            if(subheader != null && $(subheader).is(elem)) {
2099                return subheader;
2100            }
2101        }
2102        return null;
2103    }
2104
2105    protected void clearExpansions() {
2106        $("tr.expansion", getContainer()).remove();
2107    }
2108
2109    @Override
2110    public void clearRows(boolean clearData) {
2111        if(clearData) {
2112            rows.clear();
2113        } else {
2114            rows.clearWidgets();
2115        }
2116    }
2117
2118    @Override
2119    public void clearCategories() {
2120        for(CategoryComponent category : categories) {
2121            TableSubHeader subheader = category.getWidget();
2122            if(subheader != null && subheader.isAttached()) {
2123                subheader.removeFromParent();
2124            }
2125        }
2126        categories.clear();
2127    }
2128
2129    @Override
2130    public void clearRowsAndCategories(boolean clearData) {
2131        clearRows(clearData);
2132        clearCategories();
2133    }
2134
2135    @Override
2136    public List<TableHeader> getHeaders() {
2137        return Collections.unmodifiableList(headers);
2138    }
2139
2140    protected void addHeader(int index, TableHeader header) {
2141        if(headers.size() < 1) {
2142            headers.add(header);
2143        } else {
2144            headers.add(index, header);
2145        }
2146        headerRow.insert(header, index);
2147    }
2148
2149    protected void removeHeader(int index) {
2150        if(index < headers.size()) {
2151            headers.remove(index);
2152            headerRow.remove(index);
2153        }
2154        refreshStickyHeaders();
2155    }
2156
2157    protected int getCategoryRowCount(String category) {
2158        int count = 0;
2159        for(RowComponent<T> row : rows) {
2160            String rowCategory = row.getCategoryName();
2161            if(rowCategory != null) {
2162                if(rowCategory.equals(category)) {
2163                    count++;
2164                }
2165            } else if(category == null) {
2166                // no category
2167                count++;
2168            }
2169        }
2170        return count;
2171    }
2172
2173    protected void setRedrawCategories(boolean redrawCategories) {
2174        this.redrawCategories = redrawCategories;
2175    }
2176
2177    @Override
2178    public void setHeight(String height) {
2179        this.height = height;
2180
2181        // Avoid setting the height prematurely.
2182        if(setup) {
2183            tableBody.height(height);
2184        }
2185    }
2186
2187    @Override
2188    public String getHeight() {
2189        return height;
2190    }
2191
2192    public boolean isShiftDown() {
2193        return shiftDown;
2194    }
2195}