Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions web/packages/core/src/internal/player/inner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2558,3 +2558,162 @@ function detectBrowserDirection(): string {

return "ltr";
}

type NamedAccessValue = HTMLElement | HTMLCollectionOf<HTMLElement>;

/**
*
* @param v The value to check if is an HTMLCollectionOf<HTMLElement>
*
* @returns If the value is a collection
*/
function isCollection(
v: NamedAccessValue | undefined,
): v is HTMLCollectionOf<HTMLElement> {
return !!v && "item" in v && "length" in v;
}

/**
*
* @param items The items to convert to an HTMLCollectionOf<HTMLElement>
*
* @returns An HTMLCollectionOf<HTMLElement>
*/
function makeCollection(items: HTMLElement[]): HTMLCollectionOf<HTMLElement> {
const base = Object.create(
HTMLCollection.prototype,
) as HTMLCollectionOf<HTMLElement>;

Object.defineProperty(base, "length", {
get() {
return items.length;
},
enumerable: true,
configurable: true,
});

base.item = (index: number): HTMLElement | null => {
return items[index] ?? null;
};

base.namedItem = (name: string): HTMLElement | null => {
return items.find((el) => el.getAttribute("name") === name) ?? null;
};

(base as Iterable<HTMLElement>)[Symbol.iterator] =
function* (): Iterator<HTMLElement> {
for (const el of items) {
yield el;
}
};

const proxy = new Proxy(base, {
get(target, prop, receiver) {
if (typeof prop === "string") {
const index = Number(prop);
if (!Number.isNaN(index) && index >= 0) {
return items[index];
}
}
return Reflect.get(target, prop, receiver);
},
has(target, prop) {
if (typeof prop === "string") {
const index = Number(prop);
if (!Number.isNaN(index) && index >= 0) {
return index < items.length;
}
}
return Reflect.has(target, prop);
},
});

return proxy as HTMLCollectionOf<HTMLElement>;
}

const docMap = document as unknown as Record<string, NamedAccessValue>;

/**
* Clear a property on document based on embed/object name
*
* @param el The embed/object element
* @param name The name for which to remove the property from document
*/
export function clearDocumentNamedAccessor(
el: HTMLElement,
name: string | undefined | null,
) {
if (!name) {
return;
}
const existing = docMap[name];

if (
(existing instanceof HTMLCollection &&
Array.from(existing).every((el) =>
["EMBED", "OBJECT"].includes(el.tagName),
)) ||
(existing instanceof HTMLElement &&
(existing.tagName === "EMBED" || existing.tagName === "OBJECT"))
) {
return;
}

// Simple case: single element
if (existing === el) {
Reflect.deleteProperty(document, name);
return;
}
if (isCollection(existing)) {
const items = [...existing].filter((x) => x !== el);
if (items.length === 0) {
Reflect.deleteProperty(document, name);
} else if (items.length === 1 && items[0]) {
docMap[name] = items[0];
} else {
docMap[name] = makeCollection(items);
}
}
}

