diff --git a/packages/preact/src/SelectionArea.tsx b/packages/preact/src/SelectionArea.tsx index b435eb4..07b6762 100644 --- a/packages/preact/src/SelectionArea.tsx +++ b/packages/preact/src/SelectionArea.tsx @@ -1,7 +1,7 @@ import VanillaSelectionArea from '@viselect/vanilla'; import {SelectionEvents, PartialSelectionOptions} from '@viselect/vanilla'; -import {createContext, createRef, FunctionalComponent, JSX} from 'preact'; -import {useEffect, useContext, useState} from 'preact/hooks'; +import {createContext, JSX, FunctionalComponent} from 'preact'; +import {useEffect, useContext, useState, useMemo, useRef} from 'preact/hooks'; export interface SelectionAreaProps extends PartialSelectionOptions, JSX.HTMLAttributes { id?: string; @@ -18,23 +18,54 @@ const SelectionContext = createContext(undefin export const useSelection = () => useContext(SelectionContext); export const SelectionArea: FunctionalComponent = props => { + const { + boundaries, + document: doc, + selectables, + startAreas, + container, + behaviour, + features, + onBeforeStart, + onBeforeDrag, + onStart, + onMove, + onStop, + className, + id, + children, + ...rest + } = props; + const [instance, setInstance] = useState(undefined); - const root = createRef(); + const root = useRef(null); + + // Use refs for callbacks to avoid recreating the instance when only callbacks change + const callbacksRef = useRef({onBeforeStart, onBeforeDrag, onStart, onMove, onStop}); + callbacksRef.current = {onBeforeStart, onBeforeDrag, onStart, onMove, onStop}; + + // Stable serialization of object props for dependency comparison + const behaviourKey = useMemo(() => JSON.stringify(behaviour), [behaviour]); + const featuresKey = useMemo(() => JSON.stringify(features), [features]); useEffect(() => { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const {boundaries = root.current, onBeforeStart, onBeforeDrag, onStart, onMove, onStop, ...opt} = props; + const boundaryElement = boundaries ?? root.current; const selection = new VanillaSelectionArea({ - boundaries: boundaries as HTMLElement, - ...opt + boundaries: boundaryElement as HTMLElement, + document: doc, + selectables, + startAreas, + container, + behaviour, + features }); - selection.on('beforestart', evt => props.onBeforeStart?.(evt)); - selection.on('beforedrag', evt => props.onBeforeDrag?.(evt)); - selection.on('start', evt => props.onStart?.(evt)); - selection.on('move', evt => props.onMove?.(evt)); - selection.on('stop', evt => props.onStop?.(evt)); + selection.on('beforestart', evt => callbacksRef.current.onBeforeStart?.(evt)); + selection.on('beforedrag', evt => callbacksRef.current.onBeforeDrag?.(evt)); + selection.on('start', evt => callbacksRef.current.onStart?.(evt)); + selection.on('move', evt => callbacksRef.current.onMove?.(evt)); + selection.on('stop', evt => callbacksRef.current.onStop?.(evt)); setInstance(selection); @@ -42,15 +73,15 @@ export const SelectionArea: FunctionalComponent = props => { selection.destroy(); setInstance(undefined); }; - }, []); + }, [boundaries, doc, selectables, startAreas, container, behaviourKey, featuresKey]); return ( - {props.boundaries ? ( - props.children + {boundaries ? ( + children ) : ( -
- {props.children} +
+ {children}
)} diff --git a/packages/react/src/SelectionArea.tsx b/packages/react/src/SelectionArea.tsx index 57d5ff9..05827ee 100644 --- a/packages/react/src/SelectionArea.tsx +++ b/packages/react/src/SelectionArea.tsx @@ -1,6 +1,6 @@ import VanillaSelectionArea from '@viselect/vanilla'; import {SelectionEvents, PartialSelectionOptions} from '@viselect/vanilla'; -import React, {useEffect, createContext, useContext, useRef, useState} from 'react'; +import React, {useEffect, createContext, useContext, useRef, useState, useMemo} from 'react'; export interface SelectionAreaProps extends PartialSelectionOptions, React.HTMLAttributes { id?: string; @@ -17,23 +17,54 @@ const SelectionContext = createContext(undefin export const useSelection = () => useContext(SelectionContext); export const SelectionArea: React.FunctionComponent = props => { + const { + boundaries, + document: doc, + selectables, + startAreas, + container, + behaviour, + features, + onBeforeStart, + onBeforeDrag, + onStart, + onMove, + onStop, + className, + id, + children, + ...rest + } = props; + const [instance, setInstance] = useState(undefined); const root = useRef(null); + // Use refs for callbacks to avoid recreating the instance when only callbacks change + const callbacksRef = useRef({onBeforeStart, onBeforeDrag, onStart, onMove, onStop}); + callbacksRef.current = {onBeforeStart, onBeforeDrag, onStart, onMove, onStop}; + + // Stable serialization of object props for dependency comparison + const behaviourKey = useMemo(() => JSON.stringify(behaviour), [behaviour]); + const featuresKey = useMemo(() => JSON.stringify(features), [features]); + useEffect(() => { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const {boundaries = root.current, onBeforeStart, onBeforeDrag, onStart, onMove, onStop, ...opt} = props; + const boundaryElement = boundaries ?? root.current; const selection = new VanillaSelectionArea({ - boundaries: boundaries as HTMLElement, - ...opt + boundaries: boundaryElement as HTMLElement, + document: doc, + selectables, + startAreas, + container, + behaviour, + features }); - selection.on('beforestart', evt => props.onBeforeStart?.(evt)); - selection.on('beforedrag', evt => props.onBeforeDrag?.(evt)); - selection.on('start', evt => props.onStart?.(evt)); - selection.on('move', evt => props.onMove?.(evt)); - selection.on('stop', evt => props.onStop?.(evt)); + selection.on('beforestart', evt => callbacksRef.current.onBeforeStart?.(evt)); + selection.on('beforedrag', evt => callbacksRef.current.onBeforeDrag?.(evt)); + selection.on('start', evt => callbacksRef.current.onStart?.(evt)); + selection.on('move', evt => callbacksRef.current.onMove?.(evt)); + selection.on('stop', evt => callbacksRef.current.onStop?.(evt)); setInstance(selection); @@ -41,15 +72,15 @@ export const SelectionArea: React.FunctionComponent = props selection.destroy(); setInstance(undefined); }; - }, []); + }, [boundaries, doc, selectables, startAreas, container, behaviourKey, featuresKey]); return ( - {props.boundaries ? ( - props.children + {boundaries ? ( + children ) : ( -
- {props.children} +
+ {children}
)} diff --git a/packages/vanilla/src/index.ts b/packages/vanilla/src/index.ts index 16f9237..2e676aa 100644 --- a/packages/vanilla/src/index.ts +++ b/packages/vanilla/src/index.ts @@ -65,45 +65,39 @@ export default class SelectionArea extends EventTarget { super(); this._options = { - selectionAreaClass: 'selection-area', - selectionContainerClass: undefined, - selectables: [], - document: window.document, - startAreas: ['html'], - boundaries: ['html'], - container: 'body', - ...opt, + selectionAreaClass: opt.selectionAreaClass ?? 'selection-area', + selectionContainerClass: opt.selectionContainerClass ?? undefined, + selectables: opt.selectables ?? [], + document: opt.document ?? window.document, + startAreas: opt.startAreas ?? ['html'], + boundaries: opt.boundaries ?? ['html'], + container: opt.container ?? 'body', behaviour: { - overlap: 'invert', - intersect: 'touch', - triggers: [0], - ...opt.behaviour, + overlap: opt.behaviour?.overlap ?? 'invert', + intersect: opt.behaviour?.intersect ?? 'touch', + triggers: opt.behaviour?.triggers ?? [0], startThreshold: opt.behaviour?.startThreshold ? typeof opt.behaviour.startThreshold === 'number' ? opt.behaviour.startThreshold : {x: 10, y: 10, ...opt.behaviour.startThreshold} : {x: 10, y: 10}, scrolling: { - speedDivider: 10, - manualSpeed: 750, - ...opt.behaviour?.scrolling, + speedDivider: opt.behaviour?.scrolling?.speedDivider ?? 10, + manualSpeed: opt.behaviour?.scrolling?.manualSpeed ?? 750, startScrollMargins: { - x: 0, - y: 0, - ...opt.behaviour?.scrolling?.startScrollMargins, + x: opt.behaviour?.scrolling?.startScrollMargins?.x ?? 0, + y: opt.behaviour?.scrolling?.startScrollMargins?.y ?? 0 } } }, features: { - range: true, - touch: true, - deselectOnBlur: false, - ...opt.features, + range: opt.features?.range ?? true, + touch: opt.features?.touch ?? true, + deselectOnBlur: opt.features?.deselectOnBlur ?? false, singleTap: { - allow: true, - intersect: 'native', - ...opt.features?.singleTap, + allow: opt.features?.singleTap?.allow ?? true, + intersect: opt.features?.singleTap?.intersect ?? 'native', } } };