diff --git a/src/common/TransformEvent.ts b/src/common/TransformEvent.ts index d19ed84..59e62a7 100644 --- a/src/common/TransformEvent.ts +++ b/src/common/TransformEvent.ts @@ -1,5 +1,11 @@ import type { DOMRectTransformReadonly } from './DOMRectTransform'; +declare global { + interface HTMLElementEventMap { + transform: TransformEvent; + } +} + // TODO: expose previous and current rects export class TransformEvent extends Event { readonly #current: DOMRectTransformReadonly; diff --git a/src/common/client-rect-observer.ts b/src/common/client-rect-observer.ts index 99aafec..2518876 100644 --- a/src/common/client-rect-observer.ts +++ b/src/common/client-rect-observer.ts @@ -1,7 +1,6 @@ export interface ClientRectObserverEntry { target: Element; contentRect: DOMRectReadOnly; - isAppearing: boolean; } export interface ClientRectObserverCallback { @@ -112,7 +111,6 @@ export class ClientRectObserver { return { target, contentRect, - isAppearing: false, }; } @@ -147,7 +145,6 @@ export class ClientRectObserver { width, height, }), - isAppearing: true, }; } @@ -233,7 +230,7 @@ export class ClientRectObserverManager { this.#vo.observe(target); this.#elementMap.set(target, (callbacks = new Set())); } else { - callback({ target, contentRect: target.getBoundingClientRect(), isAppearing: true }); + callback({ target, contentRect: target.getBoundingClientRect() }); } callbacks.add(callback); diff --git a/src/common/folk-observer.ts b/src/common/folk-observer.ts new file mode 100644 index 0000000..e81aa39 --- /dev/null +++ b/src/common/folk-observer.ts @@ -0,0 +1,80 @@ +import { FolkShape } from '../folk-shape'; +import { ClientRectObserver, ClientRectObserverEntry } from './client-rect-observer'; +import { TransformEvent } from './TransformEvent'; + +export type ClientRectObserverEntryCallback = (entry: ClientRectObserverEntry) => void; + +export class FolkObserver { + static #instance: FolkObserver | null = null; + + // singleton so we only observe elements once + constructor() { + if (FolkObserver.#instance === null) { + FolkObserver.#instance = this; + } + return FolkObserver.#instance; + } + + #elementMap = new WeakMap>(); + + #vo = new ClientRectObserver((entries) => { + for (const entry of entries) { + this.#updateTarget(entry); + } + }); + + #updateTarget(entry: ClientRectObserverEntry) { + const callbacks = this.#elementMap.get(entry.target); + + if (callbacks) { + callbacks.forEach((callback) => callback(entry)); + } + } + + #onTransform = (event: TransformEvent) => { + this.#updateTarget({ target: event.target as HTMLElement, contentRect: event.current }); + }; + + observe(target: Element, callback: ClientRectObserverEntryCallback): void { + let callbacks = this.#elementMap.get(target); + + const isFolkShape = target instanceof FolkShape; + + if (callbacks === undefined) { + this.#elementMap.set(target, (callbacks = new Set())); + + if (isFolkShape) { + target.addEventListener('transform', this.#onTransform); + callback({ target, contentRect: target.getTransformDOMRect() }); + } else { + this.#vo.observe(target); + } + } else { + const contentRect = isFolkShape ? target.getTransformDOMRect() : target.getBoundingClientRect(); + callback({ target, contentRect }); + } + + callbacks.add(callback); + } + + unobserve(target: Element, callback: ClientRectObserverEntryCallback): void { + let callbacks = this.#elementMap.get(target); + + if (callbacks === undefined) return; + + callbacks.delete(callback); + + if (callbacks.size === 0) { + if (target instanceof FolkShape) { + target.removeEventListener('transform', this.#onTransform); + } else { + this.#vo.unobserve(target); + } + this.#elementMap.delete(target); + } + } +} + +export function parseCSSSelector(selector: string): string[] { + return selector.split('>>>').map((s) => s.trim()); +} diff --git a/src/folk-base-connection.ts b/src/folk-base-connection.ts index c741672..cb8aa3e 100644 --- a/src/folk-base-connection.ts +++ b/src/folk-base-connection.ts @@ -1,12 +1,9 @@ import { FolkShape } from './folk-shape.ts'; import { parseVertex } from './common/utils.ts'; -import { ClientRectObserverEntry, ClientRectObserverManager } from './common/client-rect-observer.ts'; +import { ClientRectObserverEntry } from './common/client-rect-observer.ts'; +import { FolkObserver } from './common/folk-observer.ts'; -const clientRectObserver = new ClientRectObserverManager(); - -function parseCSSSelector(selector: string): string[] { - return selector.split('>>>').map((s) => s.trim()); -} +const folkObserver = new FolkObserver(); export class FolkBaseConnection extends HTMLElement { static tagName = ''; @@ -44,55 +41,6 @@ export class FolkBaseConnection extends HTMLElement { this.#update(); }; - #sourceHandler = (e: Event) => { - const geometry = e.target as FolkShape; - this.#sourceRect = geometry.getTransformDOMRect(); - this.#update(); - }; - - #sourceIframeSelector = ''; - #sourceIframeRect = DOMRectReadOnly.fromRect(); - #sourceIframeChildRect = DOMRectReadOnly.fromRect(); - - #sourcePostMessage = (event: MessageEvent) => { - const iframe = this.#sourceElement as HTMLIFrameElement; - - if (event.source !== iframe.contentWindow) return; - - switch (event.data.type) { - case 'folk-iframe-ready': { - event.source?.postMessage({ - type: 'folk-observe-element', - selector: this.#sourceIframeSelector, - }); - return; - } - case 'folk-element-change': { - if (this.#sourceIframeSelector === event.data.selector) { - this.#sourceIframeChildRect = event.data.boundingBox; - this.#updateSourceIframeRect(); - } - return; - } - } - }; - - #sourceIframeCallback = (entry: ClientRectObserverEntry) => { - this.#sourceIframeRect = entry.contentRect; - this.#updateSourceIframeRect(); - }; - - #updateSourceIframeRect() { - this.#sourceRect = DOMRectReadOnly.fromRect({ - x: this.#sourceIframeRect.x + this.#sourceIframeChildRect.x, - y: this.#sourceIframeRect.y + this.#sourceIframeChildRect.y, - height: this.#sourceIframeChildRect.height, - width: this.#sourceIframeChildRect.width, - }); - - this.#update(); - } - #target = ''; /** A CSS selector for the target of the arrow. */ get target() { @@ -118,55 +66,6 @@ export class FolkBaseConnection extends HTMLElement { this.#update(); }; - #targetHandler = (e: Event) => { - const geometry = e.target as FolkShape; - this.#targetRect = geometry.getTransformDOMRect(); - this.#update(); - }; - - #targetIframeSelector = ''; - #targetIframeRect = DOMRectReadOnly.fromRect(); - #targetIframeChildRect = DOMRectReadOnly.fromRect(); - - #targetPostMessage = (event: MessageEvent) => { - const iframe = this.#targetElement as HTMLIFrameElement; - - if (event.source !== iframe.contentWindow) return; - - switch (event.data.type) { - case 'folk-iframe-ready': { - event.source?.postMessage({ - type: 'folk-observe-element', - selector: this.#targetIframeSelector, - }); - return; - } - case 'folk-element-change': { - if (this.#targetIframeSelector === event.data.selector) { - this.#targetIframeChildRect = event.data.boundingBox; - this.#updateTargetIframeRect(); - } - return; - } - } - }; - - #targetIframeCallback = (entry: ClientRectObserverEntry) => { - this.#targetIframeRect = entry.contentRect; - this.#updateTargetIframeRect(); - }; - - #updateTargetIframeRect() { - this.#targetRect = DOMRectReadOnly.fromRect({ - x: this.#targetIframeRect.x + this.#targetIframeChildRect.x, - y: this.#targetIframeRect.y + this.#targetIframeChildRect.y, - height: this.#targetIframeChildRect.height, - width: this.#targetIframeChildRect.width, - }); - - this.#update(); - } - connectedCallback() { this.source = this.getAttribute('source') || this.#source; this.target = this.getAttribute('target') || this.#target; @@ -186,49 +85,20 @@ export class FolkBaseConnection extends HTMLElement { this.#sourceRect = DOMRectReadOnly.fromRect(vertex); this.#update(); } else { - const [selector, iframeSelector] = parseCSSSelector(this.#source); - this.#sourceIframeSelector = iframeSelector; - this.#sourceElement = document.querySelector(selector); + this.#sourceElement = document.querySelector(this.source); if (this.#sourceElement === null) { throw new Error('source is not a valid element'); - } else if (this.#sourceElement instanceof FolkShape) { - this.#sourceElement.addEventListener('transform', this.#sourceHandler); - - this.#sourceRect = this.#sourceElement.getTransformDOMRect(); - - this.#update(); - } else if (this.#sourceElement instanceof HTMLIFrameElement && this.#sourceIframeSelector) { - window.addEventListener('message', this.#sourcePostMessage); - - clientRectObserver.observe(this.#sourceElement, this.#sourceIframeCallback); - - this.#sourceElement.contentWindow?.postMessage({ - type: 'folk-observe-element', - selector: this.#sourceIframeSelector, - }); - } else { - clientRectObserver.observe(this.#sourceElement, this.#sourceCallback); - this.#sourceRect = this.#sourceElement.getBoundingClientRect(); } + + folkObserver.observe(this.#sourceElement, this.#sourceCallback); } } unobserveSource() { if (this.#sourceElement === null) return; - if (this.#sourceElement instanceof FolkShape) { - this.#sourceElement.removeEventListener('transform', this.#sourceHandler); - } else if (this.#sourceElement instanceof HTMLIFrameElement && this.#sourceIframeSelector) { - window.removeEventListener('message', this.#sourcePostMessage); - clientRectObserver.unobserve(this.#sourceElement, this.#sourceIframeCallback); - this.#sourceElement.contentWindow?.postMessage({ - type: 'folk-unobserve-element', - selector: this.#sourceIframeSelector, - }); - } else { - clientRectObserver.unobserve(this.#sourceElement, this.#sourceCallback); - } + folkObserver.unobserve(this.#sourceElement, this.#sourceCallback); } observeTarget() { @@ -240,45 +110,19 @@ export class FolkBaseConnection extends HTMLElement { this.#targetRect = DOMRectReadOnly.fromRect(vertex); this.#update(); } else { - const [selector, iframeSelector] = parseCSSSelector(this.#target); - this.#targetIframeSelector = iframeSelector; - this.#targetElement = document.querySelector(selector); + this.#targetElement = document.querySelector(this.#target); if (!this.#targetElement) { throw new Error('target is not a valid element'); - } else if (this.#targetElement instanceof FolkShape) { - this.#targetElement.addEventListener('transform', this.#targetHandler); - this.#targetRect = this.#targetElement.getTransformDOMRect(); - this.#update(); - } else if (this.#targetElement instanceof HTMLIFrameElement && this.#targetIframeSelector) { - window.addEventListener('message', this.#targetPostMessage); - clientRectObserver.observe(this.#targetElement, this.#targetIframeCallback); - this.#targetElement.contentWindow?.postMessage({ - type: 'folk-observe-element', - selector: this.#targetIframeSelector, - }); - } else { - clientRectObserver.observe(this.#targetElement, this.#targetCallback); - this.#targetRect = this.#targetElement.getBoundingClientRect(); } + + folkObserver.observe(this.#targetElement, this.#targetCallback); } } unobserveTarget() { if (this.#targetElement === null) return; - - if (this.#targetElement instanceof FolkShape) { - this.#targetElement.removeEventListener('transform', this.#targetHandler); - } else if (this.#targetElement instanceof HTMLIFrameElement && this.#targetIframeSelector) { - window.removeEventListener('message', this.#targetPostMessage); - clientRectObserver.unobserve(this.#targetElement, this.#targetIframeCallback); - this.#targetElement.contentWindow?.postMessage({ - type: 'folk-unobserve-element', - selector: this.#targetIframeSelector, - }); - } else { - clientRectObserver.unobserve(this.#targetElement, this.#targetCallback); - } + folkObserver.unobserve(this.#targetElement, this.#targetCallback); } #update() { diff --git a/src/folk-base-set.ts b/src/folk-base-set.ts index 1a42a86..186f19d 100644 --- a/src/folk-base-set.ts +++ b/src/folk-base-set.ts @@ -1,6 +1,7 @@ -import { ClientRectObserverEntry, ClientRectObserverManager } from './common/client-rect-observer.ts'; +import { ClientRectObserverEntry } from './common/client-rect-observer.ts'; +import { FolkObserver } from './common/folk-observer.ts'; -const clientRectObserver = new ClientRectObserverManager(); +const folkObserver = new FolkObserver(); const defaultRect = DOMRectReadOnly.fromRect(); @@ -57,8 +58,7 @@ export class FolkBaseSet extends HTMLElement { this.unobserveSources(elementsToUnobserve); for (const el of elementsToObserve) { - this.#sourcesMap.set(el, defaultRect); - clientRectObserver.observe(el, this.#sourcesCallback); + folkObserver.observe(el, this.#sourcesCallback); } this.update(); @@ -66,7 +66,7 @@ export class FolkBaseSet extends HTMLElement { unobserveSources(elements: Iterable = this.#sourcesMap.keys()) { for (const el of elements) { - clientRectObserver.unobserve(el, this.#sourcesCallback); + folkObserver.unobserve(el, this.#sourcesCallback); this.#sourcesMap.delete(el); } }