HTML Tree: Multi-selection fixes
- Clarification between focused row and pivot: - Pivot is only the row from which shift-selection pivots - Focused row is the one with the border around it - Fixed an issue where clicking the focused row didn't select it. Closes #2402 - Allows to create a non-contiguous range-selection with ctrl/cmd+shift. Closes #2403
This commit is contained in:
parent
3ec883a7f6
commit
12cd201b48
3 changed files with 55 additions and 71 deletions
|
@ -59,7 +59,7 @@ class TreeSelection {
|
||||||
this._tree = tree;
|
this._tree = tree;
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
pivot: 0,
|
pivot: 0,
|
||||||
_focused: 0,
|
focused: 0,
|
||||||
selected: new Set([]),
|
selected: new Set([]),
|
||||||
_selectEventsSuppressed: false
|
_selectEventsSuppressed: false
|
||||||
});
|
});
|
||||||
|
@ -92,12 +92,12 @@ class TreeSelection {
|
||||||
|
|
||||||
if (this.selectEventsSuppressed) return;
|
if (this.selectEventsSuppressed) return;
|
||||||
|
|
||||||
let previousPivot = this.pivot;
|
let previousFocused = this.focused;
|
||||||
this.pivot = index;
|
this.pivot = index;
|
||||||
this._focused = index;
|
this.focused = index;
|
||||||
if (this._tree.invalidate) {
|
if (this._tree.invalidate) {
|
||||||
this._tree.invalidateRow(index);
|
this._tree.invalidateRow(index);
|
||||||
this._tree.invalidateRow(previousPivot);
|
this._tree.invalidateRow(previousFocused);
|
||||||
}
|
}
|
||||||
this._updateTree(shouldDebounce);
|
this._updateTree(shouldDebounce);
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ class TreeSelection {
|
||||||
select(index, shouldDebounce) {
|
select(index, shouldDebounce) {
|
||||||
if (!this._tree.props.isSelectable(index)) return;
|
if (!this._tree.props.isSelectable(index)) return;
|
||||||
index = Math.max(0, index);
|
index = Math.max(0, index);
|
||||||
if (this.selected.size == 1 && this._focused == index && this.pivot == index) {
|
if (this.selected.size == 1 && this.isSelected(index)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ class TreeSelection {
|
||||||
toInvalidate.add(index);
|
toInvalidate.add(index);
|
||||||
toInvalidate.add(this.pivot);
|
toInvalidate.add(this.pivot);
|
||||||
this.selected = new Set([index]);
|
this.selected = new Set([index]);
|
||||||
this._focused = index;
|
this.focused = index;
|
||||||
this.pivot = index;
|
this.pivot = index;
|
||||||
|
|
||||||
if (this.selectEventsSuppressed) return true;
|
if (this.selectEventsSuppressed) return true;
|
||||||
|
@ -174,17 +174,22 @@ class TreeSelection {
|
||||||
/**
|
/**
|
||||||
* Performs a shift-select from current pivot to provided index. Updates focused item to index.
|
* Performs a shift-select from current pivot to provided index. Updates focused item to index.
|
||||||
* @param index {Number} The index is 0-clamped.
|
* @param index {Number} The index is 0-clamped.
|
||||||
|
* @param augment {Boolean} Adds to existing selection if true
|
||||||
* @param shouldDebounce {Boolean} Whether the update to the tree should be debounced
|
* @param shouldDebounce {Boolean} Whether the update to the tree should be debounced
|
||||||
*/
|
*/
|
||||||
shiftSelect(index, shouldDebounce) {
|
shiftSelect(index, augment, shouldDebounce) {
|
||||||
if (!this._tree.props.isSelectable(index)) return;
|
if (!this._tree.props.isSelectable(index)) return;
|
||||||
|
|
||||||
index = Math.max(0, index);
|
index = Math.max(0, index);
|
||||||
let from = Math.min(index, this.pivot);
|
let from = Math.min(index, this.pivot);
|
||||||
let to = Math.max(index, this.pivot);
|
let to = Math.max(index, this.pivot);
|
||||||
this._focused = index;
|
let oldFocused = this.focused;
|
||||||
|
this.focused = index;
|
||||||
let oldSelected = this.selected;
|
let oldSelected = this.selected;
|
||||||
this._rangedSelect(from, to);
|
if (augment) {
|
||||||
|
oldSelected = new Set(oldSelected);
|
||||||
|
}
|
||||||
|
this._rangedSelect(from, to, augment);
|
||||||
|
|
||||||
if (this.selectEventsSuppressed) return;
|
if (this.selectEventsSuppressed) return;
|
||||||
|
|
||||||
|
@ -199,6 +204,7 @@ class TreeSelection {
|
||||||
for (let index of oldSelected) {
|
for (let index of oldSelected) {
|
||||||
this._tree.invalidateRow(index);
|
this._tree.invalidateRow(index);
|
||||||
}
|
}
|
||||||
|
this._tree.invalidateRow(oldFocused);
|
||||||
}
|
}
|
||||||
this._updateTree(shouldDebounce);
|
this._updateTree(shouldDebounce);
|
||||||
}
|
}
|
||||||
|
@ -218,29 +224,6 @@ class TreeSelection {
|
||||||
return this.selected.size;
|
return this.selected.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
get focused() {
|
|
||||||
return this._focused;
|
|
||||||
}
|
|
||||||
|
|
||||||
set focused(index) {
|
|
||||||
index = Math.max(0, index);
|
|
||||||
let previousFocused = this._focused;
|
|
||||||
let previousPivot = this.pivot;
|
|
||||||
this.pivot = index;
|
|
||||||
this._focused = index;
|
|
||||||
|
|
||||||
if (this.selectEventsSuppressed) return;
|
|
||||||
|
|
||||||
this._updateTree();
|
|
||||||
if (this._tree.invalidate) {
|
|
||||||
this._tree.invalidateRow(previousFocused);
|
|
||||||
if (previousPivot != previousFocused) {
|
|
||||||
this._tree.invalidateRow(previousPivot);
|
|
||||||
}
|
|
||||||
this._tree.invalidateRow(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectEventsSuppressed() {
|
get selectEventsSuppressed() {
|
||||||
return this._selectEventsSuppressed;
|
return this._selectEventsSuppressed;
|
||||||
}
|
}
|
||||||
|
@ -485,17 +468,17 @@ class VirtualizedTable extends React.Component {
|
||||||
* @param {Integer} direction - -1 for up, 1 for down
|
* @param {Integer} direction - -1 for up, 1 for down
|
||||||
* @param {Boolean} selectTo
|
* @param {Boolean} selectTo
|
||||||
*/
|
*/
|
||||||
_onJumpSelect(direction, selectTo) {
|
_onJumpSelect(direction, selectTo, toggleSelection) {
|
||||||
if (direction == 1) {
|
if (direction == 1) {
|
||||||
const lastVisible = this._jsWindow.getLastVisibleRow();
|
const lastVisible = this._jsWindow.getLastVisibleRow();
|
||||||
if (this.selection.focused != lastVisible) {
|
if (this.selection.focused != lastVisible) {
|
||||||
return this.onSelection(lastVisible, selectTo);
|
return this.onSelection(lastVisible, selectTo, toggleSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const firstVisible = this._jsWindow.getFirstVisibleRow();
|
const firstVisible = this._jsWindow.getFirstVisibleRow();
|
||||||
if (this.selection.focused != firstVisible) {
|
if (this.selection.focused != firstVisible) {
|
||||||
return this.onSelection(firstVisible, selectTo);
|
return this.onSelection(firstVisible, selectTo, toggleSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const height = document.getElementById(this._jsWindowID).clientHeight;
|
const height = document.getElementById(this._jsWindowID).clientHeight;
|
||||||
|
@ -504,7 +487,7 @@ class VirtualizedTable extends React.Component {
|
||||||
const rowCount = this.props.getRowCount();
|
const rowCount = this.props.getRowCount();
|
||||||
destination = Math.min(destination, rowCount - 1);
|
destination = Math.min(destination, rowCount - 1);
|
||||||
destination = Math.max(0, destination);
|
destination = Math.max(0, destination);
|
||||||
return this.onSelection(destination, selectTo);
|
return this.onSelection(destination, selectTo, toggleSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -520,7 +503,8 @@ class VirtualizedTable extends React.Component {
|
||||||
if (e.altKey) return;
|
if (e.altKey) return;
|
||||||
|
|
||||||
const shiftSelect = e.shiftKey;
|
const shiftSelect = e.shiftKey;
|
||||||
const movePivot = Zotero.isMac ? e.metaKey : e.ctrlKey;
|
const moveFocused = Zotero.isMac ? e.metaKey : e.ctrlKey;
|
||||||
|
const toggleSelection = shiftSelect && moveFocused;
|
||||||
const rowCount = this.props.getRowCount();
|
const rowCount = this.props.getRowCount();
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
@ -530,7 +514,7 @@ class VirtualizedTable extends React.Component {
|
||||||
prevSelect--;
|
prevSelect--;
|
||||||
}
|
}
|
||||||
prevSelect = Math.max(0, prevSelect);
|
prevSelect = Math.max(0, prevSelect);
|
||||||
this.onSelection(prevSelect, shiftSelect, false, movePivot, e.repeat);
|
this.onSelection(prevSelect, shiftSelect, toggleSelection, moveFocused, e.repeat);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
|
@ -539,20 +523,20 @@ class VirtualizedTable extends React.Component {
|
||||||
nextSelect++;
|
nextSelect++;
|
||||||
}
|
}
|
||||||
nextSelect = Math.min(nextSelect, rowCount - 1);
|
nextSelect = Math.min(nextSelect, rowCount - 1);
|
||||||
this.onSelection(nextSelect, shiftSelect, false, movePivot, e.repeat);
|
this.onSelection(nextSelect, shiftSelect, toggleSelection, moveFocused, e.repeat);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "Home":
|
case "Home":
|
||||||
this.onSelection(0, shiftSelect, false, movePivot);
|
this.onSelection(0, shiftSelect, toggleSelection, moveFocused);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "End":
|
case "End":
|
||||||
this.onSelection(rowCount - 1, shiftSelect, false, movePivot);
|
this.onSelection(rowCount - 1, shiftSelect, toggleSelection, moveFocused);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "PageUp":
|
case "PageUp":
|
||||||
if (!Zotero.isMac) {
|
if (!Zotero.isMac) {
|
||||||
this._onJumpSelect(-1, shiftSelect, e.repeat);
|
this._onJumpSelect(-1, shiftSelect, toggleSelection, e.repeat);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this._jsWindow.scrollTo(this._jsWindow.scrollOffset - this._jsWindow.getWindowHeight() + this._rowHeight);
|
this._jsWindow.scrollTo(this._jsWindow.scrollOffset - this._jsWindow.getWindowHeight() + this._rowHeight);
|
||||||
|
@ -561,7 +545,7 @@ class VirtualizedTable extends React.Component {
|
||||||
|
|
||||||
case "PageDown":
|
case "PageDown":
|
||||||
if (!Zotero.isMac) {
|
if (!Zotero.isMac) {
|
||||||
this._onJumpSelect(1, shiftSelect, e.repeat);
|
this._onJumpSelect(1, shiftSelect, toggleSelection, e.repeat);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this._jsWindow.scrollTo(this._jsWindow.scrollOffset + this._jsWindow.getWindowHeight() - this._rowHeight);
|
this._jsWindow.scrollTo(this._jsWindow.scrollOffset + this._jsWindow.getWindowHeight() - this._rowHeight);
|
||||||
|
@ -600,7 +584,7 @@ class VirtualizedTable extends React.Component {
|
||||||
this._handleTyping(e.key);
|
this._handleTyping(e.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shiftSelect || movePivot) return;
|
if (shiftSelect || moveFocused) return;
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
|
@ -641,11 +625,11 @@ class VirtualizedTable extends React.Component {
|
||||||
}
|
}
|
||||||
const rowCount = this.props.getRowCount();
|
const rowCount = this.props.getRowCount();
|
||||||
if (allSameChar) {
|
if (allSameChar) {
|
||||||
for (let i = this.selection.pivot + 1, checked = 0; checked < rowCount; i++, checked++) {
|
for (let i = this.selection.focused + 1, checked = 0; checked < rowCount; i++, checked++) {
|
||||||
i %= rowCount;
|
i %= rowCount;
|
||||||
let rowString = this.props.getRowString(i);
|
let rowString = this.props.getRowString(i);
|
||||||
if (rowString.toLowerCase().indexOf(char) == 0) {
|
if (rowString.toLowerCase().indexOf(char) == 0) {
|
||||||
if (i != this.selection.pivot) {
|
if (i != this.selection.focused) {
|
||||||
this.scrollToRow(i);
|
this.scrollToRow(i);
|
||||||
this.onSelection(i);
|
this.onSelection(i);
|
||||||
}
|
}
|
||||||
|
@ -657,7 +641,7 @@ class VirtualizedTable extends React.Component {
|
||||||
for (let i = 0; i < rowCount; i++) {
|
for (let i = 0; i < rowCount; i++) {
|
||||||
let rowString = this.props.getRowString(i);
|
let rowString = this.props.getRowString(i);
|
||||||
if (rowString.toLowerCase().indexOf(this._typingString) == 0) {
|
if (rowString.toLowerCase().indexOf(this._typingString) == 0) {
|
||||||
if (i != this.selection.pivot) {
|
if (i != this.selection.focused) {
|
||||||
this.scrollToRow(i);
|
this.scrollToRow(i);
|
||||||
this.onSelection(i);
|
this.onSelection(i);
|
||||||
}
|
}
|
||||||
|
@ -696,13 +680,13 @@ class VirtualizedTable extends React.Component {
|
||||||
|
|
||||||
_handleMouseUp = async (e, index) => {
|
_handleMouseUp = async (e, index) => {
|
||||||
const shiftSelect = e.shiftKey;
|
const shiftSelect = e.shiftKey;
|
||||||
const toggleSelection = e.ctrlKey || e.metaKey;
|
const augment = e.ctrlKey || e.metaKey;
|
||||||
if (this._isMouseDrag || e.button != 0) {
|
if (this._isMouseDrag || e.button != 0) {
|
||||||
// other mouse buttons are ignored
|
// other mouse buttons are ignored
|
||||||
this._isMouseDrag = false;
|
this._isMouseDrag = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._onSelection(index, shiftSelect, toggleSelection);
|
this._onSelection(index, shiftSelect, augment);
|
||||||
this.focus();
|
this.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -734,39 +718,39 @@ class VirtualizedTable extends React.Component {
|
||||||
* If true will select from focused up to index (does not update pivot)
|
* If true will select from focused up to index (does not update pivot)
|
||||||
* @param {Boolean} toggleSelection
|
* @param {Boolean} toggleSelection
|
||||||
* If true will add to selection
|
* If true will add to selection
|
||||||
* @param {Boolean} movePivot
|
* @param {Boolean} moveFocused
|
||||||
* Will move pivot without adding anything to the selection
|
* Will move focus without adding anything to the selection
|
||||||
*/
|
*/
|
||||||
_onSelection = (index, shiftSelect, toggleSelection, movePivot, shouldDebounce) => {
|
_onSelection = (index, shiftSelect, toggleSelection, moveFocused, shouldDebounce) => {
|
||||||
if (this.selection.selectEventsSuppressed) return;
|
if (this.selection.selectEventsSuppressed) return;
|
||||||
|
|
||||||
if (movePivot) {
|
if (!this.props.multiSelect && (shiftSelect || toggleSelection || moveFocused)) {
|
||||||
if (!this.props.multiSelect) return;
|
return;
|
||||||
let previousPivot = this.selection.pivot;
|
}
|
||||||
this.selection._focused = index;
|
else if (shiftSelect) {
|
||||||
|
this.selection.shiftSelect(index, toggleSelection, shouldDebounce);
|
||||||
|
}
|
||||||
|
else if (toggleSelection) {
|
||||||
|
this.selection.toggleSelect(index, shouldDebounce);
|
||||||
|
}
|
||||||
|
else if (moveFocused) {
|
||||||
|
let previousFocused = this.selection.focused;
|
||||||
|
this.selection.focused = index;
|
||||||
this.selection.pivot = index;
|
this.selection.pivot = index;
|
||||||
this.invalidateRow(previousPivot);
|
this.invalidateRow(previousFocused);
|
||||||
this.invalidateRow(index);
|
this.invalidateRow(index);
|
||||||
}
|
}
|
||||||
// Normal selection
|
// Normal selection
|
||||||
else if (!shiftSelect && !toggleSelection) {
|
else if (!toggleSelection) {
|
||||||
if (index > 0 && !this.props.isSelectable(index)) {
|
if (index > 0 && !this.props.isSelectable(index)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.selection.select(index, shouldDebounce);
|
this.selection.select(index, shouldDebounce);
|
||||||
}
|
}
|
||||||
// Range selection
|
|
||||||
else if (shiftSelect && this.props.multiSelect) {
|
|
||||||
this.selection.shiftSelect(index, shouldDebounce);
|
|
||||||
}
|
|
||||||
// If index is not selectable and this is not normal selection we return
|
// If index is not selectable and this is not normal selection we return
|
||||||
else if (!this.props.isSelectable(index)) {
|
else if (!this.props.isSelectable(index)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Additive selection
|
|
||||||
else if (this.props.multiSelect) {
|
|
||||||
this.selection.toggleSelect(index, shouldDebounce);
|
|
||||||
}
|
|
||||||
// None of the previous conditions were satisfied, so nothing changes
|
// None of the previous conditions were satisfied, so nothing changes
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
|
@ -1567,7 +1551,7 @@ function makeRowRenderer(getRowData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.classList.toggle('selected', selection.isSelected(index));
|
div.classList.toggle('selected', selection.isSelected(index));
|
||||||
div.classList.toggle('pivot', selection.pivot == index);
|
div.classList.toggle('focused', selection.focused == index);
|
||||||
const rowData = getRowData(index);
|
const rowData = getRowData(index);
|
||||||
|
|
||||||
for (let column of columns) {
|
for (let column of columns) {
|
||||||
|
|
|
@ -902,7 +902,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
||||||
*/
|
*/
|
||||||
handleKeyUp = (event) => {
|
handleKeyUp = (event) => {
|
||||||
if (!Zotero.locked && event.code === 'Tab' && this.selection.count == 0) {
|
if (!Zotero.locked && event.code === 'Tab' && this.selection.count == 0) {
|
||||||
this.selection.select(this.selection.pivot);
|
this.selection.select(this.selection.focused);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1011,7 +1011,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
||||||
this._getColumns();
|
this._getColumns();
|
||||||
|
|
||||||
this.selection.clearSelection();
|
this.selection.clearSelection();
|
||||||
this.selection.pivot = 0;
|
this.selection.focused = 0;
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
if (Zotero.CollectionTreeCache.error) {
|
if (Zotero.CollectionTreeCache.error) {
|
||||||
return this.setItemsPaneMessage(Zotero.getString('pane.items.loadError'));
|
return this.setItemsPaneMessage(Zotero.getString('pane.items.loadError'));
|
||||||
|
@ -2867,7 +2867,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.classList.toggle('selected', selection.isSelected(index));
|
div.classList.toggle('selected', selection.isSelected(index));
|
||||||
div.classList.toggle('pivot', selection.pivot == index);
|
div.classList.toggle('focused', selection.focused == index);
|
||||||
div.classList.remove('drop', 'drop-before', 'drop-after');
|
div.classList.remove('drop', 'drop-before', 'drop-after');
|
||||||
const rowData = this._getRowData(index);
|
const rowData = this._getRowData(index);
|
||||||
div.classList.toggle('context-row', !!rowData.contextRow);
|
div.classList.toggle('context-row', !!rowData.contextRow);
|
||||||
|
|
|
@ -138,7 +138,7 @@
|
||||||
|
|
||||||
.virtualized-table.multi-select:focus {
|
.virtualized-table.multi-select:focus {
|
||||||
|
|
||||||
.row.pivot {
|
.row.focused {
|
||||||
border: 1px dotted highlight;
|
border: 1px dotted highlight;
|
||||||
box-sizing: initial;
|
box-sizing: initial;
|
||||||
margin: -1px 0;
|
margin: -1px 0;
|
||||||
|
|
Loading…
Reference in a new issue