iframed arrows POC

This commit is contained in:
“chrisshank” 2024-11-21 21:50:18 -08:00
parent c7d6243cb9
commit 3f6d9c9ea4
6 changed files with 248 additions and 12 deletions

View File

@ -25,22 +25,17 @@
position: absolute;
inset: 0 0 0 0;
}
#box2 {
max-width: 10ch;
}
</style>
</head>
<body>
<fc-geometry id="box1" x="100" y="100" width="50" height="50"></fc-geometry>
<fc-geometry id="box2" x="200" y="300">Hello World</fc-geometry>
<fc-geometry id="box1" x="50" y="100" width="50" height="50"></fc-geometry>
<fc-geometry id="box2" x="150" y="300" width="50" height="50"></fc-geometry>
<fc-connection source="#box1" target="#box2"></fc-connection>
<fc-connection source="#box1" target="400,100"></fc-connection>
<script type="module">
import { FolkGeometry } from '../src/canvas/fc-geometry.ts';
import { FolkConnection } from '../src/arrows/fc-connection.ts';
import '../src/arrows/iframe-script.ts';
FolkGeometry.register();
FolkConnection.register();

54
demo/iframed-arrows.html Normal file
View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Iframe Arrows</title>
<style>
html {
height: 100%;
}
body {
min-height: 100%;
position: relative;
margin: 0;
}
fc-geometry {
border: 1px solid black;
border-radius: 3px;
}
fc-rope {
display: block;
position: absolute;
inset: 0 0 0 0;
pointer-events: none;
}
iframe {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<fc-geometry x="10" y="100" width="250" height="400">
<iframe id="frame1" src="./arrows.html"></iframe>
</fc-geometry>
<fc-geometry x="300" y="100" width="250" height="400">
<iframe id="frame2" src="./arrows.html"></iframe>
</fc-geometry>
<fc-rope source="#frame1 >>> #box1" target="#frame2 >>> #box1"></fc-rope>
<script type="module">
import { FolkGeometry } from '../src/canvas/fc-geometry.ts';
import { FolkRope } from '../src/arrows/fc-rope.ts';
FolkGeometry.register();
FolkRope.register();
</script>
</body>
</html>

View File

@ -21,6 +21,7 @@
<ul>
<li><a href="animated-shapes.html">Animated Shapes</a></li>
<li><a href="arrows.html">Arrows</a></li>
<li><a href="iframed-arrows.html">Iframed Arrows</a></li>
<li><a href="canvasify.html">Canvasify</a></li>
<li><a href="collision.html">Collision</a></li>
<li><a href="event-propagator.html">Event propagator</a></li>

View File

@ -17,6 +17,10 @@ function parseVertex(str: string): Vertex | null {
};
}
function parseCSSSelector(selector) {
return selector.split('>>>').map((s) => s.trim());
}
export class AbstractArrow extends HTMLElement {
static tagName = 'abstract-arrow';
@ -57,6 +61,49 @@ export class AbstractArrow extends HTMLElement {
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() {
@ -88,6 +135,49 @@ export class AbstractArrow extends HTMLElement {
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;
@ -112,18 +202,27 @@ export class AbstractArrow extends HTMLElement {
this.#sourceRect = DOMRectReadOnly.fromRect(vertex);
this.#update();
} else {
this.#sourceElement = document.querySelector(this.source);
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();
}
this.#sourceRect = this.#sourceElement.getBoundingClientRect();
}
}
@ -133,6 +232,13 @@ export class AbstractArrow extends HTMLElement {
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);
}
@ -147,13 +253,22 @@ export class AbstractArrow extends HTMLElement {
this.#targetRect = DOMRectReadOnly.fromRect(vertex);
this.#update();
} else {
this.#targetElement = document.querySelector(this.#target);
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);
}
@ -167,6 +282,13 @@ export class AbstractArrow extends HTMLElement {
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);
}

View File

@ -0,0 +1,64 @@
import { FolkGeometry } from '../canvas/fc-geometry';
// If this page is framed in then mock inject the following post message script
if (window.parent !== window) {
// keep track of count of elements being observed
const observedElements = new Map();
const observedSelectors = new Map();
function onGeometryChange(event) {
window.parent.postMessage({
type: 'folk-element-change',
selector: observedSelectors.get(event.target),
boundingBox: event.target.getClientRect(),
});
}
window.addEventListener('message', (event) => {
switch (event.data.type) {
case 'folk-observe-element': {
const selector = event.data.selector;
const element = document.querySelector(selector);
if (element === null) return;
observedElements.set(selector, element);
observedSelectors.set(element, selector);
if (element instanceof FolkGeometry) {
element.addEventListener('move', onGeometryChange);
element.addEventListener('resize', onGeometryChange);
window.parent.postMessage({
type: 'folk-element-change',
selector: selector,
boundingBox: element.getClientRect(),
});
} else {
// use BoundingBoxObserver
}
return;
}
case 'folk-unobserve-element': {
const selector = event.data.selector;
const element = observedElements.get(selector);
if (element === undefined) return;
if (element instanceof FolkGeometry) {
element.removeEventListener('move', onGeometryChange);
element.removeEventListener('resize', onGeometryChange);
observedElements.delete(selector);
observedSelectors.delete(element);
} else {
}
return;
}
}
});
window.parent.postMessage({
type: 'folk-iframe-ready',
});
}