diff --git a/docs/demos/DemoShadowRoot.vue b/docs/demos/DemoShadowRoot.vue new file mode 100644 index 0000000..77411fc --- /dev/null +++ b/docs/demos/DemoShadowRoot.vue @@ -0,0 +1,119 @@ + + + + + \ No newline at end of file diff --git a/docs/demos/DemoShadowRootTransformed.vue b/docs/demos/DemoShadowRootTransformed.vue new file mode 100644 index 0000000..ef54cd5 --- /dev/null +++ b/docs/demos/DemoShadowRootTransformed.vue @@ -0,0 +1,249 @@ + + + + + \ No newline at end of file diff --git a/docs/pages/api-reference.md b/docs/pages/api-reference.md index 360a1b0..6f7b0ad 100644 --- a/docs/pages/api-reference.md +++ b/docs/pages/api-reference.md @@ -304,7 +304,7 @@ interface SelectionOptions { selectionAreaClass: string; selectionContainerClass: string | undefined; container: Quantify; - document: Document; + document: Document | ShadowRoot; selectables: Quantify; startAreas: Quantify; boundaries: Quantify; @@ -317,9 +317,12 @@ interface SelectionOptions { Type of what can be passed to the `SelectionArea` constructor. +> [!NOTE] +> The `document` property supports both `Document` and `ShadowRoot` types, allowing you to use Viselect within shadow DOM components. Note that while ShadowRoot is used for querying elements, element creation still happens through the Document interface. + ```typescript type PartialSelectionOptions = DeepPartial> & { - document?: Document; + document?: Document | ShadowRoot; }; ``` diff --git a/docs/pages/examples.md b/docs/pages/examples.md index 3e707c5..bb3a94e 100644 --- a/docs/pages/examples.md +++ b/docs/pages/examples.md @@ -6,32 +6,260 @@ outline: deep # Examples -> [!TIP] -> This document provides a few examples of how this library can be used in practice. -> Head over to [Quickstart](./quickstart.md) to get started! +This page contains various examples of how to use Viselect in different scenarios. -### Simple Selection +## Basic Usage -A single, simple box with a standard behaviour as you know it from your trusty file explorer. -Press `Ctrl` / `Cmd` to select multiple items, or deselect items by clicking on them again. -Press `Shift` to select a range of items after selecting one ore more before. +The most basic usage involves selecting elements within a container: > You can find the source code for this example [here](https://github.com/simonwep/viselect/blob/master/docs/demos/DemoSimple.vue). -> Under [Quickstart](./quickstart.md) you can find a step-by-step guide on how to create this example from scratch! -### Scrollable Container +```ts +import SelectionArea from '@viselect/vanilla'; -A container that can be scrolled in both directions. -The selection area will automatically adjust to the scroll position and size of the container. +const selection = new SelectionArea({ + selectables: ['.item'], + boundaries: ['.container'] +}); + +selection.on('move', ({ store: { changed: { added, removed } } }) => { + added.forEach(el => el.classList.add('selected')); + removed.forEach(el => el.classList.remove('selected')); +}); +``` + +## Shadow Root Support + +Viselect fully supports shadow DOM. You can pass a `ShadowRoot` instance as the `document` option. This is particularly useful for web components and encapsulated UI elements. + + + +> You can find the source code for this example [here](https://github.com/simonwep/viselect/blob/master/docs/demos/DemoShadowRoot.vue). + +## Shadow DOM with Transformed Elements + +Viselect supports shadow DOM with transformed/zoomed elements. This is particularly useful for applications with complex layouts where elements can be transformed and overflow their containers. + + + +> You can find the source code for this example [here](https://github.com/simonwep/viselect/blob/master/docs/demos/DemoShadowRootTransformed.vue). + +### Key Points for Shadow Root Usage: + +1. **Pass the ShadowRoot**: Use the `document` option to specify the shadow root +2. **Automatic Defaults**: The library automatically adjusts defaults for shadow DOM contexts +3. **Element Creation**: ShadowRoot is used for querying, but element creation still happens through Document +4. **Event Handling**: Events are properly bound to both shadow root and shadow host for seamless interaction + +```ts +import SelectionArea from '@viselect/vanilla'; + +// Create a shadow root +const host = document.getElementById('my-host'); +const shadowRoot = host.attachShadow({ mode: 'open' }); + +// Add content to shadow root +shadowRoot.innerHTML = ` + +
+
+
+
+
+`; + +// Initialize selection within shadow root +const selection = new SelectionArea({ + selectables: ['.item'], + boundaries: ['.container'], + // startAreas is optional - defaults are automatically adjusted for shadow DOM + document: shadowRoot, // Pass the shadow root here + selectionAreaClass: 'selection-area' +}); + +selection.on('move', ({ store: { changed: { added, removed } } }) => { + added.forEach(el => el.classList.add('selected')); + removed.forEach(el => el.classList.remove('selected')); +}); +``` + +## Multiple Selection Areas + +You can have multiple selection areas on the same page: + +```ts +import SelectionArea from '@viselect/vanilla'; + +// First selection area +const selection1 = new SelectionArea({ + selectables: ['.area1 .item'], + boundaries: ['.area1'], + selectionAreaClass: 'selection-area-1' +}); + +// Second selection area +const selection2 = new SelectionArea({ + selectables: ['.area2 .item'], + boundaries: ['.area2'], + selectionAreaClass: 'selection-area-2' +}); + +// Handle events for both +[selection1, selection2].forEach(selection => { + selection.on('move', ({ store: { changed: { added, removed } } }) => { + added.forEach(el => el.classList.add('selected')); + removed.forEach(el => el.classList.remove('selected')); + }); +}); +``` + +## Custom Triggers + +You can customize which mouse buttons trigger selection: + +```ts +import SelectionArea from '@viselect/vanilla'; + +const selection = new SelectionArea({ + selectables: ['.item'], + boundaries: ['.container'], + behaviour: { + // Only right-click triggers selection + triggers: [2], + // Or require modifier keys + // triggers: [{ button: 0, modifiers: ['ctrl'] }] + } +}); +``` + +## Touch Support + +Viselect has built-in touch support for mobile devices: + +```ts +import SelectionArea from '@viselect/vanilla'; + +const selection = new SelectionArea({ + selectables: ['.item'], + boundaries: ['.container'], + features: { + touch: true, // Enable touch support (default: true) + singleTap: { + allow: true, // Allow single tap selection + intersect: 'touch' // Use touch intersection instead of native + } + } +}); +``` + +## Scrollable Containers + +Viselect automatically handles scrollable containers. Try scrolling and selecting in the demo below: -> You can find the source code for this example [here](https://github.com/simonwep/viselect/blob/master/docs/demos/DemoScrollable.vue) +> You can find the source code for this example [here](https://github.com/simonwep/viselect/blob/master/docs/demos/DemoScrollable.vue). + +```ts +import SelectionArea from '@viselect/vanilla'; + +const selection = new SelectionArea({ + selectables: ['.item'], + boundaries: ['.scrollable-container'], + behaviour: { + scrolling: { + speedDivider: 10, // Scroll speed divisor + manualSpeed: 750, // Manual scroll speed + startScrollMargins: { x: 20, y: 20 } // Margins to start scrolling + } + } +}); +``` + +## Range Selection + +Enable range selection with Shift+Click: + +```ts +import SelectionArea from '@viselect/vanilla'; + +const selection = new SelectionArea({ + selectables: ['.item'], + boundaries: ['.container'], + features: { + range: true // Enable range selection (default: true) + } +}); + +selection.on('start', ({ store, event }) => { + // Clear previous selection unless modifier key is pressed + if (!event.ctrlKey && !event.metaKey && !event.shiftKey) { + store.stored.forEach(el => el.classList.remove('selected')); + selection.clearSelection(); + } +}); +``` + +## Custom Overlap Behavior + +Control how overlapping selections behave: + +```ts +import SelectionArea from '@viselect/vanilla'; + +const selection = new SelectionArea({ + selectables: ['.item'], + boundaries: ['.container'], + behaviour: { + overlap: 'invert' // 'invert', 'keep', or 'drop' + } +}); +``` + +## Event Handling + +Complete example with all events: + +```ts +import SelectionArea from '@viselect/vanilla'; + +const selection = new SelectionArea({ + selectables: ['.item'], + boundaries: ['.container'] +}); + +selection + .on('beforestart', ({ event }) => { + console.log('About to start selection', event); + // Return false to cancel selection + }) + .on('beforedrag', ({ event }) => { + console.log('About to start dragging', event); + // Return false to cancel drag + }) + .on('start', ({ store, event }) => { + console.log('Selection started', store, event); + }) + .on('move', ({ store: { changed: { added, removed } } }) => { + console.log('Selection changed', { added, removed }); + added.forEach(el => el.classList.add('selected')); + removed.forEach(el => el.classList.remove('selected')); + }) + .on('stop', ({ store, event }) => { + console.log('Selection stopped', store, event); + }); +``` diff --git a/docs/pages/quickstart.md b/docs/pages/quickstart.md index 068ed47..86db7fc 100644 --- a/docs/pages/quickstart.md +++ b/docs/pages/quickstart.md @@ -201,7 +201,9 @@ const selection = new SelectionArea({ container: 'body', // document object - if you want to use it within an embed document (or iframe). - // If you're inside of a shadow-dom make sure to specify the shadow root here. + // For shadow DOM support, pass the ShadowRoot instance here. + // Both Document and ShadowRoot are supported. + // When using ShadowRoot, the library automatically adjusts defaults for shadow DOM contexts. document: window.document, // Query selectors for elements which can be selected. diff --git a/packages/vanilla/src/index.ts b/packages/vanilla/src/index.ts index 16f9237..570f909 100644 --- a/packages/vanilla/src/index.ts +++ b/packages/vanilla/src/index.ts @@ -23,6 +23,36 @@ const makeSelectionStore = (stored: Element[] = []): SelectionStore => ({ changed: {added: [], removed: []} }); +// Helper function to get the scrolling element from a document or shadow root +const getScrollingElement = (doc: Document | ShadowRoot): Element => { + if ('scrollingElement' in doc && doc.scrollingElement) { + return doc.scrollingElement; + } + + // For shadow roots, we need to find the scrolling element within the shadow DOM + if (doc instanceof ShadowRoot) { + // Try to find a scrollable element within the shadow root + const scrollableElement = doc.querySelector('[style*="overflow"], [style*="scroll"]') as Element; + if (scrollableElement) { + return scrollableElement; + } + // Fallback to the host element + return doc.host; + } + + // For regular documents, fallback to body + return doc.body; +}; + +// Helper function to get the document from a document or shadow root +const getDocument = (doc: Document | ShadowRoot): Document => { + if (doc instanceof Document) { + return doc; + } + // For shadow roots, get the document from the host element + return doc.host.ownerDocument || window.document; +}; + export default class SelectionArea extends EventTarget { public static version = VERSION; @@ -108,6 +138,21 @@ export default class SelectionArea extends EventTarget { } }; + // Special handling for shadow roots - adjust defaults if not explicitly set + if (this._options.document instanceof ShadowRoot) { + // If startAreas is still the default 'html', replace with shadow root host + const startAreasArray = Array.isArray(this._options.startAreas) ? this._options.startAreas : [this._options.startAreas]; + if (startAreasArray.length === 1 && startAreasArray[0] === 'html') { + this._options.startAreas = [this._options.document.host as HTMLElement]; + } + + // If boundaries is still the default 'html', replace with shadow root host + const boundariesArray = Array.isArray(this._options.boundaries) ? this._options.boundaries : [this._options.boundaries]; + if (boundariesArray.length === 1 && boundariesArray[0] === 'html') { + this._options.boundaries = [this._options.document.host as HTMLElement]; + } + } + // Bind locale functions to instance /* eslint-disable @typescript-eslint/no-explicit-any */ for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) { @@ -116,9 +161,19 @@ export default class SelectionArea extends EventTarget { } } - const {document, selectionAreaClass, selectionContainerClass} = this._options; - this._area = document.createElement('div'); - this._clippingElement = document.createElement('div'); + const {document: doc, selectionAreaClass, selectionContainerClass} = this._options; + + // Ensure we have a valid document or shadow root + if (!doc) { + throw new Error('No document or shadow root provided.'); + } + + // For element creation, we always need to use the document + // ShadowRoot doesn't have createElement - it's a Document method + const documentForCreation = doc instanceof Document ? doc : doc.host.ownerDocument || window.document; + + this._area = documentForCreation.createElement('div'); + this._clippingElement = documentForCreation.createElement('div'); this._clippingElement.appendChild(this._area); this._area.classList.add(selectionAreaClass); @@ -153,19 +208,38 @@ export default class SelectionArea extends EventTarget { } _toggleStartEvents(activate = true): void { - const {document, features} = this._options; + const {document: doc, features} = this._options; const fn = activate ? on : off; + + // For shadow roots, we need to bind events to the shadow root itself + // For regular documents, we bind to the document + const eventTarget = doc instanceof ShadowRoot ? doc : getDocument(doc); - fn(document, 'mousedown', this._onTapStart); + fn(eventTarget, 'mousedown', this._onTapStart); if (features.touch) { - fn(document, 'touchstart', this._onTapStart, {passive: false}); + fn(eventTarget, 'touchstart', this._onTapStart, {passive: false}); + } + + // For shadow roots, also bind to the shadow host to capture clicks on the wrapper + if (doc instanceof ShadowRoot) { + fn(doc.host, 'mousedown', this._onTapStart); + if (features.touch) { + fn(doc.host, 'touchstart', this._onTapStart, {passive: false}); + } } } _onTapStart(evt: MouseEvent | TouchEvent, silent = false): void { + const {x, y, target} = simplifyEvent(evt); - const {document, startAreas, boundaries, features, behaviour} = this._options; + const {document: doc, startAreas, boundaries, features, behaviour} = this._options; + + // Ensure target is an HTMLElement before calling getBoundingClientRect + if (!(target instanceof HTMLElement)) { + return; + } + const targetBoundingClientRect = target.getBoundingClientRect(); if (evt instanceof MouseEvent && !matchesTrigger(evt, behaviour.triggers)) { @@ -173,18 +247,60 @@ export default class SelectionArea extends EventTarget { } // Find start-areas and boundaries - const resolvedStartAreas = selectAll(startAreas, document); - const resolvedBoundaries = selectAll(boundaries, document); + const resolvedStartAreas = selectAll(startAreas, doc); + const resolvedBoundaries = selectAll(boundaries, doc); + + // Check in which container the user currently acts this._targetElement = resolvedBoundaries.find(el => intersects(el.getBoundingClientRect(), targetBoundingClientRect) ); + + // For shadow roots, if no target element found by intersection, + // try to find one that contains the clicked element + if (doc instanceof ShadowRoot && !this._targetElement) { + this._targetElement = resolvedBoundaries.find(boundary => { + let element: HTMLElement | null = target; + while (element) { + if (element === boundary) return true; + element = element.parentElement; + } + return false; + }); + } // Check if the area starts in one of the start areas / boundaries const evtPath = evt.composedPath(); const targetStartArea = resolvedStartAreas.find(el => evtPath.includes(el)); this._targetBoundary = resolvedBoundaries.find(el => evtPath.includes(el)); + + // For shadow roots with transformed elements, if we found a start area but no boundary, + // try to find a boundary that contains the start area or is the parent of the start area + if (doc instanceof ShadowRoot && targetStartArea && !this._targetBoundary) { + this._targetBoundary = resolvedBoundaries.find(boundary => { + // Check if boundary contains the start area + if (boundary.contains(targetStartArea)) { + return true; + } + + // Check if start area contains the boundary + if (targetStartArea.contains(boundary)) { + return true; + } + + // Check if they have a parent-child relationship + let parent = targetStartArea.parentElement; + while (parent) { + if (parent === boundary) { + return true; + } + parent = parent.parentElement; + } + + return false; + }); + } if (!this._targetElement || !targetStartArea || !this._targetBoundary) { return; @@ -197,16 +313,25 @@ export default class SelectionArea extends EventTarget { this._areaLocation = {x1: x, y1: y, x2: 0, y2: 0}; // Lock scrolling in the target container - const scrollElement = document.scrollingElement ?? document.body; + const scrollElement = getScrollingElement(doc); this._scrollDelta = {x: scrollElement.scrollLeft, y: scrollElement.scrollTop}; // To detect single-click this._singleClick = true; this.clearSelection(false, true); - on(document, ['touchmove', 'mousemove'], this._delayedTapMove, {passive: false}); - on(document, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); - on(document, 'scroll', this._onScroll); + // For shadow roots, we need to bind events to the shadow root itself + // For regular documents, we bind to the document + const eventTarget = doc instanceof ShadowRoot ? doc : getDocument(doc); + + // Also bind to the main document to catch events when mouse moves outside shadow root + const mainDocument = getDocument(doc); + + on(eventTarget, ['touchmove', 'mousemove'], this._delayedTapMove, {passive: false}); + on(mainDocument, ['touchmove', 'mousemove'], this._delayedTapMove, {passive: false}); + on(eventTarget, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); + on(mainDocument, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); + on(eventTarget, 'scroll', this._onScroll); if (features.deselectOnBlur) { this._targetBoundaryScrolled = false; @@ -289,7 +414,7 @@ export default class SelectionArea extends EventTarget { } _delayedTapMove(evt: MouseEvent | TouchEvent): void { - const {container, document, behaviour: {startThreshold}} = this._options; + const {container, behaviour: {startThreshold}} = this._options; const {x1, y1} = this._areaLocation; // Coordinates of the first "tap" const {x, y} = simplifyEvent(evt); @@ -302,20 +427,42 @@ export default class SelectionArea extends EventTarget { // Different x and y threshold (typeof startThreshold === 'object' && abs(x - x1) >= (startThreshold as Coordinates).x || abs(y - y1) >= (startThreshold as Coordinates).y) ) { - off(document, ['mousemove', 'touchmove'], this._delayedTapMove, {passive: false}); + // For shadow roots, we need to unbind from both shadow root and main document + const {document: docOpt1} = this._options; + const eventTarget = docOpt1 instanceof ShadowRoot ? docOpt1 : getDocument(docOpt1); + const mainDocument = getDocument(docOpt1); + + off(eventTarget, ['mousemove', 'touchmove'], this._delayedTapMove, {passive: false}); + off(mainDocument, ['mousemove', 'touchmove'], this._delayedTapMove, {passive: false}); if (this._emitEvent('beforedrag', evt) === false) { - off(document, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); + off(eventTarget, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); + off(mainDocument, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); return; } - on(document, ['mousemove', 'touchmove'], this._onTapMove, {passive: false}); + on(eventTarget, ['mousemove', 'touchmove'], this._onTapMove, {passive: false}); + on(mainDocument, ['mousemove', 'touchmove'], this._onTapMove, {passive: false}); // Make area element visible css(this._area, 'display', 'block'); // Append selection-area to the dom - selectAll(container, document)[0].appendChild(this._clippingElement); + // For shadow roots, append to the main document body to avoid transform issues + const {document: docOpt2} = this._options; + let containerElement = selectAll(container, docOpt2)[0]; + + // For shadow roots, always append to the main document body + // to avoid transform coordinate system issues + if (docOpt2 instanceof ShadowRoot) { + containerElement = getDocument(docOpt2).body; + } + + if (containerElement) { + containerElement.appendChild(this._clippingElement); + } else { + getDocument(docOpt2).body.appendChild(this._clippingElement); + } this.resolveSelectables(); @@ -338,14 +485,46 @@ export default class SelectionArea extends EventTarget { // Detect keyboard scrolling on(this._options.document, 'keydown', this._keyboardScroll, {passive: false}); - /** * The selection-area will also cover another element * out of the current scrollable parent. So find all elements * that are in the current scrollable element. Now these are * the only selectables instead of all. */ - this._selectables = this._selectables.filter(s => this._targetElement!.contains(s)); + + // Special handling for shadow roots with transformed elements + const {document: doc} = this._options; + if (doc instanceof ShadowRoot) { + // For shadow roots, if we have a start area that's different from the boundary, + // we should filter based on the start area instead of the boundary + const resolvedStartAreas = selectAll(this._options.startAreas, doc); + const startArea = resolvedStartAreas[0]; + + if (startArea && startArea !== this._targetElement) { + this._selectables = this._selectables.filter(s => startArea.contains(s)); + } else { + // When start area and target element are the same (e.g., both wrapper), + // we need to handle the case where selectables are in a transformed child + // Look for transformed elements within the target that might contain selectables + const transformedChildren = Array.from(this._targetElement!.querySelectorAll('[style*="transform"]')); + + if (transformedChildren.length > 0) { + // Use the first transformed child as the containment reference + const transformedChild = transformedChildren[0]; + this._selectables = this._selectables.filter(s => transformedChild.contains(s)); + } else { + // For shadow roots, if the target element is the shadow host (wrapper), + // we should NOT filter by containment because contains() doesn't work across shadow boundaries + // The selectable elements are inside the shadow root, not the shadow host + // Don't filter - keep all selectables when using shadow host as boundary + } + } + } else { + // Original logic for regular documents + this._selectables = this._selectables.filter(s => this._targetElement!.contains(s)); + } + + } // Re-setup selection area and fire event @@ -475,8 +654,9 @@ export default class SelectionArea extends EventTarget { } _onScroll(): void { - const {_scrollDelta, _options: {document}} = this; - const {scrollTop, scrollLeft} = document.scrollingElement ?? document.body; + const {_scrollDelta, _options: {document: doc}} = this; + const scrollElement = getScrollingElement(doc); + const {scrollTop, scrollLeft} = scrollElement; // Adjust area start location this._areaLocation.x1 += _scrollDelta.x - scrollLeft; @@ -534,6 +714,8 @@ export default class SelectionArea extends EventTarget { const {x1, y1} = _areaLocation; let {x2, y2} = _areaLocation; + + const {behaviour: {scrolling: {startScrollMargins}}} = _options; if (x2 < _targetRect.left + startScrollMargins.x) { @@ -573,18 +755,35 @@ export default class SelectionArea extends EventTarget { style.top = `${y}px`; style.width = `${width}px`; style.height = `${height}px`; + + } _onTapStop(evt: MouseEvent | TouchEvent | null, silent: boolean): void { - const {document, features} = this._options; + const {document: doc, features} = this._options; const {_singleClick} = this; // Remove event handlers off(this._targetElement, 'scroll', this._onStartAreaScroll); - off(document, ['mousemove', 'touchmove'], this._delayedTapMove); - off(document, ['touchmove', 'mousemove'], this._onTapMove); - off(document, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); - off(document, 'scroll', this._onScroll); + // For shadow roots, we need to unbind events from the shadow root itself + // For regular documents, we unbind from the document + const eventTarget = doc instanceof ShadowRoot ? doc : getDocument(doc); + const mainDocument = getDocument(doc); + + off(eventTarget, ['mousemove', 'touchmove'], this._delayedTapMove); + off(mainDocument, ['mousemove', 'touchmove'], this._delayedTapMove); + off(eventTarget, ['touchmove', 'mousemove'], this._onTapMove); + off(mainDocument, ['touchmove', 'mousemove'], this._onTapMove); + off(eventTarget, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); + off(mainDocument, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); + off(eventTarget, 'scroll', this._onScroll); + + // For shadow roots, also unbind from the shadow host + if (doc instanceof ShadowRoot) { + off(doc.host, ['mousemove', 'touchmove'], this._delayedTapMove); + off(doc.host, ['touchmove', 'mousemove'], this._onTapMove); + off(doc.host, ['mouseup', 'touchcancel', 'touchend'], this._onTapStop); + } // Keep selection until the next time this._keepSelection(); @@ -625,12 +824,17 @@ export default class SelectionArea extends EventTarget { const added: Element[] = []; const removed: Element[] = []; + + // Find newly selected elements for (let i = 0; i < _selectables.length; i++) { const node = _selectables[i]; + const nodeRect = node.getBoundingClientRect(); + + // Check if the area intersects an element - if (intersects(_areaRect, node.getBoundingClientRect(), intersect)) { + if (intersects(_areaRect, nodeRect, intersect)) { // Check if the element wasn't present in the last selection. if (!selected.includes(node)) { diff --git a/packages/vanilla/src/types.ts b/packages/vanilla/src/types.ts index 5ba40e6..b172df6 100644 --- a/packages/vanilla/src/types.ts +++ b/packages/vanilla/src/types.ts @@ -9,6 +9,14 @@ export type DeepPartial = export type Quantify = T[] | T; +// Common interface for Document and ShadowRoot operations +// Note: ShadowRoot doesn't have createElement - that's a Document method +export interface DocumentOrShadowRoot { + querySelector(selectors: string): Element | null; + querySelectorAll(selectors: string): NodeListOf; + getElementById(elementId: string): HTMLElement | null; +} + export interface ScrollEvent extends MouseEvent { deltaY: number; deltaX: number; @@ -86,7 +94,7 @@ export interface SelectionOptions { selectionContainerClass: string | undefined; container: Quantify; - document: Document; + document: Document | ShadowRoot; selectables: Quantify; startAreas: Quantify; @@ -97,5 +105,5 @@ export interface SelectionOptions { } export type PartialSelectionOptions = DeepPartial> & { - document?: Document; + document?: Document | ShadowRoot; }; diff --git a/packages/vanilla/src/utils/events.ts b/packages/vanilla/src/utils/events.ts index cd3ca4a..cf58218 100644 --- a/packages/vanilla/src/utils/events.ts +++ b/packages/vanilla/src/utils/events.ts @@ -58,5 +58,18 @@ export const simplifyEvent = (evt: any): { y: number; } => { const {clientX, clientY, target} = evt.touches?.[0] ?? evt; - return {x: clientX, y: clientY, target}; + + // Ensure target is an HTMLElement + let htmlTarget: HTMLElement; + if (target instanceof HTMLElement) { + htmlTarget = target; + } else if (target instanceof Element) { + // If it's an Element but not HTMLElement, try to cast it + htmlTarget = target as HTMLElement; + } else { + // Fallback to document.body if target is not a valid element + htmlTarget = document.body; + } + + return {x: clientX, y: clientY, target: htmlTarget}; }; diff --git a/packages/vanilla/src/utils/intersects.ts b/packages/vanilla/src/utils/intersects.ts index 77767ca..e9c4def 100644 --- a/packages/vanilla/src/utils/intersects.ts +++ b/packages/vanilla/src/utils/intersects.ts @@ -8,27 +8,34 @@ export type Intersection = 'center' | 'cover' | 'touch'; * @returns {boolean} If both elements intersects each other. */ export const intersects = (a: DOMRect, b: DOMRect, mode: Intersection = 'touch'): boolean => { + let result: boolean; + switch (mode) { case 'center': { const bxc = b.left + b.width / 2; const byc = b.top + b.height / 2; - return bxc >= a.left && + result = bxc >= a.left && bxc <= a.right && byc >= a.top && byc <= a.bottom; + break; } case 'cover': { - return b.left >= a.left && + result = b.left >= a.left && b.top >= a.top && b.right <= a.right && b.bottom <= a.bottom; + break; } case 'touch': { - return a.right >= b.left && + result = a.right >= b.left && a.left <= b.right && a.bottom >= b.top && a.top <= b.bottom; + break; } } + + return result; }; diff --git a/packages/vanilla/src/utils/selectAll.ts b/packages/vanilla/src/utils/selectAll.ts index 0427e73..f6863cb 100644 --- a/packages/vanilla/src/utils/selectAll.ts +++ b/packages/vanilla/src/utils/selectAll.ts @@ -8,7 +8,7 @@ export type SelectAllSelectors = (string | Element)[] | string | Element; * @param doc * @returns {Array} Array of DOM-Nodes. */ -export const selectAll = (selector: SelectAllSelectors, doc: Document = document): Element[] => +export const selectAll = (selector: SelectAllSelectors, doc: Document | ShadowRoot = document): Element[] => arrayify(selector) .map(item => typeof item === 'string'