@knotx/viselect
Version:
[Forked] Simple, lightweight and modern library library for making visual DOM Selections.
481 lines (480 loc) • 21.2 kB
JavaScript
/*! @viselect/vanilla v3.9.0 MIT | https://github.com/simonwep/viselect/tree/master/packages/vanilla */
class j {
constructor() {
this._listeners = /* @__PURE__ */ new Map(), this.on = this.addEventListener, this.off = this.removeEventListener, this.emit = this.dispatchEvent;
}
addEventListener(e, t) {
const s = this._listeners.get(e) ?? /* @__PURE__ */ new Set();
return this._listeners.set(e, s), s.add(t), this;
}
removeEventListener(e, t) {
var s;
return (s = this._listeners.get(e)) == null || s.delete(t), this;
}
dispatchEvent(e, ...t) {
let s = !0;
for (const o of this._listeners.get(e) ?? [])
s = o(...t) !== !1 && s;
return s;
}
unbindAllListeners() {
this._listeners.clear();
}
}
const P = (l, e = "px") => typeof l == "number" ? l + e : l, S = ({ style: l }, e, t) => {
if (typeof e == "object")
for (const [s, o] of Object.entries(e))
o !== void 0 && (l[s] = P(o));
else t !== void 0 && (l[e] = P(t));
}, L = (l = 0, e = 0, t = 0, s = 0) => {
const o = { x: l, y: e, width: t, height: s, top: e, left: l, right: l + t, bottom: e + s };
return { ...o, toJSON: () => JSON.stringify(o) };
}, X = (l) => {
let e, t = -1, s = !1;
return {
next: (...o) => {
e = o, s || (s = !0, t = requestAnimationFrame(() => {
l(...e), s = !1;
}));
},
cancel: () => {
cancelAnimationFrame(t), s = !1;
}
};
}, M = (l, e, t = "touch") => {
switch (t) {
case "center": {
const s = e.left + e.width / 2, o = e.top + e.height / 2;
return s >= l.left && s <= l.right && o >= l.top && o <= l.bottom;
}
case "cover":
return e.left >= l.left && e.top >= l.top && e.right <= l.right && e.bottom <= l.bottom;
case "touch":
return l.right >= e.left && l.left <= e.right && l.bottom >= e.top && l.top <= e.bottom;
}
}, Y = () => matchMedia("(hover: none), (pointer: coarse)").matches, K = () => "safari" in window, A = (l) => Array.isArray(l) ? l : [l], O = (l) => (e, t, s, o = {}) => {
(e instanceof HTMLCollection || e instanceof NodeList) && (e = Array.from(e)), t = A(t), e = A(e);
for (const i of e)
if (i)
for (const n of t)
i[l](n, s, { capture: !1, ...o });
}, y = O("addEventListener"), m = O("removeEventListener"), C = (l) => {
var o;
const { clientX: e, clientY: t, target: s } = ((o = l.touches) == null ? void 0 : o[0]) ?? l;
return { x: e, y: t, target: s };
}, E = (l, e = document) => A(l).map(
(t) => typeof t == "string" ? Array.from(e.querySelectorAll(t)) : t instanceof Element ? t : null
).flat().filter(Boolean), N = (l, e) => e.some((t) => typeof t == "number" ? l.button === t : typeof t == "object" ? t.button !== l.button ? !1 : t.modifiers.every((s) => {
switch (s) {
case "alt":
return l.altKey;
case "ctrl":
return l.ctrlKey || l.metaKey;
case "shift":
return l.shiftKey;
}
}) : !1), { abs: b, max: k, min: B, ceil: R } = Math, z = (l = []) => ({
stored: l,
selected: [],
touched: [],
changed: { added: [], removed: [] }
}), x = {
getScrollPosition: (l) => ({
x: l.scrollLeft,
y: l.scrollTop
}),
setScrollPosition: (l, e) => {
e.x !== void 0 && (l.scrollLeft = e.x), e.y !== void 0 && (l.scrollTop = e.y);
},
getScrollSize: (l) => ({
width: l.scrollWidth,
height: l.scrollHeight
}),
getClientSize: (l) => ({
width: l.clientWidth,
height: l.clientHeight
}),
alwaysScroll: !1
}, T = class T extends j {
constructor(e) {
var i, n, r, c, d, _, u, h, a, f;
super(), this._selection = z(), this._targetBoundaryScrolled = !0, this._selectables = [], this._areaLocation = { y1: 0, x2: 0, y2: 0, x1: 0 }, this._areaRect = L(), this._singleClick = !0, this._scrollAvailable = !0, this._scrollingActive = !1, this._scrollSpeed = { x: 0, y: 0 }, this._scrollDelta = { x: 0, y: 0 }, this._lastMousePosition = { x: 0, y: 0 }, this.enable = this._toggleStartEvents, this.disable = this._toggleStartEvents.bind(this, !1), this._options = {
selectionAreaClass: "selection-area",
selectionContainerClass: void 0,
selectables: [],
document: window.document,
startAreas: ["html"],
boundaries: ["html"],
container: "body",
...e,
behaviour: {
overlap: "invert",
intersect: "touch",
triggers: [0],
...e.behaviour,
startThreshold: (i = e.behaviour) != null && i.startThreshold ? typeof e.behaviour.startThreshold == "number" ? e.behaviour.startThreshold : { x: 10, y: 10, ...e.behaviour.startThreshold } : { x: 10, y: 10 },
scrolling: {
speedDivider: 10,
manualSpeed: 750,
...(n = e.behaviour) == null ? void 0 : n.scrolling,
startScrollMargins: {
x: 0,
y: 0,
...(c = (r = e.behaviour) == null ? void 0 : r.scrolling) == null ? void 0 : c.startScrollMargins
}
}
},
features: {
range: !0,
touch: !0,
deselectOnBlur: !1,
...e.features,
singleTap: {
allow: !0,
intersect: "native",
...(d = e.features) == null ? void 0 : d.singleTap
}
}
}, this._scrollController = {
getScrollPosition: ((_ = e.scrollController) == null ? void 0 : _.getScrollPosition) || x.getScrollPosition,
setScrollPosition: ((u = e.scrollController) == null ? void 0 : u.setScrollPosition) || x.setScrollPosition,
getScrollSize: ((h = e.scrollController) == null ? void 0 : h.getScrollSize) || x.getScrollSize,
getClientSize: ((a = e.scrollController) == null ? void 0 : a.getClientSize) || x.getClientSize,
alwaysScroll: ((f = e.scrollController) == null ? void 0 : f.alwaysScroll) || x.alwaysScroll
};
for (const p of Object.getOwnPropertyNames(Object.getPrototypeOf(this)))
typeof this[p] == "function" && (this[p] = this[p].bind(this));
const { document: t, selectionAreaClass: s, selectionContainerClass: o } = this._options;
this._area = t.createElement("div"), this._clippingElement = t.createElement("div"), this._clippingElement.appendChild(this._area), this._area.classList.add(s), o && this._clippingElement.classList.add(o), S(this._area, {
willChange: "top, left, bottom, right, width, height",
top: 0,
left: 0,
position: "fixed"
}), S(this._clippingElement, {
overflow: "hidden",
position: "fixed",
transform: "translate3d(0, 0, 0)",
// https://stackoverflow.com/a/38268846
pointerEvents: "none",
zIndex: "1"
}), this._frame = X((p) => {
this._recalculateSelectionAreaRect(), this._updateElementSelection(), this._emitEvent("move", p), this._redrawSelectionArea();
}), this.enable();
}
_toggleStartEvents(e = !0) {
const { document: t, features: s } = this._options, o = e ? y : m;
o(t, "mousedown", this._onTapStart), s.touch && o(t, "touchstart", this._onTapStart, { passive: !1 });
}
_onTapStart(e, t = !1) {
const { x: s, y: o, target: i } = C(e), { document: n, startAreas: r, boundaries: c, features: d, behaviour: _ } = this._options, u = i.getBoundingClientRect();
if (e instanceof MouseEvent && !N(e, _.triggers))
return;
const h = E(r, n), a = E(c, n);
this._targetElement = a.find(
(v) => M(v.getBoundingClientRect(), u)
);
const f = e.composedPath(), p = h.find((v) => f.includes(v));
if (this._targetBoundary = a.find((v) => f.includes(v)), !this._targetElement || !p || !this._targetBoundary || !t && this._emitEvent("beforestart", e) === !1)
return;
this._areaLocation = { x1: s, y1: o, x2: 0, y2: 0 };
const g = n.scrollingElement ?? n.body, w = this._scrollController.getScrollPosition(g);
this._scrollDelta = { x: w.x, y: w.y }, this._singleClick = !0, this.clearSelection(!1, !0), y(n, ["touchmove", "mousemove"], this._delayedTapMove, { passive: !1 }), y(n, ["mouseup", "touchcancel", "touchend"], this._onTapStop), y(n, "scroll", this._onScroll), d.deselectOnBlur && (this._targetBoundaryScrolled = !1, y(this._targetBoundary, "scroll", this._onStartAreaScroll));
}
_onSingleTap(e) {
const { singleTap: { intersect: t }, range: s } = this._options.features, o = C(e);
let i;
if (t === "native")
i = o.target;
else if (t === "touch") {
this.resolveSelectables();
const { x: r, y: c } = o;
i = this._selectables.find((d) => {
const { right: _, left: u, top: h, bottom: a } = d.getBoundingClientRect();
return r < _ && r > u && c < a && c > h;
});
}
if (!i)
return;
for (this.resolveSelectables(); !this._selectables.includes(i); )
if (i.parentElement)
i = i.parentElement;
else {
this._targetBoundaryScrolled || this.clearSelection();
return;
}
const { stored: n } = this._selection;
if (this._emitEvent("start", e), e.shiftKey && s && this._latestElement) {
const r = this._latestElement, [c, d] = r.compareDocumentPosition(i) & 4 ? [i, r] : [r, i], _ = [...this._selectables.filter(
(u) => u.compareDocumentPosition(c) & 4 && u.compareDocumentPosition(d) & 2
), c, d];
this.select(_), this._latestElement = r;
} else n.includes(i) && (n.length === 1 || e.ctrlKey || n.every((r) => this._selection.stored.includes(r))) ? this.deselect(i) : (this.select(i), this._latestElement = i);
}
_delayedTapMove(e) {
const { container: t, document: s, behaviour: { startThreshold: o } } = this._options, { x1: i, y1: n } = this._areaLocation, { x: r, y: c } = C(e);
if (
// Single number for both coordinates
typeof o == "number" && b(r + c - (i + n)) >= o || // Different x and y threshold
typeof o == "object" && b(r - i) >= o.x || b(c - n) >= o.y
) {
if (m(s, ["mousemove", "touchmove"], this._delayedTapMove, { passive: !1 }), this._emitEvent("beforedrag", e) === !1) {
m(s, ["mouseup", "touchcancel", "touchend"], this._onTapStop);
return;
}
y(s, ["mousemove", "touchmove"], this._onTapMove, { passive: !1 }), S(this._area, "display", "block"), E(t, s)[0].appendChild(this._clippingElement), this.resolveSelectables(), this._singleClick = !1, this._targetRect = this._targetElement.getBoundingClientRect();
const d = this._targetElement, _ = this._scrollController.getScrollSize(d), u = this._scrollController.getClientSize(d);
this._scrollAvailable = _.height !== u.height || _.width !== u.width, this._scrollAvailable && (y(this._targetElement, "wheel", this._wheelScroll, { passive: !1 }), y(this._options.document, "keydown", this._keyboardScroll, { passive: !1 }), this._selectables = this._selectables.filter((h) => this._targetElement.contains(h))), this._setupSelectionArea(), this._emitEvent("start", e), this._onTapMove(e);
}
this._handleMoveEvent(e);
}
_setupSelectionArea() {
const { _clippingElement: e, _targetElement: t, _area: s } = this, o = this._targetRect = t.getBoundingClientRect();
this._scrollAvailable ? (S(e, {
top: o.top,
left: o.left,
width: o.width,
height: o.height
}), S(s, {
marginTop: -o.top,
marginLeft: -o.left
})) : (S(e, {
top: 0,
left: 0,
width: "100%",
height: "100%"
}), S(s, {
marginTop: 0,
marginLeft: 0
}));
}
_onTapMove(e) {
const { _scrollSpeed: t, _areaLocation: s, _options: o, _frame: i } = this, { speedDivider: n } = o.behaviour.scrolling, r = this._targetElement, { x: c, y: d } = C(e);
if (s.x2 = c, s.y2 = d, this._lastMousePosition.x = c, this._lastMousePosition.y = d, this._scrollAvailable && !this._scrollingActive && (t.y || t.x)) {
this._scrollingActive = !0;
const _ = () => {
if (!t.x && !t.y) {
this._scrollingActive = !1;
return;
}
const { x: u, y: h } = this._scrollController.getScrollPosition(r), a = {};
if (t.y) {
a.y = h + R(t.y / n), this._scrollController.setScrollPosition(r, a);
const f = this._scrollController.getScrollPosition(r);
s.y1 -= f.y - h;
}
if (t.x) {
a.x = u + R(t.x / n), this._scrollController.setScrollPosition(r, a);
const f = this._scrollController.getScrollPosition(r);
s.x1 -= f.x - u;
}
i.next(e), requestAnimationFrame(_);
};
requestAnimationFrame(_);
} else
i.next(e);
this._handleMoveEvent(e);
}
_handleMoveEvent(e) {
const { features: t } = this._options;
(t.touch && Y() || this._scrollAvailable && K()) && e.preventDefault();
}
_onScroll() {
const { _scrollDelta: e, _options: { document: t } } = this, s = t.scrollingElement ?? t.body, o = this._scrollController.getScrollPosition(s);
this._areaLocation.x1 += e.x - o.x, this._areaLocation.y1 += e.y - o.y, e.x = o.x, e.y = o.y, this._setupSelectionArea(), this._frame.next(null);
}
_onStartAreaScroll() {
this._targetBoundaryScrolled = !0, m(this._targetElement, "scroll", this._onStartAreaScroll);
}
_wheelScroll(e) {
const { manualSpeed: t } = this._options.behaviour.scrolling, s = e.deltaY ? e.deltaY > 0 ? 1 : -1 : 0, o = e.deltaX ? e.deltaX > 0 ? 1 : -1 : 0;
this._scrollSpeed.y += s * t, this._scrollSpeed.x += o * t, this._onTapMove(e), e.preventDefault();
}
_keyboardScroll(e) {
const { manualSpeed: t } = this._options.behaviour.scrolling, s = e.key === "ArrowLeft" ? -1 : e.key === "ArrowRight" ? 1 : 0, o = e.key === "ArrowUp" ? -1 : e.key === "ArrowDown" ? 1 : 0;
this._scrollSpeed.x += Math.sign(s) * t, this._scrollSpeed.y += Math.sign(o) * t, e.preventDefault(), this._onTapMove({
clientX: this._lastMousePosition.x,
clientY: this._lastMousePosition.y,
preventDefault: () => {
}
});
}
_recalculateSelectionAreaRect() {
const { _scrollSpeed: e, _areaLocation: t, _targetElement: s, _options: o } = this, i = this._targetRect, n = this._scrollController.getScrollPosition(s), r = this._scrollController.getClientSize(s), c = this._scrollController.getScrollSize(s), d = this._scrollController.alwaysScroll, { x1: _, y1: u } = t;
let { x2: h, y2: a } = t;
const { behaviour: { scrolling: { startScrollMargins: f } } } = o;
h < i.left + f.x ? (e.x = n.x || d ? -b(i.left - h + f.x) : 0, h = h < i.left ? i.left : h) : h > i.right - f.x ? (e.x = c.width - n.x - r.width || d ? b(i.left + i.width - h - f.x) : 0, h = h > i.right ? i.right : h) : e.x = 0, a < i.top + f.y ? (e.y = n.y || d ? -b(i.top - a + f.y) : 0, a = a < i.top ? i.top : a) : a > i.bottom - f.y ? (e.y = c.height - n.y - r.height || d ? b(i.top + i.height - a - f.y) : 0, a = a > i.bottom ? i.bottom : a) : e.y = 0;
const p = B(_, h), g = B(u, a), w = k(_, h), v = k(u, a);
this._areaRect = L(p, g, w - p, v - g);
}
_redrawSelectionArea() {
const { x: e, y: t, width: s, height: o } = this._areaRect, { style: i } = this._area;
i.left = `${e}px`, i.top = `${t}px`, i.width = `${s}px`, i.height = `${o}px`;
}
_onTapStop(e, t) {
var n;
const { document: s, features: o } = this._options, { _singleClick: i } = this;
m(this._targetElement, "scroll", this._onStartAreaScroll), m(s, ["mousemove", "touchmove"], this._delayedTapMove), m(s, ["touchmove", "mousemove"], this._onTapMove), m(s, ["mouseup", "touchcancel", "touchend"], this._onTapStop), m(s, "scroll", this._onScroll), this._keepSelection(), e && i && o.singleTap.allow ? this._onSingleTap(e) : !i && !t && (this._updateElementSelection(), this._emitEvent("stop", e)), this._scrollSpeed.x = 0, this._scrollSpeed.y = 0, m(this._targetElement, "wheel", this._wheelScroll, { passive: !0 }), m(this._options.document, "keydown", this._keyboardScroll, { passive: !0 }), this._clippingElement.remove(), (n = this._frame) == null || n.cancel(), S(this._area, "display", "none");
}
_updateElementSelection() {
const { _selectables: e, _options: t, _selection: s, _areaRect: o } = this, { stored: i, selected: n, touched: r } = s, { intersect: c, overlap: d } = t.behaviour, _ = d === "invert", u = [], h = [], a = [];
for (let p = 0; p < e.length; p++) {
const g = e[p];
if (M(o, g.getBoundingClientRect(), c)) {
if (n.includes(g))
i.includes(g) && !r.includes(g) && r.push(g);
else if (_ && i.includes(g)) {
a.push(g);
continue;
} else
h.push(g);
u.push(g);
}
}
_ && h.push(...i.filter((p) => !n.includes(p)));
const f = d === "keep";
for (let p = 0; p < n.length; p++) {
const g = n[p];
!u.includes(g) && !// Check if the user wants to keep previously selected elements, e.g.,
// not make them part of the current selection as soon as they're touched.
(f && i.includes(g)) && a.push(g);
}
s.selected = u, s.changed = { added: h, removed: a }, this._latestElement = void 0;
}
_emitEvent(e, t) {
return this.emit(e, {
event: t,
store: this._selection,
selection: this
});
}
_keepSelection() {
const { _options: e, _selection: t } = this, { selected: s, changed: o, touched: i, stored: n } = t, r = s.filter((c) => !n.includes(c));
switch (e.behaviour.overlap) {
case "drop": {
t.stored = [
...r,
...n.filter((c) => !i.includes(c))
// Elements not touched
];
break;
}
case "invert": {
t.stored = [
...r,
...n.filter((c) => !o.removed.includes(c))
// Elements not removed from selection
];
break;
}
case "keep": {
t.stored = [
...n,
...s.filter((c) => !n.includes(c))
// Newly added
];
break;
}
}
}
/**
* Manually triggers the start of a selection
* @param evt A MouseEvent / TouchEvent-like object
* @param silent If beforestart should be fired
*/
trigger(e, t = !0) {
this._onTapStart(e, t);
}
/**
* Can be used if during a selection elements have been added
* Will update everything that can be selected
*/
resolveSelectables() {
this._selectables = E(this._options.selectables, this._options.document);
}
/**
* Same as deselecting, but for all elements currently selected
* @param includeStored If the store should also get cleared
* @param quiet If move / stop events should be fired
*/
clearSelection(e = !0, t = !1) {
const { selected: s, stored: o, changed: i } = this._selection;
i.added = [], i.removed.push(
...s,
...e ? o : []
), t || (this._emitEvent("move", null), this._emitEvent("stop", null)), this._selection = z(e ? [] : o);
}
/**
* @returns {Array} Selected elements
*/
getSelection() {
return this._selection.stored;
}
/**
* @returns {HTMLElement} The selection area element
*/
getSelectionArea() {
return this._area;
}
/**
* @returns {Element[]} Available selectable elements for current selection
*/
getSelectables() {
return this._selectables;
}
/**
* Set the location of the selection area
* @param location A partial AreaLocation object
*/
setAreaLocation(e) {
Object.assign(this._areaLocation, e), this._redrawSelectionArea();
}
/**
* @returns {AreaLocation} The current location of the selection area
*/
getAreaLocation() {
return this._areaLocation;
}
/**
* Cancel the current selection process, pass true to fire a stop event after cancel
* @param keepEvent If a stop event should be fired
*/
cancel(e = !1) {
this._onTapStop(null, !e);
}
/**
* Unbinds all events and removes the area-element.
*/
destroy() {
this.cancel(), this.disable(), this._clippingElement.remove(), super.unbindAllListeners();
}
/**
* Adds elements to the selection
* @param query CSS Query, can be an array of queries
* @param quiet If this should not trigger the move event
*/
select(e, t = !1) {
const { changed: s, selected: o, stored: i } = this._selection, n = E(e, this._options.document).filter(
(r) => !o.includes(r) && !i.includes(r)
);
return i.push(...n), o.push(...n), s.added.push(...n), s.removed = [], this._latestElement = void 0, t || (this._emitEvent("move", null), this._emitEvent("stop", null)), n;
}
/**
* Removes a particular element from the selection
* @param query CSS Query, can be an array of queries
* @param quiet If this should not trigger the move event
*/
deselect(e, t = !1) {
const { selected: s, stored: o, changed: i } = this._selection, n = E(e, this._options.document).filter(
(r) => s.includes(r) || o.includes(r)
);
this._selection.stored = o.filter((r) => !n.includes(r)), this._selection.selected = s.filter((r) => !n.includes(r)), this._selection.changed.added = [], this._selection.changed.removed.push(
...n.filter((r) => !i.removed.includes(r))
), this._latestElement = void 0, t || (this._emitEvent("move", null), this._emitEvent("stop", null));
}
};
T.version = "3.9.0";
let D = T;
export {
D as default
};
//# sourceMappingURL=viselect.mjs.map