Web Inspector: Network Table appears broken after filter - rows look collapsed
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / Table.js
1 /*
2  * Copyright (C) 2008-2018 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.Table = class Table extends WI.View
27 {
28     constructor(identifier, dataSource, delegate, rowHeight)
29     {
30         super();
31
32         console.assert(typeof identifier === "string");
33         console.assert(dataSource);
34         console.assert(delegate);
35         console.assert(rowHeight > 0);
36
37         this._identifier = identifier;
38         this._dataSource = dataSource;
39         this._delegate = delegate;
40         this._rowHeight = rowHeight;
41
42         // FIXME: Should be able to horizontally scroll non-locked table contents.
43         // To do this smoothly (without tearing) will require synchronous scroll events, or
44         // synchronized scrolling between multiple elements, or making `position: sticky`
45         // respect different vertical / horizontal scroll containers.
46
47         this.element.classList.add("table", identifier);
48         this.element.tabIndex = 0;
49         this.element.addEventListener("keydown", this._handleKeyDown.bind(this));
50
51         this._headerElement = this.element.appendChild(document.createElement("div"));
52         this._headerElement.className = "header";
53
54         let scrollHandler = this._handleScroll.bind(this);
55         this._scrollContainerElement = this.element.appendChild(document.createElement("div"));
56         this._scrollContainerElement.className = "data-container";
57         this._scrollContainerElement.addEventListener("scroll", scrollHandler);
58         this._scrollContainerElement.addEventListener("mousewheel", scrollHandler);
59         this._scrollContainerElement.addEventListener("mousedown", this._handleMouseDown.bind(this));
60         if (this._delegate.tableCellContextMenuClicked)
61             this._scrollContainerElement.addEventListener("contextmenu", this._handleContextMenu.bind(this));
62
63         this._topSpacerElement = this._scrollContainerElement.appendChild(document.createElement("div"));
64         this._topSpacerElement.className = "spacer";
65
66         this._listElement = this._scrollContainerElement.appendChild(document.createElement("ul"));
67         this._listElement.className = "data-list";
68
69         this._bottomSpacerElement = this._scrollContainerElement.appendChild(document.createElement("div"));
70         this._bottomSpacerElement.className = "spacer";
71
72         this._fillerRow = this._listElement.appendChild(document.createElement("li"));
73         this._fillerRow.className = "filler";
74
75         this._resizersElement = this._element.appendChild(document.createElement("div"));
76         this._resizersElement.className = "resizers";
77
78         this._cachedRows = new Map;
79         this._cachedNumberOfRows = NaN;
80
81         this._columnSpecs = new Map;
82         this._columnOrder = [];
83         this._visibleColumns = [];
84         this._hiddenColumns = [];
85
86         this._widthGeneration = 1;
87         this._columnWidths = null; // Calculated in _resizeColumnsAndFiller.
88         this._fillerHeight = 0; // Calculated in _resizeColumnsAndFiller.
89
90         this._selectionController = new WI.SelectionController(this);
91
92         this._resizers = [];
93         this._currentResizer = null;
94         this._resizeLeftColumns = null;
95         this._resizeRightColumns = null;
96         this._resizeOriginalColumnWidths = null;
97         this._lastColumnIndexToAcceptRemainderPixel = 0;
98
99         this._sortOrderSetting = new WI.Setting(this._identifier + "-sort-order", WI.Table.SortOrder.Indeterminate);
100         this._sortColumnIdentifierSetting = new WI.Setting(this._identifier + "-sort", null);
101         this._columnVisibilitySetting = new WI.Setting(this._identifier + "-column-visibility", {});
102
103         this._sortOrder = this._sortOrderSetting.value;
104         this._sortColumnIdentifier = this._sortColumnIdentifierSetting.value;
105
106         this._cachedWidth = NaN;
107         this._cachedHeight = NaN;
108         this._cachedScrollTop = NaN;
109         this._previousCachedWidth = NaN;
110         this._previousRevealedRowCount = NaN;
111         this._topSpacerHeight = NaN;
112         this._bottomSpacerHeight = NaN;
113         this._visibleRowIndexStart = NaN;
114         this._visibleRowIndexEnd = NaN;
115
116         console.assert(this._delegate.tablePopulateCell, "Table delegate must implement tablePopulateCell.");
117     }
118
119     // Public
120
121     get identifier() { return this._identifier; }
122     get dataSource() { return this._dataSource; }
123     get delegate() { return this._delegate; }
124     get rowHeight() { return this._rowHeight; }
125
126     get selectedRow()
127     {
128         return this._selectionController.lastSelectedItem;
129     }
130
131     get selectedRows()
132     {
133         return Array.from(this._selectionController.selectedItems);
134     }
135
136     get scrollContainer() { return this._scrollContainerElement; }
137
138     get numberOfRows()
139     {
140         if (isNaN(this._cachedNumberOfRows))
141             this._cachedNumberOfRows = this._dataSource.tableNumberOfRows(this);
142
143         return this._cachedNumberOfRows;
144     }
145
146     get sortOrder()
147     {
148         return this._sortOrder;
149     }
150
151     set sortOrder(sortOrder)
152     {
153         if (sortOrder === this._sortOrder && this.didInitialLayout)
154             return;
155
156         console.assert(sortOrder === WI.Table.SortOrder.Indeterminate || sortOrder === WI.Table.SortOrder.Ascending || sortOrder === WI.Table.SortOrder.Descending);
157
158         this._sortOrder = sortOrder;
159         this._sortOrderSetting.value = sortOrder;
160
161         if (this._sortColumnIdentifier) {
162             let column = this._columnSpecs.get(this._sortColumnIdentifier);
163             let columnIndex = this._visibleColumns.indexOf(column);
164             if (columnIndex !== -1) {
165                 let headerCell = this._headerElement.children[columnIndex];
166                 headerCell.classList.toggle("sort-ascending", this._sortOrder === WI.Table.SortOrder.Ascending);
167                 headerCell.classList.toggle("sort-descending", this._sortOrder === WI.Table.SortOrder.Descending);
168             }
169
170             if (this._dataSource.tableSortChanged)
171                 this._dataSource.tableSortChanged(this);
172         }
173     }
174
175     get sortColumnIdentifier()
176     {
177         return this._sortColumnIdentifier;
178     }
179
180     set sortColumnIdentifier(columnIdentifier)
181     {
182         if (columnIdentifier === this._sortColumnIdentifier && this.didInitialLayout)
183             return;
184
185         let column = this._columnSpecs.get(columnIdentifier);
186
187         console.assert(column, "Column not found.", columnIdentifier);
188         if (!column)
189             return;
190
191         console.assert(column.sortable, "Column is not sortable.", columnIdentifier);
192         if (!column.sortable)
193             return;
194
195         let oldSortColumnIdentifier = this._sortColumnIdentifier;
196         this._sortColumnIdentifier = columnIdentifier;
197         this._sortColumnIdentifierSetting.value = columnIdentifier;
198
199         if (oldSortColumnIdentifier) {
200             let oldColumn = this._columnSpecs.get(oldSortColumnIdentifier);
201             let oldColumnIndex = this._visibleColumns.indexOf(oldColumn);
202             if (oldColumnIndex !== -1) {
203                 let headerCell = this._headerElement.children[oldColumnIndex];
204                 headerCell.classList.remove("sort-ascending", "sort-descending");
205             }
206         }
207
208         if (this._sortColumnIdentifier) {
209             let newColumnIndex = this._visibleColumns.indexOf(column);
210             if (newColumnIndex !== -1) {
211                 let headerCell = this._headerElement.children[newColumnIndex];
212                 headerCell.classList.toggle("sort-ascending", this._sortOrder === WI.Table.SortOrder.Ascending);
213                 headerCell.classList.toggle("sort-descending", this._sortOrder === WI.Table.SortOrder.Descending);
214             } else
215                 this._sortColumnIdentifier = null;
216         }
217
218         if (this._dataSource.tableSortChanged)
219             this._dataSource.tableSortChanged(this);
220     }
221
222     get allowsMultipleSelection()
223     {
224         return this._selectionController.allowsMultipleSelection;
225     }
226
227     set allowsMultipleSelection(flag)
228     {
229         this._selectionController.allowsMultipleSelection = flag;
230     }
231
232     get columns()
233     {
234         return Array.from(this._columnSpecs.values());
235     }
236
237     isRowSelected(rowIndex)
238     {
239         return this._selectionController.hasSelectedItem(rowIndex);
240     }
241
242     reloadData()
243     {
244         this._cachedRows.clear();
245
246         this._selectionController.reset();
247
248         this._cachedNumberOfRows = NaN;
249         this._previousRevealedRowCount = NaN;
250         this.needsLayout();
251     }
252
253     reloadDataAddedToEndOnly()
254     {
255         this._previousRevealedRowCount = NaN;
256         this.needsLayout();
257     }
258
259     reloadRow(rowIndex)
260     {
261         // Visible row, repopulate the cell.
262         if (this._isRowVisible(rowIndex)) {
263             let row = this._cachedRows.get(rowIndex);
264             if (!row)
265                 return;
266             this._populateRow(row);
267             return;
268         }
269
270         // Non-visible row, will populate when it becomes visible.
271         this._cachedRows.delete(rowIndex);
272     }
273
274     reloadVisibleColumnCells(column)
275     {
276         let columnIndex = this._visibleColumns.indexOf(column);
277         if (columnIndex === -1)
278             return;
279
280         for (let rowIndex = this._visibleRowIndexStart; rowIndex < this._visibleRowIndexEnd; ++rowIndex) {
281             let row = this._cachedRows.get(rowIndex);
282             if (!row)
283                 continue;
284             let cell = row.children[columnIndex];
285             if (!cell)
286                 continue;
287             this._delegate.tablePopulateCell(this, cell, column, rowIndex);
288         }
289     }
290
291     reloadCell(rowIndex, columnIdentifier)
292     {
293         let column = this._columnSpecs.get(columnIdentifier);
294         let columnIndex = this._visibleColumns.indexOf(column);
295         if (columnIndex === -1)
296             return;
297
298         // Visible row, repopulate the cell.
299         if (this._isRowVisible(rowIndex)) {
300             let row = this._cachedRows.get(rowIndex);
301             if (!row)
302                 return;
303             let cell = row.children[columnIndex];
304             if (!cell)
305                 return;
306             this._delegate.tablePopulateCell(this, cell, column, rowIndex);
307             return;
308         }
309
310         // Non-visible row, will populate when it becomes visible.
311         this._cachedRows.delete(rowIndex);
312     }
313
314     selectRow(rowIndex, extendSelection = false)
315     {
316         this._selectionController.selectItem(rowIndex, extendSelection);
317     }
318
319     deselectRow(rowIndex)
320     {
321         this._selectionController.deselectItem(rowIndex);
322     }
323
324     selectAll()
325     {
326         this._selectionController.selectAll();
327     }
328
329     deselectAll()
330     {
331         this._selectionController.deselectAll();
332     }
333
334     removeRow(rowIndex)
335     {
336         console.assert(rowIndex >= 0 && rowIndex < this.numberOfRows);
337
338         if (this.isRowSelected(rowIndex))
339             this.deselectRow(rowIndex);
340
341         let rowIndexes = new WI.IndexSet([rowIndex]);
342         this._removeRows(rowIndexes);
343     }
344
345     removeSelectedRows()
346     {
347         // Change the selection before removing rows. This matches the behavior
348         // of macOS Finder (in list and column modes) when removing selected items.
349         let oldSelectedItems = this._selectionController.selectedItems.copy();
350
351         this._selectionController.removeSelectedItems();
352
353         if (!oldSelectedItems.equals(this._selectionController.selectedItems))
354             this._removeRows(oldSelectedItems);
355     }
356
357     revealRow(rowIndex)
358     {
359         console.assert(rowIndex >= 0 && rowIndex < this.numberOfRows);
360         if (rowIndex < 0 || rowIndex >= this.numberOfRows)
361             return;
362
363         // Force our own scroll update because we may have scrolled.
364         this._cachedScrollTop = NaN;
365
366         if (this._isRowVisible(rowIndex)) {
367             let row = this._cachedRows.get(rowIndex);
368             console.assert(row, "Visible rows should always be in the cache.");
369             if (row)
370                 row.scrollIntoViewIfNeeded(false);
371             this.needsLayout();
372         } else {
373             this._scrollContainerElement.scrollTop = rowIndex * this._rowHeight;
374             this.updateLayout();
375         }
376     }
377
378     columnWithIdentifier(identifier)
379     {
380         return this._columnSpecs.get(identifier);
381     }
382
383     cellForRowAndColumn(rowIndex, column)
384     {
385         if (!this._isRowVisible(rowIndex))
386             return null;
387
388         let row = this._cachedRows.get(rowIndex);
389         if (!row)
390             return null;
391
392         let columnIndex = this._visibleColumns.indexOf(column);
393         if (columnIndex === -1)
394             return null;
395
396         return row.children[columnIndex];
397     }
398
399     addColumn(column)
400     {
401         this._columnSpecs.set(column.identifier, column);
402         this._columnOrder.push(column.identifier);
403
404         if (column.hidden) {
405             this._hiddenColumns.push(column);
406             column.width = NaN;
407         } else {
408             this._visibleColumns.push(column);
409             this._headerElement.appendChild(this._createHeaderCell(column));
410             this._fillerRow.appendChild(this._createFillerCell(column));
411             if (column.headerView)
412                 this.addSubview(column.headerView);
413         }
414
415         // Restore saved user-specified column visibility.
416         let savedColumnVisibility = this._columnVisibilitySetting.value;
417         if (column.identifier in savedColumnVisibility) {
418             let visible = savedColumnVisibility[column.identifier];
419             if (visible)
420                 this.showColumn(column);
421             else
422                 this.hideColumn(column);
423         }
424
425         this.reloadData();
426     }
427
428     showColumn(column)
429     {
430         console.assert(this._columnSpecs.get(column.identifier) === column, "Column not in this table.");
431         console.assert(!column.locked, "Locked columns should always be shown.");
432         if (column.locked)
433             return;
434
435         if (!column.hidden)
436             return;
437
438         column.hidden = false;
439
440         let columnIndex = this._hiddenColumns.indexOf(column);
441         this._hiddenColumns.splice(columnIndex, 1);
442
443         let newColumnIndex = this._indexToInsertColumn(column);
444         this._visibleColumns.insertAtIndex(column, newColumnIndex);
445
446         // Save user preference for this column to be visible.
447         let savedColumnVisibility = this._columnVisibilitySetting.value;
448         if (savedColumnVisibility[column.identifier] !== true) {
449             let copy = Object.shallowCopy(savedColumnVisibility);
450             if (column.defaultHidden)
451                 copy[column.identifier] = true;
452             else
453                 delete copy[column.identifier];
454             this._columnVisibilitySetting.value = copy;
455         }
456
457         this._headerElement.insertBefore(this._createHeaderCell(column), this._headerElement.children[newColumnIndex]);
458         this._fillerRow.insertBefore(this._createFillerCell(column), this._fillerRow.children[newColumnIndex]);
459
460         if (column.headerView)
461             this.addSubview(column.headerView);
462
463         if (this._sortColumnIdentifier === column.identifier) {
464             let headerCell = this._headerElement.children[newColumnIndex];
465             headerCell.classList.toggle("sort-ascending", this._sortOrder === WI.Table.SortOrder.Ascending);
466             headerCell.classList.toggle("sort-descending", this._sortOrder === WI.Table.SortOrder.Descending);
467         }
468
469         // We haven't yet done any layout, nothing to do.
470         if (!this._columnWidths)
471             return;
472
473         // To avoid recreating all the cells in the row we create empty cells,
474         // size them, and then populate them. We always populate a cell after
475         // it has been sized.
476         let cellsToPopulate = [];
477         for (let row of this._listElement.children) {
478             if (row !== this._fillerRow) {
479                 let unpopulatedCell = this._createCell(column, newColumnIndex);
480                 cellsToPopulate.push(unpopulatedCell);
481                 row.insertBefore(unpopulatedCell, row.children[newColumnIndex]);
482             }
483         }
484
485         // Re-layout all columns to make space.
486         this._columnWidths = null;
487         this._resizeColumnsAndFiller();
488
489         // Now populate only the new cells for this column.
490         for (let cell of cellsToPopulate)
491             this._delegate.tablePopulateCell(this, cell, column, cell.parentElement.__index);
492     }
493
494     hideColumn(column)
495     {
496         console.assert(this._columnSpecs.get(column.identifier) === column, "Column not in this table.");
497         console.assert(!column.locked, "Locked columns should always be shown.");
498         if (column.locked)
499             return;
500
501         console.assert(column.hideable, "Column is not hideable so should always be shown.");
502         if (!column.hideable)
503             return;
504
505         if (column.hidden)
506             return;
507
508         column.hidden = true;
509
510         this._hiddenColumns.push(column);
511
512         let columnIndex = this._visibleColumns.indexOf(column);
513         this._visibleColumns.splice(columnIndex, 1);
514
515         // Save user preference for this column to be hidden.
516         let savedColumnVisibility = this._columnVisibilitySetting.value;
517         if (savedColumnVisibility[column.identifier] !== false) {
518             let copy = Object.shallowCopy(savedColumnVisibility);
519             if (column.defaultHidden)
520                 delete copy[column.identifier];
521             else
522                 copy[column.identifier] = false;
523             this._columnVisibilitySetting.value = copy;
524         }
525
526         this._headerElement.removeChild(this._headerElement.children[columnIndex]);
527         this._fillerRow.removeChild(this._fillerRow.children[columnIndex]);
528
529         if (column.headerView)
530             this.removeSubview(column.headerView);
531
532         // We haven't yet done any layout, nothing to do.
533         if (!this._columnWidths)
534             return;
535
536         this._columnWidths.splice(columnIndex, 1);
537
538         for (let row of this._listElement.children) {
539             if (row !== this._fillerRow)
540                 row.removeChild(row.children[columnIndex]);
541         }
542
543         this.needsLayout();
544     }
545
546     restoreScrollPosition()
547     {
548         if (this._cachedScrollTop && !this._scrollContainerElement.scrollTop)
549             this._scrollContainerElement.scrollTop = this._cachedScrollTop;
550     }
551
552     // Protected
553
554     initialLayout()
555     {
556         this.sortOrder = this._sortOrderSetting.value;
557
558         let restoreSortColumnIdentifier = this._sortColumnIdentifierSetting.value;
559         if (!this._columnSpecs.has(restoreSortColumnIdentifier))
560             this._sortColumnIdentifierSetting.value = null;
561         else
562             this.sortColumnIdentifier = restoreSortColumnIdentifier;
563     }
564
565     layout()
566     {
567         this._updateVisibleRows();
568         this._resizeColumnsAndFiller();
569     }
570
571     sizeDidChange()
572     {
573         super.sizeDidChange();
574
575         this._previousCachedWidth = this._cachedWidth;
576         this._cachedWidth = NaN;
577         this._cachedHeight = NaN;
578     }
579
580     // SelectionController delegate
581
582     selectionControllerSelectionDidChange(controller, deselectedItems, selectedItems)
583     {
584         if (deselectedItems.size)
585             this._toggleSelectedRowStyle(deselectedItems, false);
586         if (selectedItems.size)
587             this._toggleSelectedRowStyle(selectedItems, true);
588
589         if (selectedItems.size === 1) {
590             let rowIndex = selectedItems.firstIndex;
591             if (!this._isRowVisible(rowIndex))
592                 this.revealRow(rowIndex);
593         }
594
595         if (this._delegate.tableSelectionDidChange)
596             this._delegate.tableSelectionDidChange(this);
597     }
598
599     selectionControllerNumberOfItems(controller)
600     {
601         return this.numberOfRows;
602     }
603
604     selectionControllerNextSelectableIndex(controller, index)
605     {
606         if (index >= this.numberOfRows - 1)
607             return NaN;
608         return index + 1;
609     }
610
611     selectionControllerPreviousSelectableIndex(controller, index)
612     {
613         if (index <= 0)
614             return NaN;
615         return index - 1;
616     }
617
618     // Resizer delegate
619
620     resizerDragStarted(resizer)
621     {
622         console.assert(!this._currentResizer, resizer, this._currentResizer);
623
624         let resizerIndex = this._resizers.indexOf(resizer);
625
626         this._currentResizer = resizer;
627         this._resizeLeftColumns = this._visibleColumns.slice(0, resizerIndex + 1).reverse(); // Reversed to simplify iteration.
628         this._resizeRightColumns = this._visibleColumns.slice(resizerIndex + 1);
629         this._resizeOriginalColumnWidths = [].concat(this._columnWidths);
630     }
631
632     resizerDragging(resizer, positionDelta)
633     {
634         console.assert(resizer === this._currentResizer, resizer, this._currentResizer);
635         if (resizer !== this._currentResizer)
636             return;
637
638         if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
639             positionDelta = -positionDelta;
640
641         // Completely recalculate columns from the original sizes based on the new mouse position.
642         this._columnWidths = [].concat(this._resizeOriginalColumnWidths);
643
644         if (!positionDelta) {
645             this._applyColumnWidths();
646             return;
647         }
648
649         let delta = Math.abs(positionDelta);
650         let leftDirection = positionDelta > 0;
651         let rightDirection = !leftDirection;
652
653         let columnWidths = this._columnWidths;
654         let visibleColumns = this._visibleColumns;
655
656         function growableSize(column) {
657             let width = columnWidths[visibleColumns.indexOf(column)];
658             if (column.maxWidth)
659                 return column.maxWidth - width;
660             return Infinity;
661         }
662
663         function shrinkableSize(column) {
664             let width = columnWidths[visibleColumns.indexOf(column)];
665             if (column.minWidth)
666                 return width - column.minWidth;
667             return width;
668         }
669
670         function canGrow(column) {
671             return growableSize(column) > 0;
672         }
673
674         function canShrink(column) {
675             return shrinkableSize(column) > 0;
676         }
677
678         function columnToResize(columns, isShrinking) {
679             // First find a flexible column we can resize.
680             for (let column of columns) {
681                 if (!column.flexible)
682                     continue;
683                 if (isShrinking ? canShrink(column) : canGrow(column))
684                     return column;
685             }
686
687             // Failing that see if we can resize the immediately neighbor.
688             let immediateColumn = columns[0];
689             if ((isShrinking && canShrink(immediateColumn)) || (!isShrinking && canGrow(immediateColumn)))
690                 return immediateColumn;
691
692             // Bail. There isn't anything obvious in the table that can resize.
693             return null;
694         }
695
696         while (delta > 0) {
697             let leftColumn = columnToResize(this._resizeLeftColumns, leftDirection);
698             let rightColumn = columnToResize(this._resizeRightColumns, rightDirection);
699             if (!leftColumn || !rightColumn) {
700                 // No more left or right column to grow or shrink.
701                 break;
702             }
703
704             let incrementalDelta = Math.min(delta,
705                 leftDirection ? shrinkableSize(leftColumn) : shrinkableSize(rightColumn),
706                 leftDirection ? growableSize(rightColumn) : growableSize(leftColumn));
707
708             let leftIndex = this._visibleColumns.indexOf(leftColumn);
709             let rightIndex = this._visibleColumns.indexOf(rightColumn);
710
711             if (leftDirection) {
712                 this._columnWidths[leftIndex] -= incrementalDelta;
713                 this._columnWidths[rightIndex] += incrementalDelta;
714             } else {
715                 this._columnWidths[leftIndex] += incrementalDelta;
716                 this._columnWidths[rightIndex] -= incrementalDelta;
717             }
718
719             delta -= incrementalDelta;
720         }
721
722         // We have new column widths.
723         this._widthGeneration++;
724
725         this._applyColumnWidths();
726         this._positionHeaderViews();
727     }
728
729     resizerDragEnded(resizer)
730     {
731         console.assert(resizer === this._currentResizer, resizer, this._currentResizer);
732         if (resizer !== this._currentResizer)
733             return;
734
735         this._currentResizer = null;
736         this._resizeLeftColumns = null;
737         this._resizeRightColumns = null;
738         this._resizeOriginalColumnWidths = null;
739
740         this._positionResizerElements();
741         this._positionHeaderViews();
742     }
743
744     // Private
745
746     _createHeaderCell(column)
747     {
748         let cell = document.createElement("span");
749         cell.classList.add("cell", column.identifier);
750         cell.textContent = column.name;
751
752         if (column.align)
753             cell.classList.add("align-" + column.align);
754         if (column.sortable) {
755             cell.classList.add("sortable");
756             cell.addEventListener("click", this._handleHeaderCellClicked.bind(this, column));
757         }
758
759         cell.addEventListener("contextmenu", this._handleHeaderContextMenu.bind(this, column));
760
761         return cell;
762     }
763
764     _createFillerCell(column)
765     {
766         let cell = document.createElement("span");
767         cell.classList.add("cell", column.identifier);
768         return cell;
769     }
770
771     _createCell(column, columnIndex)
772     {
773         let cell = document.createElement("span");
774         cell.classList.add("cell", column.identifier);
775         if (column.align)
776             cell.classList.add("align-" + column.align);
777         if (this._columnWidths)
778             cell.style.width = this._columnWidths[columnIndex] + "px";
779         return cell;
780     }
781
782     _getOrCreateRow(rowIndex)
783     {
784         let cachedRow = this._cachedRows.get(rowIndex);
785         if (cachedRow)
786             return cachedRow;
787
788         let row = document.createElement("li");
789         row.__index = rowIndex;
790         row.__widthGeneration = 0;
791         if (this.isRowSelected(rowIndex))
792             row.classList.add("selected");
793
794         this._cachedRows.set(rowIndex, row);
795         return row;
796     }
797
798     _populatedCellForColumnAndRow(column, columnIndex, rowIndex)
799     {
800         console.assert(rowIndex !== undefined, "Tried to populate a row that did not know its index. Is this the filler row?");
801
802         let cell = this._createCell(column, columnIndex);
803         this._delegate.tablePopulateCell(this, cell, column, rowIndex);
804         return cell;
805     }
806
807     _populateRow(row)
808     {
809         row.removeChildren();
810
811         let rowIndex = row.__index;
812         for (let i = 0; i < this._visibleColumns.length; ++i) {
813             let column = this._visibleColumns[i];
814             let cell = this._populatedCellForColumnAndRow(column, i, rowIndex);
815             row.appendChild(cell);
816         }
817     }
818
819     _resizeColumnsAndFiller()
820     {
821         if (isNaN(this._cachedWidth) || !this._cachedWidth)
822             this._cachedWidth = Math.floor(this._scrollContainerElement.getBoundingClientRect().width);
823
824         // Not visible yet.
825         if (!this._cachedWidth)
826             return;
827
828         let availableWidth = this._cachedWidth;
829         let availableHeight = this._cachedHeight;
830
831         let contentHeight = this.numberOfRows * this._rowHeight;
832         this._fillerHeight = Math.max(availableHeight - contentHeight, 0);
833
834         // No change to layout metrics so no resizing is needed.
835         if (this._columnWidths && this._cachedWidth === this._previousCachedWidth) {
836             this._updateFillerRowWithNewHeight();
837             this._applyColumnWidthsToColumnsIfNeeded();
838             return;
839         }
840
841         this._previousCachedWidth = this._cachedWidth;
842
843         let lockedWidth = 0;
844         let lockedColumnCount = 0;
845         let totalMinimumWidth = 0;
846
847         for (let column of this._visibleColumns) {
848             if (column.locked) {
849                 lockedWidth += column.width;
850                 lockedColumnCount++;
851                 totalMinimumWidth += column.width;
852             } else if (column.minWidth)
853                 totalMinimumWidth += column.minWidth;
854         }
855
856         let flexibleWidth = availableWidth - lockedWidth;
857         let flexibleColumnCount = this._visibleColumns.length - lockedColumnCount;
858
859         // NOTE: We will often distribute pixels evenly across flexible columns in the table.
860         // If `availableWidth < totalMinimumWidth` than the table is too small for the minimum
861         // sizes of all the columns and we will start crunching the table (removing pixels from
862         // all flexible columns). This would be the appropriate time to introduce horizontal
863         // scrolling. For now we just remove pixels evenly.
864         //
865         // When distributing pixels, always start from the last column to accept remainder
866         // pixels so we don't always add from one side / to one column.
867         function distributeRemainingPixels(remainder, shrinking) {
868             // No pixels to distribute.
869             if (!remainder)
870                 return;
871
872             let indexToStartAddingRemainderPixels = (this._lastColumnIndexToAcceptRemainderPixel + 1) % this._visibleColumns.length;
873
874             // Handle tables that are too small or too large. If the size constraints
875             // cause the columns to be too small or large. A second pass will do the
876             // expanding or crunching ignoring constraints.
877             let ignoreConstraints = false;
878
879             while (remainder > 0) {
880                 let initialRemainder = remainder;
881
882                 for (let i = indexToStartAddingRemainderPixels; i < this._columnWidths.length; ++i) {
883                     let column = this._visibleColumns[i];
884                     if (column.locked)
885                         continue;
886
887                     if (shrinking) {
888                         if (ignoreConstraints || (column.minWidth && this._columnWidths[i] > column.minWidth)) {
889                             this._columnWidths[i]--;
890                             remainder--;
891                         }
892                     } else {
893                         if (ignoreConstraints || (column.maxWidth && this._columnWidths[i] < column.maxWidth)) {
894                             this._columnWidths[i]++;
895                             remainder--;
896                         } else if (!column.maxWidth) {
897                             this._columnWidths[i]++;
898                             remainder--;
899                         }
900                     }
901
902                     if (!remainder) {
903                         this._lastColumnIndexToAcceptRemainderPixel = i;
904                         break;
905                     }
906                 }
907
908                 if (remainder === initialRemainder && !indexToStartAddingRemainderPixels) {
909                     // We have remaining pixels. Start crunching if we need to.
910                     if (ignoreConstraints)
911                         break;
912                     ignoreConstraints = true;
913                 }
914
915                 indexToStartAddingRemainderPixels = 0;
916             }
917
918             console.assert(!remainder, "Should not have undistributed pixels.");
919         }
920
921         // Two kinds of layouts. Autosize or Resize.
922         if (!this._columnWidths) {
923             // Autosize: Flex all the flexes evenly and trickle out any remaining pixels.
924             this._columnWidths = [];
925             this._lastColumnIndexToAcceptRemainderPixel = 0;
926
927             let bestFitWidth = 0;
928             let bestFitColumnCount = 0;
929
930             function bestFit(callback) {
931                 while (true) {
932                     let remainingFlexibleColumnCount = flexibleColumnCount - bestFitColumnCount;
933                     if (!remainingFlexibleColumnCount)
934                         return;
935
936                     // Fair size to give each flexible column.
937                     let remainingFlexibleWidth = flexibleWidth - bestFitWidth;
938                     let flexWidth = Math.floor(remainingFlexibleWidth / remainingFlexibleColumnCount);
939
940                     let didPerformBestFit = false;
941                     for (let i = 0; i < this._visibleColumns.length; ++i) {
942                         // Already best fit this column.
943                         if (this._columnWidths[i])
944                             continue;
945
946                         let column = this._visibleColumns[i];
947                         console.assert(column.flexible, "Non-flexible columns should have been sized earlier", column);
948
949                         // Attempt best fit.
950                         let bestWidth = callback(column, flexWidth);
951                         if (bestWidth === -1)
952                             continue;
953
954                         this._columnWidths[i] = bestWidth;
955                         bestFitWidth += bestWidth;
956                         bestFitColumnCount++;
957                         didPerformBestFit = true;
958                     }
959                     if (!didPerformBestFit)
960                         return;
961
962                     // Repeat with a new flex size now that we have fewer flexible columns.
963                 }
964             }
965
966             // Fit the locked columns.
967             for (let i = 0; i < this._visibleColumns.length; ++i) {
968                 let column = this._visibleColumns[i];
969                 if (column.locked)
970                     this._columnWidths[i] = column.width;
971             }
972
973             // Best fit with the preferred initial width for flexible columns.
974             bestFit.call(this, (column, width) => {
975                 if (!column.preferredInitialWidth || width <= column.preferredInitialWidth)
976                     return -1;
977                 return column.preferredInitialWidth;
978             });
979
980             // Best fit max size flexible columns. May make more pixels available for other columns.
981             bestFit.call(this, (column, width) => {
982                 if (!column.maxWidth || width <= column.maxWidth)
983                     return -1;
984                 return column.maxWidth;
985             });
986
987             // Best fit min size flexible columns. May make less pixels available for other columns.
988             bestFit.call(this, (column, width) => {
989                 if (!column.minWidth || width >= column.minWidth)
990                     return -1;
991                 return column.minWidth;
992             });
993
994             // Best fit the remaining flexible columns with the fair remaining size.
995             bestFit.call(this, (column, width) => width);
996
997             // Distribute any remaining pixels evenly.
998             let remainder = availableWidth - (lockedWidth + bestFitWidth);
999             let shrinking = remainder < 0;
1000             distributeRemainingPixels.call(this, Math.abs(remainder), shrinking);
1001         } else {
1002             // Resize: Distribute pixels evenly across flex columns.
1003             console.assert(this._columnWidths.length === this._visibleColumns.length, "Number of columns should not change in a resize.");
1004
1005             let originalTotalColumnWidth = 0;
1006             for (let width of this._columnWidths)
1007                 originalTotalColumnWidth += width;
1008
1009             let remainder = Math.abs(availableWidth - originalTotalColumnWidth);
1010             let shrinking = availableWidth < originalTotalColumnWidth;
1011             distributeRemainingPixels.call(this, remainder, shrinking);
1012         }
1013
1014         // We have new column widths.
1015         this._widthGeneration++;
1016
1017         // Apply widths.
1018
1019         this._updateFillerRowWithNewHeight();
1020         this._applyColumnWidths();
1021         this._positionResizerElements();
1022         this._positionHeaderViews();
1023     }
1024
1025     _updateVisibleRows()
1026     {
1027         let rowHeight = this._rowHeight;
1028         let updateOffsetThreshold = rowHeight * 10;
1029         let overflowPadding = updateOffsetThreshold * 3;
1030
1031         if (isNaN(this._cachedScrollTop))
1032             this._cachedScrollTop = this._scrollContainerElement.scrollTop;
1033
1034         if (isNaN(this._cachedHeight) || !this._cachedHeight)
1035             this._cachedHeight = Math.floor(this._scrollContainerElement.getBoundingClientRect().height);
1036
1037         let scrollTop = this._cachedScrollTop;
1038         let scrollableOffsetHeight = this._cachedHeight;
1039
1040         let visibleRowCount = Math.ceil((scrollableOffsetHeight + (overflowPadding * 2)) / rowHeight);
1041         let currentTopMargin = this._topSpacerHeight;
1042         let currentBottomMargin = this._bottomSpacerHeight;
1043         let currentTableBottom = currentTopMargin + (visibleRowCount * rowHeight);
1044
1045         let belowTopThreshold = !currentTopMargin || scrollTop > currentTopMargin + updateOffsetThreshold;
1046         let aboveBottomThreshold = !currentBottomMargin || scrollTop + scrollableOffsetHeight < currentTableBottom - updateOffsetThreshold;
1047
1048         if (belowTopThreshold && aboveBottomThreshold && !isNaN(this._previousRevealedRowCount))
1049             return;
1050
1051         let numberOfRows = this.numberOfRows;
1052         this._previousRevealedRowCount = numberOfRows;
1053
1054         // Scroll back up if the number of rows was reduced such that the existing
1055         // scroll top value is larger than it could otherwise have been. We only
1056         // need to do this adjustment if there are more rows than would fit on screen,
1057         // because when the filler row activates it will reset our scroll.
1058         if (scrollTop) {
1059             let rowsThatCanFitOnScreen = Math.ceil(scrollableOffsetHeight / rowHeight);
1060             if (numberOfRows >= rowsThatCanFitOnScreen) {
1061                 let maximumScrollTop = Math.max(0, (numberOfRows * rowHeight) - scrollableOffsetHeight);
1062                 if (scrollTop > maximumScrollTop) {
1063                     this._scrollContainerElement.scrollTop = maximumScrollTop;
1064                     this._cachedScrollTop = maximumScrollTop;
1065                 }
1066             }
1067         }
1068
1069         let topHiddenRowCount = Math.max(0, Math.floor((scrollTop - overflowPadding) / rowHeight));
1070         let bottomHiddenRowCount = Math.max(0, this._previousRevealedRowCount - topHiddenRowCount - visibleRowCount);
1071
1072         let marginTop = topHiddenRowCount * rowHeight;
1073         let marginBottom = bottomHiddenRowCount * rowHeight;
1074
1075         if (this._topSpacerHeight !== marginTop) {
1076             this._topSpacerHeight = marginTop;
1077             this._topSpacerElement.style.height = marginTop + "px";
1078         }
1079
1080         if (this._bottomDataTableMarginElement !== marginBottom) {
1081             this._bottomSpacerHeight = marginBottom;
1082             this._bottomSpacerElement.style.height = marginBottom + "px";
1083         }
1084
1085         this._visibleRowIndexStart = topHiddenRowCount;
1086         this._visibleRowIndexEnd = this._visibleRowIndexStart + visibleRowCount;
1087
1088         // Completely remove all rows and add new ones.
1089         this._listElement.removeChildren();
1090         this._listElement.classList.toggle("odd-first-zebra-stripe", !!(topHiddenRowCount % 2));
1091
1092         for (let i = this._visibleRowIndexStart; i < this._visibleRowIndexEnd && i < numberOfRows; ++i) {
1093             let row = this._getOrCreateRow(i);
1094             this._listElement.appendChild(row);
1095         }
1096
1097         this._listElement.appendChild(this._fillerRow);
1098     }
1099
1100     _updateFillerRowWithNewHeight()
1101     {
1102         if (!this._fillerHeight) {
1103             this._scrollContainerElement.classList.remove("not-scrollable");
1104             this._fillerRow.remove();
1105             return;
1106         }
1107
1108         this._scrollContainerElement.classList.add("not-scrollable");
1109
1110         // In the event that we just made the table not scrollable then the number
1111         // of rows can fit on screen. Reset the scroll top.
1112         if (this._cachedScrollTop) {
1113             this._scrollContainerElement.scrollTop = 0;
1114             this._cachedScrollTop = 0;
1115         }
1116
1117         // Extend past edge some reasonable amount. At least 200px.
1118         const paddingPastTheEdge = 200;
1119         this._fillerHeight += paddingPastTheEdge;
1120
1121         for (let cell of this._fillerRow.children)
1122             cell.style.height = this._fillerHeight + "px";
1123
1124         if (!this._fillerRow.parentElement)
1125             this._listElement.appendChild(this._fillerRow);
1126     }
1127
1128     _applyColumnWidths()
1129     {
1130         for (let i = 0; i < this._headerElement.children.length; ++i)
1131             this._headerElement.children[i].style.width = this._columnWidths[i] + "px";
1132
1133         for (let row of this._listElement.children) {
1134             for (let i = 0; i < row.children.length; ++i)
1135                 row.children[i].style.width = this._columnWidths[i] + "px";
1136             row.__widthGeneration = this._widthGeneration;
1137         }
1138
1139         // Update Table Columns after cells since events may respond to this.
1140         for (let i = 0; i < this._visibleColumns.length; ++i)
1141             this._visibleColumns[i].width = this._columnWidths[i];
1142
1143         // Create missing cells after we've sized.
1144         for (let row of this._listElement.children) {
1145             if (row !== this._fillerRow) {
1146                 if (row.children.length !== this._visibleColumns.length)
1147                     this._populateRow(row);
1148             }
1149         }
1150     }
1151
1152     _applyColumnWidthsToColumnsIfNeeded()
1153     {
1154         // Apply and create missing cells only if row needs a width update.
1155         for (let row of this._listElement.children) {
1156             if (row.__widthGeneration !== this._widthGeneration) {
1157                 for (let i = 0; i < row.children.length; ++i)
1158                     row.children[i].style.width = this._columnWidths[i] + "px";
1159                 if (row !== this._fillerRow) {
1160                     if (row.children.length !== this._visibleColumns.length)
1161                         this._populateRow(row);
1162                 }
1163                 row.__widthGeneration = this._widthGeneration;
1164             }
1165         }
1166     }
1167
1168     _positionResizerElements()
1169     {
1170         console.assert(this._visibleColumns.length === this._columnWidths.length);
1171
1172         // Create the appropriate number of resizers.
1173         let resizersNeededCount = this._visibleColumns.length - 1;
1174         if (this._resizers.length !== resizersNeededCount) {
1175             if (this._resizers.length < resizersNeededCount) {
1176                 do {
1177                     let resizer = new WI.Resizer(WI.Resizer.RuleOrientation.Vertical, this);
1178                     this._resizers.push(resizer);
1179                     this._resizersElement.appendChild(resizer.element);
1180                 } while (this._resizers.length < resizersNeededCount);
1181             } else {
1182                 do {
1183                     let resizer = this._resizers.pop();
1184                     this._resizersElement.removeChild(resizer.element);
1185                 } while (this._resizers.length > resizersNeededCount);
1186             }
1187         }
1188
1189         // Position them.
1190         const columnResizerAdjustment = 3;
1191         let positionAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
1192         let totalWidth = 0;
1193         for (let i = 0; i < resizersNeededCount; ++i) {
1194             totalWidth += this._columnWidths[i];
1195             this._resizers[i].element.style[positionAttribute] = (totalWidth - columnResizerAdjustment) + "px";
1196         }
1197     }
1198
1199     _positionHeaderViews()
1200     {
1201         if (!this.subviews.length)
1202             return;
1203
1204         let offset = 0;
1205         let updates = [];
1206         for (let i = 0; i < this._visibleColumns.length; ++i) {
1207             let column = this._visibleColumns[i];
1208             let width = this._columnWidths[i];
1209             if (column.headerView)
1210                 updates.push({headerView: column.headerView, offset, width});
1211             offset += width;
1212         }
1213
1214         let styleProperty = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
1215         for (let {headerView, offset, width} of updates) {
1216             headerView.element.style.setProperty(styleProperty, offset + "px");
1217             headerView.element.style.width = width + "px";
1218             headerView.updateLayout(WI.View.LayoutReason.Resize);
1219         }
1220     }
1221
1222     _isRowVisible(rowIndex)
1223     {
1224         if (!this._previousRevealedRowCount)
1225             return false;
1226
1227         return rowIndex >= this._visibleRowIndexStart && rowIndex <= this._visibleRowIndexEnd;
1228     }
1229
1230     _indexToInsertColumn(column)
1231     {
1232         let currentVisibleColumnIndex = 0;
1233
1234         for (let columnIdentifier of this._columnOrder) {
1235             if (columnIdentifier === column.identifier)
1236                 return currentVisibleColumnIndex;
1237             if (columnIdentifier === this._visibleColumns[currentVisibleColumnIndex].identifier) {
1238                 currentVisibleColumnIndex++;
1239                 if (currentVisibleColumnIndex >= this._visibleColumns.length)
1240                     break;
1241             }
1242         }
1243
1244         return currentVisibleColumnIndex;
1245     }
1246
1247     _handleScroll(event)
1248     {
1249         if (event.type === "mousewheel" && !event.wheelDeltaY)
1250             return;
1251
1252         this._cachedScrollTop = NaN;
1253         this.needsLayout();
1254     }
1255
1256     _handleKeyDown(event)
1257     {
1258         this._selectionController.handleKeyDown(event);
1259     }
1260
1261     _handleMouseDown(event)
1262     {
1263         let cell = event.target.enclosingNodeOrSelfWithClass("cell");
1264         if (!cell)
1265             return;
1266
1267         let row = cell.parentElement;
1268         if (row === this._fillerRow)
1269             return;
1270
1271         let rowIndex = row.__index;
1272
1273         // Before checking if multiple selection is allowed, check if clicking the
1274         // row would cause it to be selected, and whether it is allowed by the delegate.
1275         if (!this.isRowSelected(rowIndex) && this._delegate.tableShouldSelectRow) {
1276             let columnIndex = Array.from(row.children).indexOf(cell);
1277             let column = this._visibleColumns[columnIndex];
1278             if (!this._delegate.tableShouldSelectRow(this, cell, column, rowIndex))
1279                 return;
1280         }
1281
1282         this._selectionController.handleItemMouseDown(rowIndex, event);
1283     }
1284
1285     _handleContextMenu(event)
1286     {
1287         let cell = event.target.enclosingNodeOrSelfWithClass("cell");
1288         if (!cell)
1289             return;
1290
1291         let row = cell.parentElement;
1292         if (row === this._fillerRow)
1293             return;
1294
1295         let columnIndex = Array.from(row.children).indexOf(cell);
1296         let column = this._visibleColumns[columnIndex];
1297         let rowIndex = row.__index;
1298
1299         this._delegate.tableCellContextMenuClicked(this, cell, column, rowIndex, event);
1300     }
1301
1302     _handleHeaderCellClicked(column, event)
1303     {
1304         let sortOrder = this._sortOrder;
1305         if (sortOrder === WI.Table.SortOrder.Indeterminate)
1306             sortOrder = WI.Table.SortOrder.Descending;
1307         else if (this._sortColumnIdentifier === column.identifier)
1308             sortOrder = sortOrder === WI.Table.SortOrder.Ascending ? WI.Table.SortOrder.Descending : WI.Table.SortOrder.Ascending;
1309
1310         this.sortColumnIdentifier = column.identifier;
1311         this.sortOrder = sortOrder;
1312     }
1313
1314     _handleHeaderContextMenu(column, event)
1315     {
1316         let contextMenu = WI.ContextMenu.createFromEvent(event);
1317
1318         if (column.sortable) {
1319             if (this.sortColumnIdentifier !== column.identifier || this.sortOrder !== WI.Table.SortOrder.Ascending) {
1320                 contextMenu.appendItem(WI.UIString("Sort Ascending"), () => {
1321                     this.sortColumnIdentifier = column.identifier;
1322                     this.sortOrder = WI.Table.SortOrder.Ascending;
1323                 });
1324             }
1325
1326             if (this.sortColumnIdentifier !== column.identifier || this.sortOrder !== WI.Table.SortOrder.Descending) {
1327                 contextMenu.appendItem(WI.UIString("Sort Descending"), () => {
1328                     this.sortColumnIdentifier = column.identifier;
1329                     this.sortOrder = WI.Table.SortOrder.Descending;
1330                 });
1331             }
1332         }
1333
1334         contextMenu.appendSeparator();
1335
1336         let didAppendHeaderItem = false;
1337
1338         for (let [columnIdentifier, column] of this._columnSpecs) {
1339             if (column.locked)
1340                 continue;
1341             if (!column.hideable)
1342                 continue;
1343
1344             // Add a header item before the list of toggleable columns.
1345             if (!didAppendHeaderItem) {
1346                 const disabled = true;
1347                 contextMenu.appendItem(WI.UIString("Displayed Columns"), () => {}, disabled);
1348                 didAppendHeaderItem = true;
1349             }
1350
1351             let checked = !column.hidden;
1352             contextMenu.appendCheckboxItem(column.name, () => {
1353                 if (column.hidden)
1354                     this.showColumn(column);
1355                 else
1356                     this.hideColumn(column);
1357             }, checked);
1358         }
1359     }
1360
1361     _removeRows(rowIndexes)
1362     {
1363         let removed = 0;
1364
1365         let adjustRowAtIndex = (index) => {
1366             let row = this._cachedRows.get(index);
1367             if (row) {
1368                 this._cachedRows.delete(index);
1369                 row.__index -= removed;
1370                 this._cachedRows.set(row.__index, row);
1371             }
1372         };
1373
1374         for (let index = rowIndexes.firstIndex; index <= rowIndexes.lastIndex; ++index) {
1375             if (rowIndexes.has(index)) {
1376                 let row = this._cachedRows.get(index);
1377                 if (row) {
1378                     this._cachedRows.delete(index);
1379                     row.remove();
1380                 }
1381                 removed++;
1382                 continue;
1383             }
1384
1385             if (removed)
1386                 adjustRowAtIndex(index);
1387         }
1388
1389         if (!removed)
1390             return;
1391
1392         for (let index = rowIndexes.lastIndex + 1; index < this._cachedNumberOfRows; ++index)
1393             adjustRowAtIndex(index);
1394
1395         this._cachedNumberOfRows -= removed;
1396         console.assert(this._cachedNumberOfRows >= 0);
1397
1398         this._selectionController.didRemoveItems(rowIndexes);
1399
1400         if (this._delegate.tableDidRemoveRows) {
1401             this._delegate.tableDidRemoveRows(this, Array.from(rowIndexes));
1402             console.assert(this._cachedNumberOfRows === this._dataSource.tableNumberOfRows(this), "Table data source should update after removing rows.");
1403         }
1404     }
1405
1406     _toggleSelectedRowStyle(rowIndexes, flag)
1407     {
1408         for (let index of rowIndexes) {
1409             let row = this._cachedRows.get(index);
1410             if (row)
1411                 row.classList.toggle("selected", flag);
1412         }
1413     }
1414 };
1415
1416 WI.Table.SortOrder = {
1417     Indeterminate: "table-sort-order-indeterminate",
1418     Ascending: "table-sort-order-ascending",
1419     Descending: "table-sort-order-descending",
1420 };