Skip to content

Commit

Permalink
Merge pull request #17762 from ahmedhamidawan/content_item_select_key…
Browse files Browse the repository at this point in the history
…board_unify

[24.0] Improve Shift+Click select in history panel and add it for `ContentItem` selector checkboxes as well
  • Loading branch information
mvdbeek committed Mar 22, 2024
2 parents e1b4d3f + f4d958f commit 21213c0
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 65 deletions.
43 changes: 32 additions & 11 deletions client/src/components/History/Content/ContentItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { faCheckSquare, faSquare } from "@fortawesome/free-regular-svg-icons";
import { faArrowCircleDown, faArrowCircleUp, faCheckCircle, faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BBadge, BButton, BCollapse } from "bootstrap-vue";
import { computed } from "vue";
import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router/composables";
import type { ItemUrls } from "@/components/History/Content/Dataset/index";
Expand Down 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 All @@ -77,6 +79,8 @@ const emit = defineEmits<{
const entryPointStore = useEntryPointStore();
const eventStore = useEventStore();
const contentItem = ref<HTMLElement | null>(null);
const jobState = computed(() => {
return new JobStateSummary(props.item);
});
Expand Down Expand Up @@ -171,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 @@ -197,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 @@ -215,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 @@ -298,6 +303,17 @@ function onTagClick(tag: string) {
}
}
function onButtonSelect(e: Event) {
const event = e as KeyboardEvent;
if (event.shiftKey) {
onClick(e);
} else {
emit("init-key-selection");
}
contentItem.value?.focus();
emit("update:selected", !props.selected);
}
function toggleHighlights() {
emit("toggleHighlights", props.item);
}
Expand All @@ -312,7 +328,8 @@ function unexpandedClick(event: Event) {
<template>
<div
:id="contentId"
:class="['content-item m-1 p-0 rounded btn-transparent-background', contentCls, isBeingUsed]"
ref="contentItem"
:class="['content-item m-1 p-0 rounded btn-transparent-background', contentCls, isBeingUsed, rangeSelectClass]"
:data-hid="id"
:data-state="dataState"
tabindex="0"
Expand All @@ -325,7 +342,7 @@ function unexpandedClick(event: Event) {
<div class="p-1 cursor-pointer" @click.stop="onClick">
<div class="d-flex justify-content-between">
<span class="p-1" data-description="content item header info">
<BButton v-if="selectable" class="selector p-0" @click.stop="emit('update:selected', !selected)">
<BButton v-if="selectable" class="selector p-0" @click.stop="onButtonSelect">
<FontAwesomeIcon v-if="selected" fixed-width size="lg" :icon="faCheckSquare" />
<FontAwesomeIcon v-else fixed-width size="lg" :icon="faSquare" />
</BButton>
Expand Down Expand Up @@ -439,6 +456,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,
});
},
};
Loading

0 comments on commit 21213c0

Please sign in to comment.