Skip to content

Commit cfe5234

Browse files
committed
web: Use Proxy instead of fake HTMLCollection for document.embeds
1 parent 598d8f0 commit cfe5234

File tree

2 files changed

+117
-23
lines changed

2 files changed

+117
-23
lines changed

web/packages/core/src/internal/register-element.ts

Lines changed: 109 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -92,31 +92,117 @@ export function registerElement(
9292
Document.prototype,
9393
"embeds",
9494
);
95-
96-
if (orig && orig.get) {
95+
if (orig?.get) {
96+
const CACHE_SYM: unique symbol = Symbol(
97+
"ruffle_embeds_cache",
98+
);
99+
interface CachedCollection extends HTMLCollection {
100+
[CACHE_SYM]?: true;
101+
}
97102
Object.defineProperty(Document.prototype, "embeds", {
98-
get() {
99-
const nodes = this.querySelectorAll(
100-
"embed, ruffle-embed",
101-
);
102-
103-
const list = Array.from(nodes) as Element[];
104-
(list as unknown as HTMLCollection).item = (
105-
i: number,
106-
): Element | null => list[i] ?? null;
107-
108-
(list as unknown as HTMLCollection).namedItem =
109-
(name: string): Element | null =>
110-
list.find((el) => {
111-
const htmlEl = el as HTMLElement;
112-
return (
113-
htmlEl.getAttribute("name") ===
114-
name || htmlEl.id === name
115-
);
116-
}) ?? null;
117-
118-
return list;
103+
get(this: Document): CachedCollection {
104+
const existing = (
105+
this as unknown as Record<
106+
symbol,
107+
HTMLCollection
108+
>
109+
)[CACHE_SYM];
110+
if (existing) {
111+
return existing;
112+
}
113+
114+
const nodes = (): NodeListOf<Element> =>
115+
this.querySelectorAll(
116+
"embed, ruffle-embed",
117+
);
118+
119+
const base = Object.create(
120+
HTMLCollection.prototype,
121+
) as HTMLCollection;
122+
123+
Object.defineProperty(base, "length", {
124+
enumerable: true,
125+
configurable: true,
126+
get() {
127+
return nodes().length;
128+
},
129+
});
130+
131+
base.item = function (
132+
index: number,
133+
): Element | null {
134+
return nodes()[index] ?? null;
135+
};
136+
137+
base.namedItem = function (
138+
name: string,
139+
): Element | null {
140+
const list = nodes();
141+
for (const el of list) {
142+
const htmlEl = el as HTMLElement;
143+
if (
144+
name &&
145+
(htmlEl.getAttribute("name") ===
146+
name ||
147+
htmlEl.id === name)
148+
) {
149+
return htmlEl;
150+
}
151+
}
152+
return null;
153+
};
154+
155+
(base as Iterable<Element>)[Symbol.iterator] =
156+
function* (): Iterator<Element> {
157+
for (const el of nodes()) {
158+
yield el;
159+
}
160+
};
161+
162+
const proxy = new Proxy(base, {
163+
get(target, prop, receiver) {
164+
if (typeof prop === "string") {
165+
const index = Number(prop);
166+
if (
167+
!Number.isNaN(index) &&
168+
index >= 0
169+
) {
170+
return nodes()[index];
171+
}
172+
}
173+
return Reflect.get(
174+
target,
175+
prop,
176+
receiver,
177+
);
178+
},
179+
has(target, prop) {
180+
if (typeof prop === "string") {
181+
const index = Number(prop);
182+
if (
183+
!Number.isNaN(index) &&
184+
index >= 0
185+
) {
186+
return index < nodes().length;
187+
}
188+
}
189+
return Reflect.has(target, prop);
190+
},
191+
}) as CachedCollection;
192+
193+
proxy[CACHE_SYM] = true;
194+
195+
(
196+
this as unknown as Record<
197+
symbol,
198+
CachedCollection
199+
>
200+
)[CACHE_SYM] = proxy;
201+
202+
return proxy;
119203
},
204+
configurable: true,
205+
enumerable: true,
120206
});
121207
}
122208
}

web/packages/selfhosted/test/polyfill/document_embeds/test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ describe("Document embeds", () => {
1313
});
1414
}
1515

16+
async function documentEmbedsSelfIdentity() {
17+
return await browser.execute(() => {
18+
return document.embeds === document.embeds;
19+
});
20+
}
21+
1622
async function removeEl(selector: string) {
1723
const el = await $(selector);
1824
await browser.execute((element) => {
@@ -33,6 +39,8 @@ describe("Document embeds", () => {
3339
browser,
3440
await browser.$("#test-container").$("ruffle-embed#emb3"),
3541
);
42+
const documentEmbedsIdentity = await documentEmbedsSelfIdentity();
43+
expect(documentEmbedsIdentity).to.equal(true);
3644
const embeds1 = await countDocumentEmbeds();
3745
expect(embeds1).to.equal(3);
3846
await removeEl("#emb1");

0 commit comments

Comments
 (0)