folk-canvas/src/arrows/abstract-arrow.ts

306 lines
9.3 KiB
TypeScript

import { FolkGeometry } from '../canvas/fc-geometry';
import { Vertex } from './utils';
import { VisualObserverEntry, VisualObserverManager } from './visual-observer';
const visualObserver = new VisualObserverManager();
const vertexRegex = /(?<x>-?([0-9]*[.])?[0-9]+),\s*(?<y>-?([0-9]*[.])?[0-9]+)/;
function parseVertex(str: string): Vertex | null {
const results = vertexRegex.exec(str);
if (results === null) return null;
return {
x: Number(results.groups?.x),
y: Number(results.groups?.y),
};
}
function parseCSSSelector(selector: string): string[] {
return selector.split('>>>').map((s) => s.trim());
}
export class AbstractArrow extends HTMLElement {
static tagName = 'abstract-arrow';
static register() {
customElements.define(this.tagName, this);
}
#source = '';
/** A CSS selector for the source of the arrow. */
get source() {
return this.#source;
}
set source(source) {
this.#source = source;
this.observeSource();
}
#sourceRect: DOMRectReadOnly | undefined;
get sourceRect() {
return this.#sourceRect;
}
#sourceElement: Element | null = null;
get sourceElement() {
return this.#sourceElement;
}
#sourceCallback = (entry: VisualObserverEntry) => {
this.#sourceRect = entry.contentRect;
this.#update();
};
#sourceHandler = (e: Event) => {
const geometry = e.target as FolkGeometry;
this.#sourceRect = geometry.getClientRect();
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: VisualObserverEntry) => {
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() {
return this.#target;
}
set target(target) {
this.#target = target;
this.observeTarget();
}
#targetRect: DOMRectReadOnly | undefined;
get targetRect() {
return this.#targetRect;
}
#targetElement: Element | null = null;
get targetElement() {
return this.#targetElement;
}
#targetCallback = (entry: VisualObserverEntry) => {
this.#targetRect = entry.contentRect;
this.#update();
};
#targetHandler = (e: Event) => {
const geometry = e.target as FolkGeometry;
this.#targetRect = geometry.getClientRect();
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: VisualObserverEntry) => {
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;
}
disconnectedCallback() {
this.unobserveSource();
this.unobserveTarget();
}
// TODO: why reparse the vertex?
setSourceVertex(vertex: Vertex) {
this.target = `${vertex.x},${vertex.y}`;
}
observeSource() {
this.unobserveSource();
const vertex = parseVertex(this.#source);
if (vertex) {
this.#sourceRect = DOMRectReadOnly.fromRect(vertex);
this.#update();
} else {
const [selector, iframeSelector] = parseCSSSelector(this.#source);
this.#sourceIframeSelector = iframeSelector;
this.#sourceElement = document.querySelector(selector);
if (this.#sourceElement === null) {
throw new Error('source is not a valid element');
} else if (this.#sourceElement instanceof FolkGeometry) {
this.#sourceElement.addEventListener('resize', this.#sourceHandler);
this.#sourceElement.addEventListener('move', this.#sourceHandler);
this.#sourceRect = this.#sourceElement.getBoundingClientRect();
} else if (this.#sourceElement instanceof HTMLIFrameElement && this.#sourceIframeSelector) {
window.addEventListener('message', this.#sourcePostMessage);
visualObserver.observe(this.#sourceElement, this.#sourceIframeCallback);
this.#sourceElement.contentWindow?.postMessage({
type: 'folk-observe-element',
selector: this.#sourceIframeSelector,
});
} else {
visualObserver.observe(this.#sourceElement, this.#sourceCallback);
this.#sourceRect = this.#sourceElement.getBoundingClientRect();
}
}
}
unobserveSource() {
if (this.#sourceElement === null) return;
if (this.#sourceElement instanceof FolkGeometry) {
this.#sourceElement.removeEventListener('resize', this.#sourceHandler);
this.#sourceElement.removeEventListener('move', this.#sourceHandler);
} else if (this.#sourceElement instanceof HTMLIFrameElement && this.#sourceIframeSelector) {
window.removeEventListener('message', this.#sourcePostMessage);
visualObserver.unobserve(this.#sourceElement, this.#sourceIframeCallback);
this.#sourceElement.contentWindow?.postMessage({
type: 'folk-unobserve-element',
selector: this.#sourceIframeSelector,
});
} else {
visualObserver.unobserve(this.#sourceElement, this.#sourceCallback);
}
}
observeTarget() {
this.unobserveTarget();
const vertex = parseVertex(this.#target);
if (vertex) {
this.#targetRect = DOMRectReadOnly.fromRect(vertex);
this.#update();
} else {
const [selector, iframeSelector] = parseCSSSelector(this.#target);
this.#targetIframeSelector = iframeSelector;
this.#targetElement = document.querySelector(selector);
if (!this.#targetElement) {
throw new Error('target is not a valid element');
} else if (this.#targetElement instanceof FolkGeometry) {
this.#targetElement.addEventListener('resize', this.#targetHandler);
this.#targetElement.addEventListener('move', this.#targetHandler);
} else if (this.#targetElement instanceof HTMLIFrameElement && this.#targetIframeSelector) {
window.addEventListener('message', this.#targetPostMessage);
visualObserver.observe(this.#targetElement, this.#targetIframeCallback);
this.#targetElement.contentWindow?.postMessage({
type: 'folk-observe-element',
selector: this.#targetIframeSelector,
});
} else {
visualObserver.observe(this.#targetElement, this.#targetCallback);
}
this.#targetRect = this.#targetElement.getBoundingClientRect();
}
}
unobserveTarget() {
if (this.#targetElement === null) return;
if (this.#targetElement instanceof FolkGeometry) {
this.#targetElement.removeEventListener('resize', this.#targetHandler);
this.#targetElement.removeEventListener('move', this.#targetHandler);
} else if (this.#targetElement instanceof HTMLIFrameElement && this.#targetIframeSelector) {
window.removeEventListener('message', this.#targetPostMessage);
visualObserver.unobserve(this.#targetElement, this.#targetIframeCallback);
this.#targetElement.contentWindow?.postMessage({
type: 'folk-unobserve-element',
selector: this.#targetIframeSelector,
});
} else {
visualObserver.unobserve(this.#targetElement, this.#targetCallback);
}
}
#update() {
if (this.#sourceRect === undefined || this.#targetRect === undefined) return;
this.render(this.#sourceRect, this.#targetRect);
}
// @ts-ignore
render(sourceRect: DOMRectReadOnly, targetRect: DOMRectReadOnly) {}
}