UNPKG

@knotx/viselect

Version:

[Forked] Simple, lightweight and modern library library for making visual DOM Selections.

481 lines (480 loc) 21.2 kB
/*! @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