@atolljs/islands 0.1.0
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/README.md +346 -0
- package/package.json +29 -0
- package/src/app.ts +81 -0
- package/src/callbackProps.ts +75 -0
- package/src/index.ts +40 -0
- package/src/island.ts +905 -0
- package/src/memory.ts +30 -0
- package/src/ops.ts +215 -0
- package/src/react-reconciler.d.ts +18 -0
- package/src/worker/defineWorkers.ts +666 -0
- package/src/worker/dom/css.ts +152 -0
- package/src/worker/dom/document.ts +589 -0
- package/src/worker/dom/element.ts +656 -0
- package/src/worker/dom/html.ts +107 -0
- package/src/worker/dom/node.ts +481 -0
- package/src/worker/dom/selectors.ts +108 -0
- package/src/worker/dom/window.ts +528 -0
- package/src/worker/hostConfig.ts +446 -0
- package/src/worker/index.ts +78 -0
- package/src/worker/instance.ts +317 -0
- package/src/worker/proxyDom.ts +28 -0
- package/src/worker/reactInstance.ts +74 -0
- package/src/worker/slot.tsx +18 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allocId,
|
|
3
|
+
bumpOpsVersion,
|
|
4
|
+
getActiveInstance,
|
|
5
|
+
getLastActiveInstance,
|
|
6
|
+
getInstanceSize,
|
|
7
|
+
instances,
|
|
8
|
+
markInstanceActive,
|
|
9
|
+
pushOp,
|
|
10
|
+
registerHandler,
|
|
11
|
+
unregisterHandler,
|
|
12
|
+
} from '../instance';
|
|
13
|
+
import type { ElementInstance, HostInstance, TextInstance } from '../instance';
|
|
14
|
+
import type { EventPayload, Op } from '../../ops';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ProxyNode,
|
|
18
|
+
ProxyFragment,
|
|
19
|
+
listenerOptsForWire,
|
|
20
|
+
normalizeListenerOptions,
|
|
21
|
+
type ProxyEventHandler,
|
|
22
|
+
} from './node';
|
|
23
|
+
import { parseChildren, serializeNode } from './html';
|
|
24
|
+
import { parseSelector, matchCompound, matchesChain } from './selectors';
|
|
25
|
+
import { classListFor, styleProxyFor, datasetProxyFor, type ProxyClassList } from './css';
|
|
26
|
+
import type { InternalDocument } from './document';
|
|
27
|
+
|
|
28
|
+
/* ── ProxyElement ──────────────────────────────────────────────────────── */
|
|
29
|
+
|
|
30
|
+
/** Positions `insertAdjacentHTML`/`insertAdjacentElement` accept. */
|
|
31
|
+
export type AdjacentPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend';
|
|
32
|
+
|
|
33
|
+
/** `<template>` element → its cached `.content` fragment view (shares the
|
|
34
|
+
* element's shadow child array). */
|
|
35
|
+
const templateContents = new WeakMap<ProxyElement, ProxyFragment>();
|
|
36
|
+
|
|
37
|
+
export class ProxyElement extends ProxyNode {
|
|
38
|
+
declare readonly instance: ElementInstance;
|
|
39
|
+
readonly _attrs = new Map<string, string>();
|
|
40
|
+
_classes = new Set<string>();
|
|
41
|
+
readonly _styleProps: Record<string, string> = {};
|
|
42
|
+
private readonly _listeners: Array<{
|
|
43
|
+
type: string;
|
|
44
|
+
fn: ProxyEventHandler;
|
|
45
|
+
hid: number;
|
|
46
|
+
capture: boolean;
|
|
47
|
+
}> = [];
|
|
48
|
+
|
|
49
|
+
override get nodeType(): number {
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
get tagName(): string {
|
|
53
|
+
return this.instance.type.toUpperCase();
|
|
54
|
+
}
|
|
55
|
+
/** The element's namespace URI — HTML for ordinary elements, the `create`
|
|
56
|
+
* op's `ns` for namespaced (svg/mathml) ones. */
|
|
57
|
+
get namespaceURI(): string {
|
|
58
|
+
return this.instance.ns ?? 'http://www.w3.org/1999/xhtml';
|
|
59
|
+
}
|
|
60
|
+
/** Element children only — the shadow tree's HTMLCollection equivalent. */
|
|
61
|
+
get children(): ProxyElement[] {
|
|
62
|
+
return this._children.filter((c): c is ProxyElement => c instanceof ProxyElement);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get id(): string {
|
|
66
|
+
return this._attrs.get('id') ?? '';
|
|
67
|
+
}
|
|
68
|
+
set id(v: string) {
|
|
69
|
+
this._setAttr('id', v);
|
|
70
|
+
}
|
|
71
|
+
get className(): string {
|
|
72
|
+
return this._attrs.get('class') ?? '';
|
|
73
|
+
}
|
|
74
|
+
set className(v: string) {
|
|
75
|
+
this._setAttr('class', v);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getAttribute(name: string): string | null {
|
|
79
|
+
return this._attrs.get(name) ?? null;
|
|
80
|
+
}
|
|
81
|
+
hasAttribute(name: string): boolean {
|
|
82
|
+
return this._attrs.has(name);
|
|
83
|
+
}
|
|
84
|
+
setAttribute(name: string, value: string): void {
|
|
85
|
+
this._setAttr(name, value);
|
|
86
|
+
}
|
|
87
|
+
removeAttribute(name: string): void {
|
|
88
|
+
this.doc._assertAlive();
|
|
89
|
+
if (!this._attrs.has(name)) return;
|
|
90
|
+
this._attrs.delete(name);
|
|
91
|
+
this._afterAttrChange(name);
|
|
92
|
+
this._op({ t: 'attr', id: this.instance.id, name, value: null });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Namespaced attribute surface — the wire `attr` op has no namespace
|
|
96
|
+
* field, so the qualified name ('xlink:href') is the whole shadow key
|
|
97
|
+
* and the driver's setAttribute covers prefixed names. Limitation: two
|
|
98
|
+
* same-local-name attributes in different namespaces can't be told
|
|
99
|
+
* apart. get/remove follow the DOM convention of receiving the LOCAL
|
|
100
|
+
* name — resolve it to the stored qualified key (or accept a qualified
|
|
101
|
+
* name directly, since some callers pass both ways). */
|
|
102
|
+
setAttributeNS(_ns: string | null, qualifiedName: string, value: string): void {
|
|
103
|
+
this.setAttribute(qualifiedName, value);
|
|
104
|
+
}
|
|
105
|
+
getAttributeNS(_ns: string | null, name: string): string | null {
|
|
106
|
+
const direct = this.getAttribute(name);
|
|
107
|
+
if (direct !== null) return direct;
|
|
108
|
+
for (const key of this._attrs.keys()) {
|
|
109
|
+
if (key.endsWith(`:${name}`)) return this.getAttribute(key);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
removeAttributeNS(_ns: string | null, name: string): void {
|
|
114
|
+
if (this.getAttribute(name) !== null) {
|
|
115
|
+
this.removeAttribute(name);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const key of [...this._attrs.keys()]) {
|
|
119
|
+
if (key.endsWith(`:${name}`)) this.removeAttribute(key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* classList — a live view over _classes synced to the `class` attr. */
|
|
124
|
+
get classList(): ProxyClassList {
|
|
125
|
+
return classListFor(this);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Inline style — a Proxy over a plain prop record. Writes re-send only
|
|
130
|
+
* the changed key; `delete style.x`/`style.removeProperty('x')` send ''
|
|
131
|
+
* (the protocol's "clear this key"). `setProperty`/`getPropertyValue`
|
|
132
|
+
* accept kebab-case and camelize it, like the real CSSStyleDeclaration.
|
|
133
|
+
*/
|
|
134
|
+
get style(): CSSStyleDeclaration {
|
|
135
|
+
return styleProxyFor(this);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* dataset — a Proxy mapping camelCase keys to `data-*` attribute ops.
|
|
140
|
+
* `swatch.dataset.color = c` emits attr {name:'data-color', value:c};
|
|
141
|
+
* `delete el.dataset.x` emits the removal form.
|
|
142
|
+
*/
|
|
143
|
+
get dataset(): DOMStringMap {
|
|
144
|
+
return datasetProxyFor(this);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Focus/blur — NO-OPS. Real focus can't be delivered over async postMessage
|
|
149
|
+
* (the driver's dispatch fires after the event; there is no focus op).
|
|
150
|
+
* They exist because gesture libraries call `el.focus()` unconditionally
|
|
151
|
+
* during mousedown paths (Leaflet's Keyboard handler does).
|
|
152
|
+
*/
|
|
153
|
+
focus(): void {}
|
|
154
|
+
blur(): void {}
|
|
155
|
+
|
|
156
|
+
/* Events — registers a worker handler-table entry and emits listen.
|
|
157
|
+
* The driver's listenerFor dispatches EventPayloads back into the worker;
|
|
158
|
+
* the payload is enriched with a synthesized `target`/`composedPath` and
|
|
159
|
+
* the handler runs with `this` = this element, like the real DOM's
|
|
160
|
+
* currentTarget. `{once, passive, capture}` ride the op's `opts` field;
|
|
161
|
+
* `once` additionally auto-detaches worker-side after the first dispatch
|
|
162
|
+
* (matching the real listener's own auto-removal driver-side). */
|
|
163
|
+
addEventListener(
|
|
164
|
+
type: string,
|
|
165
|
+
fn: ProxyEventHandler,
|
|
166
|
+
options?: boolean | AddEventListenerOptions,
|
|
167
|
+
): void {
|
|
168
|
+
this.doc._assertAlive();
|
|
169
|
+
const opts = normalizeListenerOptions(options);
|
|
170
|
+
// The real DOM dedupes identical (type, listener, capture) triples.
|
|
171
|
+
if (
|
|
172
|
+
this._listeners.some((l) => l.type === type && l.fn === fn && l.capture === opts.capture)
|
|
173
|
+
) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const hid = registerHandler(
|
|
177
|
+
(p) => {
|
|
178
|
+
try {
|
|
179
|
+
fn.call(this, this.doc._enrichEvent(p as EventPayload, this));
|
|
180
|
+
} finally {
|
|
181
|
+
if (opts.once) this.removeEventListener(type, fn, { capture: opts.capture });
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
this.instance.instance,
|
|
185
|
+
this.instance.id,
|
|
186
|
+
);
|
|
187
|
+
this._listeners.push({ type, fn, hid, capture: opts.capture });
|
|
188
|
+
this.doc._handlerIds.add(hid);
|
|
189
|
+
this._op({
|
|
190
|
+
t: 'listen',
|
|
191
|
+
id: this.instance.id,
|
|
192
|
+
type,
|
|
193
|
+
handler: hid,
|
|
194
|
+
opts: listenerOptsForWire(options),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
removeEventListener(
|
|
198
|
+
type: string,
|
|
199
|
+
fn: ProxyEventHandler,
|
|
200
|
+
options?: boolean | EventListenerOptions,
|
|
201
|
+
): void {
|
|
202
|
+
this.doc._assertAlive();
|
|
203
|
+
const capture = normalizeListenerOptions(options).capture;
|
|
204
|
+
const index = this._listeners.findIndex(
|
|
205
|
+
(l) => l.type === type && l.fn === fn && l.capture === capture,
|
|
206
|
+
);
|
|
207
|
+
if (index === -1) return;
|
|
208
|
+
const [entry] = this._listeners.splice(index, 1);
|
|
209
|
+
unregisterHandler(entry.hid);
|
|
210
|
+
this.doc._handlerIds.delete(entry.hid);
|
|
211
|
+
this._op({
|
|
212
|
+
t: 'unlisten',
|
|
213
|
+
id: this.instance.id,
|
|
214
|
+
type,
|
|
215
|
+
handler: entry.hid,
|
|
216
|
+
opts: entry.capture ? { capture: true } : undefined,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Live-descendant search over the shadow tree. Nodes mounted by the
|
|
221
|
+
* reconciler aren't reachable (the op stream doesn't mirror React's
|
|
222
|
+
* structure into _children) — adopted subtrees match only their own
|
|
223
|
+
* proxy-built descendants. Class matching reads _classes AND the
|
|
224
|
+
* instance's serialized className prop, so adopted React elements with
|
|
225
|
+
* className set still match. */
|
|
226
|
+
getElementsByClassName(name: string): ProxyElement[] {
|
|
227
|
+
this.doc._assertAlive();
|
|
228
|
+
const out: ProxyElement[] = [];
|
|
229
|
+
const visit = (el: ProxyElement): void => {
|
|
230
|
+
for (const child of el._children) {
|
|
231
|
+
if (!(child instanceof ProxyElement)) continue;
|
|
232
|
+
const viaProps = ((child.instance.props?.className as string | undefined) ?? '')
|
|
233
|
+
.split(/\s+/)
|
|
234
|
+
.includes(name);
|
|
235
|
+
if (child._classes.has(name) || viaProps) out.push(child);
|
|
236
|
+
visit(child);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
visit(this);
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getElementsByTagName(tag: string): ProxyElement[] {
|
|
244
|
+
this.doc._assertAlive();
|
|
245
|
+
const want = tag.toLowerCase();
|
|
246
|
+
const out: ProxyElement[] = [];
|
|
247
|
+
const visit = (el: ProxyElement): void => {
|
|
248
|
+
for (const child of el._children) {
|
|
249
|
+
if (!(child instanceof ProxyElement)) continue;
|
|
250
|
+
if (want === '*' || child.instance.type.toLowerCase() === want) out.push(child);
|
|
251
|
+
visit(child);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
visit(this);
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* SVG measurement APIs — no layout/text engine exists worker-side, so
|
|
259
|
+
* these report honest zeros (warned once), exactly like the rest of the
|
|
260
|
+
* geometry surface. Libraries doing animation sizing (recharts'
|
|
261
|
+
* getTotalLength paths) degrade to instant/no animation. */
|
|
262
|
+
getBBox(): {
|
|
263
|
+
x: number;
|
|
264
|
+
y: number;
|
|
265
|
+
width: number;
|
|
266
|
+
height: number;
|
|
267
|
+
top: number;
|
|
268
|
+
right: number;
|
|
269
|
+
bottom: number;
|
|
270
|
+
left: number;
|
|
271
|
+
} {
|
|
272
|
+
this.doc._assertAlive();
|
|
273
|
+
this.doc._warn('getBBox');
|
|
274
|
+
return { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getTotalLength(): number {
|
|
278
|
+
this.doc._assertAlive();
|
|
279
|
+
this.doc._warn('getTotalLength');
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getComputedTextLength(): number {
|
|
284
|
+
this.doc._assertAlive();
|
|
285
|
+
this.doc._warn('getComputedTextLength');
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Selector matching — compound selectors only (see parseSelector). */
|
|
290
|
+
matches(selector: string): boolean {
|
|
291
|
+
return matchesChain(this, parseSelector(selector));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Nearest element (self inclusive) matching the selector — walks the
|
|
295
|
+
* shadow tree upward through ancestors. */
|
|
296
|
+
closest(selector: string): ProxyElement | null {
|
|
297
|
+
const chain = parseSelector(selector);
|
|
298
|
+
let n: ProxyNode | null = this;
|
|
299
|
+
while (n !== null) {
|
|
300
|
+
if (n instanceof ProxyElement && matchesChain(n, chain)) return n;
|
|
301
|
+
n = n._parent;
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Scoped local-tree query — descendants only, same engine and the same
|
|
307
|
+
* supported-selector subset as document.querySelectorAll. */
|
|
308
|
+
querySelector(selector: string): ProxyElement | null {
|
|
309
|
+
return this.querySelectorAll(selector)[0] ?? null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
querySelectorAll(selector: string): ProxyElement[] {
|
|
313
|
+
const chain = parseSelector(selector);
|
|
314
|
+
const out: ProxyElement[] = [];
|
|
315
|
+
const walk = (node: ProxyNode): void => {
|
|
316
|
+
for (const child of node._children) {
|
|
317
|
+
if (child instanceof ProxyElement && matchesChain(child, chain)) out.push(child);
|
|
318
|
+
walk(child);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
walk(this);
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* ── innerHTML + friends: real HTML parsing worker-side ──────────────── */
|
|
326
|
+
|
|
327
|
+
/** Serialized shadow-tree children — the only innerHTML there CAN be over
|
|
328
|
+
* an async op channel (it reflects writes made through this facade, not
|
|
329
|
+
* the real DOM's current markup). */
|
|
330
|
+
get innerHTML(): string {
|
|
331
|
+
return this._children.map((child) => serializeNode(child)).join('');
|
|
332
|
+
}
|
|
333
|
+
get outerHTML(): string {
|
|
334
|
+
return serializeNode(this);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Parse `html` worker-side (htmlparser2 — pure JS, worker-safe) and
|
|
338
|
+
* rebuild this element's children as proxy nodes: each parsed element is
|
|
339
|
+
* createElement + setAttribute + recursive children, each text run a
|
|
340
|
+
* createTextNode — all emitting ordinary create/attr/append ops. Existing
|
|
341
|
+
* children are removed first (one remove op each). Comments and
|
|
342
|
+
* directives are skipped.
|
|
343
|
+
*/
|
|
344
|
+
set innerHTML(html: string) {
|
|
345
|
+
this.doc._assertAlive();
|
|
346
|
+
for (const child of this._children.slice()) this.removeChild(child);
|
|
347
|
+
for (const node of parseChildren(this.doc, html)) this.appendChild(node);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Parse `html` and insert the resulting nodes relative to this element —
|
|
352
|
+
* 'beforebegin'/'afterend' require a parent and no-op without one (like
|
|
353
|
+
* the DOM on detached elements).
|
|
354
|
+
*/
|
|
355
|
+
insertAdjacentHTML(position: AdjacentPosition, html: string): void {
|
|
356
|
+
this.doc._assertAlive();
|
|
357
|
+
this._insertAt(position, parseChildren(this.doc, html));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** insertAdjacentElement — returns the inserted element like the DOM. */
|
|
361
|
+
insertAdjacentElement(position: AdjacentPosition, el: ProxyElement): ProxyElement | null {
|
|
362
|
+
this.doc._assertAlive();
|
|
363
|
+
if (!(el instanceof ProxyElement)) return null;
|
|
364
|
+
this._insertAt(position, [el]);
|
|
365
|
+
return el;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private _insertAt(position: AdjacentPosition, nodes: ProxyNode[]): void {
|
|
369
|
+
switch (position) {
|
|
370
|
+
case 'beforebegin': {
|
|
371
|
+
if (this._parent === null) return;
|
|
372
|
+
for (const n of nodes) this._parent.insertBefore(n, this);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
case 'afterbegin': {
|
|
376
|
+
const ref = this.firstChild;
|
|
377
|
+
for (const n of nodes) this.insertBefore(n, ref);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
case 'beforeend': {
|
|
381
|
+
for (const n of nodes) this.appendChild(n);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
case 'afterend': {
|
|
385
|
+
if (this._parent === null) return;
|
|
386
|
+
const ref = this.nextSibling;
|
|
387
|
+
for (const n of nodes) this._parent.insertBefore(n, ref);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
default:
|
|
391
|
+
throw new Error(`proxyDom.insertAdjacent*: unknown position "${String(position)}"`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* `<template>.content` — a fragment VIEW sharing the element's shadow
|
|
397
|
+
* child array (same backing store) AND the element's instance id, so
|
|
398
|
+
* `content.appendChild` emits `append` ops targeted at the template. The
|
|
399
|
+
* driver routes template-parented ops into `el.content` — the DOM spec's
|
|
400
|
+
* home for template children anyway. Non-template elements read
|
|
401
|
+
* undefined.
|
|
402
|
+
*/
|
|
403
|
+
get content(): ProxyFragment | undefined {
|
|
404
|
+
if (this.instance.type !== 'template') return undefined;
|
|
405
|
+
let frag = templateContents.get(this);
|
|
406
|
+
if (frag === undefined) {
|
|
407
|
+
frag = this.doc.createDocumentFragment();
|
|
408
|
+
frag._children = this._children;
|
|
409
|
+
// A fresh fragment's phantom (negative) id would swallow every op —
|
|
410
|
+
// adopt the element's instance so parent id, instance, and op targeting
|
|
411
|
+
// all read as the template.
|
|
412
|
+
(frag as { instance: unknown }).instance = this.instance;
|
|
413
|
+
templateContents.set(this, frag);
|
|
414
|
+
}
|
|
415
|
+
return frag;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Clone as new ops — the copy gets its own instance id (it's a real new
|
|
420
|
+
* element on the main thread, not a shared record). Attributes, the
|
|
421
|
+
* namespace and the style-proxy properties are copied; listeners are NOT
|
|
422
|
+
* (matches the DOM). `deep` clones children polymorphically — comment
|
|
423
|
+
* children keep nodeType 8, texts and nested elements/fragments their
|
|
424
|
+
* own kind.
|
|
425
|
+
*/
|
|
426
|
+
cloneNode(deep?: boolean): ProxyElement {
|
|
427
|
+
this.doc._assertAlive();
|
|
428
|
+
const copy =
|
|
429
|
+
this.instance.ns === undefined
|
|
430
|
+
? this.doc.createElement(this.instance.type)
|
|
431
|
+
: this.doc.createElementNS(this.instance.ns, this.instance.type);
|
|
432
|
+
for (const [name, value] of this._attrs) copy.setAttribute(name, value);
|
|
433
|
+
for (const [k, v] of Object.entries(this._styleProps)) copy._writeStyle(k, v);
|
|
434
|
+
if (deep === true) {
|
|
435
|
+
for (const child of this._children) copy.appendChild(child.cloneNode(true));
|
|
436
|
+
}
|
|
437
|
+
return copy;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* ── Pushed-size geometry. The ONLY measured box is the island container:
|
|
441
|
+
* `doc.body`/`documentElement` and markContainer()ed elements report the
|
|
442
|
+
* last setSize push; everything else keeps the honest 0 and warns once
|
|
443
|
+
* per document per API. A marked element reports the CONTAINER's box —
|
|
444
|
+
* the driver can only measure the island's root, so "marked" means
|
|
445
|
+
* "pretend my box is the container's", which is only honest for elements
|
|
446
|
+
* that genuinely fill it. ── */
|
|
447
|
+
getBoundingClientRect(): DOMRect {
|
|
448
|
+
const size = this.doc._sizeFor(this);
|
|
449
|
+
if (size === undefined) {
|
|
450
|
+
this.doc._warn('getBoundingClientRect');
|
|
451
|
+
return {
|
|
452
|
+
x: 0, y: 0, top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0,
|
|
453
|
+
toJSON: () => ({}),
|
|
454
|
+
} as DOMRect;
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
x: 0, y: 0, top: 0, left: 0, right: size.w, bottom: size.h,
|
|
458
|
+
width: size.w, height: size.h,
|
|
459
|
+
toJSON: () => ({}),
|
|
460
|
+
} as DOMRect;
|
|
461
|
+
}
|
|
462
|
+
get clientWidth(): number {
|
|
463
|
+
return this._geom('clientWidth', (s) => s.w);
|
|
464
|
+
}
|
|
465
|
+
get clientHeight(): number {
|
|
466
|
+
return this._geom('clientHeight', (s) => s.h);
|
|
467
|
+
}
|
|
468
|
+
get offsetWidth(): number {
|
|
469
|
+
return this._geom('offsetWidth', (s) => s.w);
|
|
470
|
+
}
|
|
471
|
+
get offsetHeight(): number {
|
|
472
|
+
return this._geom('offsetHeight', (s) => s.h);
|
|
473
|
+
}
|
|
474
|
+
/** Border edge widths — honestly 0 (the pushed box doesn't measure borders). */
|
|
475
|
+
get clientLeft(): number {
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
get clientTop(): number {
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
/** Position within offsetParent — not measured; 0 is honest for root-level boxes. */
|
|
482
|
+
get offsetLeft(): number {
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
get offsetTop(): number {
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
get offsetParent(): null {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
get scrollTop(): number {
|
|
492
|
+
this.doc._warn('scrollTop');
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
set scrollTop(_v: number) {
|
|
496
|
+
this.doc._warn('scrollTop');
|
|
497
|
+
}
|
|
498
|
+
get scrollLeft(): number {
|
|
499
|
+
this.doc._warn('scrollLeft');
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
set scrollLeft(_v: number) {
|
|
503
|
+
this.doc._warn('scrollLeft');
|
|
504
|
+
}
|
|
505
|
+
get scrollHeight(): number {
|
|
506
|
+
this.doc._warn('scrollHeight');
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
get scrollWidth(): number {
|
|
510
|
+
this.doc._warn('scrollWidth');
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Reflected properties. Library code assigns `img.src = url`,
|
|
516
|
+
* `el.tabIndex = 0`, `link.href = '#'` — on a bare class instance those
|
|
517
|
+
* become silent expandos and nothing crosses the wire. The attribute-backed
|
|
518
|
+
* accessors below (defined for the properties real libraries poke) make
|
|
519
|
+
* those assignments emit ordinary `attr` ops. Everything else still falls
|
|
520
|
+
* through to a plain own-property — which is also exactly what library
|
|
521
|
+
* expandos like `_leaflet_pos` need, so proxy elements are deliberately
|
|
522
|
+
* NOT frozen/sealed.
|
|
523
|
+
*/
|
|
524
|
+
get src(): string {
|
|
525
|
+
return this._attrs.get('src') ?? '';
|
|
526
|
+
}
|
|
527
|
+
set src(v: string) {
|
|
528
|
+
this._setAttr('src', v);
|
|
529
|
+
}
|
|
530
|
+
get srcset(): string {
|
|
531
|
+
return this._attrs.get('srcset') ?? '';
|
|
532
|
+
}
|
|
533
|
+
set srcset(v: string) {
|
|
534
|
+
this._setAttr('srcset', v);
|
|
535
|
+
}
|
|
536
|
+
get href(): string {
|
|
537
|
+
return this._attrs.get('href') ?? '';
|
|
538
|
+
}
|
|
539
|
+
set href(v: string) {
|
|
540
|
+
this._setAttr('href', v);
|
|
541
|
+
}
|
|
542
|
+
get alt(): string {
|
|
543
|
+
return this._attrs.get('alt') ?? '';
|
|
544
|
+
}
|
|
545
|
+
set alt(v: string) {
|
|
546
|
+
this._setAttr('alt', v);
|
|
547
|
+
}
|
|
548
|
+
get title(): string {
|
|
549
|
+
return this._attrs.get('title') ?? '';
|
|
550
|
+
}
|
|
551
|
+
set title(v: string) {
|
|
552
|
+
this._setAttr('title', v);
|
|
553
|
+
}
|
|
554
|
+
get tabIndex(): number {
|
|
555
|
+
return Number(this._attrs.get('tabindex') ?? -1);
|
|
556
|
+
}
|
|
557
|
+
set tabIndex(v: number) {
|
|
558
|
+
this._setAttr('tabindex', String(v));
|
|
559
|
+
}
|
|
560
|
+
get draggable(): boolean {
|
|
561
|
+
return this._attrs.get('draggable') === 'true';
|
|
562
|
+
}
|
|
563
|
+
set draggable(v: boolean) {
|
|
564
|
+
this._setAttr('draggable', String(v));
|
|
565
|
+
}
|
|
566
|
+
get crossOrigin(): string | null {
|
|
567
|
+
return this._attrs.get('crossorigin') ?? null;
|
|
568
|
+
}
|
|
569
|
+
set crossOrigin(v: string | null) {
|
|
570
|
+
if (v === null) this.removeAttribute('crossorigin');
|
|
571
|
+
else this._setAttr('crossorigin', v);
|
|
572
|
+
}
|
|
573
|
+
get width(): number {
|
|
574
|
+
return Number(this._attrs.get('width') ?? 0);
|
|
575
|
+
}
|
|
576
|
+
set width(v: number) {
|
|
577
|
+
this._setAttr('width', String(v));
|
|
578
|
+
}
|
|
579
|
+
get height(): number {
|
|
580
|
+
return Number(this._attrs.get('height') ?? 0);
|
|
581
|
+
}
|
|
582
|
+
set height(v: number) {
|
|
583
|
+
this._setAttr('height', String(v));
|
|
584
|
+
}
|
|
585
|
+
get type(): string {
|
|
586
|
+
return this._attrs.get('type') ?? '';
|
|
587
|
+
}
|
|
588
|
+
set type(v: string) {
|
|
589
|
+
this._setAttr('type', v);
|
|
590
|
+
}
|
|
591
|
+
get loading(): string {
|
|
592
|
+
return this._attrs.get('loading') ?? '';
|
|
593
|
+
}
|
|
594
|
+
set loading(v: string) {
|
|
595
|
+
this._setAttr('loading', v);
|
|
596
|
+
}
|
|
597
|
+
get decoding(): string {
|
|
598
|
+
return this._attrs.get('decoding') ?? '';
|
|
599
|
+
}
|
|
600
|
+
set decoding(v: string) {
|
|
601
|
+
this._setAttr('decoding', v);
|
|
602
|
+
}
|
|
603
|
+
get value(): string {
|
|
604
|
+
return this._attrs.get('value') ?? '';
|
|
605
|
+
}
|
|
606
|
+
set value(v: string) {
|
|
607
|
+
this._setAttr('value', v);
|
|
608
|
+
}
|
|
609
|
+
get checked(): boolean {
|
|
610
|
+
return this._attrs.has('checked');
|
|
611
|
+
}
|
|
612
|
+
set checked(v: boolean) {
|
|
613
|
+
if (v) this._setAttr('checked', '');
|
|
614
|
+
else this.removeAttribute('checked');
|
|
615
|
+
}
|
|
616
|
+
get disabled(): boolean {
|
|
617
|
+
return this._attrs.has('disabled');
|
|
618
|
+
}
|
|
619
|
+
set disabled(v: boolean) {
|
|
620
|
+
if (v) this._setAttr('disabled', '');
|
|
621
|
+
else this.removeAttribute('disabled');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/* internals */
|
|
625
|
+
/** Geometry read shared by the pushed-size getters above. */
|
|
626
|
+
_geom(feature: string, pick: (size: { w: number; h: number }) => number): number {
|
|
627
|
+
const size = this.doc._sizeFor(this);
|
|
628
|
+
if (size === undefined) {
|
|
629
|
+
this.doc._warn(feature);
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
return pick(size);
|
|
633
|
+
}
|
|
634
|
+
_writeStyle(key: string, value: string): void {
|
|
635
|
+
this.doc._assertAlive();
|
|
636
|
+
if (this._styleProps[key] === value) return;
|
|
637
|
+
this._styleProps[key] = value;
|
|
638
|
+
this._op({ t: 'style', id: this.instance.id, props: { [key]: value } });
|
|
639
|
+
}
|
|
640
|
+
private _setAttr(name: string, value: string): void {
|
|
641
|
+
this.doc._assertAlive();
|
|
642
|
+
const v = String(value);
|
|
643
|
+
if (this._attrs.get(name) === v) return; // unchanged → no op
|
|
644
|
+
this._attrs.set(name, v);
|
|
645
|
+
this._afterAttrChange(name);
|
|
646
|
+
this._op({ t: 'attr', id: this.instance.id, name, value: v });
|
|
647
|
+
}
|
|
648
|
+
private _afterAttrChange(name: string): void {
|
|
649
|
+
if (name === 'class') {
|
|
650
|
+
this._classes = new Set((this._attrs.get('class') ?? '').split(/\s+/).filter(Boolean));
|
|
651
|
+
} else if (name === 'id') {
|
|
652
|
+
this.doc._trackId(this);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|