observer iframe rects
This commit is contained in:
parent
2d158d1875
commit
b2d8bb1088
|
|
@ -23,6 +23,8 @@
|
||||||
iframe {
|
iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
/* margins cause infinite resize loops */
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -34,7 +36,7 @@
|
||||||
<iframe id="frame2" src="./sticky-html-arrow.html"></iframe>
|
<iframe id="frame2" src="./sticky-html-arrow.html"></iframe>
|
||||||
</folk-shape>
|
</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">
|
<script type="module">
|
||||||
import '../src/standalone/folk-shape.ts';
|
import '../src/standalone/folk-shape.ts';
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
/* margins cause infinite resize loops */
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
&:nth-child(1) {
|
&:nth-child(1) {
|
||||||
left: 50px;
|
left: 50px;
|
||||||
|
|
|
||||||
|
|
@ -171,12 +171,8 @@ export class ClientRectObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
takeRecords(): ClientRectObserverEntry[] {
|
takeRecords(): ClientRectObserverEntry[] {
|
||||||
if (this.#rafId === 0) return [];
|
|
||||||
|
|
||||||
const entries = this.#entries;
|
const entries = this.#entries;
|
||||||
this.#entries = [];
|
this.#entries = [];
|
||||||
cancelAnimationFrame(this.#rafId);
|
|
||||||
this.#rafId = 0;
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,130 @@ import { TransformEvent } from './TransformEvent';
|
||||||
|
|
||||||
export type ClientRectObserverEntryCallback = (entry: ClientRectObserverEntry) => void;
|
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 {
|
export class FolkObserver {
|
||||||
static #instance: FolkObserver | null = null;
|
static #instance: FolkObserver | null = null;
|
||||||
|
|
||||||
|
|
@ -16,6 +140,7 @@ export class FolkObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
#elementMap = new WeakMap<Element, Set<ClientRectObserverEntryCallback>>();
|
#elementMap = new WeakMap<Element, Set<ClientRectObserverEntryCallback>>();
|
||||||
|
#iframeMap = new WeakMap<HTMLIFrameElement, IframeObserver>();
|
||||||
|
|
||||||
#vo = new ClientRectObserver((entries) => {
|
#vo = new ClientRectObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
|
@ -35,29 +160,62 @@ export class FolkObserver {
|
||||||
this.#updateTarget({ target: event.target as HTMLElement, contentRect: event.current });
|
this.#updateTarget({ target: event.target as HTMLElement, contentRect: event.current });
|
||||||
};
|
};
|
||||||
|
|
||||||
observe(target: Element, callback: ClientRectObserverEntryCallback): void {
|
observe(
|
||||||
let callbacks = this.#elementMap.get(target);
|
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) {
|
if (callbacks === undefined) {
|
||||||
this.#elementMap.set(target, (callbacks = new Set()));
|
this.#elementMap.set(target, (callbacks = new Set()));
|
||||||
|
|
||||||
if (isFolkShape) {
|
if (target instanceof FolkShape) {
|
||||||
target.addEventListener('transform', this.#onTransform);
|
target.addEventListener('transform', this.#onTransform);
|
||||||
callback({ target, contentRect: target.getTransformDOMRect() });
|
callback({ target, contentRect: target.getTransformDOMRect() });
|
||||||
} else {
|
} else {
|
||||||
this.#vo.observe(target);
|
this.#vo.observe(target);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const contentRect = isFolkShape ? target.getTransformDOMRect() : target.getBoundingClientRect();
|
const contentRect = target instanceof FolkShape ? target.getTransformDOMRect() : target.getBoundingClientRect();
|
||||||
callback({ target, contentRect });
|
callback({ target, contentRect });
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacks.add(callback);
|
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);
|
let callbacks = this.#elementMap.get(target);
|
||||||
|
|
||||||
if (callbacks === undefined) return;
|
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());
|
return selector.split('>>>').map((s) => s.trim());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,7 @@
|
||||||
import { FolkShape } from '../folk-shape.ts';
|
import { ClientRectObserverEntry } from './client-rect-observer.ts';
|
||||||
import { ClientRectObserverManager, ClientRectObserverEntry } from './client-rect-observer.ts';
|
import { FolkObserver } from './folk-observer.ts';
|
||||||
import { TransformEvent } from './TransformEvent.ts';
|
|
||||||
|
|
||||||
const clientRectObserver = new ClientRectObserverManager();
|
const folkObserver = new FolkObserver();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this page is framed in then mock inject the following post message script
|
// If this page is framed in then mock inject the following post message script
|
||||||
if (window.parent !== window) {
|
if (window.parent !== window) {
|
||||||
|
|
@ -58,15 +13,7 @@ if (window.parent !== window) {
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: 'folk-element-change',
|
type: 'folk-element-change',
|
||||||
selector: observedSelectors.get(entry.target),
|
selector: observedSelectors.get(entry.target),
|
||||||
boundingBox: entry.contentRect,
|
contentRect: entry.contentRect.toJSON(),
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onGeometryChange(event: TransformEvent) {
|
|
||||||
window.parent.postMessage({
|
|
||||||
type: 'folk-element-change',
|
|
||||||
selector: observedSelectors.get(event.target),
|
|
||||||
boundingBox: (event.target as FolkShape)?.getTransformDOMRect(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,17 +28,7 @@ if (window.parent !== window) {
|
||||||
observedElements.set(selector, element);
|
observedElements.set(selector, element);
|
||||||
observedSelectors.set(element, selector);
|
observedSelectors.set(element, selector);
|
||||||
|
|
||||||
if (element instanceof FolkShape) {
|
folkObserver.observe(element, boundingBoxCallback);
|
||||||
element.addEventListener('transform', onGeometryChange);
|
|
||||||
|
|
||||||
window.parent.postMessage({
|
|
||||||
type: 'folk-element-change',
|
|
||||||
selector: selector,
|
|
||||||
boundingBox: element.getTransformDOMRect(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
clientRectObserver.observe(element, boundingBoxCallback);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'folk-unobserve-element': {
|
case 'folk-unobserve-element': {
|
||||||
|
|
@ -100,15 +37,7 @@ if (window.parent !== window) {
|
||||||
|
|
||||||
if (element === undefined) return;
|
if (element === undefined) return;
|
||||||
|
|
||||||
if (element instanceof FolkShape) {
|
folkObserver.unobserve(element, boundingBoxCallback);
|
||||||
element.removeEventListener('transform', onGeometryChange);
|
|
||||||
observedElements.delete(selector);
|
|
||||||
observedSelectors.delete(element);
|
|
||||||
} else {
|
|
||||||
clientRectObserver.unobserve(element, boundingBoxCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { parseVertex } from './common/utils.ts';
|
import { parseVertex } from './common/utils.ts';
|
||||||
import { ClientRectObserverEntry } from './common/client-rect-observer.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 { FolkElement } from './common/folk-element.ts';
|
||||||
import { property, state } from '@lit/reactive-element/decorators.js';
|
import { property, state } from '@lit/reactive-element/decorators.js';
|
||||||
import { css, CSSResultGroup, PropertyValues } from '@lit/reactive-element';
|
import { css, CSSResultGroup, PropertyValues } from '@lit/reactive-element';
|
||||||
|
|
@ -19,12 +19,16 @@ export class FolkBaseConnection extends FolkElement {
|
||||||
|
|
||||||
@property({ type: String, reflect: true }) source?: string;
|
@property({ type: String, reflect: true }) source?: string;
|
||||||
|
|
||||||
|
#sourceIframeSelector: string | undefined = undefined;
|
||||||
|
|
||||||
@state() sourceElement: Element | null = null;
|
@state() sourceElement: Element | null = null;
|
||||||
|
|
||||||
@state() sourceRect: DOMRectReadOnly | null = null;
|
@state() sourceRect: DOMRectReadOnly | null = null;
|
||||||
|
|
||||||
@property({ type: String, reflect: true }) target?: string;
|
@property({ type: String, reflect: true }) target?: string;
|
||||||
|
|
||||||
|
#targetIframeSelector: string | undefined = undefined;
|
||||||
|
|
||||||
@state() targetRect: DOMRectReadOnly | null = null;
|
@state() targetRect: DOMRectReadOnly | null = null;
|
||||||
|
|
||||||
@state() targetElement: Element | null = null;
|
@state() targetElement: Element | null = null;
|
||||||
|
|
@ -48,7 +52,9 @@ export class FolkBaseConnection extends FolkElement {
|
||||||
if (vertex) {
|
if (vertex) {
|
||||||
this.sourceRect = DOMRectReadOnly.fromRect(vertex);
|
this.sourceRect = DOMRectReadOnly.fromRect(vertex);
|
||||||
} else {
|
} 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) {
|
if (this.sourceElement === null) {
|
||||||
this.sourceRect = null;
|
this.sourceRect = null;
|
||||||
} else {
|
} 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) {
|
if (vertex) {
|
||||||
this.targetRect = DOMRectReadOnly.fromRect(vertex);
|
this.targetRect = DOMRectReadOnly.fromRect(vertex);
|
||||||
} else {
|
} 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) {
|
if (this.targetElement === null) {
|
||||||
this.targetRect = null;
|
this.targetRect = null;
|
||||||
} else {
|
} 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() {
|
#unobserveSource() {
|
||||||
if (this.sourceElement === null) return;
|
if (this.sourceElement === null) return;
|
||||||
|
|
||||||
folkObserver.unobserve(this.sourceElement, this.#sourceCallback);
|
folkObserver.unobserve(this.sourceElement, this.#sourceCallback, { iframeSelector: this.#sourceIframeSelector });
|
||||||
}
|
}
|
||||||
|
|
||||||
#targetCallback = (entry: ClientRectObserverEntry) => {
|
#targetCallback = (entry: ClientRectObserverEntry) => {
|
||||||
|
|
@ -99,6 +107,6 @@ export class FolkBaseConnection extends FolkElement {
|
||||||
|
|
||||||
#unobserveTarget() {
|
#unobserveTarget() {
|
||||||
if (this.targetElement === null) return;
|
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