Skip to content

Commit

Permalink
add a first and last item tracker to SelectedItems, remove Shift+Ctrl
Browse files Browse the repository at this point in the history
Modified `SelectedItems` so that for range select, we track the first and last item in a range, along with the anchor. This way, we can persist with a selected range even if the user deselects a few items within it, since we keep track of the beginning and end of the range.

Also, added a CSS indicator to `ContentItem`s which actually indicates which item if the Shift+click range select anchor.
This also removes the `Shift+Ctrl+Click` multiple range select.
  • Loading branch information
ahmedhamidawan committed Mar 21, 2024
1 parent c9e9ec1 commit 1d20ce1
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 41 deletions.
25 changes: 16 additions & 9 deletions client/src/components/History/Content/ContentItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface Props {
addHighlightBtn?: boolean;
highlight?: string;
isDataset?: boolean;
isRangeSelectAnchor?: boolean;
isHistoryItem?: boolean;
selected?: boolean;
selectable?: boolean;
Expand All @@ -48,6 +49,7 @@ const props = withDefaults(defineProps<Props>(), {
addHighlightBtn: false,
highlight: undefined,
isDataset: true,
isRangeSelectAnchor: false,
isHistoryItem: false,
selected: false,
selectable: false,
Expand All @@ -58,12 +60,12 @@ const props = withDefaults(defineProps<Props>(), {
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;
Expand Down Expand Up @@ -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)
*/
Expand All @@ -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) {
Expand All @@ -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");
Expand Down Expand Up @@ -326,7 +329,7 @@ function unexpandedClick(event: Event) {
<div
:id="contentId"
ref="contentItem"
:class="['content-item m-1 p-0 rounded btn-transparent-background', contentCls, isBeingUsed]"
:class="['content-item m-1 p-0 rounded btn-transparent-background', contentCls, isBeingUsed, rangeSelectClass]"
:data-hid="id"
:data-state="dataState"
tabindex="0"
Expand Down Expand Up @@ -450,6 +453,10 @@ function unexpandedClick(event: Event) {
box-shadow: 0 0 0 0.2rem transparentize($brand-primary, 0.75);
}
&.range-select-anchor {
box-shadow: 0 0 0 0.2rem transparentize($brand-primary, 0.75);
}
&.being-used {
border-left: 0.25rem solid $brand-primary;
margin-left: 0rem !important;
Expand Down
147 changes: 122 additions & 25 deletions client/src/components/History/Content/SelectedItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
getItemKey: { type: Function, required: true },
filterText: { type: String, required: true },
totalItemsInQuery: { type: Number, required: false },
allItems: { type: Array, required: true },
},
data() {
return {
Expand All @@ -19,6 +20,8 @@ export default {
allSelected: false,
initSelectedItem: null,
initDirection: null,
firstInRange: null,
lastInRange: null,
};
},
computed: {
Expand All @@ -34,18 +37,27 @@ export default {
initSelectedKey() {
return this.initSelectedItem ? this.getItemKey(this.initSelectedItem) : null;
},
lastInRangeIndex() {
return this.lastInRange ? this.allItems.indexOf(this.lastInRange) : null;
},
firstInRangeIndex() {
return this.firstInRange ? this.allItems.indexOf(this.firstInRange) : null;
},
rangeSelectActive() {
return this.lastInRange && this.initDirection;
},
},
methods: {
setShowSelection(val) {
this.showSelection = val;
},
selectAllInCurrentQuery(loadedItems = [], force = true) {
selectAllInCurrentQuery(force = true) {
// if we are not forcing selectAll, and all items are already selected; deselect them
if (!force && this.allSelected) {
this.setShowSelection(false);
return;
}
this.selectItems(loadedItems);
this.selectItems(this.allItems);
this.allSelected = true;
},
isSelected(item) {
Expand All @@ -55,70 +67,151 @@ export default {
const key = this.getItemKey(item);
return this.items.has(key);
},
setSelected(item, selected) {
/** Adds/Removes an item from the selected items
*
* @param {Object} item - the item to be selected/deselected
* @param {Boolean} selected - whether to select or deselect the item
* @param {Boolean} checkInRange - whether to check if the item lies outside the range
*/
setSelected(item, selected, checkInRange = true) {
const key = this.getItemKey(item);
const newSelected = new Map(this.items);
selected ? newSelected.set(key, item) : newSelected.delete(key);
this.items = newSelected;
this.breakQuerySelection();

// item selected from panel, range select is active, check if item lies outside the range
if (checkInRange && this.rangeSelectActive) {
const currentItemIndex = this.allItems.indexOf(item);

/** If either of the following are `true`, change the range anchor */
// if range select was downwards and the current item is below the lastInRange or above the firstInRange
if (
this.initDirection === "ArrowDown" &&
(currentItemIndex > 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) {
this.initDirection = eventKey;
}
// 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];
Expand All @@ -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;
Expand Down Expand Up @@ -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,
});
},
};
19 changes: 12 additions & 7 deletions client/src/components/History/CurrentHistory/HistoryPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
Expand Down Expand Up @@ -545,7 +547,7 @@ function setItemDragstart(
<HistorySelectionStatus
v-if="showSelection"
:selection-size="selectionSize"
@select-all="selectAllInCurrentQuery(historyItems)"
@select-all="selectAllInCurrentQuery()"
@reset-selection="resetSelection" />
</template>
</HistoryOperations>
Expand Down Expand Up @@ -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"
Expand All @@ -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)"
Expand Down

0 comments on commit 1d20ce1

Please sign in to comment.