147 lines
3.7 KiB
TypeScript
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';
|
|
}
|
|
};
|
|
}
|