Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | import Viselect from '@viselect/vanilla';
import './dragselect.css';
import { nonnull } from '$lib/utils.js';
export class DragSelect {
/** @type {HTMLElement|undefined} */
imagesContainer;
/** @type {string[]} */
selection = $state([]);
/** @type {Viselect|undefined} */
#instance;
/**
* id of last selected item that wasn't selected by shift-clicking.
* needed to properly do shift-click selection
*
* See https://stackoverflow.com/a/16530782
*
* @type {string|undefined}
*/
shiftSelectionAnchor;
/**
*
* @param {(element: HTMLElement, index: number, array: HTMLElement[]) => boolean} predicate
*/
selectMatching(predicate) {
if (!this.#instance) return;
// Tell DragSelect that the set of selected items changed
// Since we store the selection as an array of ids, we
// find the corresponding elements in the imagesContainer by [data-id]
const elements = [...(this.imagesContainer?.querySelectorAll('[data-selectable]') ?? [])]
.filter((el) => el !== null && el !== undefined)
.filter((el) => el instanceof HTMLElement)
.filter(predicate);
this.#instance.clearSelection(true, true);
const result = /** @type {HTMLElement[]} */ (this.#instance.select(elements, true));
this.selection = result.map((e) => e.dataset.id).filter((id) => id !== undefined);
return result;
}
selectAll() {
this.selectMatching(() => true);
}
selectNone() {
this.selectMatching(() => false);
}
/**
* @param {string[]} newSelection
*/
setSelection(newSelection) {
this.selectMatching((e) => newSelection.includes(e.dataset.id ?? ''));
}
refreshSelectables() {
if (!this.#instance) return;
this.#instance.resolveSelectables();
}
destroy() {
this.#instance?.destroy();
}
/**
*
* @param {HTMLElement} container the container where all the [data-selectable] elements are. Selection square will be available in the parent of this container.
* @param {string[]} [initialSelection]
*/
constructor(container, initialSelection = []) {
// Save the container element on the class instance
this.selection = initialSelection;
this.imagesContainer = container;
// If the container doesn't exist, we can't do anything
if (!this.imagesContainer) return;
const boundary = this.imagesContainer.parentElement ?? this.imagesContainer;
// Create a new Viselect instance: this comes from the dragselect package
// It manages the selection of items in a container by dragging or clicking
this.#instance = new Viselect({
// Tell it what elements we want to be able to select: they have a [data-selectable] attribute
selectables: '[data-selectable]',
// Tell it where it should take over mouse dragging and stuff: the parent of the images container
// We use the parent to allow users of the AreaObservations component to decide how much padding they want around the images
// Using the parent, we can let the user start their selection in the padding area, and still select the images
boundaries: boundary,
startAreas: '[data-starts-selection]',
// file://./dragselect.css
selectionAreaClass: 'viselect-selection-area',
behaviour: {
overlap: 'keep',
},
features: {
deselectOnBlur: true,
},
});
// Clear selection if no modifier key is pressed
this.#instance.on('start', ({ event }) => {
if (!event) return;
if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {
this.selection = [];
}
});
// React to a move event: we just changed the selection
this.#instance.on('move', ({ store: { changed } }) => {
const removed = idsOfElements(changed.removed);
const added = idsOfElements(changed.added);
this.selection = [...this.selection.filter((id) => !removed.includes(id)), ...added];
});
}
}
/** @param {Element[]} elements */
function idsOfElements(elements) {
return elements
.filter((el) => el instanceof HTMLElement)
.map((el) => el.dataset.id)
.filter(nonnull);
}
|