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}