UNPKG

@knotx/viselect

Version:

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

1 lines 65.2 kB
{"version":3,"file":"viselect.mjs","sources":["../src/EventEmitter.ts","../src/utils/css.ts","../src/utils/domRect.ts","../src/utils/frames.ts","../src/utils/intersects.ts","../src/utils/browser.ts","../src/utils/arrayify.ts","../src/utils/events.ts","../src/utils/selectAll.ts","../src/utils/matchesTrigger.ts","../src/index.ts"],"sourcesContent":["\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFunction = (...args: any[]) => any;\ntype EventMap = Record<string, AnyFunction>;\n\nexport class EventTarget<Events extends EventMap> {\n private readonly _listeners = new Map<keyof Events, Set<AnyFunction>>();\n\n public addEventListener<K extends keyof Events>(event: K, cb: Events[K]): this {\n const set = this._listeners.get(event) ?? new Set();\n this._listeners.set(event, set);\n set.add(cb as AnyFunction);\n return this;\n }\n\n public removeEventListener<K extends keyof Events>(event: K, cb: Events[K]): this {\n this._listeners.get(event)?.delete(cb as AnyFunction);\n return this;\n }\n\n public dispatchEvent<K extends keyof Events>(event: K, ...data: Parameters<Events[K]>): boolean {\n let ok = true;\n for (const cb of (this._listeners.get(event) ?? [])) {\n ok = (cb(...data) !== false) && ok;\n }\n\n return ok;\n }\n\n public unbindAllListeners(): void {\n this._listeners.clear();\n }\n\n // Let's also support on, off and emit like node\n public on = this.addEventListener;\n public off = this.removeEventListener;\n public emit = this.dispatchEvent;\n}\n","const unitify = (val: string | number, unit = 'px'): string => {\n return typeof val === 'number' ? val + unit : val;\n};\n\n/**\n * Add css to a DOM-Element or returns the current\n * value of a property.\n *\n * @param el The Element.\n * @param attr The attribute or an object which holds css key-properties.\n * @param val The value for a single attribute.\n * @returns {*}\n */\nexport const css = ({style}: HTMLElement, attr: Partial<Record<keyof CSSStyleDeclaration, string | number>> | string, val?: string | number): void => {\n if (typeof attr === 'object') {\n for (const [key, value] of Object.entries(attr)) {\n if (value !== undefined) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n style[key as any] = unitify(value);\n }\n }\n } else if (val !== undefined) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n style[attr as any] = unitify(val);\n }\n};\n\n","// Polyfill for DOMRect as happy-dom and jsdom don't support it\nexport const domRect = (x = 0, y = 0, width = 0, height = 0): DOMRect => {\n const rect = {x, y, width, height, top: y, left: x, right: x + width, bottom: y + height};\n const toJSON = () => JSON.stringify(rect);\n return {...rect, toJSON};\n};\n","/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFunction = (...args: any[]) => void;\n\nexport interface Frames<F extends AnyFunction = AnyFunction> {\n next(...args: Parameters<F>): void;\n\n cancel(): void;\n}\n\nexport const frames = <F extends AnyFunction>(fn: F): Frames<F> => {\n let previousArgs: Parameters<F>;\n let frameId = -1;\n let lock = false;\n\n return {\n next: (...args: Parameters<F>): void => {\n previousArgs = args;\n\n if (!lock) {\n lock = true;\n frameId = requestAnimationFrame(() => {\n fn(...previousArgs);\n lock = false;\n });\n }\n },\n cancel: () => {\n cancelAnimationFrame(frameId);\n lock = false;\n }\n };\n};\n","export type Intersection = 'center' | 'cover' | 'touch';\n\n/**\n * Check if two DOM-Elements intersects each other.\n * @param a BoundingClientRect of the first element.\n * @param b BoundingClientRect of the second element.\n * @param mode Options are center, cover or touch.\n * @returns {boolean} If both elements intersects each other.\n */\nexport const intersects = (a: DOMRect, b: DOMRect, mode: Intersection = 'touch'): boolean => {\n switch (mode) {\n case 'center': {\n const bxc = b.left + b.width / 2;\n const byc = b.top + b.height / 2;\n\n return bxc >= a.left &&\n bxc <= a.right &&\n byc >= a.top &&\n byc <= a.bottom;\n }\n case 'cover': {\n return b.left >= a.left &&\n b.top >= a.top &&\n b.right <= a.right &&\n b.bottom <= a.bottom;\n }\n case 'touch': {\n return a.right >= b.left &&\n a.left <= b.right &&\n a.bottom >= b.top &&\n a.top <= b.bottom;\n }\n }\n};\n","// Determines if the device's primary input supports touch\n// See this article: https://css-tricks.com/touch-devices-not-judged-size/\nexport const isTouchDevice = (): boolean => matchMedia('(hover: none), (pointer: coarse)').matches;\n\n// Determines if the browser is safari\nexport const isSafariBrowser = (): boolean => 'safari' in window;\n","// Turns a value into an array if it's not already an array\nexport const arrayify = <T>(value: T | T[]): T[] => (Array.isArray(value) ? value : [value]);\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {arrayify} from './arrayify';\n\ntype Method = 'addEventListener' | 'removeEventListener';\ntype AnyFunction = (...arg: any) => any;\n\nconst eventListener = (method: Method) => (\n items: (EventTarget | undefined) | (EventTarget | undefined)[],\n events: string | string[],\n fn: AnyFunction,\n options = {}\n) => {\n\n // Normalize array\n if (items instanceof HTMLCollection || items instanceof NodeList) {\n items = Array.from(items);\n }\n\n events = arrayify(events)\n items = arrayify(items);\n\n for (const el of items) {\n if (el) {\n for (const ev of events) {\n el[method](ev, fn as EventListener, {capture: false, ...options});\n }\n }\n }\n};\n\n/**\n * Add event(s) to element(s).\n * @param elements DOM-Elements\n * @param events Event names\n * @param fn Callback\n * @param options Optional options\n * @return Array passed arguments\n */\nexport const on = eventListener('addEventListener');\n\n/**\n * Remove event(s) from element(s).\n * @param elements DOM-Elements\n * @param events Event names\n * @param fn Callback\n * @param options Optional options\n * @return Array passed arguments\n */\nexport const off = eventListener('removeEventListener');\n\n/**\n * Simplifies a touch / mouse-event\n * @param evt\n */\nexport const simplifyEvent = (evt: any): {\n target: HTMLElement;\n x: number;\n y: number;\n} => {\n const {clientX, clientY, target} = evt.touches?.[0] ?? evt;\n return {x: clientX, y: clientY, target};\n};\n","import {arrayify} from './arrayify';\n\nexport type SelectAllSelectors = (string | Element)[] | string | Element;\n\n/**\n * Takes a selector (or array of selectors) and returns the matched nodes.\n * @param selector The selector or an Array of selectors.\n * @param doc\n * @returns {Array} Array of DOM-Nodes.\n */\nexport const selectAll = (selector: SelectAllSelectors, doc: Document = document): Element[] =>\n arrayify(selector)\n .map(item =>\n typeof item === 'string'\n ? Array.from(doc.querySelectorAll(item))\n : item instanceof Element\n ? item\n : null\n )\n .flat()\n .filter(Boolean) as Element[];\n","\n// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button#value\nexport type MouseButton = 0 // Main\n | 1 // Auxiliary\n | 2 // Secondary\n | 3 // Fourth\n | 4; // Fifth\n\nexport type Modifier = 'ctrl'\n | 'alt'\n | 'shift';\n\nexport type MouseButtonWithModifiers = {\n button: MouseButton,\n modifiers: Modifier[]\n};\n\nexport type Trigger = MouseButton | MouseButtonWithModifiers;\n\n/**\n * Determines whether a MouseEvent should execute until completion depending on\n * which button and modifier(s) are active for the MouseEvent.\n * The Event will execute to completion if ANY of the triggers \"matches\"\n * @param event MouseEvent that should be checked\n * @param triggers A list of Triggers that signify that the event should execute until completion\n * @returns Whether the MouseEvent should execute until completion\n */\nexport const matchesTrigger = (event: MouseEvent, triggers: Trigger[]): boolean =>\n triggers.some((trigger) => {\n\n // The trigger requires only a specific button to be pressed\n if (typeof trigger === 'number') {\n return event.button === trigger;\n }\n\n // The trigger requires a specific button to be pressed AND some modifiers\n if (typeof trigger === 'object') {\n if (trigger.button !== event.button) {\n return false;\n }\n\n return trigger.modifiers.every((modifier) => {\n switch (modifier) {\n case 'alt':\n return event.altKey;\n case 'ctrl':\n return event.ctrlKey || event.metaKey;\n case 'shift':\n return event.shiftKey;\n }\n });\n }\n\n return false;\n });\n","import {EventTarget} from './EventEmitter';\nimport type {AreaLocation, Coordinates, Dimensions, ScrollEvent, SelectionEvents, SelectionOptions, SelectionStore, ScrollController} from './types';\nimport {PartialSelectionOptions} from './types';\nimport {css} from './utils/css';\nimport {domRect} from './utils/domRect';\nimport {Frames, frames} from './utils/frames';\nimport {intersects} from './utils/intersects';\nimport {isSafariBrowser, isTouchDevice} from './utils/browser';\nimport {on, off, simplifyEvent} from './utils/events';\nimport {selectAll, SelectAllSelectors} from './utils/selectAll';\nimport {matchesTrigger} from './utils/matchesTrigger';\n\n// Re-export types\nexport * from './types';\n\n// Some var shorting for better compression and readability\nconst {abs, max, min, ceil} = Math;\n\nconst makeSelectionStore = (stored: Element[] = []): SelectionStore => ({\n stored,\n selected: [],\n touched: [],\n changed: {added: [], removed: []}\n});\n\n// Default scroll controller implementation\nconst defaultScrollController: ScrollController = {\n getScrollPosition: (element: Element): Coordinates => ({\n x: element.scrollLeft,\n y: element.scrollTop\n }),\n setScrollPosition: (element: Element, position: Partial<Coordinates>): void => {\n if (position.x !== undefined) {\n element.scrollLeft = position.x;\n }\n if (position.y !== undefined) {\n element.scrollTop = position.y;\n }\n },\n getScrollSize: (element: Element): Dimensions => ({\n width: element.scrollWidth,\n height: element.scrollHeight\n }),\n getClientSize: (element: Element): Dimensions => ({\n width: element.clientWidth,\n height: element.clientHeight\n }),\n alwaysScroll: false\n};\n\nexport default class SelectionArea extends EventTarget<SelectionEvents> {\n public static version = VERSION;\n\n // Options\n private readonly _options: SelectionOptions;\n private readonly _scrollController: ScrollController;\n\n // Selection store\n private _selection: SelectionStore = makeSelectionStore();\n\n // Area element and clipping element\n private readonly _area: HTMLElement;\n private readonly _clippingElement: HTMLElement;\n\n // Target container (element) and boundary (cached)\n private _targetElement?: Element;\n private _targetBoundary?: Element;\n private _targetBoundaryScrolled = true;\n private _targetRect?: DOMRect;\n private _selectables: Element[] = [];\n private _latestElement?: Element;\n\n // Dynamically constructed area rect\n private _areaLocation: AreaLocation = {y1: 0, x2: 0, y2: 0, x1: 0};\n private _areaRect = domRect();\n\n // If a single click is being performed, it's a single-click until the user dragged the mouse\n private _singleClick = true;\n private _frame: Frames;\n\n // Required data for scrolling\n private _scrollAvailable = true;\n private _scrollingActive = false;\n private _scrollSpeed: Coordinates = {x: 0, y: 0};\n private _scrollDelta: Coordinates = {x: 0, y: 0};\n\n // Required for keydown scrolling\n private _lastMousePosition = {x: 0, y: 0};\n\n constructor(opt: PartialSelectionOptions) {\n super();\n\n this._options = {\n selectionAreaClass: 'selection-area',\n selectionContainerClass: undefined,\n selectables: [],\n document: window.document,\n startAreas: ['html'],\n boundaries: ['html'],\n container: 'body',\n ...opt,\n\n behaviour: {\n overlap: 'invert',\n intersect: 'touch',\n triggers: [0],\n ...opt.behaviour,\n startThreshold: opt.behaviour?.startThreshold ?\n typeof opt.behaviour.startThreshold === 'number' ?\n opt.behaviour.startThreshold :\n {x: 10, y: 10, ...opt.behaviour.startThreshold} : {x: 10, y: 10},\n scrolling: {\n speedDivider: 10,\n manualSpeed: 750,\n ...opt.behaviour?.scrolling,\n startScrollMargins: {\n x: 0,\n y: 0,\n ...opt.behaviour?.scrolling?.startScrollMargins,\n }\n }\n },\n\n features: {\n range: true,\n touch: true,\n deselectOnBlur: false,\n ...opt.features,\n singleTap: {\n allow: true,\n intersect: 'native',\n ...opt.features?.singleTap,\n }\n }\n };\n\n // Initialize scroll controller\n this._scrollController = {\n getScrollPosition: opt.scrollController?.getScrollPosition || defaultScrollController.getScrollPosition,\n setScrollPosition: opt.scrollController?.setScrollPosition || defaultScrollController.setScrollPosition,\n getScrollSize: opt.scrollController?.getScrollSize || defaultScrollController.getScrollSize,\n getClientSize: opt.scrollController?.getClientSize || defaultScrollController.getClientSize,\n alwaysScroll: opt.scrollController?.alwaysScroll || defaultScrollController.alwaysScroll\n };\n\n // Bind locale functions to instance\n /* eslint-disable @typescript-eslint/no-explicit-any */\n for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) {\n if (typeof (this as any)[key] === 'function') {\n (this as any)[key] = (this as any)[key].bind(this);\n }\n }\n\n const {document, selectionAreaClass, selectionContainerClass} = this._options;\n this._area = document.createElement('div');\n this._clippingElement = document.createElement('div');\n this._clippingElement.appendChild(this._area);\n\n this._area.classList.add(selectionAreaClass);\n\n if (selectionContainerClass) {\n this._clippingElement.classList.add(selectionContainerClass);\n }\n\n css(this._area, {\n willChange: 'top, left, bottom, right, width, height',\n top: 0,\n left: 0,\n position: 'fixed'\n });\n\n css(this._clippingElement, {\n overflow: 'hidden',\n position: 'fixed',\n transform: 'translate3d(0, 0, 0)', // https://stackoverflow.com/a/38268846\n pointerEvents: 'none',\n zIndex: '1'\n });\n\n this._frame = frames((evt: MouseEvent | TouchEvent) => {\n this._recalculateSelectionAreaRect();\n this._updateElementSelection();\n this._emitEvent('move', evt);\n this._redrawSelectionArea();\n });\n\n this.enable();\n }\n\n _toggleStartEvents(activate = true): void {\n const {document, features} = this._options;\n const fn = activate ? on : off;\n\n fn(document, 'mousedown', this._onTapStart);\n\n if (features.touch) {\n fn(document, 'touchstart', this._onTapStart, {passive: false});\n }\n }\n\n _onTapStart(evt: MouseEvent | TouchEvent, silent = false): void {\n const {x, y, target} = simplifyEvent(evt);\n const {document, startAreas, boundaries, features, behaviour} = this._options;\n const targetBoundingClientRect = target.getBoundingClientRect();\n\n if (evt instanceof MouseEvent && !matchesTrigger(evt, behaviour.triggers)) {\n return;\n }\n\n // Find start-areas and boundaries\n const resolvedStartAreas = selectAll(startAreas, document);\n const resolvedBoundaries = selectAll(boundaries, document);\n\n // Check in which container the user currently acts\n this._targetElement = resolvedBoundaries.find(el =>\n intersects(el.getBoundingClientRect(), targetBoundingClientRect)\n );\n\n // Check if the area starts in one of the start areas / boundaries\n const evtPath = evt.composedPath();\n const targetStartArea = resolvedStartAreas.find(el => evtPath.includes(el));\n this._targetBoundary = resolvedBoundaries.find(el => evtPath.includes(el));\n\n if (!this._targetElement || !targetStartArea || !this._targetBoundary) {\n return;\n }\n\n if (!silent && this._emitEvent('beforestart', evt) === false) {\n return;\n }\n\n this._areaLocation = {x1: x, y1: y, x2: 0, y2: 0};\n\n // Lock scrolling in the target container\n const scrollElement = document.scrollingElement ?? document.body;\n const scrollPosition = this._scrollController.getScrollPosition(scrollElement);\n this._scrollDelta = {x: scrollPosition.x, y: scrollPosition.y};\n\n // To detect single-click\n this._singleClick = true;\n this.clearSelection(false, true);\n\n on(document, ['touchmove', 'mousemove'], this._delayedTapMove, {passive: false});\n on(document, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop);\n on(document, 'scroll', this._onScroll);\n\n if (features.deselectOnBlur) {\n this._targetBoundaryScrolled = false;\n on(this._targetBoundary, 'scroll', this._onStartAreaScroll);\n }\n }\n\n _onSingleTap(evt: MouseEvent | TouchEvent): void {\n const {singleTap: {intersect}, range} = this._options.features;\n const e = simplifyEvent(evt);\n let target;\n\n if (intersect === 'native') {\n target = e.target;\n } else if (intersect === 'touch') {\n this.resolveSelectables();\n\n const {x, y} = e;\n target = this._selectables.find(v => {\n const {right, left, top, bottom} = v.getBoundingClientRect();\n return x < right && x > left && y < bottom && y > top;\n });\n }\n\n if (!target) {\n return;\n }\n\n /**\n * Resolve selectables again.\n * If the user started in a scrollable area, they will be reduced\n * to the current area. Prevent the exclusion of these if a range-selection\n * gets performed.\n */\n this.resolveSelectables();\n\n // Traverse dom upwards to check if the target is selectable\n while (!this._selectables.includes(target)) {\n if (target.parentElement) {\n target = target.parentElement;\n } else {\n if (!this._targetBoundaryScrolled) {\n this.clearSelection();\n }\n\n return;\n }\n\n }\n\n // Grab the current store first in case it gets set back\n const {stored} = this._selection;\n this._emitEvent('start', evt);\n\n if (evt.shiftKey && range && this._latestElement) {\n const reference = this._latestElement;\n\n // Resolve the correct range\n const [preceding, following] = reference.compareDocumentPosition(target) & 4 ?\n [target, reference] : [reference, target];\n\n const rangeItems = [...this._selectables.filter(el =>\n (el.compareDocumentPosition(preceding) & 4) &&\n (el.compareDocumentPosition(following) & 2)\n ), preceding, following];\n\n this.select(rangeItems);\n this._latestElement = reference; // the latestElement is by default cleared in .select()\n } else if (\n stored.includes(target) && (\n stored.length === 1 || evt.ctrlKey ||\n stored.every(v => this._selection.stored.includes(v))\n )\n ) {\n this.deselect(target);\n } else {\n this.select(target);\n this._latestElement = target;\n }\n }\n\n _delayedTapMove(evt: MouseEvent | TouchEvent): void {\n const {container, document, behaviour: {startThreshold}} = this._options;\n const {x1, y1} = this._areaLocation; // Coordinates of the first \"tap\"\n const {x, y} = simplifyEvent(evt);\n\n // Check the pixel threshold\n if (\n\n // Single number for both coordinates\n (typeof startThreshold === 'number' && abs((x + y) - (x1 + y1)) >= startThreshold) ||\n\n // Different x and y threshold\n (typeof startThreshold === 'object' && abs(x - x1) >= (startThreshold as Coordinates).x || abs(y - y1) >= (startThreshold as Coordinates).y)\n ) {\n off(document, ['mousemove', 'touchmove'], this._delayedTapMove, {passive: false});\n\n if (this._emitEvent('beforedrag', evt) === false) {\n off(document, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop);\n return;\n }\n\n on(document, ['mousemove', 'touchmove'], this._onTapMove, {passive: false});\n\n // Make area element visible\n css(this._area, 'display', 'block');\n\n // Append selection-area to the dom\n selectAll(container, document)[0].appendChild(this._clippingElement);\n\n this.resolveSelectables();\n\n // An action is recognized as single-select until the user performed a multi-selection\n this._singleClick = false;\n\n // Just saving the boundaries of this container for later\n this._targetRect = this._targetElement!.getBoundingClientRect();\n\n // Find a container and check if it's scrollable\n const targetElement = this._targetElement as Element;\n const scrollSize = this._scrollController.getScrollSize(targetElement);\n const clientSize = this._scrollController.getClientSize(targetElement);\n \n this._scrollAvailable =\n scrollSize.height !== clientSize.height ||\n scrollSize.width !== clientSize.width;\n\n if (this._scrollAvailable) {\n\n // Detect mouse scrolling\n on(this._targetElement, 'wheel', this._wheelScroll, {passive: false});\n\n // Detect keyboard scrolling\n on(this._options.document, 'keydown', this._keyboardScroll, {passive: false});\n\n\n /**\n * The selection-area will also cover another element\n * out of the current scrollable parent. So find all elements\n * that are in the current scrollable element. Now these are\n * the only selectables instead of all.\n */\n this._selectables = this._selectables.filter(s => this._targetElement!.contains(s));\n }\n\n // Re-setup selection area and fire event\n this._setupSelectionArea();\n this._emitEvent('start', evt);\n this._onTapMove(evt);\n }\n\n this._handleMoveEvent(evt);\n }\n\n _setupSelectionArea(): void {\n const {_clippingElement, _targetElement, _area} = this;\n const tr = this._targetRect = _targetElement!.getBoundingClientRect();\n\n if (this._scrollAvailable) {\n\n /**\n * To clip the area, the selection area has a parent\n * which has exactly the same dimensions as the scrollable element.\n * Now if the area exceeds these boundaries, it will be cropped.\n */\n css(_clippingElement, {\n top: tr.top,\n left: tr.left,\n width: tr.width,\n height: tr.height\n });\n\n /**\n * The area element is relative to the clipping element,\n * but when this is moved or transformed, we need to correct\n * the positions via a negative margin.\n */\n css(_area, {\n marginTop: -tr.top,\n marginLeft: -tr.left\n });\n } else {\n\n // \"Reset\" styles\n css(_clippingElement, {\n top: 0,\n left: 0,\n width: '100%',\n height: '100%'\n });\n\n css(_area, {\n marginTop: 0,\n marginLeft: 0\n });\n }\n }\n\n _onTapMove(evt: MouseEvent | TouchEvent): void {\n const {_scrollSpeed, _areaLocation, _options, _frame} = this;\n const {speedDivider} = _options.behaviour.scrolling;\n const _targetElement = this._targetElement as Element;\n\n const {x, y} = simplifyEvent(evt);\n _areaLocation.x2 = x;\n _areaLocation.y2 = y;\n\n this._lastMousePosition.x = x;\n this._lastMousePosition.y = y;\n\n if (this._scrollAvailable && !this._scrollingActive && (_scrollSpeed.y || _scrollSpeed.x)) {\n\n // Continuous scrolling\n this._scrollingActive = true;\n\n const scroll = () => {\n if (!_scrollSpeed.x && !_scrollSpeed.y) {\n this._scrollingActive = false;\n return;\n }\n\n // Reduce velocity, use ceil in both directions to scroll at least 1px per frame\n const {x: currentScrollPositionX, y: currentScrollPositionY} = this._scrollController.getScrollPosition(_targetElement);\n const newScrollPosition: Partial<Coordinates> = {};\n\n if (_scrollSpeed.y) {\n newScrollPosition.y = currentScrollPositionY + ceil(_scrollSpeed.y / speedDivider);\n this._scrollController.setScrollPosition(_targetElement, newScrollPosition);\n const updatedPosition = this._scrollController.getScrollPosition(_targetElement);\n _areaLocation.y1 -= updatedPosition.y - currentScrollPositionY;\n }\n\n if (_scrollSpeed.x) {\n newScrollPosition.x = currentScrollPositionX + ceil(_scrollSpeed.x / speedDivider);\n this._scrollController.setScrollPosition(_targetElement, newScrollPosition);\n const updatedPosition = this._scrollController.getScrollPosition(_targetElement);\n _areaLocation.x1 -= updatedPosition.x - currentScrollPositionX;\n }\n\n /**\n * We changed the start coordinates -> redraw the selection-area\n * We changed the dimensions of the area element -> re-calc selected elements\n * The selected elements array has been changed -> fire event\n */\n _frame.next(evt);\n\n // Keep scrolling even if the user stops to move his pointer\n requestAnimationFrame(scroll);\n };\n\n requestAnimationFrame(scroll);\n } else {\n\n /**\n * Perform redrawing only if scrolling is not active.\n * If scrolling is active, this area is getting re-dragged by the\n * anonymize scroll function.\n */\n _frame.next(evt);\n }\n\n this._handleMoveEvent(evt);\n }\n\n _handleMoveEvent(evt: MouseEvent | TouchEvent) {\n const {features} = this._options;\n\n /**\n * - Prevent auto-refresh for when pulling down on touch devices.\n * - Prevent auto-scroll by the browser when on safari, and scrolling is handled by this library.\n */\n if ((features.touch && isTouchDevice()) || (this._scrollAvailable && isSafariBrowser())) {\n evt.preventDefault(); // Prevent swipe-down refresh\n }\n }\n\n _onScroll(): void {\n const {_scrollDelta, _options: {document}} = this;\n const scrollElement = document.scrollingElement ?? document.body;\n const scrollPosition = this._scrollController.getScrollPosition(scrollElement);\n\n // Adjust area start location\n this._areaLocation.x1 += _scrollDelta.x - scrollPosition.x;\n this._areaLocation.y1 += _scrollDelta.y - scrollPosition.y;\n _scrollDelta.x = scrollPosition.x;\n _scrollDelta.y = scrollPosition.y;\n\n // The area needs to be set back as the target-container has changed in its position\n this._setupSelectionArea();\n this._frame.next(null);\n }\n\n _onStartAreaScroll(): void {\n this._targetBoundaryScrolled = true;\n off(this._targetElement, 'scroll', this._onStartAreaScroll);\n }\n\n _wheelScroll(evt: ScrollEvent): void {\n const {manualSpeed} = this._options.behaviour.scrolling;\n\n // Consistent scrolling speed on all browsers\n const deltaY = evt.deltaY ? (evt.deltaY > 0 ? 1 : -1) : 0;\n const deltaX = evt.deltaX ? (evt.deltaX > 0 ? 1 : -1) : 0;\n this._scrollSpeed.y += deltaY * manualSpeed;\n this._scrollSpeed.x += deltaX * manualSpeed;\n this._onTapMove(evt);\n\n // Prevent default scrolling behavior, e.g. page scrolling\n evt.preventDefault();\n }\n\n _keyboardScroll(evt: KeyboardEvent): void {\n const {manualSpeed} = this._options.behaviour.scrolling;\n\n const deltaX = evt.key === 'ArrowLeft' ? -1 : evt.key === 'ArrowRight' ? 1 : 0;\n const deltaY = evt.key === 'ArrowUp' ? -1 : evt.key === 'ArrowDown' ? 1 : 0;\n\n this._scrollSpeed.x += Math.sign(deltaX) * manualSpeed;\n this._scrollSpeed.y += Math.sign(deltaY) * manualSpeed;\n\n evt.preventDefault();\n\n this._onTapMove({\n clientX: this._lastMousePosition.x,\n clientY: this._lastMousePosition.y,\n preventDefault: () => void 0,\n } as ScrollEvent);\n }\n\n _recalculateSelectionAreaRect(): void {\n const {_scrollSpeed, _areaLocation, _targetElement, _options} = this;\n const _targetRect = this._targetRect as DOMRect;\n const scrollPosition = this._scrollController.getScrollPosition(_targetElement as Element);\n const clientSize = this._scrollController.getClientSize(_targetElement as Element);\n const scrollSize = this._scrollController.getScrollSize(_targetElement as Element);\n const alwaysScroll = this._scrollController.alwaysScroll;\n\n const {x1, y1} = _areaLocation;\n let {x2, y2} = _areaLocation;\n\n const {behaviour: {scrolling: {startScrollMargins}}} = _options;\n\n if (x2 < _targetRect.left + startScrollMargins.x) {\n _scrollSpeed.x = (scrollPosition.x || alwaysScroll) ? -abs(_targetRect.left - x2 + startScrollMargins.x) : 0;\n x2 = x2 < _targetRect.left ? _targetRect.left : x2;\n } else if (x2 > _targetRect.right - startScrollMargins.x) {\n _scrollSpeed.x = (scrollSize.width - scrollPosition.x - clientSize.width || alwaysScroll) ? abs(_targetRect.left + _targetRect.width - x2 - startScrollMargins.x) : 0;\n x2 = x2 > _targetRect.right ? _targetRect.right : x2;\n } else {\n _scrollSpeed.x = 0;\n }\n\n if (y2 < _targetRect.top + startScrollMargins.y) {\n _scrollSpeed.y = (scrollPosition.y || alwaysScroll) ? -abs(_targetRect.top - y2 + startScrollMargins.y) : 0;\n y2 = y2 < _targetRect.top ? _targetRect.top : y2;\n } else if (y2 > _targetRect.bottom - startScrollMargins.y) {\n _scrollSpeed.y = (scrollSize.height - scrollPosition.y - clientSize.height || alwaysScroll) ? abs(_targetRect.top + _targetRect.height - y2 - startScrollMargins.y) : 0;\n y2 = y2 > _targetRect.bottom ? _targetRect.bottom : y2;\n } else {\n _scrollSpeed.y = 0;\n }\n\n const x3 = min(x1, x2);\n const y3 = min(y1, y2);\n const x4 = max(x1, x2);\n const y4 = max(y1, y2);\n\n this._areaRect = domRect(x3, y3, x4 - x3, y4 - y3);\n }\n\n _redrawSelectionArea(): void {\n const {x, y, width, height} = this._areaRect;\n const {style} = this._area;\n\n // Using transform will make the area's borders look blurry\n style.left = `${x}px`;\n style.top = `${y}px`;\n style.width = `${width}px`;\n style.height = `${height}px`;\n }\n\n _onTapStop(evt: MouseEvent | TouchEvent | null, silent: boolean): void {\n const {document, features} = this._options;\n const {_singleClick} = this;\n\n // Remove event handlers\n off(this._targetElement, 'scroll', this._onStartAreaScroll);\n off(document, ['mousemove', 'touchmove'], this._delayedTapMove);\n off(document, ['touchmove', 'mousemove'], this._onTapMove);\n off(document, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop);\n off(document, 'scroll', this._onScroll);\n\n // Keep selection until the next time\n this._keepSelection();\n\n if (evt && _singleClick && features.singleTap.allow) {\n this._onSingleTap(evt);\n } else if (!_singleClick && !silent) {\n this._updateElementSelection();\n this._emitEvent('stop', evt);\n }\n\n this._scrollSpeed.x = 0;\n this._scrollSpeed.y = 0;\n\n // Unbind mouse scrolling listener\n off(this._targetElement, 'wheel', this._wheelScroll, {passive: true});\n\n // Unbind keyboard scrolling listener\n off(this._options.document, 'keydown', this._keyboardScroll, {passive: true,});\n\n // Remove selection-area from dom\n this._clippingElement.remove();\n\n // Cancel current frame\n this._frame?.cancel();\n\n // Hide selection area\n css(this._area, 'display', 'none');\n }\n\n _updateElementSelection(): void {\n const {_selectables, _options, _selection, _areaRect} = this;\n const {stored, selected, touched} = _selection;\n const {intersect, overlap} = _options.behaviour;\n\n const invert = overlap === 'invert';\n const newlyTouched: Element[] = [];\n const added: Element[] = [];\n const removed: Element[] = [];\n\n // Find newly selected elements\n for (let i = 0; i < _selectables.length; i++) {\n const node = _selectables[i];\n\n // Check if the area intersects an element\n if (intersects(_areaRect, node.getBoundingClientRect(), intersect)) {\n\n // Check if the element wasn't present in the last selection.\n if (!selected.includes(node)) {\n\n // Check if the user wants to invert the selection for already selected elements\n if (invert && stored.includes(node)) {\n removed.push(node);\n continue;\n } else {\n added.push(node);\n }\n } else if (stored.includes(node) && !touched.includes(node)) {\n touched.push(node);\n }\n\n newlyTouched.push(node);\n }\n }\n\n // Re-select elements which were previously stored\n if (invert) {\n added.push(...stored.filter(v => !selected.includes(v)));\n }\n\n // Check which elements where removed since last selection\n const keep = overlap === 'keep';\n for (let i = 0; i < selected.length; i++) {\n const node = selected[i];\n\n if (!newlyTouched.includes(node) && !(\n\n // Check if the user wants to keep previously selected elements, e.g.,\n // not make them part of the current selection as soon as they're touched.\n keep && stored.includes(node)\n )) {\n removed.push(node);\n }\n }\n\n _selection.selected = newlyTouched;\n _selection.changed = {added, removed};\n\n // Prevent range selection when selection an area.\n this._latestElement = undefined;\n }\n\n _emitEvent(name: keyof SelectionEvents, evt: MouseEvent | TouchEvent | null): unknown {\n return this.emit(name, {\n event: evt,\n store: this._selection,\n selection: this\n });\n }\n\n _keepSelection(): void {\n const {_options, _selection} = this;\n const {selected, changed, touched, stored} = _selection;\n const addedElements = selected.filter(el => !stored.includes(el));\n\n switch (_options.behaviour.overlap) {\n case 'drop': {\n _selection.stored = [\n ...addedElements,\n ...stored.filter(el => !touched.includes(el)) // Elements not touched\n ];\n break;\n }\n case 'invert': {\n _selection.stored = [\n ...addedElements,\n ...stored.filter(el => !changed.removed.includes(el)) // Elements not removed from selection\n ];\n break;\n }\n case 'keep': {\n _selection.stored = [\n ...stored,\n ...selected.filter(el => !stored.includes(el)) // Newly added\n ];\n break;\n }\n }\n }\n\n /**\n * Manually triggers the start of a selection\n * @param evt A MouseEvent / TouchEvent-like object\n * @param silent If beforestart should be fired\n */\n trigger(evt: MouseEvent | TouchEvent, silent = true): void {\n this._onTapStart(evt, silent);\n }\n\n /**\n * Can be used if during a selection elements have been added\n * Will update everything that can be selected\n */\n resolveSelectables(): void {\n this._selectables = selectAll(this._options.selectables, this._options.document);\n }\n\n /**\n * Same as deselecting, but for all elements currently selected\n * @param includeStored If the store should also get cleared\n * @param quiet If move / stop events should be fired\n */\n clearSelection(includeStored = true, quiet = false): void {\n const {selected, stored, changed} = this._selection;\n\n changed.added = [];\n changed.removed.push(\n ...selected,\n ...(includeStored ? stored : [])\n );\n\n // Fire event\n if (!quiet) {\n this._emitEvent('move', null);\n this._emitEvent('stop', null);\n }\n\n // Reset state\n this._selection = makeSelectionStore(includeStored ? [] : stored);\n }\n\n /**\n * @returns {Array} Selected elements\n */\n getSelection(): Element[] {\n return this._selection.stored;\n }\n\n /**\n * @returns {HTMLElement} The selection area element\n */\n getSelectionArea(): HTMLElement {\n return this._area;\n }\n\n /**\n * @returns {Element[]} Available selectable elements for current selection\n */\n getSelectables(): Element[] {\n return this._selectables;\n }\n\n /**\n * Set the location of the selection area\n * @param location A partial AreaLocation object\n */\n setAreaLocation(location: Partial<AreaLocation>) {\n Object.assign(this._areaLocation, location);\n this._redrawSelectionArea();\n }\n\n /**\n * @returns {AreaLocation} The current location of the selection area\n */\n getAreaLocation(): AreaLocation {\n return this._areaLocation;\n }\n\n /**\n * Cancel the current selection process, pass true to fire a stop event after cancel\n * @param keepEvent If a stop event should be fired\n */\n cancel(keepEvent = false): void {\n this._onTapStop(null, !keepEvent);\n }\n\n /**\n * Unbinds all events and removes the area-element.\n */\n destroy(): void {\n this.cancel();\n this.disable();\n this._clippingElement.remove();\n super.unbindAllListeners();\n }\n\n /**\n * Enable selecting elements\n */\n enable = this._toggleStartEvents;\n\n /**\n * Disable selecting elements\n */\n disable = this._toggleStartEvents.bind(this, false);\n\n /**\n * Adds elements to the selection\n * @param query CSS Query, can be an array of queries\n * @param quiet If this should not trigger the move event\n */\n select(query: SelectAllSelectors, quiet = false): Element[] {\n const {changed, selected, stored} = this._selection;\n const elements = selectAll(query, this._options.document).filter(el =>\n !selected.includes(el) &&\n !stored.includes(el)\n );\n\n // Update element lists\n stored.push(...elements);\n selected.push(...elements);\n changed.added.push(...elements);\n changed.removed = [];\n\n // We don't know which element was \"selected\" first, so clear it\n this._latestElement = undefined;\n\n // Fire event\n if (!quiet) {\n this._emitEvent('move', null);\n this._emitEvent('stop', null);\n }\n\n return elements;\n }\n\n /**\n * Removes a particular element from the selection\n * @param query CSS Query, can be an array of queries\n * @param quiet If this should not trigger the move event\n */\n deselect(query: SelectAllSelectors, quiet = false) {\n const {selected, stored, changed} = this._selection;\n\n const elements = selectAll(query, this._options.document).filter(el =>\n selected.includes(el) ||\n stored.includes(el)\n );\n\n this._selection.stored = stored.filter(el => !elements.includes(el));\n this._selection.selected = selected.filter(el => !elements.includes(el));\n this._selection.changed.added = [];\n this._selection.changed.removed.push(\n ...elements.filter(el => !changed.removed.includes(el))\n );\n\n // We don't know which element was \"selected\" first, so clear it\n this._latestElement = undefined;\n\n // Fire event\n if (!quiet) {\n this._emitEvent('move', null);\n this._emitEvent('stop', null);\n }\n }\n}\n"],"names":["EventTarget","event","cb","set","_a","data","ok","unitify","val","unit","css","style","attr","key","value","domRect","x","y","width","height","rect","frames","fn","previousArgs","frameId","lock","args","intersects","a","b","mode","bxc","byc","isTouchDevice","isSafariBrowser","arrayify","eventListener","method","items","events","options","el","ev","on","off","simplifyEvent","evt","clientX","clientY","target","selectAll","selector","doc","item","matchesTrigger","triggers","trigger","modifier","abs","max","min","ceil","makeSelectionStore","stored","defaultScrollController","element","position","_SelectionArea","opt","_b","_c","_d","_e","_f","_g","_h","_i","_j","document","selectionAreaClass","selectionContainerClass","activate","features","silent","startAreas","boundaries","behaviour","targetBoundingClientRect","resolvedStartAreas","resolvedBoundaries","evtPath","targetStartArea","scrollElement","scrollPosition","intersect","range","e","v","right","left","top","bottom","reference","preceding","following","rangeItems","container","startThreshold","x1","y1","targetElement","scrollSize","clientSize","s","_clippingElement","_targetElement","_area","tr","_scrollSpeed","_areaLocation","_options","_frame","speedDivider","scroll","currentScrollPositionX","currentScrollPositionY","newScrollPosition","updatedPosition","_scrollDelta","manualSpeed","deltaY","deltaX","_targetRect","alwaysScroll","x2","y2","startScrollMargins","x3","y3","x4","y4","_singleClick","_selectables","_selection","_areaRect","selected","touched","overlap","invert","newlyTouched","added","removed","i","node","keep","name","changed","addedElements","includeStored","quiet","location","keepEvent","query","elements","SelectionArea"],"mappings":"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