@selenite/commons
Version:
This typescript package provides a set of frequently used utilities, types and svelte actions for building projects with Typescript and Svelte.
153 lines (152 loc) • 4.42 kB
JavaScript
import { distance, posFromClient, Rect, stopPropagation } from '../utils';
/**
* Action to enable box selection on an element.
* @param node Element to attach the action to
* @param params Parameters for the action
* @returns Svelte action
*/
export const boxSelection = (node, params = {}) => {
function pOver() {
node.style.cursor = 'crosshair';
}
function pLeave() {
node.style.cursor = '';
}
function pMove(e) {
if (!box)
return;
stopPropagation(e);
setBoxPos(startPos, posFromClient(e));
}
let box;
let startPos;
function pDown(e) {
if (e.button !== 0)
return;
document.body.style.userSelect = 'none';
if (box) {
destroyBox();
document.removeEventListener('pointermove', pMove);
return;
}
createBox(posFromClient(e));
document.addEventListener('pointermove', pMove, { passive: true });
document.addEventListener('pointerup', pUp, { once: true, capture: true });
}
function pUp(e) {
if (startPos) {
const pos = posFromClient(e);
if (distance(startPos, pos) < 12) {
destroyBox();
return;
}
}
document.body.style.userSelect = '';
if (!box)
return;
const selected = [];
const selectionRect = box.getBoundingClientRect();
for (const n of (params.holder ?? node).children) {
const r = n.getBoundingClientRect();
const intersection = Rect.intersection(selectionRect, r);
const a = Rect.area(r);
const ai = Rect.area(intersection);
const k = ai / a;
if (k > (params.threshold ?? 0.9)) {
selected.push(n);
}
}
console.debug('Box selection', selected);
params.onselection?.(selected);
destroyBox();
}
function setBoxPos(a, b) {
if (!box)
return;
let x, y, w, h;
if (a.x < b.x) {
x = a.x;
w = b.x - a.x;
}
else {
x = b.x;
w = a.x - b.x;
}
if (a.y < b.y) {
y = a.y;
h = b.y - a.y;
}
else {
y = b.y;
h = a.y - b.y;
}
const rect = node.getBoundingClientRect();
if (x < rect.left) {
w -= rect.left - x;
x = rect.left;
}
else if (x + w > rect.right) {
w = rect.right - x;
}
if (y < rect.top) {
h -= rect.top - y;
y = rect.top;
}
else if (y + h > rect.bottom) {
h = rect.bottom - y;
}
box.style.left = x + 'px';
box.style.top = y + 'px';
box.style.width = w + 'px';
box.style.height = h + 'px';
}
function createBox(pos) {
if (box)
return;
box = document.createElement('div');
box.style.position = 'absolute';
box.classList.add('border', 'border-accent-content/50', 'bg-accent/25');
box.style.pointerEvents = 'none';
document.body.appendChild(box);
setBoxPos(pos, pos);
startPos = pos;
}
function destroyBox() {
if (box) {
document.body.removeChild(box);
box = undefined;
}
}
function setup() {
node.style.cursor = 'crosshair';
node.addEventListener('pointerover', pOver);
node.addEventListener('pointerleave', pLeave);
node.addEventListener('pointerdown', pDown, { capture: true });
}
function destroy() {
node.style.cursor = '';
node.removeEventListener('pointerover', pOver);
node.removeEventListener('pointerleave', pLeave);
node.removeEventListener('pointerdown', pDown, { capture: true });
node.removeEventListener('pointermove', pMove);
if (box) {
document.body.removeChild(box);
box = undefined;
}
}
if (params.enabled ?? true) {
setup();
}
return {
update(newParams) {
params = newParams ?? {};
if (params.enabled ?? true) {
setup();
}
else {
destroy();
}
},
destroy
};
};