observer iframe rects
This commit is contained in:
parent
2d158d1875
commit
b2d8bb1088
|
|
@ -23,6 +23,8 @@
|
|||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* margins cause infinite resize loops */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
@ -34,7 +36,7 @@
|
|||
<iframe id="frame2" src="./sticky-html-arrow.html"></iframe>
|
||||
</folk-shape>
|
||||
|
||||
<folk-rope source="#frame1 >>> #box1" target="#frame2 >>> #box1"></folk-rope>
|
||||
<folk-rope source="#frame1 >>> #box1" target="#frame2 >>> #box2"></folk-rope>
|
||||
|
||||
<script type="module">
|
||||
import '../src/standalone/folk-shape.ts';
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
/* margins cause infinite resize loops */
|
||||
box-sizing: border-box;
|
||||
|
||||
&:nth-child(1) {
|
||||
left: 50px;
|
||||
|
|
|
|||
|
|
@ -171,12 +171,8 @@ export class ClientRectObserver {
|
|||
}
|
||||
|
||||
takeRecords(): ClientRectObserverEntry[] {
|
||||
if (this.#rafId === 0) return [];
|
||||
|
||||
const entries = this.#entries;
|
||||
this.#entries = [];
|
||||
cancelAnimationFrame(this.#rafId);
|
||||
this.#rafId = 0;
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,130 @@ import { TransformEvent } from './TransformEvent';
|
|||
|
||||
export type ClientRectObserverEntryCallback = (entry: ClientRectObserverEntry) => void;
|
||||
|
||||
export type FolkObserverOptions = {
|
||||
iframeSelector?: string;
|
||||
};
|
||||
|
||||
interface IframeChild {
|
||||
rect: DOMRectReadOnly | null;
|
||||
callbacks: Set<ClientRectObserverEntryCallback>;
|
||||
}
|
||||
|
||||
type PostMessageSendEvent =
|
||||
| { type: 'folk-observe-element'; selector: string }
|
||||
| { type: 'folk-unobserve-element'; selector: string };
|
||||
|
||||
class IframeObserver {
|
||||
#iframe;
|
||||
#observer;
|
||||
#iframeRect!: DOMRectReadOnly;
|
||||
#iframeChildren = new Map<string, IframeChild>();
|
||||
#isDisposed = false;
|
||||
|
||||
get isDisposed() {
|
||||
return this.#isDisposed;
|
||||
}
|
||||
|
||||
constructor(iframe: HTMLIFrameElement, observer: FolkObserver) {
|
||||
this.#iframe = iframe;
|
||||
this.#observer = observer;
|
||||
|
||||
observer.observe(iframe, this.#iframeCallback);
|
||||
window.addEventListener('message', this.#onPostmessage);
|
||||
}
|
||||
|
||||
observeChild(selector: string, callback: ClientRectObserverEntryCallback) {
|
||||
let child = this.#iframeChildren.get(selector);
|
||||
|
||||
if (child === undefined) {
|
||||
child = {
|
||||
callbacks: new Set(),
|
||||
rect: null,
|
||||
};
|
||||
|
||||
this.#iframeChildren.set(selector, child);
|
||||
|
||||
this.#postMessage({ type: 'folk-observe-element', selector });
|
||||
}
|
||||
|
||||
child.callbacks.add(callback);
|
||||
}
|
||||
|
||||
unobserveChild(selector: string, callback: ClientRectObserverEntryCallback) {
|
||||
let child = this.#iframeChildren.get(selector);
|
||||
|
||||
if (child === undefined) return;
|
||||
|
||||
child.callbacks.delete(callback);
|
||||
|
||||
if (child.callbacks.size === 0) {
|
||||
this.#iframeChildren.delete(selector);
|
||||
this.#postMessage({ type: 'folk-unobserve-element', selector });
|
||||
}
|
||||
|
||||
if (this.#iframeChildren.size === 0) {
|
||||
this.#observer.unobserve(this.#iframe, this.#iframeCallback);
|
||||
window.removeEventListener('message', this.#onPostmessage);
|
||||
this.#isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#iframeCallback = (entry: ClientRectObserverEntry) => {
|
||||
this.#iframeRect = entry.contentRect;
|
||||
|
||||
for (const selector of this.#iframeChildren.keys()) {
|
||||
this.#updatedChildRect(selector);
|
||||
}
|
||||
};
|
||||
|
||||
#onPostmessage = (event: MessageEvent) => {
|
||||
if (event.source !== this.#iframe.contentWindow) return;
|
||||
|
||||
switch (event.data.type) {
|
||||
case 'folk-iframe-ready': {
|
||||
for (const selector of this.#iframeChildren.keys()) {
|
||||
this.#postMessage({ type: 'folk-observe-element', selector });
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'folk-element-change': {
|
||||
this.#updatedChildRect(event.data.selector, event.data.contentRect);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#updatedChildRect(selector: string, rect?: DOMRectReadOnly) {
|
||||
const child = this.#iframeChildren.get(selector);
|
||||
|
||||
if (child === undefined) return;
|
||||
|
||||
if (rect) {
|
||||
child.rect = rect;
|
||||
}
|
||||
|
||||
if (child.rect === null) return;
|
||||
|
||||
const contentRect = DOMRectReadOnly.fromRect({
|
||||
x: this.#iframeRect.x + child.rect.x,
|
||||
y: this.#iframeRect.y + child.rect.y,
|
||||
height: child.rect.height,
|
||||
width: child.rect.width,
|
||||
});
|
||||
|
||||
child.callbacks.forEach((callback) =>
|
||||
callback({
|
||||
target: this.#iframe,
|
||||
contentRect,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#postMessage(event: PostMessageSendEvent) {
|
||||
this.#iframe.contentWindow?.postMessage(event);
|
||||
}
|
||||
}
|
||||
|
||||
export class FolkObserver {
|
||||
static #instance: FolkObserver | null = null;
|
||||
|
||||
|
|
@ -16,6 +140,7 @@ export class FolkObserver {
|
|||
}
|
||||
|
||||
#elementMap = new WeakMap<Element, Set<ClientRectObserverEntryCallback>>();
|
||||
#iframeMap = new WeakMap<HTMLIFrameElement, IframeObserver>();
|
||||
|
||||
#vo = new ClientRectObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
|
|
@ -35,29 +160,62 @@ export class FolkObserver {
|
|||
this.#updateTarget({ target: event.target as HTMLElement, contentRect: event.current });
|
||||
};
|
||||
|
||||
observe(target: Element, callback: ClientRectObserverEntryCallback): void {
|
||||
let callbacks = this.#elementMap.get(target);
|
||||
observe(
|
||||
target: Element,
|
||||
callback: ClientRectObserverEntryCallback,
|
||||
{ iframeSelector }: FolkObserverOptions = {}
|
||||
): void {
|
||||
if (target instanceof HTMLIFrameElement && iframeSelector) {
|
||||
let iframeObserver = this.#iframeMap.get(target);
|
||||
|
||||
const isFolkShape = target instanceof FolkShape;
|
||||
if (iframeObserver === undefined) {
|
||||
iframeObserver = new IframeObserver(target, this);
|
||||
this.#iframeMap.set(target, iframeObserver);
|
||||
}
|
||||
|
||||
iframeObserver.observeChild(iframeSelector, callback);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let callbacks = this.#elementMap.get(target);
|
||||
|
||||
if (callbacks === undefined) {
|
||||
this.#elementMap.set(target, (callbacks = new Set()));
|
||||
|
||||
if (isFolkShape) {
|
||||
if (target instanceof FolkShape) {
|
||||
target.addEventListener('transform', this.#onTransform);
|
||||
callback({ target, contentRect: target.getTransformDOMRect() });
|
||||
} else {
|
||||
this.#vo.observe(target);
|
||||
}
|
||||
} else {
|
||||
const contentRect = isFolkShape ? target.getTransformDOMRect() : target.getBoundingClientRect();
|
||||
const contentRect = target instanceof FolkShape ? target.getTransformDOMRect() : target.getBoundingClientRect();
|
||||
callback({ target, contentRect });
|
||||
}
|
||||
|
||||
callbacks.add(callback);
|
||||
}
|
||||
|
||||
unobserve(target: Element, callback: ClientRectObserverEntryCallback): void {
|
||||
unobserve(
|
||||
target: Element,
|
||||
callback: ClientRectObserverEntryCallback,
|
||||
{ iframeSelector }: FolkObserverOptions = {}
|
||||
): void {
|
||||
if (target instanceof HTMLIFrameElement && iframeSelector) {
|
||||
let iframeObserver = this.#iframeMap.get(target);
|
||||
|
||||
if (iframeObserver === undefined) return;
|
||||
|
||||
iframeObserver.unobserveChild(iframeSelector, callback);
|
||||
|
||||
if (iframeObserver.isDisposed) {
|
||||
this.#iframeMap.delete(target);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let callbacks = this.#elementMap.get(target);
|
||||
|
||||
if (callbacks === undefined) return;
|
||||
|
|
@ -75,6 +233,6 @@ export class FolkObserver {
|
|||
}
|
||||
}
|
||||
|
||||
export function parseCSSSelector(selector: string): string[] {
|
||||
export function parseDeepCSSSelector(selector: string): string[] {
|
||||
return selector.split('>>>').map((s) => s.trim());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,7 @@
|
|||
import { FolkShape } from '../folk-shape.ts';
|
||||
import { ClientRectObserverManager, ClientRectObserverEntry } from './client-rect-observer.ts';
|
||||
import { TransformEvent } from './TransformEvent.ts';
|
||||
import { ClientRectObserverEntry } from './client-rect-observer.ts';
|
||||
import { FolkObserver } from './folk-observer.ts';
|
||||
|
||||
const clientRectObserver = new ClientRectObserverManager();
|
||||
|
||||
interface ObservedElementEntry {
|
||||
selector: string;
|
||||
element: Element;
|
||||
count: number;
|
||||
}
|
||||
|
||||
class ObservedElements {
|
||||
#elements: ObservedElementEntry[] = [];
|
||||
|
||||
observe(selector: string) {
|
||||
let entry = this.#elements.find((e) => e.selector === selector);
|
||||
|
||||
if (entry === undefined) {
|
||||
entry = { selector, element: document.querySelector(selector)!, count: 0 };
|
||||
this.#elements.push(entry);
|
||||
}
|
||||
|
||||
entry.count += 1;
|
||||
|
||||
return entry.element;
|
||||
}
|
||||
|
||||
unobserve(selector: string) {
|
||||
const entryIndex = this.#elements.findIndex((e) => e.selector === selector);
|
||||
const entry = this.#elements[entryIndex];
|
||||
|
||||
if (entry === undefined) return;
|
||||
|
||||
entry.count -= 1;
|
||||
|
||||
if (entry.count === 0) {
|
||||
this.#elements.splice(entryIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
getElement(selector: string) {
|
||||
return this.#elements.find((e) => e.selector === selector)?.element;
|
||||
}
|
||||
|
||||
getSelector(element: Element) {
|
||||
return this.#elements.find((e) => e.element === element)?.selector;
|
||||
}
|
||||
}
|
||||
const folkObserver = new FolkObserver();
|
||||
|
||||
// If this page is framed in then mock inject the following post message script
|
||||
if (window.parent !== window) {
|
||||
|
|
@ -58,15 +13,7 @@ if (window.parent !== window) {
|
|||
window.parent.postMessage({
|
||||
type: 'folk-element-change',
|
||||
selector: observedSelectors.get(entry.target),
|
||||
boundingBox: entry.contentRect,
|
||||
});
|
||||
}
|
||||
|
||||
function onGeometryChange(event: TransformEvent) {
|
||||
window.parent.postMessage({
|
||||
type: 'folk-element-change',
|
||||
selector: observedSelectors.get(event.target),
|
||||
boundingBox: (event.target as FolkShape)?.getTransformDOMRect(),
|
||||
contentRect: entry.contentRect.toJSON(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -81,17 +28,7 @@ if (window.parent !== window) {
|
|||
observedElements.set(selector, element);
|
||||
observedSelectors.set(element, selector);
|
||||
|
||||
if (element instanceof FolkShape) {
|
||||
element.addEventListener('transform', onGeometryChange);
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'folk-element-change',
|
||||
selector: selector,
|
||||
boundingBox: element.getTransformDOMRect(),
|
||||
});
|
||||
} else {
|
||||
clientRectObserver.observe(element, boundingBoxCallback);
|
||||
}
|
||||
folkObserver.observe(element, boundingBoxCallback);
|
||||
return;
|
||||
}
|
||||
case 'folk-unobserve-element': {
|
||||
|
|
@ -100,15 +37,7 @@ if (window.parent !== window) {
|
|||
|
||||
if (element === undefined) return;
|
||||
|
||||
if (element instanceof FolkShape) {
|
||||
element.removeEventListener('transform', onGeometryChange);
|
||||
observedElements.delete(selector);
|
||||
observedSelectors.delete(element);
|
||||
} else {
|
||||
clientRectObserver.unobserve(element, boundingBoxCallback);
|
||||
}
|
||||
|
||||
return;
|
||||
folkObserver.unobserve(element, boundingBoxCallback);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { parseVertex } from './common/utils.ts';
|
||||
import { ClientRectObserverEntry } from './common/client-rect-observer.ts';
|
||||
import { FolkObserver } from './common/folk-observer.ts';
|
||||
import { FolkObserver, parseDeepCSSSelector } from './common/folk-observer.ts';
|
||||
import { FolkElement } from './common/folk-element.ts';
|
||||
import { property, state } from '@lit/reactive-element/decorators.js';
|
||||
import { css, CSSResultGroup, PropertyValues } from '@lit/reactive-element';
|
||||
|
|
@ -19,12 +19,16 @@ export class FolkBaseConnection extends FolkElement {
|
|||
|
||||
@property({ type: String, reflect: true }) source?: string;
|
||||
|
||||
#sourceIframeSelector: string | undefined = undefined;
|
||||
|
||||
@state() sourceElement: Element | null = null;
|
||||
|
||||
@state() sourceRect: DOMRectReadOnly | null = null;
|
||||
|
||||
@property({ type: String, reflect: true }) target?: string;
|
||||
|
||||
#targetIframeSelector: string | undefined = undefined;
|
||||
|
||||
@state() targetRect: DOMRectReadOnly | null = null;
|
||||
|
||||
@state() targetElement: Element | null = null;
|
||||
|
|
@ -48,7 +52,9 @@ export class FolkBaseConnection extends FolkElement {
|
|||
if (vertex) {
|
||||
this.sourceRect = DOMRectReadOnly.fromRect(vertex);
|
||||
} else {
|
||||
this.sourceElement = document.querySelector(this.source);
|
||||
const [selector, iframeSelector] = parseDeepCSSSelector(this.source);
|
||||
this.#sourceIframeSelector = iframeSelector;
|
||||
this.sourceElement = document.querySelector(selector);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +62,7 @@ export class FolkBaseConnection extends FolkElement {
|
|||
if (this.sourceElement === null) {
|
||||
this.sourceRect = null;
|
||||
} else {
|
||||
folkObserver.observe(this.sourceElement, this.#sourceCallback);
|
||||
folkObserver.observe(this.sourceElement, this.#sourceCallback, { iframeSelector: this.#sourceIframeSelector });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +76,9 @@ export class FolkBaseConnection extends FolkElement {
|
|||
if (vertex) {
|
||||
this.targetRect = DOMRectReadOnly.fromRect(vertex);
|
||||
} else {
|
||||
this.targetElement = document.querySelector(this.target);
|
||||
const [selector, iframeSelector] = parseDeepCSSSelector(this.target);
|
||||
this.#targetIframeSelector = iframeSelector;
|
||||
this.targetElement = document.querySelector(selector);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +86,7 @@ export class FolkBaseConnection extends FolkElement {
|
|||
if (this.targetElement === null) {
|
||||
this.targetRect = null;
|
||||
} else {
|
||||
folkObserver.observe(this.targetElement, this.#targetCallback);
|
||||
folkObserver.observe(this.targetElement, this.#targetCallback, { iframeSelector: this.#targetIframeSelector });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +98,7 @@ export class FolkBaseConnection extends FolkElement {
|
|||
#unobserveSource() {
|
||||
if (this.sourceElement === null) return;
|
||||
|
||||
folkObserver.unobserve(this.sourceElement, this.#sourceCallback);
|
||||
folkObserver.unobserve(this.sourceElement, this.#sourceCallback, { iframeSelector: this.#sourceIframeSelector });
|
||||
}
|
||||
|
||||
#targetCallback = (entry: ClientRectObserverEntry) => {
|
||||
|
|
@ -99,6 +107,6 @@ export class FolkBaseConnection extends FolkElement {
|
|||
|
||||
#unobserveTarget() {
|
||||
if (this.targetElement === null) return;
|
||||
folkObserver.unobserve(this.targetElement, this.#targetCallback);
|
||||
folkObserver.unobserve(this.targetElement, this.#targetCallback, { iframeSelector: this.#targetIframeSelector });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue