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

154 lines
3.6 KiB
TypeScript

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),
};
}
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();
};
#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();
};
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 {
this.#sourceElement = document.querySelector(this.source);
if (this.#sourceElement === null) {
throw new Error('source is not a valid element');
}
visualObserver.observe(this.#sourceElement, this.#sourceCallback);
this.#sourceRect = this.#sourceElement.getBoundingClientRect();
}
}
unobserveSource() {
if (this.#sourceElement === null) return;
visualObserver.unobserve(this.#sourceElement, this.#sourceCallback);
}
observeTarget() {
this.unobserveTarget();
const vertex = parseVertex(this.#target);
if (vertex) {
this.#targetRect = DOMRectReadOnly.fromRect(vertex);
this.#update();
} else {
this.#targetElement = document.querySelector(this.#target);
if (!this.#targetElement) {
throw new Error('target is not a valid element');
}
visualObserver.observe(this.#targetElement, this.#targetCallback);
this.#targetRect = this.#targetElement.getBoundingClientRect();
}
}
unobserveTarget() {
if (this.#targetElement === null) return;
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) {}
}