Ghost/ghost/admin/app/utils/selection-list.js
Fabien 'egg' O'Carroll 27976381f8
Updated GhContextMenu to handle freezing the SelectionList (#16624)
refs https://github.com/TryGhost/Team/issues/2677

Fixed a bug that the current selection is deselected when clicking inside a modal when performing a context menu action.

Refactored the context menu component and its usage in the posts list to
improve the user experience and code quality. Introduced a `state`
property and a `setState` method to the `gh-context-menu` component to
handle different scenarios. Used the `gh-context-menu` component in the
`posts-list/context-menu` component to simplify the modal and loading
logic. Added a `#frozen` property and methods to the `selection-list`
utility to prevent the selection of posts from changing while the
context menu is active.

---------

Co-authored-by: Simon Backx <simon@ghost.org>
2023-04-13 15:04:06 +02:00

217 lines
5.6 KiB
JavaScript

import {tracked} from '@glimmer/tracking';
export default class SelectionList {
@tracked selectedIds = new Set();
@tracked inverted = false;
@tracked lastSelectedId = null;
@tracked lastShiftSelectionGroup = new Set();
enabled = true;
infinityModel;
#frozen = false;
constructor(infinityModel) {
this.infinityModel = infinityModel ?? {content: []};
}
freeze() {
this.#frozen = true;
}
unfreeze() {
this.#frozen = false;
}
/**
* Returns an NQL filter for all items, not the selection
*/
get allFilter() {
return this.infinityModel.extraParams?.filter ?? '';
}
/**
* Returns an NQL filter for the current selection
*/
get filter() {
if (this.inverted) {
if (this.allFilter) {
if (this.selectedIds.size === 0) {
return this.allFilter;
}
return `(${this.allFilter})+id:-['${[...this.selectedIds].join('\',\'')}']`;
}
if (this.selectedIds.size === 0) {
// Select all
return '';
}
return `id:-['${[...this.selectedIds].join('\',\'')}']`;
}
if (this.selectedIds.size === 0) {
// Select nothing
return 'id:nothing';
}
// Only based on the ids
return `id:['${[...this.selectedIds].join('\',\'')}']`;
}
/**
* Create an empty copy
*/
cloneEmpty() {
return new SelectionList(this.infinityModel);
}
/**
* Return a list of models that are already loaded in memory.
* Keep in mind that when using CMD + A, we don't have all items in memory!
*/
get availableModels() {
const arr = [];
for (const item of this.infinityModel.content) {
if (this.isSelected(item.id)) {
arr.push(item);
}
}
return arr;
}
get first() {
return this.availableModels[0];
}
get isSingle() {
return this.selectedIds.size === 1 && !this.inverted;
}
get count() {
if (!this.inverted) {
return this.selectedIds.size;
}
return Math.max((this.infinityModel.meta?.pagination?.total ?? 0) - this.selectedIds.size, 1);
}
isSelected(id) {
if (this.inverted) {
return !this.selectedIds.has(id);
}
return this.selectedIds.has(id);
}
toggleItem(id) {
if (this.#frozen) {
return;
}
this.lastShiftSelectionGroup = new Set();
if (this.selectedIds.has(id)) {
this.selectedIds.delete(id);
if (!this.inverted) {
if (this.lastSelectedId === id) {
this.lastSelectedId = null;
}
} else {
// Shift behaviour in inverted mode needs a review
this.lastSelectedId = id;
}
} else {
this.selectedIds.add(id);
if (!this.inverted) {
this.lastSelectedId = id;
} else {
// Shift behaviour in inverted mode needs a review
this.lastSelectedId = id;
}
}
// Force update
// eslint-disable-next-line no-self-assign
this.selectedIds = this.selectedIds;
}
/**
* Select all items between the last selection or the first one if none
*/
shiftItem(id) {
if (this.#frozen) {
return;
}
// Unselect last selected items
for (const item of this.lastShiftSelectionGroup) {
if (this.inverted) {
this.selectedIds.add(item);
} else {
this.selectedIds.delete(item);
}
}
this.lastShiftSelectionGroup = new Set();
// todo
let running = false;
if (this.lastSelectedId === null) {
running = true;
}
for (const item of this.infinityModel.content) {
// Exlusing the last selected item
if (item.id === this.lastSelectedId || item.id === id) {
if (!running) {
running = true;
// Skip last selected on its own
if (item.id === this.lastSelectedId) {
continue;
}
} else {
// Still include id
if (item.id === id) {
this.lastShiftSelectionGroup.add(item.id);
if (this.inverted) {
this.selectedIds.delete(item.id);
} else {
this.selectedIds.add(item.id);
}
}
break;
}
}
if (running) {
this.lastShiftSelectionGroup.add(item.id);
if (this.inverted) {
this.selectedIds.delete(item.id);
} else {
this.selectedIds.add(item.id);
}
}
}
// Force update
// eslint-disable-next-line no-self-assign
this.selectedIds = this.selectedIds;
}
selectAll() {
if (this.#frozen) {
return;
}
this.selectedIds = new Set();
this.inverted = !this.inverted;
this.lastSelectedId = null;
}
clearSelection() {
if (this.#frozen) {
return;
}
this.selectedIds = new Set();
this.inverted = false;
this.lastSelectedId = null;
}
}