folk-canvas/src/arrows/event-propagator.ts

147 lines
3.7 KiB
TypeScript

import { FolkRope } from './fc-rope.ts';
const styles = new CSSStyleSheet();
styles.replaceSync(`
textarea {
position: absolute;
width: auto;
min-width: 3ch;
height: auto;
resize: none;
background: rgba(256, 256, 256, 0.8);
border: 1px solid #ccc;
padding: 4px;
pointer-events: auto;
overflow: hidden;
field-sizing: content;
translate: -50% -50%;
border-radius: 5px;
}
`);
export class EventPropagator extends FolkRope {
static override tagName = 'event-propagator';
#triggers: string[] = [];
get triggers() {
return this.#triggers;
}
set triggers(triggers: string | string[]) {
if (typeof triggers === 'string') {
triggers = triggers.split(',');
}
this.#removeEventListenersToSource();
this.#triggers = triggers;
this.#addEventListenersToSource();
}
#expression = '';
#function = new Function();
get expression() {
return this.#expression;
}
set expression(expression) {
this.mend();
this.#expression = expression;
try {
this.#function = new Function('$source', '$target', '$event', expression);
} catch (error) {
console.warn('Failed to parse expression:', error);
// Use no-op function when parsing fails
this.cut();
this.#function = () => {};
}
}
#triggerTextarea = document.createElement('textarea');
#expressionTextarea = document.createElement('textarea');
constructor() {
super();
this.shadowRoot?.adoptedStyleSheets.push(styles);
this.#triggerTextarea.addEventListener('change', () => {
this.triggers = this.#triggerTextarea.value;
});
this.triggers = this.#triggerTextarea.value = this.getAttribute('triggers') || '';
this.shadowRoot?.appendChild(this.#triggerTextarea);
this.#expressionTextarea.addEventListener('input', () => {
this.expression = this.#expressionTextarea.value;
});
this.shadowRoot?.appendChild(this.#expressionTextarea);
this.expression = this.#expressionTextarea.value = this.getAttribute('expression') || '';
}
override render(sourceRect: DOMRectReadOnly, targetRect: DOMRectReadOnly) {
super.render(sourceRect, targetRect);
}
override draw() {
super.draw();
const triggerPoint = this.points[Math.floor(this.points.length / 5)];
if (triggerPoint) {
this.#triggerTextarea.style.left = `${triggerPoint.pos.x}px`;
this.#triggerTextarea.style.top = `${triggerPoint.pos.y}px`;
}
const expressionPoint = this.points[Math.floor((this.points.length * 3) / 5)];
if (expressionPoint) {
this.#expressionTextarea.style.left = `${expressionPoint.pos.x}px`;
this.#expressionTextarea.style.top = `${expressionPoint.pos.y}px`;
}
}
override observeSource() {
super.observeSource();
this.#addEventListenersToSource();
}
#addEventListenersToSource() {
for (const trigger of this.#triggers) {
// TODO: add special triggers for intersection, rAF, etc.
this.sourceElement?.addEventListener(trigger, this.evaluateExpression);
}
}
override unobserveSource() {
super.unobserveSource();
this.#removeEventListenersToSource();
}
#removeEventListenersToSource() {
for (const trigger of this.#triggers) {
this.sourceElement?.removeEventListener(trigger, this.evaluateExpression);
}
}
override observeTarget() {
super.observeTarget();
}
override unobserveTarget() {
super.unobserveTarget();
}
// Do we need the event at all?
evaluateExpression = (event?: Event) => {
if (this.sourceElement === null || this.targetElement === null) return;
this.stroke = 'black';
try {
this.#function(this.sourceElement, this.targetElement, event);
} catch (error) {
this.stroke = 'red';
}
};
}