Diff
Modified: trunk/Source/WebInspectorUI/ChangeLog (238562 => 238563)
--- trunk/Source/WebInspectorUI/ChangeLog 2018-11-27 19:38:18 UTC (rev 238562)
+++ trunk/Source/WebInspectorUI/ChangeLog 2018-11-27 19:41:17 UTC (rev 238563)
@@ -1,3 +1,75 @@
+2018-11-27 Matt Baker <[email protected]>
+
+ Web Inspector: Table selection should be handled by a SelectionController
+ https://bugs.webkit.org/show_bug.cgi?id=191977
+ <rdar://problem/46253093>
+
+ Reviewed by Devin Rousso.
+
+ Add a SelectionController class, which manages an IndexSet of selected
+ items, and provides operations for adding and removing items from the
+ selection. Complex behaviors such as shift-clicking to select a range of
+ items, and updating the selection using the keyboard, are forwarded to
+ the controller using special-purpose methods that accept DOM Event objects.
+
+ * UserInterface/Base/Utilities.js:
+
+ * UserInterface/Controllers/SelectionController.js: Added.
+ (WI.SelectionController):
+ (WI.SelectionController.prototype.get delegate):
+ (WI.SelectionController.prototype.get lastSelectedItem):
+ (WI.SelectionController.prototype.get selectedItems):
+ (WI.SelectionController.prototype.get allowsMultipleSelection):
+ (WI.SelectionController.prototype.set allowsMultipleSelection):
+ (WI.SelectionController.prototype.get numberOfItems):
+ (WI.SelectionController.prototype.hasSelectedItem):
+ (WI.SelectionController.prototype.selectItem):
+ (WI.SelectionController.prototype.deselectItem):
+ (WI.SelectionController.prototype.selectAll):
+ (WI.SelectionController.prototype.deselectAll):
+ (WI.SelectionController.prototype.removeSelectedItems):
+ (WI.SelectionController.prototype.reset):
+ (WI.SelectionController.prototype.didRemoveItem):
+ (WI.SelectionController.prototype.handleKeyDown):
+ (WI.SelectionController.prototype.handleItemMouseDown.normalizeRange):
+ (WI.SelectionController.prototype.handleItemMouseDown):
+ (WI.SelectionController.prototype._deselectAllAndSelect):
+ (WI.SelectionController.prototype._selectItemsFromArrowKey):
+ (WI.SelectionController.prototype._nextSelectableIndex):
+ (WI.SelectionController.prototype._previousSelectableIndex):
+ (WI.SelectionController.prototype._updateSelectedItems):
+
+ * UserInterface/Main.html:
+ * UserInterface/Test.html:
+
+ * UserInterface/Views/Table.js:
+ (WI.Table):
+ (WI.Table.prototype.get selectedRow):
+ (WI.Table.prototype.get selectedRows):
+ (WI.Table.prototype.get allowsMultipleSelection):
+ (WI.Table.prototype.set allowsMultipleSelection):
+ (WI.Table.prototype.isRowSelected):
+ (WI.Table.prototype.reloadData):
+ (WI.Table.prototype.selectRow):
+ (WI.Table.prototype.deselectRow):
+ (WI.Table.prototype.selectAll):
+ (WI.Table.prototype.deselectAll):
+ (WI.Table.prototype.removeRow):
+ (WI.Table.prototype.removeSelectedRows):
+ (WI.Table.prototype.selectionControllerSelectionDidChange):
+ (WI.Table.prototype.selectionControllerNumberOfItems):
+ (WI.Table.prototype.selectionControllerNextSelectableIndex):
+ (WI.Table.prototype.selectionControllerPreviousSelectableIndex):
+ (WI.Table.prototype._handleKeyDown):
+ (WI.Table.prototype._handleMouseDown):
+ (WI.Table.prototype._removeRows):
+ (WI.Table.prototype._toggleSelectedRowStyle):
+ (WI.Table.prototype._selectRowsFromArrowKey): Deleted.
+ (WI.Table.prototype._handleMouseDown.normalizeRange): Deleted.
+ (WI.Table.prototype._deselectAllAndSelect): Deleted.
+ (WI.Table.prototype._notifySelectionDidChange): Deleted.
+ (WI.Table.prototype._updateSelectedRows): Deleted.
+
2018-11-26 Devin Rousso <[email protected]>
Web Inspector: "No Filter Results" in navigation sidebar should have a button to clear filters
Modified: trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js (238562 => 238563)
--- trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js 2018-11-27 19:38:18 UTC (rev 238562)
+++ trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js 2018-11-27 19:41:17 UTC (rev 238563)
@@ -430,6 +430,14 @@
}
});
+Object.defineProperty(MouseEvent.prototype, "commandOrControlKey",
+{
+ get()
+ {
+ return WI.Platform.name === "mac" ? this.metaKey : this.ctrlKey;
+ }
+});
+
Object.defineProperty(Array, "isTypedArray",
{
value(array)
Added: trunk/Source/WebInspectorUI/UserInterface/Controllers/SelectionController.js (0 => 238563)
--- trunk/Source/WebInspectorUI/UserInterface/Controllers/SelectionController.js (rev 0)
+++ trunk/Source/WebInspectorUI/UserInterface/Controllers/SelectionController.js 2018-11-27 19:41:17 UTC (rev 238563)
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.SelectionController = class SelectionController extends WI.Object
+{
+ constructor(delegate)
+ {
+ super();
+
+ console.assert(delegate);
+ this._delegate = delegate;
+
+ this._allowsMultipleSelection = false;
+ this._lastSelectedIndex = NaN;
+ this._shiftAnchorIndex = NaN;
+ this._selectedIndexes = new WI.IndexSet;
+
+ console.assert(this._delegate.selectionControllerNumberOfItems, "SelectionController delegate must implement selectionControllerNumberOfItems.");
+ console.assert(this._delegate.selectionControllerNextSelectableIndex, "SelectionController delegate must implement selectionControllerNextSelectableIndex.");
+ console.assert(this._delegate.selectionControllerPreviousSelectableIndex, "SelectionController delegate must implement selectionControllerPreviousSelectableIndex.");
+ }
+
+ // Public
+
+ get delegate() { return this._delegate; }
+ get lastSelectedItem() { return this._lastSelectedIndex; }
+ get selectedItems() { return this._selectedIndexes; }
+
+ get allowsMultipleSelection()
+ {
+ return this._allowsMultipleSelection;
+ }
+
+ set allowsMultipleSelection(flag)
+ {
+ if (this._allowsMultipleSelection === flag)
+ return;
+
+ this._allowsMultipleSelection = flag;
+ if (this._allowsMultipleSelection)
+ return;
+
+ if (this._selectedIndexes.size > 1) {
+ console.assert(this._lastSelectedIndex >= 0);
+ this._updateSelectedItems(new WI.IndexSet([this._lastSelectedIndex]));
+ }
+ }
+
+ get numberOfItems()
+ {
+ return this._delegate.selectionControllerNumberOfItems(this);
+ }
+
+ hasSelectedItem(index)
+ {
+ return this._selectedIndexes.has(index);
+ }
+
+ selectItem(index, extendSelection = false)
+ {
+ console.assert(!extendSelection || this._allowsMultipleSelection, "Cannot extend selection with multiple selection disabled.");
+ console.assert(index >= 0 && index < this.numberOfItems);
+
+ if (this.hasSelectedItem(index)) {
+ if (!extendSelection)
+ this._deselectAllAndSelect(index);
+ return;
+ }
+
+ let newSelectedItems = extendSelection ? this._selectedIndexes.copy() : new WI.IndexSet;
+ newSelectedItems.add(index);
+
+ this._shiftAnchorIndex = NaN;
+ this._lastSelectedIndex = index;
+
+ this._updateSelectedItems(newSelectedItems);
+ }
+
+ deselectItem(index)
+ {
+ console.assert(index >= 0 && index < this.numberOfItems);
+
+ if (!this.hasSelectedItem(index))
+ return;
+
+ let newSelectedItems = this._selectedIndexes.copy();
+ newSelectedItems.delete(index);
+
+ if (this._shiftAnchorIndex === index)
+ this._shiftAnchorIndex = NaN;
+
+ if (this._lastSelectedIndex === index) {
+ this._lastSelectedIndex = NaN;
+ if (newSelectedItems.size) {
+ // Find selected item closest to deselected item.
+ let preceding = newSelectedItems.indexLessThan(index);
+ let following = newSelectedItems.indexGreaterThan(index);
+
+ if (isNaN(preceding))
+ this._lastSelectedIndex = following;
+ else if (isNaN(following))
+ this._lastSelectedIndex = preceding;
+ else {
+ if ((following - index) < (index - preceding))
+ this._lastSelectedIndex = following;
+ else
+ this._lastSelectedIndex = preceding;
+ }
+ }
+ }
+
+ this._updateSelectedItems(newSelectedItems);
+ }
+
+ selectAll()
+ {
+ if (!this.numberOfItems || !this._allowsMultipleSelection)
+ return;
+
+ if (this._selectedIndexes.size === this.numberOfItems)
+ return;
+
+ let newSelectedItems = new WI.IndexSet;
+ newSelectedItems.addRange(0, this.numberOfItems);
+
+ this._lastSelectedIndex = newSelectedItems.lastIndex;
+ if (isNaN(this._shiftAnchorIndex))
+ this._shiftAnchorIndex = this._lastSelectedIndex;
+
+ this._updateSelectedItems(newSelectedItems);
+ }
+
+ deselectAll()
+ {
+ const index = NaN;
+ this._deselectAllAndSelect(index);
+ }
+
+ removeSelectedItems()
+ {
+ let numberOfSelectedItems = this._selectedIndexes.size;
+ if (!numberOfSelectedItems)
+ return;
+
+ // Try selecting the item following the selection.
+ let lastSelectedIndex = this._selectedIndexes.lastIndex;
+ let indexToSelect = lastSelectedIndex + 1;
+ if (indexToSelect === this.numberOfItems) {
+ // If no item exists after the last item in the selection, try selecting
+ // a deselected item (hole) within the selection.
+ let firstSelectedIndex = this._selectedIndexes.firstIndex;
+ if (lastSelectedIndex - firstSelectedIndex > numberOfSelectedItems) {
+ indexToSelect = this._selectedIndexes.firstIndex + 1;
+ while (this._selectedIndexes.has(indexToSelect))
+ indexToSelect++;
+ } else {
+ // If the selection contains no holes, try selecting the item
+ // preceding the selection.
+ indexToSelect = firstSelectedIndex > 0 ? firstSelectedIndex - 1 : NaN;
+ }
+ }
+
+ this._deselectAllAndSelect(indexToSelect);
+ }
+
+ reset()
+ {
+ this._shiftAnchorIndex = NaN;
+ this._lastSelectedIndex = NaN;
+ this._selectedIndexes.clear();
+ }
+
+ didRemoveItem(index)
+ {
+ if (this.hasSelectedItem(index))
+ this.deselectItem(index);
+
+ while (index = this._selectedIndexes.indexGreaterThan(index)) {
+ this._selectedIndexes.delete(index);
+ this._selectedIndexes.add(index - 1);
+ }
+ }
+
+ handleKeyDown(event)
+ {
+ if (!this.numberOfItems)
+ return false;
+
+ if (event.key === "a" && event.commandOrControlKey()) {
+ this.selectAll();
+ return true;
+ }
+
+ if (event.metaKey || event.ctrlKey)
+ return false;
+
+ if (event.keyIdentifier === "Up" || event.keyIdentifier === "Down") {
+ this._selectItemsFromArrowKey(event.keyIdentifier === "Up", event.shiftKey);
+
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+
+ return false;
+ }
+
+ handleItemMouseDown(index, event)
+ {
+ if (event.button !== 0 || event.ctrlKey)
+ return;
+
+ // Command (macOS) or Control (Windows) key takes precedence over shift
+ // whether or not multiple selection is enabled, so handle it first.
+ if (event.commandOrControlKey) {
+ if (this.hasSelectedItem(index))
+ this.deselectItem(index);
+ else
+ this.selectItem(index, this._allowsMultipleSelection);
+ return;
+ }
+
+ let shiftExtendSelection = this._allowsMultipleSelection && event.shiftKey;
+ if (!shiftExtendSelection) {
+ this.selectItem(index);
+ return;
+ }
+
+ let newSelectedItems = this._selectedIndexes.copy();
+
+ // Shift-clicking when nothing is selected should cause the first item
+ // through the clicked item to be selected.
+ if (!newSelectedItems.size) {
+ this._shiftAnchorIndex = 0;
+ this._lastSelectedIndex = index;
+ newSelectedItems.addRange(0, index + 1);
+ this._updateSelectedItems(newSelectedItems);
+ return;
+ }
+
+ if (isNaN(this._shiftAnchorIndex))
+ this._shiftAnchorIndex = this._lastSelectedIndex;
+
+ // Shift-clicking will add to or delete from the current selection, or
+ // pivot the selection around the anchor (a delete followed by an add).
+ // We could check for all three cases, and add or delete only those items
+ // that are necessary, but it is simpler to throw out the previous shift-
+ // selected range and add the new range between the anchor and clicked item.
+
+ function normalizeRange(startIndex, endIndex) {
+ return startIndex > endIndex ? [endIndex, startIndex] : [startIndex, endIndex];
+ }
+
+ if (this._shiftAnchorIndex !== this._lastSelectedIndex) {
+ let [startIndex, endIndex] = normalizeRange(this._shiftAnchorIndex, this._lastSelectedIndex);
+ newSelectedItems.deleteRange(startIndex, endIndex - startIndex + 1);
+ }
+
+ let [startIndex, endIndex] = normalizeRange(this._shiftAnchorIndex, index);
+ newSelectedItems.addRange(startIndex, endIndex - startIndex + 1);
+
+ this._lastSelectedIndex = index;
+
+ this._updateSelectedItems(newSelectedItems);
+ }
+
+ // Private
+
+ _deselectAllAndSelect(index)
+ {
+ if (!this._selectedIndexes.size)
+ return;
+
+ if (this._selectedIndexes.size === 1 && this._selectedIndexes.firstIndex === index)
+ return;
+
+ this._shiftAnchorIndex = NaN;
+ this._lastSelectedIndex = index;
+
+ let newSelectedItems = new WI.IndexSet;
+ if (!isNaN(index))
+ newSelectedItems.add(index);
+
+ this._updateSelectedItems(newSelectedItems);
+ }
+
+ _selectItemsFromArrowKey(goingUp, shiftKey)
+ {
+ if (!this._selectedIndexes.size) {
+ let index = goingUp ? this.numberOfItems - 1 : 0;
+ this.selectItem(index);
+ return;
+ }
+
+ let index = goingUp ? this._previousSelectableIndex(this._lastSelectedIndex) : this._nextSelectableIndex(this._lastSelectedIndex);
+ if (isNaN(index))
+ return;
+
+ let extendSelection = shiftKey && this._allowsMultipleSelection;
+ if (!extendSelection || !this.hasSelectedItem(index)) {
+ this.selectItem(index, extendSelection);
+ return;
+ }
+
+ // Since the item in the direction of movement is selected, we are either
+ // extending the selection into the item, or deselecting. Determine which
+ // by checking whether the item opposite the anchor item is selected.
+ let priorIndex = goingUp ? this._nextSelectableIndex(this._lastSelectedIndex) : this._previousSelectableIndex(this._lastSelectedIndex);
+ if (!this.hasSelectedItem(priorIndex)) {
+ this.deselectItem(this._lastSelectedIndex);
+ return;
+ }
+
+ // The selection is being extended into the item; make it the new
+ // anchor item then continue searching in the direction of movement
+ // for an unselected item to select.
+ while (!isNaN(index)) {
+ if (!this.hasSelectedItem(index)) {
+ this.selectItem(index, extendSelection);
+ break;
+ }
+
+ this._lastSelectedIndex = index;
+ index = goingUp ? this._previousSelectableIndex(index) : this._nextSelectableIndex(index);
+ }
+ }
+
+ _nextSelectableIndex(index)
+ {
+ return this._delegate.selectionControllerNextSelectableIndex(this, index);
+ }
+
+ _previousSelectableIndex(index)
+ {
+ return this._delegate.selectionControllerPreviousSelectableIndex(this, index);
+ }
+
+ _updateSelectedItems(indexes)
+ {
+ if (this._selectedIndexes.equals(indexes))
+ return;
+
+ let oldSelectedIndexes = this._selectedIndexes.copy();
+ this._selectedIndexes = indexes;
+
+ if (!this._delegate.selectionControllerSelectionDidChange)
+ return;
+
+ let deselectedItems = oldSelectedIndexes.difference(indexes);
+ let selectedItems = indexes.difference(oldSelectedIndexes);
+ this._delegate.selectionControllerSelectionDidChange(this, deselectedItems, selectedItems);
+ }
+};
Modified: trunk/Source/WebInspectorUI/UserInterface/Main.html (238562 => 238563)
--- trunk/Source/WebInspectorUI/UserInterface/Main.html 2018-11-27 19:38:18 UTC (rev 238562)
+++ trunk/Source/WebInspectorUI/UserInterface/Main.html 2018-11-27 19:41:17 UTC (rev 238563)
@@ -473,6 +473,8 @@
<script src=""
+ <script src=""
+
<script src=""
<script src=""
<script src=""
Modified: trunk/Source/WebInspectorUI/UserInterface/Test.html (238562 => 238563)
--- trunk/Source/WebInspectorUI/UserInterface/Test.html 2018-11-27 19:38:18 UTC (rev 238562)
+++ trunk/Source/WebInspectorUI/UserInterface/Test.html 2018-11-27 19:41:17 UTC (rev 238563)
@@ -243,6 +243,7 @@
<script src=""
<script src=""
+ <script src=""
<script src=""
<script src=""
<script src=""
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/Table.js (238562 => 238563)
--- trunk/Source/WebInspectorUI/UserInterface/Views/Table.js 2018-11-27 19:38:18 UTC (rev 238562)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/Table.js 2018-11-27 19:41:17 UTC (rev 238563)
@@ -87,10 +87,7 @@
this._columnWidths = null; // Calculated in _resizeColumnsAndFiller.
this._fillerHeight = 0; // Calculated in _resizeColumnsAndFiller.
- this._shiftAnchorIndex = NaN;
- this._selectedRowIndex = NaN;
- this._allowsMultipleSelection = false;
- this._selectedRows = new WI.IndexSet;
+ this._selectionController = new WI.SelectionController(this);
this._resizers = [];
this._currentResizer = null;
@@ -125,11 +122,15 @@
get dataSource() { return this._dataSource; }
get delegate() { return this._delegate; }
get rowHeight() { return this._rowHeight; }
- get selectedRow() { return this._selectedRowIndex; }
+ get selectedRow()
+ {
+ return this._selectionController.lastSelectedItem;
+ }
+
get selectedRows()
{
- return Array.from(this._selectedRows);
+ return Array.from(this._selectionController.selectedItems);
}
get scrollContainer() { return this._scrollContainerElement; }
@@ -220,28 +221,17 @@
get allowsMultipleSelection()
{
- return this._allowsMultipleSelection;
+ return this._selectionController.allowsMultipleSelection;
}
set allowsMultipleSelection(flag)
{
- if (this._allowsMultipleSelection === flag)
- return;
-
- this._allowsMultipleSelection = flag;
- if (this._allowsMultipleSelection)
- return;
-
- if (this._selectedRows.size > 1) {
- console.assert(this._selectedRowIndex >= 0);
- this._selectedRows = new WI.IndexSet([this._selectedRowIndex]);
- this._notifySelectionDidChange();
- }
+ this._selectionController.allowsMultipleSelection = flag;
}
isRowSelected(rowIndex)
{
- return this._selectedRows.has(rowIndex);
+ return this._selectionController.hasSelectedItem(rowIndex);
}
reloadData()
@@ -248,9 +238,7 @@
{
this._cachedRows.clear();
- this._shiftAnchorIndex = NaN;
- this._selectedRowIndex = NaN;
- this._selectedRows.clear();
+ this._selectionController.reset();
this._cachedNumberOfRows = NaN;
this._previousRevealedRowCount = NaN;
@@ -320,87 +308,22 @@
selectRow(rowIndex, extendSelection = false)
{
- console.assert(!extendSelection || this._allowsMultipleSelection, "Cannot extend selection with multiple selection disabled.");
- console.assert(rowIndex >= 0 && rowIndex < this.numberOfRows);
-
- if (this.isRowSelected(rowIndex)) {
- if (!extendSelection)
- this._deselectAllAndSelect(rowIndex);
- return;
- }
-
- if (!extendSelection && this._selectedRows.size) {
- this._suppressNextSelectionDidChange = true;
- this.deselectAll();
- }
-
- this._shiftAnchorIndex = NaN;
- this._selectedRowIndex = rowIndex;
- this._selectedRows.add(rowIndex);
-
- this._toggleSelectedRowStyle([this._selectedRowIndex], true);
-
- this._notifySelectionDidChange();
+ this._selectionController.selectItem(rowIndex, extendSelection);
}
deselectRow(rowIndex)
{
- console.assert(rowIndex >= 0 && rowIndex < this.numberOfRows);
-
- if (!this.isRowSelected(rowIndex))
- return;
-
- this._toggleSelectedRowStyle([rowIndex], false);
-
- this._selectedRows.delete(rowIndex);
-
- if (this._shiftAnchorIndex === rowIndex)
- this._shiftAnchorIndex = NaN;
-
- if (this._selectedRowIndex === rowIndex) {
- this._selectedRowIndex = NaN;
- if (this._selectedRows.size) {
- // Find selected row closest to deselected row.
- let preceding = this._selectedRows.indexLessThan(rowIndex);
- let following = this._selectedRows.indexGreaterThan(rowIndex);
-
- if (isNaN(preceding))
- this._selectedRowIndex = following;
- else if (isNaN(following))
- this._selectedRowIndex = preceding;
- else {
- if ((following - rowIndex) < (rowIndex - preceding))
- this._selectedRowIndex = following;
- else
- this._selectedRowIndex = preceding;
- }
- }
- }
-
- this._notifySelectionDidChange();
+ this._selectionController.deselectItem(rowIndex);
}
selectAll()
{
- if (!this.numberOfRows || !this._allowsMultipleSelection)
- return;
-
- if (this._selectedRows.size === this.numberOfRows)
- return;
-
- this._selectedRows.addRange(0, this.numberOfRows);
- this._selectedRowIndex = this._selectedRows.size - 1;
-
- for (let row of this._cachedRows.values())
- row.classList.add("selected");
-
- this._notifySelectionDidChange();
+ this._selectionController.selectAll();
}
deselectAll()
{
- const rowIndex = NaN;
- this._deselectAllAndSelect(rowIndex);
+ this._selectionController.deselectAll();
}
removeRow(rowIndex)
@@ -411,37 +334,19 @@
this.deselectRow(rowIndex);
this._removeRows(new WI.IndexSet([rowIndex]));
+ this._selectionController.didRemoveItem(rowIndex);
}
removeSelectedRows()
{
- let numberOfSelectedRows = this._selectedRows.size;
- if (!numberOfSelectedRows)
- return;
+ // Change the selection before removing rows. This matches the behavior
+ // of macOS Finder (in list and column modes) when removing selected items.
+ let oldSelectedItems = this._selectionController.selectedItems.copy();
- // Try selecting the row following the selection.
- let lastSelectedRow = this._selectedRows.lastIndex;
- let rowToSelect = lastSelectedRow + 1;
- if (rowToSelect === this.numberOfRows) {
- // If no row exists after the last selected row, try selecting a
- // deselected row (hole) within the selection.
- let firstSelectedRow = this._selectedRows.firstIndex;
- if (lastSelectedRow - firstSelectedRow > numberOfSelectedRows) {
- rowToSelect = this._selectedRows.firstIndex + 1;
- while (this._selectedRows.has(rowToSelect))
- rowToSelect++;
- } else {
- // If the selection contains no holes, try selecting the row
- // preceding the selection.
- rowToSelect = firstSelectedRow > 0 ? firstSelectedRow - 1 : NaN;
- }
- }
+ this._selectionController.removeSelectedItems();
- // Change the selection before removing rows. This matches the behavior
- // of macOS Finder (in list and column modes) when removing selected items.
- let oldSelectedRows = this._selectedRows.copy();
- this._deselectAllAndSelect(rowToSelect);
- this._removeRows(oldSelectedRows);
+ if (!oldSelectedItems.equals(this._selectionController.selectedItems))
+ this._removeRows(oldSelectedItems);
}
revealRow(rowIndex)
@@ -667,6 +572,44 @@
this._cachedHeight = NaN;
}
+ // SelectionController delegate
+
+ selectionControllerSelectionDidChange(controller, deselectedItems, selectedItems)
+ {
+ if (deselectedItems.size)
+ this._toggleSelectedRowStyle(deselectedItems, false);
+ if (selectedItems.size)
+ this._toggleSelectedRowStyle(selectedItems, true);
+
+ if (selectedItems.size === 1) {
+ let rowIndex = selectedItems.firstIndex;
+ if (!this._isRowVisible(rowIndex))
+ this.revealRow(rowIndex);
+ }
+
+ if (this._delegate.tableSelectionDidChange)
+ this._delegate.tableSelectionDidChange(this);
+ }
+
+ selectionControllerNumberOfItems(controller)
+ {
+ return this.numberOfRows;
+ }
+
+ selectionControllerNextSelectableIndex(controller, index)
+ {
+ if (index >= this.numberOfRows - 1)
+ return NaN;
+ return index + 1;
+ }
+
+ selectionControllerPreviousSelectableIndex(controller, index)
+ {
+ if (index <= 0)
+ return NaN;
+ return index - 1;
+ }
+
// Resizer delegate
resizerDragStarted(resizer)
@@ -1305,74 +1248,11 @@
_handleKeyDown(event)
{
- if (!this.numberOfRows)
- return;
-
- if (event.key === "a" && event.commandOrControlKey) {
- this.selectAll();
- return;
- }
-
- if (event.metaKey || event.ctrlKey)
- return;
-
- if (event.keyIdentifier === "Up" || event.keyIdentifier === "Down") {
- this._selectRowsFromArrowKey(event.keyIdentifier === "Up", event.shiftKey);
-
- this.revealRow(this._selectedRowIndex);
-
- event.preventDefault();
- event.stopPropagation();
- }
+ this._selectionController.handleKeyDown(event);
}
- _selectRowsFromArrowKey(goingUp, shiftKey)
- {
- if (!this._selectedRows.size) {
- let rowIndex = goingUp ? this.numberOfRows - 1 : 0;
- this.selectRow(rowIndex);
- return;
- }
-
- let rowIncrement = goingUp ? -1 : 1;
- let rowIndex = this._selectedRowIndex + rowIncrement;
- if (rowIndex < 0 || rowIndex >= this.numberOfRows)
- return;
-
- let extendSelection = shiftKey && this._allowsMultipleSelection;
-
- if (!extendSelection || !this.isRowSelected(rowIndex)) {
- this.selectRow(rowIndex, extendSelection);
- return;
- }
-
- // Since the row in the direction of movement is selected, we are either
- // extending the selection into the row, or deselecting. Determine which
- // by checking whether the row opposite the anchor row is selected.
- let priorRowIndex = this._selectedRowIndex - rowIncrement;
- if (!this.isRowSelected(priorRowIndex)) {
- this.deselectRow(this._selectedRowIndex);
- return;
- }
-
- // The selection is being extended into the row; make it the new
- // anchor row then continue searching in the direction of movement
- // for an unselected row to select.
- for (; rowIndex >= 0 && rowIndex < this.numberOfRows; rowIndex += rowIncrement) {
- if (!this.isRowSelected(rowIndex)) {
- this.selectRow(rowIndex, extendSelection);
- break;
- }
-
- this._selectedRowIndex = rowIndex;
- }
- }
-
_handleMouseDown(event)
{
- if (event.button !== 0 || event.ctrlKey)
- return;
-
let cell = event.target.enclosingNodeOrSelfWithClass("cell");
if (!cell)
return;
@@ -1382,11 +1262,10 @@
return;
let rowIndex = row.__index;
- let isRowSelected = this.isRowSelected(rowIndex);
// Before checking if multiple selection is allowed, check if clicking the
// row would cause it to be selected, and whether it is allowed by the delegate.
- if (!isRowSelected && this._delegate.tableShouldSelectRow) {
+ if (!this.isRowSelected(rowIndex) && this._delegate.tableShouldSelectRow) {
let columnIndex = Array.from(row.children).indexOf(cell);
let column = this._visibleColumns[columnIndex];
if (!this._delegate.tableShouldSelectRow(this, cell, column, rowIndex))
@@ -1393,58 +1272,7 @@
return;
}
- // Command (meta) key takes precedence over shift whether or not multiple
- // selection is enabled, so handle it first.
- if (event.metaKey) {
- if (isRowSelected)
- this.deselectRow(rowIndex);
- else
- this.selectRow(rowIndex, this._allowsMultipleSelection);
- return;
- }
-
- let shiftExtendSelection = this._allowsMultipleSelection && event.shiftKey;
- if (!shiftExtendSelection) {
- this.selectRow(rowIndex);
- return;
- }
-
- let newSelectedRows = this._selectedRows.copy();
-
- // Shift-clicking when nothing is selected should cause the first row
- // through the clicked row to be selected.
- if (!newSelectedRows.size) {
- this._shiftAnchorIndex = 0;
- this._selectedRowIndex = rowIndex;
- newSelectedRows.addRange(0, rowIndex + 1);
- this._updateSelectedRows(newSelectedRows);
- return;
- }
-
- if (isNaN(this._shiftAnchorIndex))
- this._shiftAnchorIndex = this._selectedRowIndex;
-
- // Shift-clicking will add to or delete from the current selection, or
- // pivot the selection around the anchor (a delete followed by an add).
- // We could check for all three cases, and add or delete only those rows
- // that are necessary, but it is simpler to throw out the previous shift-
- // selected range and add the new range between the anchor and clicked row.
-
- function normalizeRange(startIndex, endIndex) {
- return startIndex > endIndex ? [endIndex, startIndex] : [startIndex, endIndex];
- }
-
- if (this._shiftAnchorIndex !== this._selectedRowIndex) {
- let [startIndex, endIndex] = normalizeRange(this._shiftAnchorIndex, this._selectedRowIndex);
- newSelectedRows.deleteRange(startIndex, endIndex - startIndex + 1);
- }
-
- let [startIndex, endIndex] = normalizeRange(this._shiftAnchorIndex, rowIndex);
- newSelectedRows.addRange(startIndex, endIndex - startIndex + 1);
-
- this._selectedRowIndex = rowIndex;
-
- this._updateSelectedRows(newSelectedRows);
+ this._selectionController.handleItemMouseDown(rowIndex, event);
}
_handleContextMenu(event)
@@ -1523,54 +1351,19 @@
}
}
- _deselectAllAndSelect(rowIndex)
- {
- if (!this._selectedRows.size)
- return;
-
- if (this._selectedRows.size === 1 && this._selectedRows.firstIndex === rowIndex)
- return;
-
- this._toggleSelectedRowStyle(this._selectedRows, false);
-
- this._shiftAnchorIndex = NaN;
- this._selectedRowIndex = rowIndex;
- this._selectedRows.clear();
-
- if (!isNaN(rowIndex)) {
- this._selectedRows.add(rowIndex);
- this._toggleSelectedRowStyle(this._selectedRows, true);
- }
-
- this._notifySelectionDidChange();
- }
-
_removeRows(rowIndexes)
{
let removed = 0;
let adjustRowAtIndex = (index) => {
- let newIndex = index - removed;
let row = this._cachedRows.get(index);
if (row) {
- this._cachedRows.delete(row.__index);
- row.__index = newIndex;
- this._cachedRows.set(newIndex, row);
+ this._cachedRows.delete(index);
+ row.__index -= removed;
+ this._cachedRows.set(row.__index, row);
}
-
- if (this.isRowSelected(index)) {
- this._selectedRows.delete(index);
- this._selectedRows.add(newIndex);
- if (this._selectedRowIndex === index)
- this._selectedRowIndex = newIndex;
- }
};
- if (rowIndexes.has(this._shiftAnchorIndex))
- this._shiftAnchorIndex = NaN;
- if (rowIndexes.has(this._selectedRowIndex))
- this._selectedRowIndex = NaN;
-
for (let index = rowIndexes.firstIndex; index <= rowIndexes.lastIndex; ++index) {
if (rowIndexes.has(index)) {
let row = this._cachedRows.get(index);
@@ -1601,17 +1394,6 @@
}
}
- _notifySelectionDidChange()
- {
- if (this._suppressNextSelectionDidChange) {
- this._suppressNextSelectionDidChange = false;
- return;
- }
-
- if (this._delegate.tableSelectionDidChange)
- this._delegate.tableSelectionDidChange(this);
- }
-
_toggleSelectedRowStyle(rowIndexes, flag)
{
for (let index of rowIndexes) {
@@ -1620,24 +1402,6 @@
row.classList.toggle("selected", flag);
}
}
-
- _updateSelectedRows(rowIndexes)
- {
- if (this._selectedRows.equals(rowIndexes))
- return;
-
- let deselectedRows = this._selectedRows.difference(rowIndexes);
- if (deselectedRows.size)
- this._toggleSelectedRowStyle(deselectedRows, false);
-
- let selectedRows = rowIndexes.difference(this._selectedRows);
- if (selectedRows.size)
- this._toggleSelectedRowStyle(selectedRows, true);
-
- this._selectedRows = rowIndexes;
-
- this._notifySelectionDidChange();
- }
};
WI.Table.SortOrder = {