@atolljs/islands 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/island.ts +261 -23
- package/src/ops.ts +79 -9
- package/src/worker/defineWorkers.ts +29 -1
- package/src/worker/dom/css.ts +3 -1
- package/src/worker/dom/document.ts +122 -13
- package/src/worker/dom/element.ts +60 -26
- package/src/worker/dom/html.ts +1 -0
- package/src/worker/dom/node.ts +36 -0
- package/src/worker/dom/resizeObserver.ts +104 -0
- package/src/worker/dom/window.ts +19 -3
- package/src/worker/index.ts +2 -1
- package/src/worker/instance.ts +103 -8
- package/src/worker/proxyDom.ts +3 -0
- package/src/worker/subIsland.ts +52 -8
|
@@ -5,14 +5,17 @@ import {
|
|
|
5
5
|
getActiveInstance,
|
|
6
6
|
getLastActiveInstance,
|
|
7
7
|
getInstanceSize,
|
|
8
|
+
getInstanceRect,
|
|
9
|
+
setInstanceRect,
|
|
8
10
|
instances,
|
|
11
|
+
lookupInstance,
|
|
9
12
|
markInstanceActive,
|
|
10
13
|
pushOp,
|
|
11
14
|
registerHandler,
|
|
12
15
|
unregisterHandler,
|
|
13
16
|
} from '../instance';
|
|
14
17
|
import type { ElementInstance, HostInstance, TextInstance } from '../instance';
|
|
15
|
-
import type { EventPayload, Op } from '../../ops';
|
|
18
|
+
import type { EventPayload, MeasuredRect, Op } from '../../ops';
|
|
16
19
|
|
|
17
20
|
import {
|
|
18
21
|
ProxyNode,
|
|
@@ -105,6 +108,18 @@ export interface ProxyDocument {
|
|
|
105
108
|
* fill the island's box (a map viewport, a canvas host).
|
|
106
109
|
*/
|
|
107
110
|
markContainer(el: ProxyElement): void;
|
|
111
|
+
/**
|
|
112
|
+
* Request REAL geometry for `el` — emits a `measure` op asking the driver
|
|
113
|
+
* to measure the actual node and ResizeObserver it. `cb` fires inside the
|
|
114
|
+
* instance's task scope on every `setRects` push (the first arrives as
|
|
115
|
+
* soon as the driver answers, then on every element resize), so observer
|
|
116
|
+
* mutations emit ops like onResize handlers. The element's geometry reads
|
|
117
|
+
* (`clientWidth`, `getBoundingClientRect`, …) serve the last pushed rect
|
|
118
|
+
* from then on. Returns a detacher — removing the last observer emits
|
|
119
|
+
* `unmeasure`. This backs the `ResizeObserver` shim; call it directly for
|
|
120
|
+
* the plain (rect) => void form.
|
|
121
|
+
*/
|
|
122
|
+
observeSize(el: ProxyElement, cb: (rect: MeasuredRect) => void): () => void;
|
|
108
123
|
/**
|
|
109
124
|
* Register a handler fired every time the driver pushes a new container
|
|
110
125
|
* size — the app's resize signal. Handlers run inside the instance's task
|
|
@@ -140,6 +155,11 @@ export class InternalDocument implements ProxyDocument {
|
|
|
140
155
|
readonly _containerIds = new Set<number>([0]);
|
|
141
156
|
/** Handlers fired when a setSize task pushes a new container size. */
|
|
142
157
|
private readonly _resizeHandlers = new Set<(w: number, h: number) => void>();
|
|
158
|
+
/** element id → observeSize callbacks, fired on each setRects push. */
|
|
159
|
+
private readonly _sizeObservers = new Map<number, Set<(rect: MeasuredRect) => void>>();
|
|
160
|
+
/** ids a `measure` op was already emitted for — dedupes observeSize and
|
|
161
|
+
* the lazy auto-request a bare geometry read performs. */
|
|
162
|
+
readonly _measureRequested = new Set<number>();
|
|
143
163
|
/** The window facade bundle — built lazily by windowFacadeFor, shared by
|
|
144
164
|
* installDomShim and the instance dispatcher. */
|
|
145
165
|
_windowBundle?: WindowFacadeBundle;
|
|
@@ -198,11 +218,21 @@ export class InternalDocument implements ProxyDocument {
|
|
|
198
218
|
}
|
|
199
219
|
}
|
|
200
220
|
|
|
201
|
-
_warn(feature: string): void {
|
|
221
|
+
_warn(feature: string, message?: string): void {
|
|
202
222
|
if (this._warned.has(feature)) return;
|
|
203
223
|
this._warned.add(feature);
|
|
204
224
|
console.warn(
|
|
205
|
-
|
|
225
|
+
message ??
|
|
226
|
+
`[proxyDom] '${feature}' has no synchronous answer — a measure request was emitted and reads return real geometry once the driver pushes it; subscribe via doc.observeSize or a ResizeObserver for updates.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Scroll-geometry warn — the measure channel delivers border boxes, not
|
|
231
|
+
* scroll offsets/overflow extents, so scroll* reads stay honest 0. */
|
|
232
|
+
_scrollWarn(feature: string): void {
|
|
233
|
+
this._warn(
|
|
234
|
+
feature,
|
|
235
|
+
`[proxyDom] '${feature}' has no channel — the worker sees border-box geometry only (setSize/measure pushes); scroll offsets and overflow extents read 0.`,
|
|
206
236
|
);
|
|
207
237
|
}
|
|
208
238
|
|
|
@@ -212,13 +242,71 @@ export class InternalDocument implements ProxyDocument {
|
|
|
212
242
|
}
|
|
213
243
|
|
|
214
244
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
245
|
+
* Emit `measure` for `el` exactly once — shared by observeSize (explicit
|
|
246
|
+
* subscription) and by geometry reads on unmarked connected elements (the
|
|
247
|
+
* lazy path: a library reading clientWidth gets the honest 0 for now, and
|
|
248
|
+
* REAL geometry on subsequent reads once the driver's push lands).
|
|
249
|
+
* Disconnected/phantom nodes have no driver-side element to measure;
|
|
250
|
+
* requesting is skipped until the read happens while connected.
|
|
251
|
+
*/
|
|
252
|
+
_requestMeasure(el: ProxyElement): void {
|
|
253
|
+
if (!el.isConnected) return;
|
|
254
|
+
const id = el.instance.id;
|
|
255
|
+
if (this._measureRequested.has(id)) return;
|
|
256
|
+
this._measureRequested.add(id);
|
|
257
|
+
el._op({ t: 'measure', id });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
observeSize(el: ProxyElement, cb: (rect: MeasuredRect) => void): () => void {
|
|
261
|
+
this._assertAlive();
|
|
262
|
+
const id = el.instance.id;
|
|
263
|
+
let set = this._sizeObservers.get(id);
|
|
264
|
+
if (set === undefined) this._sizeObservers.set(id, (set = new Set()));
|
|
265
|
+
set.add(cb);
|
|
266
|
+
// Connected or not, request now: the driver's ResizeObserver delivers
|
|
267
|
+
// the first rect when the element mounts, so an early observe is safe.
|
|
268
|
+
if (!this._measureRequested.has(id)) {
|
|
269
|
+
this._measureRequested.add(id);
|
|
270
|
+
el._op({ t: 'measure', id });
|
|
271
|
+
}
|
|
272
|
+
return () => {
|
|
273
|
+
const observers = this._sizeObservers.get(id);
|
|
274
|
+
if (observers === undefined) return;
|
|
275
|
+
observers.delete(cb);
|
|
276
|
+
if (observers.size === 0) {
|
|
277
|
+
this._sizeObservers.delete(id);
|
|
278
|
+
this._measureRequested.delete(id);
|
|
279
|
+
el._op({ t: 'unmeasure', id });
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Store pushed rects and fire per-element observers — the `setRects`
|
|
286
|
+
* task calls this inside the instance's scope, so observer mutations
|
|
287
|
+
* emit ops and ride back in the task's return batch.
|
|
288
|
+
*/
|
|
289
|
+
_notifyRects(rects: MeasuredRect[]): void {
|
|
290
|
+
for (const rect of rects) {
|
|
291
|
+
setInstanceRect(this.instance, rect);
|
|
292
|
+
const observers = this._sizeObservers.get(rect.id);
|
|
293
|
+
if (observers !== undefined) for (const cb of [...observers]) cb(rect);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The rect a geometry read on `el` may report: the element's own measured
|
|
299
|
+
* box when a setRects push arrived for it, else the pushed CONTAINER box
|
|
300
|
+
* when `el` is the root or was markContainer()ed (the old contract — a
|
|
301
|
+
* marked element claims "my box is the container's"), undefined otherwise
|
|
217
302
|
* (also undefined when nothing has been pushed yet — reads return 0).
|
|
218
303
|
*/
|
|
219
|
-
|
|
304
|
+
_rectFor(el: ProxyElement): { x: number; y: number; w: number; h: number } | undefined {
|
|
305
|
+
const measured = getInstanceRect(this.instance, el.instance.id);
|
|
306
|
+
if (measured !== undefined) return measured;
|
|
220
307
|
if (!this._containerIds.has(el.instance.id)) return undefined;
|
|
221
|
-
|
|
308
|
+
const size = getInstanceSize(this.instance);
|
|
309
|
+
return size === undefined ? undefined : { x: 0, y: 0, ...size };
|
|
222
310
|
}
|
|
223
311
|
|
|
224
312
|
onResize(cb: (w: number, h: number) => void): void {
|
|
@@ -288,7 +376,7 @@ export class InternalDocument implements ProxyDocument {
|
|
|
288
376
|
if (self !== undefined) out.currentTarget = self;
|
|
289
377
|
const id = payload.targetId;
|
|
290
378
|
if (typeof id === 'number') {
|
|
291
|
-
const instance =
|
|
379
|
+
const instance = lookupInstance(id);
|
|
292
380
|
if (instance !== undefined) {
|
|
293
381
|
const node = this._wrap(instance);
|
|
294
382
|
// Reflect the dispatched form state into the SHADOW attrs —
|
|
@@ -307,6 +395,13 @@ export class InternalDocument implements ProxyDocument {
|
|
|
307
395
|
out.target = node;
|
|
308
396
|
}
|
|
309
397
|
}
|
|
398
|
+
// Materialize relatedTarget the same way — over/out handlers walk it
|
|
399
|
+
// (zrender's isLocalEl climbs parentNode to the listener root) to tell
|
|
400
|
+
// "moved to a child" apart from "actually left".
|
|
401
|
+
if (typeof payload.relatedTargetId === 'number') {
|
|
402
|
+
const related = lookupInstance(payload.relatedTargetId);
|
|
403
|
+
if (related !== undefined) out.relatedTarget = this._wrap(related);
|
|
404
|
+
}
|
|
310
405
|
if (out.composedPath === undefined) {
|
|
311
406
|
const path: unknown[] = [];
|
|
312
407
|
let n = out.target as ProxyNode | undefined;
|
|
@@ -521,11 +616,23 @@ export class InternalDocument implements ProxyDocument {
|
|
|
521
616
|
return out;
|
|
522
617
|
}
|
|
523
618
|
|
|
524
|
-
getComputedStyle(
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
619
|
+
getComputedStyle(el: ProxyElement): CSSStyleDeclaration {
|
|
620
|
+
// A measured box doubles as the computed box — 'width'/'height' are
|
|
621
|
+
// what layout libraries actually read (zrender's parseInt(stl.width),
|
|
622
|
+
// echarts' elementFromPoint-free sizing). Every other property stays
|
|
623
|
+
// the honest empty string.
|
|
624
|
+
const rect = this._rectFor(el);
|
|
625
|
+
const w = rect === undefined ? '' : `${rect.w}px`;
|
|
626
|
+
const h = rect === undefined ? '' : `${rect.h}px`;
|
|
627
|
+
if (rect === undefined) {
|
|
628
|
+
this._requestMeasure(el);
|
|
629
|
+
this._warn('getComputedStyle');
|
|
630
|
+
}
|
|
631
|
+
return new Proxy({ width: w, height: h } as Record<string, unknown>, {
|
|
632
|
+
get: (t, prop) =>
|
|
633
|
+
prop === 'getPropertyValue'
|
|
634
|
+
? (k: string): string => (k === 'width' ? w : k === 'height' ? h : '')
|
|
635
|
+
: (t[prop as string] ?? ''),
|
|
529
636
|
}) as unknown as CSSStyleDeclaration;
|
|
530
637
|
}
|
|
531
638
|
|
|
@@ -537,6 +644,8 @@ export class InternalDocument implements ProxyDocument {
|
|
|
537
644
|
this._disposed = true;
|
|
538
645
|
if (instanceDocs.get(this.instance) === this) instanceDocs.delete(this.instance);
|
|
539
646
|
this._resizeHandlers.clear();
|
|
647
|
+
this._sizeObservers.clear();
|
|
648
|
+
this._measureRequested.clear();
|
|
540
649
|
for (const hid of this._handlerIds) unregisterHandler(hid);
|
|
541
650
|
this._handlerIds.clear();
|
|
542
651
|
}
|
|
@@ -16,6 +16,7 @@ import type { EventPayload, Op } from '../../ops';
|
|
|
16
16
|
import {
|
|
17
17
|
ProxyNode,
|
|
18
18
|
ProxyFragment,
|
|
19
|
+
camelize,
|
|
19
20
|
listenerOptsForWire,
|
|
20
21
|
normalizeListenerOptions,
|
|
21
22
|
type ProxyEventHandler,
|
|
@@ -437,16 +438,18 @@ export class ProxyElement extends ProxyNode {
|
|
|
437
438
|
return copy;
|
|
438
439
|
}
|
|
439
440
|
|
|
440
|
-
/* ──
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
441
|
+
/* ── Geometry. Two pushed channels: the island container's setSize box
|
|
442
|
+
* (root + markContainer()ed elements) and per-element setRects boxes for
|
|
443
|
+
* elements the worker asked the driver to measure — via doc.observeSize,
|
|
444
|
+
* a ResizeObserver shim, or lazily by any of the reads below. Until the
|
|
445
|
+
* first push lands a read keeps the honest 0 and warns once per doc per
|
|
446
|
+
* API; a marked element keeps reporting the CONTAINER's box — "marked"
|
|
447
|
+
* still means "pretend my box is the container's" and is only honest
|
|
448
|
+
* for elements that genuinely fill it. ── */
|
|
447
449
|
getBoundingClientRect(): DOMRect {
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
450
|
+
const rect = this.doc._rectFor(this);
|
|
451
|
+
if (rect === undefined) {
|
|
452
|
+
this.doc._requestMeasure(this);
|
|
450
453
|
this.doc._warn('getBoundingClientRect');
|
|
451
454
|
return {
|
|
452
455
|
x: 0, y: 0, top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0,
|
|
@@ -454,22 +457,23 @@ export class ProxyElement extends ProxyNode {
|
|
|
454
457
|
} as DOMRect;
|
|
455
458
|
}
|
|
456
459
|
return {
|
|
457
|
-
x:
|
|
458
|
-
|
|
460
|
+
x: rect.x, y: rect.y, top: rect.y, left: rect.x,
|
|
461
|
+
right: rect.x + rect.w, bottom: rect.y + rect.h,
|
|
462
|
+
width: rect.w, height: rect.h,
|
|
459
463
|
toJSON: () => ({}),
|
|
460
464
|
} as DOMRect;
|
|
461
465
|
}
|
|
462
466
|
get clientWidth(): number {
|
|
463
|
-
return this._geom('clientWidth', (
|
|
467
|
+
return this._geom('clientWidth', (r) => r.w);
|
|
464
468
|
}
|
|
465
469
|
get clientHeight(): number {
|
|
466
|
-
return this._geom('clientHeight', (
|
|
470
|
+
return this._geom('clientHeight', (r) => r.h);
|
|
467
471
|
}
|
|
468
472
|
get offsetWidth(): number {
|
|
469
|
-
return this._geom('offsetWidth', (
|
|
473
|
+
return this._geom('offsetWidth', (r) => r.w);
|
|
470
474
|
}
|
|
471
475
|
get offsetHeight(): number {
|
|
472
|
-
return this._geom('offsetHeight', (
|
|
476
|
+
return this._geom('offsetHeight', (r) => r.h);
|
|
473
477
|
}
|
|
474
478
|
/** Border edge widths — honestly 0 (the pushed box doesn't measure borders). */
|
|
475
479
|
get clientLeft(): number {
|
|
@@ -489,25 +493,25 @@ export class ProxyElement extends ProxyNode {
|
|
|
489
493
|
return null;
|
|
490
494
|
}
|
|
491
495
|
get scrollTop(): number {
|
|
492
|
-
this.doc.
|
|
496
|
+
this.doc._scrollWarn('scrollTop');
|
|
493
497
|
return 0;
|
|
494
498
|
}
|
|
495
499
|
set scrollTop(_v: number) {
|
|
496
|
-
this.doc.
|
|
500
|
+
this.doc._scrollWarn('scrollTop');
|
|
497
501
|
}
|
|
498
502
|
get scrollLeft(): number {
|
|
499
|
-
this.doc.
|
|
503
|
+
this.doc._scrollWarn('scrollLeft');
|
|
500
504
|
return 0;
|
|
501
505
|
}
|
|
502
506
|
set scrollLeft(_v: number) {
|
|
503
|
-
this.doc.
|
|
507
|
+
this.doc._scrollWarn('scrollLeft');
|
|
504
508
|
}
|
|
505
509
|
get scrollHeight(): number {
|
|
506
|
-
this.doc.
|
|
510
|
+
this.doc._scrollWarn('scrollHeight');
|
|
507
511
|
return 0;
|
|
508
512
|
}
|
|
509
513
|
get scrollWidth(): number {
|
|
510
|
-
this.doc.
|
|
514
|
+
this.doc._scrollWarn('scrollWidth');
|
|
511
515
|
return 0;
|
|
512
516
|
}
|
|
513
517
|
|
|
@@ -622,21 +626,51 @@ export class ProxyElement extends ProxyNode {
|
|
|
622
626
|
}
|
|
623
627
|
|
|
624
628
|
/* internals */
|
|
625
|
-
/** Geometry read shared by the
|
|
626
|
-
_geom(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
+
/** Geometry read shared by the size getters above. */
|
|
630
|
+
_geom(
|
|
631
|
+
feature: string,
|
|
632
|
+
pick: (rect: { x: number; y: number; w: number; h: number }) => number,
|
|
633
|
+
): number {
|
|
634
|
+
const rect = this.doc._rectFor(this);
|
|
635
|
+
if (rect === undefined) {
|
|
636
|
+
// Lazy measure request — the read keeps its honest 0 for now, but
|
|
637
|
+
// the driver's setRects push makes later reads real (and observeSize
|
|
638
|
+
// subscribers fire).
|
|
639
|
+
this.doc._requestMeasure(this);
|
|
629
640
|
this.doc._warn(feature);
|
|
630
641
|
return 0;
|
|
631
642
|
}
|
|
632
|
-
return pick(
|
|
643
|
+
return pick(rect);
|
|
633
644
|
}
|
|
634
645
|
_writeStyle(key: string, value: string): void {
|
|
635
646
|
this.doc._assertAlive();
|
|
647
|
+
// '' is the protocol's "clear this key" — tracked as an empty entry so
|
|
648
|
+
// `key in style` stays true for touched props, like real CSSOM.
|
|
636
649
|
if (this._styleProps[key] === value) return;
|
|
637
650
|
this._styleProps[key] = value;
|
|
638
651
|
this._op({ t: 'style', id: this.instance.id, props: { [key]: value } });
|
|
639
652
|
}
|
|
653
|
+
|
|
654
|
+
/** style.cssText — a FULL replace, not another key. Parse the declarations
|
|
655
|
+
* and write each through the dedup path; keys absent from the new text
|
|
656
|
+
* clear. Storing the text under a 'cssText' key left stale per-key values
|
|
657
|
+
* that deduped real writes away (a hide's `visibility:hidden` stuck after
|
|
658
|
+
* a show's cssText replay). */
|
|
659
|
+
_replaceStyleText(cssText: string): void {
|
|
660
|
+
const next = new Map<string, string>();
|
|
661
|
+
for (const decl of cssText.split(';')) {
|
|
662
|
+
const sep = decl.indexOf(':');
|
|
663
|
+
if (sep <= 0) continue;
|
|
664
|
+
const raw = decl.slice(0, sep).trim();
|
|
665
|
+
const value = decl.slice(sep + 1).trim();
|
|
666
|
+
if (raw === '' || value === '') continue;
|
|
667
|
+
next.set(raw.startsWith('--') ? raw : camelize(raw), value);
|
|
668
|
+
}
|
|
669
|
+
for (const key of Object.keys(this._styleProps)) {
|
|
670
|
+
if (!next.has(key)) this._writeStyle(key, '');
|
|
671
|
+
}
|
|
672
|
+
for (const [key, value] of next) this._writeStyle(key, value);
|
|
673
|
+
}
|
|
640
674
|
private _setAttr(name: string, value: string): void {
|
|
641
675
|
this.doc._assertAlive();
|
|
642
676
|
const v = String(value);
|
package/src/worker/dom/html.ts
CHANGED
|
@@ -94,6 +94,7 @@ export function serializeNode(node: ProxyNode): string {
|
|
|
94
94
|
const tag = node.instance.type;
|
|
95
95
|
const attrs = new Map(node._attrs);
|
|
96
96
|
const styleText = Object.entries(node._styleProps)
|
|
97
|
+
.filter(([, v]) => v !== '')
|
|
97
98
|
.map(([k, v]) => `${hyphenate(k)}: ${v};`)
|
|
98
99
|
.join(' ');
|
|
99
100
|
if (styleText !== '') attrs.set('style', styleText);
|
package/src/worker/dom/node.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
allocId,
|
|
3
|
+
attachInstance,
|
|
3
4
|
bumpOpsVersion,
|
|
5
|
+
detachInstance,
|
|
4
6
|
getActiveInstance,
|
|
5
7
|
getLastActiveInstance,
|
|
6
8
|
getInstanceSize,
|
|
@@ -251,11 +253,21 @@ export class ProxyNode {
|
|
|
251
253
|
this._children.splice(index, 0, child);
|
|
252
254
|
if (ref.instance.id > 0) beforeId = ref.instance.id;
|
|
253
255
|
}
|
|
256
|
+
// Restore instance/handler records if this child was previously
|
|
257
|
+
// tombstoned by a removeChild — re-append is legal (renderers reorder).
|
|
258
|
+
child._attachRecords();
|
|
254
259
|
// Phantom nodes (fragments, shadow-only texts) have NEGATIVE ids and no
|
|
255
260
|
// driver-side counterpart — skip ops when either endpoint is phantom.
|
|
256
261
|
// Id 0 is NOT phantom: it's the island's real root container.
|
|
257
262
|
if (this.instance.id >= 0 && child.instance.id > 0) {
|
|
258
263
|
this._op({ t: 'append', parent: this.instance.id, child: child.instance.id, before: beforeId });
|
|
264
|
+
// A `remove` op unobserved this element driver-side — if it was still
|
|
265
|
+
// measure-requested worker-side, re-request so the driver's RO
|
|
266
|
+
// re-arms on the fresh attachment. Subtree descendants keep their
|
|
267
|
+
// driver-side observation (only the removed ROOT unobserved).
|
|
268
|
+
if (child.nodeType === 1 && this.doc._measureRequested.has(child.instance.id)) {
|
|
269
|
+
child._op({ t: 'measure', id: child.instance.id });
|
|
270
|
+
}
|
|
259
271
|
}
|
|
260
272
|
return child;
|
|
261
273
|
}
|
|
@@ -268,6 +280,9 @@ export class ProxyNode {
|
|
|
268
280
|
}
|
|
269
281
|
this._children.splice(index, 1);
|
|
270
282
|
child._parent = null;
|
|
283
|
+
// Tombstone the subtree's instance/handler records — without this the
|
|
284
|
+
// shared maps retain every node ever created. Re-append restores them.
|
|
285
|
+
child._detachRecords();
|
|
271
286
|
if (this.instance.id >= 0 && child.instance.id > 0) {
|
|
272
287
|
this._op({ t: 'remove', child: child.instance.id });
|
|
273
288
|
}
|
|
@@ -351,6 +366,27 @@ export class ProxyNode {
|
|
|
351
366
|
if (i !== -1) this._children.splice(i, 1);
|
|
352
367
|
child._parent = null;
|
|
353
368
|
}
|
|
369
|
+
|
|
370
|
+
/** Handler ids this node's records own: addEventListener hids (element
|
|
371
|
+
* subclass only) plus the prop-listener slots on the instance record. */
|
|
372
|
+
_recordHandlerIds(): number[] {
|
|
373
|
+
const listeners = (this as { _listeners?: Array<{ hid: number }> })._listeners;
|
|
374
|
+
const slots = (this.instance as ElementInstance).listenerSlots;
|
|
375
|
+
return [...(listeners?.map((l) => l.hid) ?? []), ...Object.values(slots ?? {})];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Tombstone this subtree's instance/handler records (removeChild). */
|
|
379
|
+
_detachRecords(): void {
|
|
380
|
+
if (this.instance.id > 0) detachInstance(this.instance.id, this._recordHandlerIds());
|
|
381
|
+
for (const c of this._children) c._detachRecords();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Restore this subtree's records on re-append (idempotent — a live node
|
|
385
|
+
* is simply not in the tombstone maps). */
|
|
386
|
+
_attachRecords(): void {
|
|
387
|
+
if (this.instance.id > 0) attachInstance(this.instance.id, this._recordHandlerIds());
|
|
388
|
+
for (const c of this._children) c._attachRecords();
|
|
389
|
+
}
|
|
354
390
|
}
|
|
355
391
|
|
|
356
392
|
/* ── ProxyText ─────────────────────────────────────────────────────────── */
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { MeasuredRect } from '../../ops';
|
|
2
|
+
import {
|
|
3
|
+
activeInstanceDoc,
|
|
4
|
+
ambientDoc,
|
|
5
|
+
type InternalDocument,
|
|
6
|
+
} from './document';
|
|
7
|
+
import { ProxyElement } from './element';
|
|
8
|
+
import type { HostInstance } from '../instance';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A `ResizeObserver` over the pushed-measure channel — the class
|
|
12
|
+
* `globalThis.ResizeObserver`/`window.ResizeObserver` resolve to inside an
|
|
13
|
+
* island instance. Each `observe(el)` registers a `doc.observeSize`
|
|
14
|
+
* subscription (which emits the `measure` op once per element); each pushed
|
|
15
|
+
* rect invokes the callback with a single-entry batch, matching how real RO
|
|
16
|
+
* delivers per-element observations (the driver already batches the wire
|
|
17
|
+
* push, the DOM API just doesn't promise same-batch grouping).
|
|
18
|
+
*
|
|
19
|
+
* Entries carry the fields layout libraries read: `target`, `contentRect`
|
|
20
|
+
* (the measured box), and `borderBoxSize`/`contentBoxSize`/`devicePixelContentBoxSize`
|
|
21
|
+
* arrays (echarts zrender, recharts' ResponsiveContainer, and RO-polyfill-
|
|
22
|
+
* style code paths). Box options are accepted but ignored — the channel
|
|
23
|
+
* delivers one box and it answers both reads.
|
|
24
|
+
*
|
|
25
|
+
* No doc resolvable (module-scope or post-unmount construction) means there
|
|
26
|
+
* is nothing that could ever be measured — observe() silently no-ops rather
|
|
27
|
+
* than pinning a foreign document, the same resolution rule the
|
|
28
|
+
* `document`/`window` global dispatchers use.
|
|
29
|
+
*/
|
|
30
|
+
export class ProxyResizeObserver {
|
|
31
|
+
private readonly _callback: ResizeObserverCallback;
|
|
32
|
+
/** proxy el → the observeSize detacher (unobserve/disconnect paths). */
|
|
33
|
+
private readonly _targets = new Map<ProxyElement, () => void>();
|
|
34
|
+
|
|
35
|
+
constructor(callback: ResizeObserverCallback) {
|
|
36
|
+
this._callback = callback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
observe(target: unknown, _options?: ResizeObserverOptions): void {
|
|
40
|
+
const doc = activeInstanceDoc() ?? ambientDoc();
|
|
41
|
+
if (doc === undefined) return;
|
|
42
|
+
const el = toProxyElement(doc, target);
|
|
43
|
+
if (el === null || this._targets.has(el)) return;
|
|
44
|
+
const unobserve = (doc as InternalDocument).observeSize(el, (rect) => {
|
|
45
|
+
this._callback(
|
|
46
|
+
[entryFor(el, rect)] as unknown as ResizeObserverEntry[],
|
|
47
|
+
this as unknown as ResizeObserver,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
this._targets.set(el, unobserve);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
unobserve(target: unknown): void {
|
|
54
|
+
const doc = activeInstanceDoc() ?? ambientDoc();
|
|
55
|
+
if (doc === undefined) return;
|
|
56
|
+
const el = toProxyElement(doc, target);
|
|
57
|
+
if (el === null) return;
|
|
58
|
+
this._targets.get(el)?.();
|
|
59
|
+
this._targets.delete(el);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
disconnect(): void {
|
|
63
|
+
for (const unobserve of this._targets.values()) unobserve();
|
|
64
|
+
this._targets.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Normalize an observe target — React/framework refs arrive adopted
|
|
69
|
+
* (ProxyElement) already, but a raw HostInstance record reached through a
|
|
70
|
+
* portal/instance lookup adopts on the spot. Anything else can't be
|
|
71
|
+
* measured. */
|
|
72
|
+
const toProxyElement = (doc: InternalDocument, target: unknown): ProxyElement | null => {
|
|
73
|
+
if (target instanceof ProxyElement) return target;
|
|
74
|
+
if (
|
|
75
|
+
target !== null &&
|
|
76
|
+
typeof target === 'object' &&
|
|
77
|
+
(target as HostInstance).kind === 'element'
|
|
78
|
+
) {
|
|
79
|
+
const adopted = doc.adopt(target as HostInstance);
|
|
80
|
+
return adopted instanceof ProxyElement ? adopted : null;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** The per-push entry: measured box as contentRect plus the *BoxSize
|
|
86
|
+
* triple libraries feature-probe for. `target` is the proxy element so
|
|
87
|
+
* handlers can correlate against their own refs. */
|
|
88
|
+
const entryFor = (el: ProxyElement, rect: MeasuredRect): Record<string, unknown> => ({
|
|
89
|
+
target: el,
|
|
90
|
+
contentRect: {
|
|
91
|
+
x: rect.x,
|
|
92
|
+
y: rect.y,
|
|
93
|
+
top: rect.y,
|
|
94
|
+
left: rect.x,
|
|
95
|
+
right: rect.x + rect.w,
|
|
96
|
+
bottom: rect.y + rect.h,
|
|
97
|
+
width: rect.w,
|
|
98
|
+
height: rect.h,
|
|
99
|
+
toJSON: () => ({}),
|
|
100
|
+
},
|
|
101
|
+
borderBoxSize: [{ inlineSize: rect.w, blockSize: rect.h }],
|
|
102
|
+
contentBoxSize: [{ inlineSize: rect.w, blockSize: rect.h }],
|
|
103
|
+
devicePixelContentBoxSize: [{ inlineSize: rect.w, blockSize: rect.h }],
|
|
104
|
+
});
|
package/src/worker/dom/window.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type ProxyDocument,
|
|
24
24
|
} from './document';
|
|
25
25
|
import { ProxyElement } from './element';
|
|
26
|
+
import { ProxyResizeObserver } from './resizeObserver';
|
|
26
27
|
import {
|
|
27
28
|
ProxyNode,
|
|
28
29
|
ProxyText,
|
|
@@ -58,6 +59,9 @@ export interface WindowShim {
|
|
|
58
59
|
): number;
|
|
59
60
|
clearInterval(id?: number): void;
|
|
60
61
|
getComputedStyle(el: unknown): Record<string, never>;
|
|
62
|
+
/** ResizeObserver over the pushed-measure channel — observe() registers
|
|
63
|
+
* a doc.observeSize subscription, pushed rects arrive as entries. */
|
|
64
|
+
ResizeObserver: typeof ProxyResizeObserver;
|
|
61
65
|
/** Media queries can't be answered worker-side — always `matches: false`. */
|
|
62
66
|
matchMedia(query: string): MediaQueryList;
|
|
63
67
|
addEventListener(
|
|
@@ -251,9 +255,14 @@ const buildWindowFacade = (internal: InternalDocument): WindowFacadeBundle => {
|
|
|
251
255
|
intervals.delete(id);
|
|
252
256
|
globalClearInterval(id);
|
|
253
257
|
},
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
getComputedStyle: () =>
|
|
258
|
+
// Delegates to doc.getComputedStyle — measured elements report real
|
|
259
|
+
// width/height ('NNpx'), the unmeasured path warns once doc-side.
|
|
260
|
+
getComputedStyle: (el: unknown) =>
|
|
261
|
+
(el instanceof ProxyElement ? internal.getComputedStyle(el) : {}) as Record<string, never>,
|
|
262
|
+
// The measure channel as a DOM-standard observer — chart/map libraries
|
|
263
|
+
// that size off ResizeObserver (echarts, recharts ResponsiveContainer)
|
|
264
|
+
// get real pushed boxes unmodified.
|
|
265
|
+
ResizeObserver: ProxyResizeObserver,
|
|
257
266
|
// No preference/quiz surface exists worker-side — media queries never
|
|
258
267
|
// match. The shape is complete enough for feature-detection code.
|
|
259
268
|
matchMedia: (query: string) =>
|
|
@@ -356,6 +365,7 @@ export function installInstanceDispatcher(): void {
|
|
|
356
365
|
const prevComment = g.Comment;
|
|
357
366
|
const prevMedia = g.HTMLMediaElement;
|
|
358
367
|
const prevCustomElements = g.customElements;
|
|
368
|
+
const prevResizeObserver = g.ResizeObserver;
|
|
359
369
|
// Explicit assignments override the fallback (never the in-instance doc) —
|
|
360
370
|
// keeps pre-dispatcher semantics for out-of-instance code, and lets test
|
|
361
371
|
// harnesses/suites restore globals by plain assignment.
|
|
@@ -438,6 +448,9 @@ export function installInstanceDispatcher(): void {
|
|
|
438
448
|
prevCustomElements !== undefined ? prevCustomElements : CUSTOM_ELEMENTS_STUB,
|
|
439
449
|
prevCustomElements,
|
|
440
450
|
);
|
|
451
|
+
// A REAL ResizeObserver (happy-dom tests) keeps serving out-of-instance
|
|
452
|
+
// code; in-instance resolves to the measure-channel shim.
|
|
453
|
+
installClassGlobal('ResizeObserver', ProxyResizeObserver, prevResizeObserver);
|
|
441
454
|
}
|
|
442
455
|
|
|
443
456
|
/**
|
|
@@ -492,6 +505,7 @@ export function installDomShim(doc: ProxyDocument): () => void {
|
|
|
492
505
|
const prevComment = g.Comment;
|
|
493
506
|
const prevMedia = g.HTMLMediaElement;
|
|
494
507
|
const prevCustomElements = g.customElements;
|
|
508
|
+
const prevResizeObserver = g.ResizeObserver;
|
|
495
509
|
|
|
496
510
|
// The installed document becomes the ambient one for out-of-instance
|
|
497
511
|
// readers: the explicit assignment wins the dispatcher's resolution,
|
|
@@ -506,6 +520,7 @@ export function installDomShim(doc: ProxyDocument): () => void {
|
|
|
506
520
|
g.Comment = ProxyComment;
|
|
507
521
|
if (typeof g.HTMLMediaElement !== 'function') g.HTMLMediaElement = HTML_MEDIA_STUB;
|
|
508
522
|
if (g.customElements === undefined) g.customElements = CUSTOM_ELEMENTS_STUB;
|
|
523
|
+
if (g.ResizeObserver === undefined) g.ResizeObserver = ProxyResizeObserver;
|
|
509
524
|
|
|
510
525
|
const uninstall = (): void => {
|
|
511
526
|
bundle.teardown();
|
|
@@ -518,6 +533,7 @@ export function installDomShim(doc: ProxyDocument): () => void {
|
|
|
518
533
|
g.Comment = prevComment;
|
|
519
534
|
g.HTMLMediaElement = prevMedia;
|
|
520
535
|
g.customElements = prevCustomElements;
|
|
536
|
+
g.ResizeObserver = prevResizeObserver;
|
|
521
537
|
markInstanceActive(prevInstance);
|
|
522
538
|
if (activeShimUninstall === uninstall) activeShimUninstall = null;
|
|
523
539
|
};
|
package/src/worker/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ export {
|
|
|
69
69
|
ProxyElement,
|
|
70
70
|
ProxyFragment,
|
|
71
71
|
ProxyNode,
|
|
72
|
+
ProxyResizeObserver,
|
|
72
73
|
ProxyText,
|
|
73
74
|
docForInstance,
|
|
74
75
|
} from './proxyDom';
|
|
@@ -82,7 +83,7 @@ export type {
|
|
|
82
83
|
WireListenerOpts,
|
|
83
84
|
} from './proxyDom';
|
|
84
85
|
|
|
85
|
-
export type { EventPayload, IslandWorkerMethods, Op, WireProps } from '../ops';
|
|
86
|
+
export type { EventPayload, IslandWorkerMethods, MeasuredRect, Op, WireProps } from '../ops';
|
|
86
87
|
export { isEventRef } from '../ops';
|
|
87
88
|
export { renderMemory } from '../memory';
|
|
88
89
|
export type { DoorbellSpec } from '../memory';
|