let folk-set observe any element
This commit is contained in:
parent
b13256306c
commit
8d3a4319ac
|
|
@ -1,5 +1,11 @@
|
||||||
import type { DOMRectTransformReadonly } from './DOMRectTransform';
|
import type { DOMRectTransformReadonly } from './DOMRectTransform';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementEventMap {
|
||||||
|
transform: TransformEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: expose previous and current rects
|
// TODO: expose previous and current rects
|
||||||
export class TransformEvent extends Event {
|
export class TransformEvent extends Event {
|
||||||
readonly #current: DOMRectTransformReadonly;
|
readonly #current: DOMRectTransformReadonly;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
export interface ClientRectObserverEntry {
|
export interface ClientRectObserverEntry {
|
||||||
target: Element;
|
target: Element;
|
||||||
contentRect: DOMRectReadOnly;
|
contentRect: DOMRectReadOnly;
|
||||||
isAppearing: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientRectObserverCallback {
|
export interface ClientRectObserverCallback {
|
||||||
|
|
@ -112,7 +111,6 @@ export class ClientRectObserver {
|
||||||
return {
|
return {
|
||||||
target,
|
target,
|
||||||
contentRect,
|
contentRect,
|
||||||
isAppearing: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +145,6 @@ export class ClientRectObserver {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
}),
|
}),
|
||||||
isAppearing: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +230,7 @@ export class ClientRectObserverManager {
|
||||||
this.#vo.observe(target);
|
this.#vo.observe(target);
|
||||||
this.#elementMap.set(target, (callbacks = new Set()));
|
this.#elementMap.set(target, (callbacks = new Set()));
|
||||||
} else {
|
} else {
|
||||||
callback({ target, contentRect: target.getBoundingClientRect(), isAppearing: true });
|
callback({ target, contentRect: target.getBoundingClientRect() });
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacks.add(callback);
|
callbacks.add(callback);
|
||||||
|
|
|
||||||
|
|
@ -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<Element, Set<ClientRectObserverEntryCallback>>();
|
||||||
|
|
||||||
|
#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());
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { FolkShape } from './folk-shape.ts';
|
import { FolkShape } from './folk-shape.ts';
|
||||||
import { parseVertex } from './common/utils.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();
|
const folkObserver = new FolkObserver();
|
||||||
|
|
||||||
function parseCSSSelector(selector: string): string[] {
|
|
||||||
return selector.split('>>>').map((s) => s.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FolkBaseConnection extends HTMLElement {
|
export class FolkBaseConnection extends HTMLElement {
|
||||||
static tagName = '';
|
static tagName = '';
|
||||||
|
|
@ -44,55 +41,6 @@ export class FolkBaseConnection extends HTMLElement {
|
||||||
this.#update();
|
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 = '';
|
#target = '';
|
||||||
/** A CSS selector for the target of the arrow. */
|
/** A CSS selector for the target of the arrow. */
|
||||||
get target() {
|
get target() {
|
||||||
|
|
@ -118,55 +66,6 @@ export class FolkBaseConnection extends HTMLElement {
|
||||||
this.#update();
|
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() {
|
connectedCallback() {
|
||||||
this.source = this.getAttribute('source') || this.#source;
|
this.source = this.getAttribute('source') || this.#source;
|
||||||
this.target = this.getAttribute('target') || this.#target;
|
this.target = this.getAttribute('target') || this.#target;
|
||||||
|
|
@ -186,49 +85,20 @@ export class FolkBaseConnection extends HTMLElement {
|
||||||
this.#sourceRect = DOMRectReadOnly.fromRect(vertex);
|
this.#sourceRect = DOMRectReadOnly.fromRect(vertex);
|
||||||
this.#update();
|
this.#update();
|
||||||
} else {
|
} else {
|
||||||
const [selector, iframeSelector] = parseCSSSelector(this.#source);
|
this.#sourceElement = document.querySelector(this.source);
|
||||||
this.#sourceIframeSelector = iframeSelector;
|
|
||||||
this.#sourceElement = document.querySelector(selector);
|
|
||||||
|
|
||||||
if (this.#sourceElement === null) {
|
if (this.#sourceElement === null) {
|
||||||
throw new Error('source is not a valid element');
|
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() {
|
unobserveSource() {
|
||||||
if (this.#sourceElement === null) return;
|
if (this.#sourceElement === null) return;
|
||||||
|
|
||||||
if (this.#sourceElement instanceof FolkShape) {
|
folkObserver.unobserve(this.#sourceElement, this.#sourceCallback);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
observeTarget() {
|
observeTarget() {
|
||||||
|
|
@ -240,45 +110,19 @@ export class FolkBaseConnection extends HTMLElement {
|
||||||
this.#targetRect = DOMRectReadOnly.fromRect(vertex);
|
this.#targetRect = DOMRectReadOnly.fromRect(vertex);
|
||||||
this.#update();
|
this.#update();
|
||||||
} else {
|
} else {
|
||||||
const [selector, iframeSelector] = parseCSSSelector(this.#target);
|
this.#targetElement = document.querySelector(this.#target);
|
||||||
this.#targetIframeSelector = iframeSelector;
|
|
||||||
this.#targetElement = document.querySelector(selector);
|
|
||||||
|
|
||||||
if (!this.#targetElement) {
|
if (!this.#targetElement) {
|
||||||
throw new Error('target is not a valid element');
|
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() {
|
unobserveTarget() {
|
||||||
if (this.#targetElement === null) return;
|
if (this.#targetElement === null) return;
|
||||||
|
folkObserver.unobserve(this.#targetElement, this.#targetCallback);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#update() {
|
#update() {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
const defaultRect = DOMRectReadOnly.fromRect();
|
||||||
|
|
||||||
|
|
@ -57,8 +58,7 @@ export class FolkBaseSet extends HTMLElement {
|
||||||
this.unobserveSources(elementsToUnobserve);
|
this.unobserveSources(elementsToUnobserve);
|
||||||
|
|
||||||
for (const el of elementsToObserve) {
|
for (const el of elementsToObserve) {
|
||||||
this.#sourcesMap.set(el, defaultRect);
|
folkObserver.observe(el, this.#sourcesCallback);
|
||||||
clientRectObserver.observe(el, this.#sourcesCallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
|
|
@ -66,7 +66,7 @@ export class FolkBaseSet extends HTMLElement {
|
||||||
|
|
||||||
unobserveSources(elements: Iterable<Element> = this.#sourcesMap.keys()) {
|
unobserveSources(elements: Iterable<Element> = this.#sourcesMap.keys()) {
|
||||||
for (const el of elements) {
|
for (const el of elements) {
|
||||||
clientRectObserver.unobserve(el, this.#sourcesCallback);
|
folkObserver.unobserve(el, this.#sourcesCallback);
|
||||||
this.#sourcesMap.delete(el);
|
this.#sourcesMap.delete(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue