diff --git a/client/src/components/History/Content/ContentItem.vue b/client/src/components/History/Content/ContentItem.vue index 6136f91c2782..6cfb3135db57 100644 --- a/client/src/components/History/Content/ContentItem.vue +++ b/client/src/components/History/Content/ContentItem.vue @@ -35,6 +35,7 @@ interface Props { addHighlightBtn?: boolean; highlight?: string; isDataset?: boolean; + isRangeSelectAnchor?: boolean; isHistoryItem?: boolean; selected?: boolean; selectable?: boolean; @@ -48,6 +49,7 @@ const props = withDefaults(defineProps(), { addHighlightBtn: false, highlight: undefined, isDataset: true, + isRangeSelectAnchor: false, isHistoryItem: false, selected: false, selectable: false, @@ -58,12 +60,12 @@ const props = withDefaults(defineProps(), { const emit = defineEmits<{ (e: "update:selected", selected: boolean): void; (e: "update:expand-dataset", expand: boolean): void; - (e: "shift-select", direction: string): void; + (e: "shift-arrow-select", direction: string): void; (e: "init-key-selection"): void; (e: "arrow-navigate", direction: string): void; (e: "hide-selection"): void; (e: "select-all"): void; - (e: "selected-to", reset: boolean): void; + (e: "selected-to"): void; (e: "delete", item: any, recursive: boolean): void; (e: "undelete"): void; (e: "unhide"): void; @@ -173,6 +175,10 @@ const isBeingUsed = computed(() => { return Object.values(itemUrls.value).includes(route.path) ? "being-used" : ""; }); +const rangeSelectClass = computed(() => { + return props.isRangeSelectAnchor ? "range-select-anchor" : ""; +}); + /** Based on the user's keyboard platform, checks if it is the * typical key for selection (ctrl for windows/linux, cmd for mac) */ @@ -199,7 +205,7 @@ function onKeyDown(event: KeyboardEvent) { } else { event.preventDefault(); if ((event.key === "ArrowUp" || event.key === "ArrowDown") && event.shiftKey) { - emit("shift-select", event.key); + emit("shift-arrow-select", event.key); } else if (event.key === "ArrowUp" || event.key === "ArrowDown") { emit("init-key-selection"); } else if (event.key === "Delete" && !props.selected && !props.item.deleted) { @@ -217,15 +223,12 @@ function onKeyDown(event: KeyboardEvent) { function onClick(e?: Event) { const event = e as KeyboardEvent; if (event && props.writable) { - if (event.shiftKey && isSelectKey(event)) { - emit("selected-to", false); - return; - } else if (isSelectKey(event)) { + if (isSelectKey(event)) { emit("init-key-selection"); emit("update:selected", !props.selected); return; } else if (event.shiftKey) { - emit("selected-to", true); + emit("selected-to"); return; } else { emit("init-key-selection"); @@ -326,7 +329,7 @@ function unexpandedClick(event: Event) {
this.lastInRangeIndex || currentItemIndex < this.firstInRangeIndex) + ) { + this.firstInRange = item; + this.lastInRange = item; + } + // if range select was upwards and the current item is above the lastInRange or below the firstInRange + else if ( + this.initDirection === "ArrowUp" && + (currentItemIndex < this.lastInRangeIndex || currentItemIndex > this.firstInRangeIndex) + ) { + this.firstInRange = item; + this.lastInRange = item; + } + } }, - shiftSelect(item, nextItem, eventKey) { + /** Selecting items using `Shift+ArrowUp/ArrowDown` keys */ + shiftArrowKeySelect(item, nextItem, eventKey) { const currentItemKey = this.getItemKey(item); if (!this.initSelectedKey) { this.initSelectedItem = item; this.initDirection = eventKey; - this.setSelected(item, true); + this.setSelected(item, true, false); } // got back to the initial selected item else if (this.initSelectedKey === currentItemKey) { @@ -75,50 +117,101 @@ export default { } // same direction else if (this.initDirection === eventKey) { - this.setSelected(item, true); + this.setSelected(item, true, false); } // different direction else { - this.setSelected(item, false); + this.setSelected(item, false, false); } if (nextItem) { - this.setSelected(nextItem, true); + this.setSelected(nextItem, true, false); } }, - selectTo(item, prevItem, allItems, reset = true) { + /** Range selecting items using `Shift+Click` */ + rangeSelect(item, prevItem) { if (prevItem && item) { - // we are staring a new shift+click selectTo from `prevItem` - if (!this.initSelectedKey) { - this.initSelectedItem = prevItem; - } + // either a range select is not active or we have modified a range select (changing the anchor) + const noRangeSelectOrModified = !this.initSelectedKey; + if (noRangeSelectOrModified) { + // there is a range select active + if (this.rangeSelectActive) { + const currentItemIndex = this.allItems.indexOf(item); - // `reset = false` in the case user is holding shift+ctrl key - if (reset) { - // clear this.items of any other selections - this.items = new Map(); + // the current item is outside the range in the same direction; + // the new range will follow the last item in prev range + if ( + (this.initDirection === "ArrowDown" && currentItemIndex > this.lastInRangeIndex) || + (this.initDirection === "ArrowUp" && currentItemIndex < this.lastInRangeIndex) + ) { + this.initSelectedItem = this.lastInRange; + } + // the current item is outside the range in the opposite direction; + // the new range will follow the first item in prev range + else if ( + (this.initDirection === "ArrowDown" && currentItemIndex < this.firstInRangeIndex) || + (this.initDirection === "ArrowUp" && currentItemIndex > this.firstInRangeIndex) + ) { + this.initSelectedItem = this.firstInRange; + } else { + this.initSelectedItem = prevItem; + this.firstInRange = prevItem; + } + } + // there is no range select active + else { + // we are staring a new shift+click rangeSelect from `prevItem` + this.initSelectedItem = prevItem; + this.firstInRange = prevItem; + } } - this.setSelected(this.initSelectedItem, true); - const initItemIndex = allItems.indexOf(this.initSelectedItem); - const currentItemIndex = allItems.indexOf(item); + const initItemIndex = this.allItems.indexOf(this.initSelectedItem); + const currentItemIndex = this.allItems.indexOf(item); + const lastDirection = this.initDirection; let selections = []; // from allItems, get the items between the init item and the current item if (initItemIndex < currentItemIndex) { this.initDirection = "ArrowDown"; - selections = allItems.slice(initItemIndex + 1, currentItemIndex + 1); + selections = this.allItems.slice(initItemIndex + 1, currentItemIndex + 1); } else if (initItemIndex > currentItemIndex) { this.initDirection = "ArrowUp"; - selections = allItems.slice(currentItemIndex, initItemIndex); + selections = this.allItems.slice(currentItemIndex, initItemIndex); + } + + let deselections; + // there is an existing range select; deselect items in certain conditions + if (this.rangeSelectActive) { + // if there is an active, uninterrupted range-select and the direction changed; + // deselect the items between the lastInRange and initSelectedItem + if (!noRangeSelectOrModified && lastDirection && lastDirection !== this.initDirection) { + if (this.lastInRangeIndex >= initItemIndex) { + deselections = this.allItems.slice(initItemIndex + 1, this.lastInRangeIndex + 1); + } else { + deselections = this.allItems.slice(this.lastInRangeIndex, initItemIndex); + } + } + + // if the range has become smaller, deselect items between the lastInRange and the current item + else if (this.lastInRangeIndex < currentItemIndex && this.initDirection === "ArrowUp") { + deselections = this.allItems.slice(this.lastInRangeIndex, currentItemIndex); + } else if (this.lastInRangeIndex > currentItemIndex && this.initDirection === "ArrowDown") { + deselections = this.allItems.slice(currentItemIndex + 1, this.lastInRangeIndex + 1); + } } + this.selectItems(selections); + if (deselections) { + deselections.forEach((deselected) => this.setSelected(deselected, false, false)); + } } else { this.setSelected(item, true); } + this.lastInRange = item; }, + /** Resets the initial item in a range select (or shiftArrowKeySelect) */ initKeySelection() { this.initSelectedItem = null; - this.initDirection = null; }, selectItems(items = []) { const newItems = [...this.items.values(), ...items]; @@ -139,6 +232,9 @@ export default { this.items = new Map(); this.allSelected = false; this.initKeySelection(); + this.lastInRange = null; + this.firstInRange = null; + this.initDirection = null; }, cancelSelection() { this.showSelection = false; @@ -176,12 +272,13 @@ export default { setShowSelection: this.setShowSelection, selectAllInCurrentQuery: this.selectAllInCurrentQuery, selectItems: this.selectItems, - selectTo: this.selectTo, + rangeSelect: this.rangeSelect, isSelected: this.isSelected, setSelected: this.setSelected, resetSelection: this.reset, - shiftSelect: this.shiftSelect, + shiftArrowKeySelect: this.shiftArrowKeySelect, initKeySelection: this.initKeySelection, + initSelectedItem: this.initSelectedItem, }); }, }; diff --git a/client/src/components/History/CurrentHistory/HistoryPanel.vue b/client/src/components/History/CurrentHistory/HistoryPanel.vue index 8b2062323cae..4d2858dbddb7 100644 --- a/client/src/components/History/CurrentHistory/HistoryPanel.vue +++ b/client/src/components/History/CurrentHistory/HistoryPanel.vue @@ -470,15 +470,17 @@ function setItemDragstart( setShowSelection, selectAllInCurrentQuery, isSelected, - selectTo, + rangeSelect, setSelected, - shiftSelect, + shiftArrowKeySelect, initKeySelection, resetSelection, + initSelectedItem, }" :scope-key="queryKey" :get-item-key="getItemKey" :filter-text="filterText" + :all-items="historyItems" :total-items-in-query="totalMatchesCount" @query-selection-break="querySelectionBreak = true"> @@ -545,7 +547,7 @@ function setItemDragstart( @@ -597,6 +599,9 @@ function setItemDragstart( :writable="canEditHistory" :expand-dataset="isExpanded(item)" :is-dataset="isDataset(item)" + :is-range-select-anchor=" + initSelectedItem && itemUniqueKey(item) === itemUniqueKey(initSelectedItem) + " :highlight="getHighlight(item)" :selected="isSelected(item)" :selectable="showSelection" @@ -613,11 +618,11 @@ function setItemDragstart( " @hide-selection="setShowSelection(false)" @init-key-selection="initKeySelection" - @shift-select=" - (eventKey) => shiftSelect(item, arrowNavigate(item, eventKey), eventKey) + @shift-arrow-select=" + (eventKey) => shiftArrowKeySelect(item, arrowNavigate(item, eventKey), eventKey) " - @select-all="selectAllInCurrentQuery(historyItems, false)" - @selected-to="(reset) => selectTo(item, lastItemFocused, historyItems, reset)" + @select-all="selectAllInCurrentQuery(false)" + @selected-to="rangeSelect(item, lastItemFocused)" @tag-click="updateFilterValue('tag', $event)" @tag-change="onTagChange" @toggleHighlights="updateFilterValue('related', item.hid)"