/**
* Define a property on document based on embed/object name
*
* @param el The embed/object element
* @param name The name for which to define the property on document
*/
export function defineDocumentNamedAccessor(
el: HTMLElement,
name: string | null | undefined,
) {
if (!name) {
return;
}

const existing = docMap[name];

if (
(existing instanceof HTMLCollection &&
Array.from(existing).every((el) =>
["EMBED", "OBJECT"].includes(el.tagName),
)) ||
(existing instanceof HTMLElement &&
(existing.tagName === "EMBED" || existing.tagName === "OBJECT"))
) {
return;
}

if (!existing) {
docMap[name] = el;
return;
}

const items = isCollection(existing)
? [...existing].filter((x) => x !== el)
: existing === el
? []
: [existing];

items.push(el);
docMap[name] = makeCollection(items);
}
50 changes: 48 additions & 2 deletions web/packages/core/src/internal/player/ruffle-embed-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
isFallbackElement,
isYoutubeFlashSource,
workaroundYoutubeMixedContent,
clearDocumentNamedAccessor,
defineDocumentNamedAccessor,
} from "./inner";
import { registerElement } from "../register-element";
import { isSwf } from "../../swf-utils";
Expand All @@ -17,6 +19,7 @@ import { isSwf } from "../../swf-utils";
* @internal
*/
export class RuffleEmbedElement extends RufflePlayerElement {
private _name: string = "";
/**
* @ignore
* @internal
Expand All @@ -31,10 +34,23 @@ export class RuffleEmbedElement extends RufflePlayerElement {
const options = getPolyfillOptions(src.value, getOptionString);

// Kick off the SWF download.
this.load(options, true);
this.load(options, true).then(() => {
const name = this.attributes.getNamedItem("name")?.value;
defineDocumentNamedAccessor(this, name);
});
}
}

/**
* @ignore
* @internal
*/
override disconnectedCallback(): void {
const name = this.attributes.getNamedItem("name")?.value;
clearDocumentNamedAccessor(this, name);
super.disconnectedCallback();
}

/**
* Polyfill of HTMLEmbedElement.
*
Expand Down Expand Up @@ -76,7 +92,7 @@ export class RuffleEmbedElement extends RufflePlayerElement {
* @internal
*/
static override get observedAttributes(): string[] {
return [...RufflePlayerElement.observedAttributes, "src"];
return [...RufflePlayerElement.observedAttributes, "src", "name"];
}

/**
Expand All @@ -89,6 +105,10 @@ export class RuffleEmbedElement extends RufflePlayerElement {
newValue: string | undefined,
): void {
super.attributeChangedCallback(name, oldValue, newValue);
if (name === "name" && oldValue !== newValue) {
this.name = newValue ?? "";
return;
}
if (this.isConnected && name === "src") {
const src = this.attributes.getNamedItem("src");
if (src) {
Expand Down Expand Up @@ -208,4 +228,30 @@ export class RuffleEmbedElement extends RufflePlayerElement {
set type(typeVal: string) {
this.setAttribute("type", typeVal);
}

/**
* Polyfill of name getter
*
* @ignore
* @internal
*/
get name(): string {
return this._name ?? "";
}

/**
* Polyfill of name setter
*
* @ignore
* @internal
*/
set name(name: string) {
const oldName = this._name;
this._name = name;
if (oldName !== name) {
clearDocumentNamedAccessor(this, oldName);
defineDocumentNamedAccessor(this, name);
}
this.setAttribute("name", name);
}
}
68 changes: 67 additions & 1 deletion web/packages/core/src/internal/player/ruffle-object-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
isFallbackElement,
isYoutubeFlashSource,
workaroundYoutubeMixedContent,
clearDocumentNamedAccessor,
defineDocumentNamedAccessor,
} from "./inner";
import { FLASH_ACTIVEX_CLASSID } from "../../flash-identifiers";
import { registerElement } from "../register-element";
Expand Down Expand Up @@ -65,6 +67,7 @@ function paramsOf(elem: Element): Record<string, string> {
*/
export class RuffleObjectElement extends RufflePlayerElement {
private params: Record<string, string> = {};
private _name: string = "";

/**
* @ignore
Expand Down Expand Up @@ -101,10 +104,23 @@ export class RuffleObjectElement extends RufflePlayerElement {
const options = getPolyfillOptions(url, getOptionString);

// Kick off the SWF download.
this.load(options, true);
this.load(options, true).then(() => {
const name = this.attributes.getNamedItem("name")?.value;
defineDocumentNamedAccessor(this, name);
});
}
}

/**
* @ignore
* @internal
*/
override disconnectedCallback(): void {
const name = this.attributes.getNamedItem("name")?.value;
clearDocumentNamedAccessor(this, name);
super.disconnectedCallback();
}

protected override debugPlayerInfo(): string {
let result = "Player type: Object\n";

Expand Down Expand Up @@ -344,4 +360,54 @@ export class RuffleObjectElement extends RufflePlayerElement {
set type(typeVal: string) {
this.setAttribute("type", typeVal);
}

/**
* @ignore
* @internal
*/
static override get observedAttributes(): string[] {
return [...RufflePlayerElement.observedAttributes, "name"];
}

/**
* @ignore
* @internal
*/
override attributeChangedCallback(
name: string,
oldValue: string | undefined,
newValue: string | undefined,
): void {
super.attributeChangedCallback(name, oldValue, newValue);
if (name === "name" && oldValue !== newValue) {
this.name = newValue ?? "";
return;
}
}

/**
* Polyfill of name getter
*
* @ignore
* @internal
*/
get name(): string {
return this._name ?? "";
}

/**
* Polyfill of name setter
*
* @ignore
* @internal
*/
set name(name: string) {
const oldName = this._name;
this._name = name;
if (oldName !== name) {
clearDocumentNamedAccessor(this, oldName);
defineDocumentNamedAccessor(this, name);
}
this.setAttribute("name", name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>DOCUMENT ACCESSORS</title>
</head>

<body>
<div id="test-container">
<embed id="emb1" name="fl" src="/test_assets/example.swf" width="200" height="200"></embed>
<embed id="emb2" name="fl" src="/test_assets/example.swf" width="200" height="200"></embed>
<embed id="emb3" name="fl" src="/test_assets/example.swf" width="200" height="200"></embed>
<object id="obj1" name="fl" width="200" height="200"><param name="movie" value="/test_assets/example.swf"></object>
</div>
<ul id="output"></ul>
</body>
</html>
Loading