import { RevealStateManager } from './RevealStateManager.js'; import { RevealBoundaryStore } from './RevealBoundryStore.js'; import { config } from './config.js'; function closest(dom: Element, selector: string): Element | null { const r = dom.closest(selector); if (r) return r; const host = findParentShadowRoot(dom)?.host; if (!host) return null; return closest(host, selector) } function findParentShadowRoot(x: Node | undefined | null): ShadowRoot | null { if (!x) return null; if (x.parentNode && x.parentNode.nodeType === document.DOCUMENT_FRAGMENT_NODE) return x.parentNode as ShadowRoot; return findParentShadowRoot(x.parentNode); } class ServerSideHTMLElement { root = { innerHTML: '', querySelector: (_x: string) => null, }; attachShadow = () => null; } const PatchedHTMLElement = typeof globalThis.Window === 'undefined' ? ((ServerSideHTMLElement as unknown) as typeof HTMLElement) : HTMLElement; export class AxRevealProvider extends PatchedHTMLElement { static readonly ElementName = 'ax-reveal-provider'; readonly stateManager = new RevealStateManager(); } export class AxRevealBoundary extends PatchedHTMLElement { static readonly ElementName = 'ax-reveal-bound'; static readonly removeStorageEvent = 'removeStorage'; static readonly attachStorageEvent = 'attachStorage'; static readonly replaceStorageEvent = 'replaceStorage'; private root = this.attachShadow({ mode: 'open' }); static readonly stateManager = new RevealStateManager(); private _storage!: RevealBoundaryStore | undefined; private get storage() { return this._storage; } private set storage(newStorage) { const oldStorage = this._storage; if (oldStorage) { this.dispatchEvent(new CustomEvent(AxRevealBoundary.removeStorageEvent, { detail: oldStorage })); AxRevealBoundary.stateManager.removeBoundary(oldStorage); } if (newStorage) { this._storage = newStorage; this.dispatchEvent(new CustomEvent(AxRevealBoundary.attachStorageEvent, { detail: newStorage })); } if (oldStorage && newStorage) { this.dispatchEvent( new CustomEvent(AxRevealBoundary.replaceStorageEvent, { detail: { old: oldStorage, new: newStorage }, }) ); } } public waitForStorage(f: (storage: RevealBoundaryStore) => void) { if (this.storage === undefined) { this.addEventListener(AxRevealBoundary.attachStorageEvent, () => f(this.storage!), { once: true }); } else { f(this.storage); } } private appendStorage(force = false) { if (!force && this.storage) { return; } const parent = closest(this, AxRevealProvider.ElementName) as AxRevealProvider; const stateManager = parent ? parent.stateManager : AxRevealBoundary.stateManager; this.storage = stateManager.newBoundary(this); } /** * Update the position of your pointer. * @param ev Pointer event from the listener. */ updatePointerPosition = (ev: PointerEvent) => { this.waitForStorage((storage) => { storage.clientX = ev.clientX; storage.clientY = ev.clientY; }); }; handlePointerEnter = () => this.waitForStorage((storage) => storage.onPointerEnterBoundary()); handlePointerLeave = () => this.waitForStorage((storage) => storage.onPointerLeaveBoundary()); handlePointerMove = this.updatePointerPosition; handlePointerUp = (ev: PointerEvent) => { if (ev.pointerType === 'mouse') { this.waitForStorage((storage) => storage.switchAnimation()); } else { this.waitForStorage((storage) => storage.clearAll(false)); } }; handlePointerDown = (ev: PointerEvent) => this.waitForStorage((storage) => { this.updatePointerPosition(ev); storage.initializeAnimation(); }); connectedCallback() { this.appendStorage(true); if (config.borderDetectionMode === 'strictEdge') { this.addEventListener('pointerenter', this.handlePointerEnter); this.addEventListener('pointerleave', this.handlePointerLeave); this.addEventListener('pointermove', this.handlePointerMove); } this.addEventListener('pointerdown', this.handlePointerDown); this.addEventListener('pointerup', this.handlePointerUp); } disconnectedCallback() { this.storage = undefined; } constructor() { super(); this.root.innerHTML = `