web: Remove all occurences of innerHTML (except in test files) (#12937)

* web: Remove most occurences of innerHTML

* web: Use helper methods for shadow template element creation

* web: Refactor createErrorFooter function

* web: Shadow template code cleanup

* web: Add helper function to add CSS rules to shadow template

---------

Co-authored-by: nosamu <71368227+n0samu@users.noreply.github.com>
This commit is contained in:
Daniel Jacobs 2023-08-26 15:51:16 -04:00 committed by GitHub
parent 5a10ccb22a
commit 18e89f6132
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 653 additions and 280 deletions

View File

@ -108,7 +108,7 @@ export function text(
export function textAsParagraphs( export function textAsParagraphs(
id: string, id: string,
args?: Record<string, FluentVariable> | null, args?: Record<string, FluentVariable> | null,
): string { ): HTMLDivElement {
const result = document.createElement("div"); const result = document.createElement("div");
text(id, args) text(id, args)
.split("\n") .split("\n")
@ -117,5 +117,5 @@ export function textAsParagraphs(
p.innerText = line; p.innerText = line;
result.appendChild(p); result.appendChild(p);
}); });
return result.innerHTML; return result;
} }

View File

@ -1,6 +1,6 @@
import type { Ruffle } from "../dist/ruffle_web"; import type { Ruffle } from "../dist/ruffle_web";
import { loadRuffle } from "./load-ruffle"; import { loadRuffle } from "./load-ruffle";
import { ruffleShadowTemplate } from "./shadow-template"; import { applyStaticStyles, ruffleShadowTemplate } from "./shadow-template";
import { lookupElement } from "./register-element"; import { lookupElement } from "./register-element";
import { DEFAULT_CONFIG } from "./config"; import { DEFAULT_CONFIG } from "./config";
import type { DataLoadOptions, URLLoadOptions } from "./load-options"; import type { DataLoadOptions, URLLoadOptions } from "./load-options";
@ -119,6 +119,13 @@ class Point {
} }
} }
class PanicLinkInfo {
constructor(
public url: string = "#",
public label: string = text("view-error-details"),
) {}
}
/** /**
* The ruffle player element that should be inserted onto the page. * The ruffle player element that should be inserted onto the page.
* *
@ -127,6 +134,7 @@ class Point {
export class RufflePlayer extends HTMLElement { export class RufflePlayer extends HTMLElement {
private readonly shadow: ShadowRoot; private readonly shadow: ShadowRoot;
private readonly dynamicStyles: HTMLStyleElement; private readonly dynamicStyles: HTMLStyleElement;
private readonly staticStyles: HTMLStyleElement;
private readonly container: HTMLElement; private readonly container: HTMLElement;
private readonly playButton: HTMLElement; private readonly playButton: HTMLElement;
private readonly unmuteOverlay: HTMLElement; private readonly unmuteOverlay: HTMLElement;
@ -226,13 +234,16 @@ export class RufflePlayer extends HTMLElement {
this.shadow.appendChild(ruffleShadowTemplate.content.cloneNode(true)); this.shadow.appendChild(ruffleShadowTemplate.content.cloneNode(true));
this.dynamicStyles = <HTMLStyleElement>( this.dynamicStyles = <HTMLStyleElement>(
this.shadow.getElementById("dynamic_styles") this.shadow.getElementById("dynamic-styles")
);
this.staticStyles = <HTMLStyleElement>(
this.shadow.getElementById("static-styles")
); );
this.container = this.shadow.getElementById("container")!; this.container = this.shadow.getElementById("container")!;
this.playButton = this.shadow.getElementById("play_button")!; this.playButton = this.shadow.getElementById("play-button")!;
this.playButton.addEventListener("click", () => this.play()); this.playButton.addEventListener("click", () => this.play());
this.unmuteOverlay = this.shadow.getElementById("unmute_overlay")!; this.unmuteOverlay = this.shadow.getElementById("unmute-overlay")!;
this.splashScreen = this.shadow.getElementById("splash-screen")!; this.splashScreen = this.shadow.getElementById("splash-screen")!;
this.virtualKeyboard = <HTMLInputElement>( this.virtualKeyboard = <HTMLInputElement>(
this.shadow.getElementById("virtual-keyboard")! this.shadow.getElementById("virtual-keyboard")!
@ -258,11 +269,11 @@ export class RufflePlayer extends HTMLElement {
} }
const unmuteSvg = <SVGElement>( const unmuteSvg = <SVGElement>(
this.unmuteOverlay.querySelector("#unmute_overlay_svg") this.unmuteOverlay.querySelector("#unmute-overlay-svg")
); );
if (unmuteSvg) { if (unmuteSvg) {
const unmuteText = <SVGTextElement>( const unmuteText = <SVGTextElement>(
unmuteSvg.querySelector("#unmute_text") unmuteSvg.querySelector("#unmute-text")
); );
unmuteText.textContent = text("click-to-unmute"); unmuteText.textContent = text("click-to-unmute");
} }
@ -376,6 +387,7 @@ export class RufflePlayer extends HTMLElement {
*/ */
connectedCallback(): void { connectedCallback(): void {
this.updateStyles(); this.updateStyles();
applyStaticStyles(this.staticStyles);
} }
/** /**
@ -1403,14 +1415,14 @@ export class RufflePlayer extends HTMLElement {
for (const item of this.contextMenuItems()) { for (const item of this.contextMenuItems()) {
if (item === null) { if (item === null) {
const menuSeparator = document.createElement("li"); const menuSeparator = document.createElement("li");
menuSeparator.className = "menu_separator"; menuSeparator.className = "menu-separator";
const hr = document.createElement("hr"); const hr = document.createElement("hr");
menuSeparator.appendChild(hr); menuSeparator.appendChild(hr);
this.contextMenuElement.appendChild(menuSeparator); this.contextMenuElement.appendChild(menuSeparator);
} else { } else {
const { text, onClick, enabled } = item; const { text, onClick, enabled } = item;
const menuItem = document.createElement("li"); const menuItem = document.createElement("li");
menuItem.className = "menu_item"; menuItem.className = "menu-item";
menuItem.textContent = text; menuItem.textContent = text;
this.contextMenuElement.appendChild(menuItem); this.contextMenuElement.appendChild(menuItem);
@ -1676,6 +1688,31 @@ export class RufflePlayer extends HTMLElement {
return result; return result;
} }
/**
* @param footerInfo An array of PanicLinkInfo objects.
*
* @returns The <ul> element to be used as the error footer
*/
private createErrorFooter(
footerInfo: Array<PanicLinkInfo>,
): HTMLUListElement {
const errorFooter = document.createElement("ul");
for (const linkInfo of footerInfo) {
const footerItem = document.createElement("li");
const footerLink = document.createElement("a");
footerLink.href = linkInfo.url;
footerLink.textContent = linkInfo.label;
if (linkInfo.url === "#") {
footerLink.id = "panic-view-details";
} else {
footerLink.target = "_top";
}
footerItem.appendChild(footerLink);
errorFooter.appendChild(footerItem);
}
return errorFooter;
}
/** /**
* Panics this specific player, forcefully destroying all resources and displays an error message to the user. * Panics this specific player, forcefully destroying all resources and displays an error message to the user.
* *
@ -1752,7 +1789,7 @@ export class RufflePlayer extends HTMLElement {
// Create a link to GitHub with all of the error data, if the build is not outdated. // Create a link to GitHub with all of the error data, if the build is not outdated.
// Otherwise, create a link to the downloads section on the Ruffle website. // Otherwise, create a link to the downloads section on the Ruffle website.
let actionTag; let actionLink: PanicLinkInfo;
if (!isBuildOutdated) { if (!isBuildOutdated) {
let url; let url;
if (document.location.protocol.includes("extension")) { if (document.location.protocol.includes("extension")) {
@ -1782,13 +1819,12 @@ export class RufflePlayer extends HTMLElement {
issueBody = encodeURIComponent(errorArray.join("")); issueBody = encodeURIComponent(errorArray.join(""));
} }
issueLink += issueBody; issueLink += issueBody;
actionTag = `<a target="_top" href="${issueLink}">${text( actionLink = new PanicLinkInfo(issueLink, text("report-bug"));
"report-bug",
)}</a>`;
} else { } else {
actionTag = `<a target="_top" href="${RUFFLE_ORIGIN}#downloads">${text( actionLink = new PanicLinkInfo(
"update-ruffle", RUFFLE_ORIGIN + "#downloads",
)}</a>`; text("update-ruffle"),
);
} }
// Clears out any existing content (ie play button or canvas) and replaces it with the error screen // Clears out any existing content (ie play button or canvas) and replaces it with the error screen
@ -1797,144 +1833,129 @@ export class RufflePlayer extends HTMLElement {
case PanicError.FileProtocol: case PanicError.FileProtocol:
// General error: Running on the `file:` protocol // General error: Running on the `file:` protocol
errorBody = textAsParagraphs("error-file-protocol"); errorBody = textAsParagraphs("error-file-protocol");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="${RUFFLE_ORIGIN}/demo">${text( new PanicLinkInfo(
"ruffle-demo", RUFFLE_ORIGIN + "/demo",
)}</a></li> text("ruffle-demo"),
<li><a target="_top" href="${RUFFLE_ORIGIN}#downloads">${text( ),
"ruffle-desktop", new PanicLinkInfo(
)}</a></li> RUFFLE_ORIGIN + "#downloads",
`; text("ruffle-desktop"),
),
]);
break; break;
case PanicError.JavascriptConfiguration: case PanicError.JavascriptConfiguration:
// General error: Incorrect JavaScript configuration // General error: Incorrect JavaScript configuration
errorBody = textAsParagraphs("error-javascript-config"); errorBody = textAsParagraphs("error-javascript-config");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#javascript-api">${text( new PanicLinkInfo(
"ruffle-wiki", "https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#javascript-api",
)}</a></li> text("ruffle-wiki"),
<li><a href="#" id="panic-view-details">${text( ),
"view-error-details", new PanicLinkInfo(),
)}</a></li> ]);
`;
break; break;
case PanicError.WasmNotFound: case PanicError.WasmNotFound:
// Self hosted: Cannot load `.wasm` file - file not found // Self hosted: Cannot load `.wasm` file - file not found
errorBody = textAsParagraphs("error-wasm-not-found"); errorBody = textAsParagraphs("error-wasm-not-found");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configuration-options">${text( new PanicLinkInfo(
"ruffle-wiki", "https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configuration-options",
)}</a></li> text("ruffle-wiki"),
<li><a href="#" id="panic-view-details">${text( ),
"view-error-details", new PanicLinkInfo(),
)}</a></li> ]);
`;
break; break;
case PanicError.WasmMimeType: case PanicError.WasmMimeType:
// Self hosted: Cannot load `.wasm` file - incorrect MIME type // Self hosted: Cannot load `.wasm` file - incorrect MIME type
errorBody = textAsParagraphs("error-wasm-mime-type"); errorBody = textAsParagraphs("error-wasm-mime-type");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-webassembly-mime-type">${text( new PanicLinkInfo(
"ruffle-wiki", "https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-webassembly-mime-type",
)}</a></li> text("ruffle-wiki"),
<li><a href="#" id="panic-view-details">${text( ),
"view-error-details", new PanicLinkInfo(),
)}</a></li> ]);
`;
break; break;
case PanicError.SwfFetchError: case PanicError.SwfFetchError:
errorBody = textAsParagraphs("error-swf-fetch"); errorBody = textAsParagraphs("error-swf-fetch");
errorFooter = ` errorFooter = this.createErrorFooter([new PanicLinkInfo()]);
<li><a href="#" id="panic-view-details">${text(
"view-error-details",
)}</a></li>
`;
break; break;
case PanicError.SwfCors: case PanicError.SwfCors:
// Self hosted: Cannot load SWF file - CORS issues // Self hosted: Cannot load SWF file - CORS issues
errorBody = textAsParagraphs("error-swf-cors"); errorBody = textAsParagraphs("error-swf-cors");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-cors-header">${text( new PanicLinkInfo(
"ruffle-wiki", "https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-cors-header",
)}</a></li> text("ruffle-wiki"),
<li><a href="#" id="panic-view-details">${text( ),
"view-error-details", new PanicLinkInfo(),
)}</a></li> ]);
`;
break; break;
case PanicError.WasmCors: case PanicError.WasmCors:
// Self hosted: Cannot load `.wasm` file - CORS issues // Self hosted: Cannot load `.wasm` file - CORS issues
errorBody = textAsParagraphs("error-wasm-cors"); errorBody = textAsParagraphs("error-wasm-cors");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-cors-header">${text( new PanicLinkInfo(
"ruffle-wiki", "https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-cors-header",
)}</a></li> text("ruffle-wiki"),
<li><a href="#" id="panic-view-details">${text( ),
"view-error-details", new PanicLinkInfo(),
)}</a></li> ]);
`;
break; break;
case PanicError.InvalidWasm: case PanicError.InvalidWasm:
// Self hosted: Cannot load `.wasm` file - incorrect configuration or missing files // Self hosted: Cannot load `.wasm` file - incorrect configuration or missing files
errorBody = textAsParagraphs("error-wasm-invalid"); errorBody = textAsParagraphs("error-wasm-invalid");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#addressing-a-compileerror">${text( new PanicLinkInfo(
"ruffle-wiki", "https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#addressing-a-compileerror",
)}</a></li> text("ruffle-wiki"),
<li><a href="#" id="panic-view-details">${text( ),
"view-error-details", new PanicLinkInfo(),
)}</a></li> ]);
`;
break; break;
case PanicError.WasmDownload: case PanicError.WasmDownload:
// Usually a transient network error or botched deployment // Usually a transient network error or botched deployment
errorBody = textAsParagraphs("error-wasm-download"); errorBody = textAsParagraphs("error-wasm-download");
errorFooter = ` errorFooter = this.createErrorFooter([new PanicLinkInfo()]);
<li><a href="#" id="panic-view-details">${text(
"view-error-details",
)}</a></li>
`;
break; break;
case PanicError.WasmDisabledMicrosoftEdge: case PanicError.WasmDisabledMicrosoftEdge:
// Self hosted: User has disabled WebAssembly in Microsoft Edge through the // Self hosted: User has disabled WebAssembly in Microsoft Edge through the
// "Enhance your Security on the web" setting. // "Enhance your Security on the web" setting.
errorBody = textAsParagraphs("error-wasm-disabled-on-edge"); errorBody = textAsParagraphs("error-wasm-disabled-on-edge");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Frequently-Asked-Questions-For-Users#edge-webassembly-error">${text( new PanicLinkInfo(
"more-info", "https://github.com/ruffle-rs/ruffle/wiki/Frequently-Asked-Questions-For-Users#edge-webassembly-error",
)}</a></li> text("more-info"),
<li><a href="#" id="panic-view-details">${text( ),
"view-error-details", new PanicLinkInfo(),
)}</a></li> ]);
`;
break; break;
case PanicError.JavascriptConflict: case PanicError.JavascriptConflict:
// Self hosted: Cannot load `.wasm` file - a native object / function is overriden // Self hosted: Cannot load `.wasm` file - a native object / function is overriden
errorBody = textAsParagraphs("error-javascript-conflict"); errorBody = textAsParagraphs("error-javascript-conflict");
if (isBuildOutdated) { if (isBuildOutdated) {
errorBody += textAsParagraphs( errorBody.appendChild(
"error-javascript-conflict-outdated", textAsParagraphs("error-javascript-conflict-outdated", {
{ buildDate: buildInfo.buildDate }, buildDate: buildInfo.buildDate,
}),
); );
} }
errorFooter = ` errorFooter = this.createErrorFooter([
<li>${actionTag}</li> actionLink,
<li><a href="#" id="panic-view-details">${text( new PanicLinkInfo(),
"view-error-details", ]);
)}</a></li>
`;
break; break;
case PanicError.CSPConflict: case PanicError.CSPConflict:
// General error: Cannot load `.wasm` file - a native object / function is overriden // General error: Cannot load `.wasm` file - a native object / function is overriden
errorBody = textAsParagraphs("error-csp-conflict"); errorBody = textAsParagraphs("error-csp-conflict");
errorFooter = ` errorFooter = this.createErrorFooter([
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-wasm-csp">${text( new PanicLinkInfo(
"ruffle-wiki", "https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-wasm-csp",
)}</a></li> text("ruffle-wiki"),
<li><a href="#" id="panic-view-details">${text( ),
"view-error-details", new PanicLinkInfo(),
)}</a></li> ]);
`;
break; break;
default: default:
// Unknown error // Unknown error
@ -1942,23 +1963,28 @@ export class RufflePlayer extends HTMLElement {
buildDate: buildInfo.buildDate, buildDate: buildInfo.buildDate,
outdated: String(isBuildOutdated), outdated: String(isBuildOutdated),
}); });
errorFooter = ` errorFooter = this.createErrorFooter([
<li>${actionTag}</li> actionLink,
<li><a href="#" id="panic-view-details">${text( new PanicLinkInfo(),
"view-error-details", ]);
)}</a></li>
`;
break; break;
} }
this.container.innerHTML = ` const panicDiv = document.createElement("div");
<div id="panic"> panicDiv.id = "panic";
<div id="panic-title">${text("panic-title")}</div> const panicTitle = document.createElement("div");
<div id="panic-body">${errorBody}</div> panicTitle.id = "panic-title";
<div id="panic-footer"> panicTitle.textContent = text("panic-title");
<ul>${errorFooter}</ul> panicDiv.appendChild(panicTitle);
</div> const panicBody = document.createElement("div");
</div> panicBody.id = "panic-body";
`; panicBody.appendChild(errorBody);
panicDiv.appendChild(panicBody);
const panicFooter = document.createElement("div");
panicFooter.id = "panic-footer";
panicFooter.appendChild(errorFooter);
panicDiv.appendChild(panicFooter);
this.container.textContent = "";
this.container.appendChild(panicDiv);
const viewDetails = <HTMLLinkElement>( const viewDetails = <HTMLLinkElement>(
this.container.querySelector("#panic-view-details") this.container.querySelector("#panic-view-details")
); );
@ -1996,10 +2022,10 @@ export class RufflePlayer extends HTMLElement {
this.hideSplashScreen(); this.hideSplashScreen();
const div = document.createElement("div"); const div = document.createElement("div");
div.id = "message_overlay"; div.id = "message-overlay";
const innerDiv = document.createElement("div"); const innerDiv = document.createElement("div");
innerDiv.className = "message"; innerDiv.className = "message";
innerDiv.innerHTML = textAsParagraphs("message-cant-embed"); innerDiv.appendChild(textAsParagraphs("message-cant-embed"));
const buttonDiv = document.createElement("div"); const buttonDiv = document.createElement("div");
const link = document.createElement("a"); const link = document.createElement("a");
@ -2035,13 +2061,19 @@ export class RufflePlayer extends HTMLElement {
*/ */
protected displayMessage(message: string): void { protected displayMessage(message: string): void {
const div = document.createElement("div"); const div = document.createElement("div");
div.id = "message_overlay"; div.id = "message-overlay";
div.innerHTML = `<div class="message"> const messageDiv = document.createElement("div");
<p>${message}</p> messageDiv.className = "message";
<div> const messageP = document.createElement("p");
<button id="continue-btn">${text("continue")}</button> messageP.textContent = message;
</div> messageDiv.appendChild(messageP);
</div>`; const buttonDiv = document.createElement("div");
const continueButton = document.createElement("button");
continueButton.id = "continue-btn";
continueButton.textContent = text("continue");
buttonDiv.appendChild(continueButton);
messageDiv.appendChild(buttonDiv);
div.appendChild(messageDiv);
this.container.prepend(div); this.container.prepend(div);
(<HTMLButtonElement>( (<HTMLButtonElement>(
this.container.querySelector("#continue-btn") this.container.querySelector("#continue-btn")

View File

@ -1,11 +1,32 @@
/** /**
* The shadow template which is used to fill the actual Ruffle player element * Insert all rules from array in the style sheet.
* on the page. *
* @param sheet The style sheet to which to apply the rules.
* @param rules An array of rules to be applied.
*/ */
export const ruffleShadowTemplate = document.createElement("template"); function insertRules(sheet: CSSStyleSheet, rules: Array<string>) {
ruffleShadowTemplate.innerHTML = ` for (const rule of rules) {
<style> try {
:host { sheet.insertRule(rule);
} catch (err) {
// Ignore unsupported rules
}
}
}
/**
* The default styles to apply to the shadow template.
* This function must be run after the shadow template is added to the page.
*
* @param styleElement The static style element to which to add the rules
*/
export function applyStaticStyles(styleElement: HTMLStyleElement) {
if (!styleElement.sheet) {
return;
}
const rules = [
`:host {
all: initial; all: initial;
--ruffle-blue: #37528c; --ruffle-blue: #37528c;
@ -22,57 +43,57 @@ ruffleShadowTemplate.innerHTML = `
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }`,
/* Ruffle's width/height CSS interferes with Safari's fullscreen CSS. */ /* Ruffle's width/height CSS interferes with Safari's fullscreen CSS. */
/* Ensure that Safari's fullscreen mode actually fills the screen. */ /* Ensure that Safari's fullscreen mode actually fills the screen. */
:host(:-webkit-full-screen) { `:host(:-webkit-full-screen) {
display: block; display: block;
width: 100% !important; width: 100% !important;
height: 100% !important; height: 100% !important;
} }`,
.hidden { `.hidden {
display: none !important; display: none !important;
} }`,
/* All of these use the dimensions specified by the embed. */ /* All of these use the dimensions specified by the embed. */
#container, `#container,
#play_button, #play-button,
#unmute_overlay, #unmute-overlay,
#unmute_overlay .background, #unmute-overlay .background,
#panic, #panic,
#splash-screen, #splash-screen,
#message_overlay { #message-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
} }`,
#container { `#container {
overflow: hidden; overflow: hidden;
} }`,
#container canvas { `#container canvas {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }`,
#play_button, `#play-button,
#unmute_overlay { #unmute-overlay {
cursor: pointer; cursor: pointer;
display: none; display: none;
} }`,
#unmute_overlay .background { `#unmute-overlay .background {
background: black; background: black;
opacity: 0.7; opacity: 0.7;
} }`,
#play_button .icon, `#play-button .icon,
#unmute_overlay .icon { #unmute-overlay .icon {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -82,53 +103,53 @@ ruffleShadowTemplate.innerHTML = `
max-height: 384px; max-height: 384px;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
opacity: 0.8; opacity: 0.8;
} }`,
#play_button:hover .icon, `#play-button:hover .icon,
#unmute_overlay:hover .icon { #unmute-overlay:hover .icon {
opacity: 1; opacity: 1;
} }`,
#panic { /* Includes inverted colors from play button! */
`#panic {
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
/* Inverted colors from play button! */
background: linear-gradient(180deg, #fd3a40 0%, #fda138 100%); background: linear-gradient(180deg, #fd3a40 0%, #fda138 100%);
color: white; color: white;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
justify-content: space-around; justify-content: space-around;
} }`,
#panic a { `#panic a {
color: var(--ruffle-blue); color: var(--ruffle-blue);
font-weight: bold; font-weight: bold;
} }`,
#panic-title { `#panic-title {
font-size: xxx-large; font-size: xxx-large;
font-weight: bold; font-weight: bold;
} }`,
#panic-body.details { `#panic-body.details {
flex: 0.9; flex: 0.9;
margin: 0 10px; margin: 0 10px;
} }`,
#panic-body textarea { `#panic-body textarea {
width: 100%; width: 100%;
height: 100%; height: 100%;
resize: none; resize: none;
} }`,
#panic ul { `#panic ul {
padding: 0; padding: 0;
display: flex; display: flex;
list-style-type: none; list-style-type: none;
justify-content: space-evenly; justify-content: space-evenly;
} }`,
#message_overlay { `#message-overlay {
position: absolute; position: absolute;
background: var(--ruffle-blue); background: var(--ruffle-blue);
color: var(--ruffle-orange); color: var(--ruffle-orange);
@ -138,28 +159,28 @@ ruffleShadowTemplate.innerHTML = `
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: auto; overflow: auto;
} }`,
#message_overlay .message { `#message-overlay .message {
text-align: center; text-align: center;
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;
padding: 5%; padding: 5%;
font-size: 20px; font-size: 20px;
} }`,
#message_overlay p { `#message-overlay p {
margin: 0.5em 0; margin: 0.5em 0;
} }`,
#message_overlay .message div { `#message-overlay .message div {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
column-gap: 1em; column-gap: 1em;
} }`,
#message_overlay a, #message_overlay button { `#message-overlay a, #message-overlay button {
cursor: pointer; cursor: pointer;
background: var(--ruffle-blue); background: var(--ruffle-blue);
color: var(--ruffle-orange); color: var(--ruffle-orange);
@ -170,13 +191,13 @@ ruffleShadowTemplate.innerHTML = `
padding: 10px; padding: 10px;
text-decoration: none; text-decoration: none;
margin: 2% 0; margin: 2% 0;
} }`,
#message_overlay a:hover, #message_overlay button:hover { `#message-overlay a:hover, #message-overlay button:hover {
background: #ffffff4c; background: #ffffff4c;
} }`,
#continue-btn { `#continue-btn {
cursor: pointer; cursor: pointer;
background: var(--ruffle-blue); background: var(--ruffle-blue);
color: var(--ruffle-orange); color: var(--ruffle-orange);
@ -185,20 +206,20 @@ ruffleShadowTemplate.innerHTML = `
font-size: 20px; font-size: 20px;
border-radius: 20px; border-radius: 20px;
padding: 10px; padding: 10px;
} }`,
#continue-btn:hover { `#continue-btn:hover {
background: #ffffff4c; background: #ffffff4c;
} }`,
#context-menu-overlay { `#context-menu-overlay {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 1; z-index: 1;
position: absolute; position: absolute;
} }`,
#context-menu { `#context-menu {
color: black; color: black;
background: #fafafa; background: #fafafa;
border: 1px solid gray; border: 1px solid gray;
@ -209,117 +230,117 @@ ruffleShadowTemplate.innerHTML = `
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }`,
#context-menu .menu_item { `#context-menu .menu-item {
padding: 5px 10px; padding: 5px 10px;
cursor: pointer; cursor: pointer;
color: black; color: black;
} }`,
#context-menu .menu_item.disabled { `#context-menu .menu-item.disabled {
cursor: default; cursor: default;
color: gray; color: gray;
} }`,
#context-menu .menu_item:not(.disabled):hover { `#context-menu .menu-item:not(.disabled):hover {
background: lightgray; background: lightgray;
} }`,
#context-menu .menu_separator hr { `#context-menu .menu-separator hr {
border: none; border: none;
border-bottom: 1px solid lightgray; border-bottom: 1px solid lightgray;
margin: 2px; margin: 2px;
} }`,
#splash-screen { `#splash-screen {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--splash-screen-background, var(--preloader-background, var(--ruffle-blue))); background: var(--splash-screen-background, var(--preloader-background, var(--ruffle-blue)));
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }`,
.loadbar { `.loadbar {
width: 100%; width: 100%;
max-width: 316px; max-width: 316px;
max-height: 10px; max-height: 10px;
height: 20%; height: 20%;
background: #253559; background: #253559;
} }`,
.loadbar-inner { `.loadbar-inner {
width: 0px; width: 0px;
max-width: 100%; max-width: 100%;
height: 100%; height: 100%;
background: var(--ruffle-orange); background: var(--ruffle-orange);
} }`,
.logo { `.logo {
display: var(--logo-display, block); display: var(--logo-display, block);
max-width: 380px; max-width: 380px;
max-height: 150px; max-height: 150px;
} }`,
.loading-animation { `.loading-animation {
max-width: 28px; max-width: 28px;
max-height: 28px; max-height: 28px;
margin-bottom: 2%; margin-bottom: 2%;
width: 10%; width: 10%;
aspect-ratio: 1; aspect-ratio: 1;
} }`,
.spinner { `.spinner {
stroke-dasharray: 180; stroke-dasharray: 180;
stroke-dashoffset: 135; stroke-dashoffset: 135;
stroke: var(--ruffle-orange); stroke: var(--ruffle-orange);
transform-origin: 50% 50%; transform-origin: 50% 50%;
animation: rotate 1.5s linear infinite; animation: rotate 1.5s linear infinite;
} }`,
@keyframes rotate { `@keyframes rotate {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }`,
#virtual-keyboard { `#virtual-keyboard {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
top: -100px; top: -100px;
width: 1px; width: 1px;
height: 1px; height: 1px;
} }`,
.modal { `.modal {
height: inherit; height: inherit;
user-select: text; user-select: text;
} }`,
.modal-area { `.modal-area {
position: sticky; position: sticky;
background: white; background: white;
width: fit-content; width: fit-content;
padding: 16px; padding: 16px;
border: 3px solid black; border: 3px solid black;
margin: auto; margin: auto;
} }`,
#modal-area { `#modal-area {
height: 500px; height: 500px;
max-height: calc(100% - 38px); max-height: calc(100% - 38px);
min-height: 80px; min-height: 80px;
} }`,
#restore-save { `#restore-save {
display: none; display: none;
} }`,
.replace-save { `.replace-save {
display: none; display: none;
} }`,
.save-option { `.save-option {
display: inline-block; display: inline-block;
padding: 3px 10px; padding: 3px 10px;
margin: 5px 2px; margin: 5px 2px;
@ -327,83 +348,403 @@ ruffleShadowTemplate.innerHTML = `
border-radius: 50px; border-radius: 50px;
background-color: var(--ruffle-blue); background-color: var(--ruffle-blue);
color: white; color: white;
} }`,
.close-modal { `.close-modal {
position: absolute; position: absolute;
top: 5px; top: 5px;
right: 10px; right: 10px;
cursor: pointer; cursor: pointer;
font-size: x-large; font-size: x-large;
} }`,
.general-save-options { `.general-save-options {
text-align: center; text-align: center;
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 2px solid #888; border-bottom: 2px solid #888;
} }`,
#local-saves { `#local-saves {
border-collapse: collapse; border-collapse: collapse;
overflow-y: auto; overflow-y: auto;
display: block; display: block;
padding-right: 16px; padding-right: 16px;
height: calc(100% - 45px); height: calc(100% - 45px);
min-height: 30px; min-height: 30px;
} }`,
#local-saves td { `#local-saves td {
border-bottom: 1px solid #bbb; border-bottom: 1px solid #bbb;
height: 30px; height: 30px;
} }`,
#local-saves tr td:nth-child(1) { `#local-saves tr td:nth-child(1) {
padding-right: 1em; padding-right: 1em;
word-break: break-all; word-break: break-all;
} }`,
#local-saves tr:nth-child(even) { `#local-saves tr:nth-child(even) {
background-color: #f2f2f2; background-color: #f2f2f2;
} }`,
#video-holder { `#video-holder {
padding-top: 20px; padding-top: 20px;
}`,
];
insertRules(styleElement.sheet, rules);
}
/**
*
* @param tag The HTML tag name of the new element.
* @param id The id of the new element.
* @param className The class name of the new element.
* @param attributes A hash of attributes for the new element.
* @param ns The namespace of the new element.
*
* @returns The newly created Element
*/
function createElement(
tag: string,
id?: string,
className?: string,
attributes?: Record<string, string>,
ns?: string,
): Element {
const element = ns
? document.createElementNS(ns, tag)
: document.createElement(tag);
if (id) {
element.id = id;
}
if (className && ns) {
element.classList.add(className);
} else if (className) {
element.className = className;
}
if (attributes) {
for (const [key, attr] of Object.entries(attributes)) {
element.setAttribute(key, attr);
} }
</style> }
<style id="dynamic_styles"></style> return element;
}
<div id="container"> /**
<div id="play_button"><div class="icon"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" viewBox="0 0 250 250" width="100%" height="100%"><defs><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="125" y1="0" x2="125" y2="250" spreadMethod="pad"><stop offset="0%" stop-color="#FDA138"/><stop offset="100%" stop-color="#FD3A40"/></linearGradient><g id="b"><path fill="url(#a)" d="M250 125q0-52-37-88-36-37-88-37T37 37Q0 73 0 125t37 88q36 37 88 37t88-37q37-36 37-88M87 195V55l100 70-100 70z"/><path fill="#FFF" d="M87 55v140l100-70L87 55z"/></g></defs><use xlink:href="#b"/></svg></div></div> *
<div id="unmute_overlay"><div class="background"></div><div class="icon"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" viewBox="0 0 512 584" width="100%" height="100%" scale="0.8" id="unmute_overlay_svg"><path fill="#FFF" stroke="#FFF" d="m457.941 256 47.029-47.029c9.372-9.373 9.372-24.568 0-33.941-9.373-9.373-24.568-9.373-33.941 0l-47.029 47.029-47.029-47.029c-9.373-9.373-24.568-9.373-33.941 0-9.372 9.373-9.372 24.568 0 33.941l47.029 47.029-47.029 47.029c-9.372 9.373-9.372 24.568 0 33.941 4.686 4.687 10.827 7.03 16.97 7.03s12.284-2.343 16.971-7.029l47.029-47.03 47.029 47.029c4.687 4.687 10.828 7.03 16.971 7.03s12.284-2.343 16.971-7.029c9.372-9.373 9.372-24.568 0-33.941z"/><path fill="#FFF" stroke="#FFF" d="m99 160h-55c-24.301 0-44 19.699-44 44v104c0 24.301 19.699 44 44 44h55c2.761 0 5-2.239 5-5v-182c0-2.761-2.239-5-5-5z"/><path fill="#FFF" stroke="#FFF" d="m280 56h-24c-5.269 0-10.392 1.734-14.578 4.935l-103.459 79.116c-1.237.946-1.963 2.414-1.963 3.972v223.955c0 1.557.726 3.026 1.963 3.972l103.459 79.115c4.186 3.201 9.309 4.936 14.579 4.936h23.999c13.255 0 24-10.745 24-24v-352.001c0-13.255-10.745-24-24-24z"/><text x="256" y="560" text-anchor="middle" font-size="60px" fill="#FFF" stroke="#FFF" id="unmute_text"></text></svg></div></div> * @param parentElement The node to which to append a child element.
<input id="virtual-keyboard" type="text" autocapitalize="off" autocomplete="off" autocorrect="off"> * @param childElement The node to be appended to the parent element.
</div> */
function appendElement(parentElement: Node, childElement: Node) {
parentElement.appendChild(childElement);
}
<div class="hidden" id="splash-screen"> /**
<svg class="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" viewBox="0 0 380 150"><g><path fill="#966214" d="M58.75 85.6q.75-.1 1.5-.35.85-.25 1.65-.75.55-.35 1.05-.8.5-.45.95-1 .5-.5.75-1.2-.05.05-.15.1-.1.15-.25.25l-.1.2q-.15.05-.25.1-.4 0-.8.05-.5-.25-.9-.5-.3-.1-.55-.3l-.6-.6-4.25-6.45-1.5 11.25h3.45m83.15-.2h3.45q.75-.1 1.5-.35.25-.05.45-.15.35-.15.65-.3l.5-.3q.25-.15.5-.35.45-.35.9-.75.45-.35.75-.85l.1-.1q.1-.2.2-.35.2-.3.35-.6l-.3.4-.15.15q-.5.15-1.1.1-.25 0-.4-.05-.5-.15-.8-.4-.15-.1-.25-.25-.3-.3-.55-.6l-.05-.05v-.05l-4.25-6.4-1.5 11.25m-21.15-3.95q-.3-.3-.55-.6l-.05-.05v-.05l-4.25-6.4-1.5 11.25h3.45q.75-.1 1.5-.35.85-.25 1.6-.75.75-.5 1.4-1.1.45-.35.75-.85.35-.5.65-1.05l-.45.55q-.5.15-1.1.1-.9 0-1.45-.7m59.15.3q-.75-.5-1.4-1-3.15-2.55-3.5-6.4l-1.5 11.25h21q-3.1-.25-5.7-.75-5.6-1.05-8.9-3.1m94.2 3.85h3.45q.6-.1 1.2-.3.4-.1.75-.2.35-.15.65-.3.7-.35 1.35-.8.75-.55 1.3-1.25.1-.15.25-.3-2.55-.25-3.25-1.8l-4.2-6.3-1.5 11.25m-45.3-4.85q-.5-.4-.9-.8-2.3-2.35-2.6-5.6l-1.5 11.25h21q-11.25-.95-16-4.85m97.7 4.85q-.3-.05-.6-.05-10.8-1-15.4-4.8-3.15-2.55-3.5-6.35l-1.5 11.2h21Z"/><path fill="var(--ruffle-orange)" d="M92.6 54.8q-1.95-1.4-4.5-1.4H60.35q-1.35 0-2.6.45-1.65.55-3.15 1.8-2.75 2.25-3.25 5.25l-1.65 12h.05v.3l5.85 1.15h-9.5q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45-.5.3-.95.7-.45.35-.85.8-.35.4-.65.85-.3.45-.5.9-.15.45-.3.95l-5.85 41.6H50.3l5-35.5 1.5-11.25 4.25 6.45.6.6q.25.2.55.3.4.25.9.5.4-.05.8-.05.1-.05.25-.1l.1-.2q.15-.1.25-.25.1-.05.15-.1l.3-1.05 1.75-12.3h11.15L75.8 82.6h16.5l2.3-16.25h-.05l.8-5.7q.4-2.45-1-4.2-.35-.4-.75-.8-.25-.25-.55-.5-.2-.2-.45-.35m16.2 18.1h.05l-.05.3 5.85 1.15H105.2q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45-.5.3-1 .65-.4.4-.8.85-.25.3-.55.65-.05.1-.15.2-.25.45-.4.9-.2.45-.3.95-.1.65-.2 1.25-.2 1.15-.4 2.25l-4.3 30.6q-.25 3 1.75 5.25 1.6 1.8 4 2.15.6.1 1.25.1h27.35q3.25 0 6-2.25.35-.35.7-.55l.3-.2q2-2 2.25-4.5l1.65-11.6q.05-.05.1-.05l1.65-11.35h.05l.7-5.2 1.5-11.25 4.25 6.4v.05l.05.05q.25.3.55.6.1.15.25.25.3.25.8.4.15.05.4.05.6.05 1.1-.1l.15-.15.3-.4.3-1.05 1.3-9.05h-.05l.7-5.05h-.05l.15-1.25h-.05l1.65-11.7h-16.25l-2.65 19.5h.05v.2l-.05.1h.05l5.8 1.15H132.7q-.5.05-1 .15-.5.15-1 .35-.15.05-.3.15-.3.1-.55.25-.05 0-.1.05-.5.3-1 .65-.4.35-.7.7-.55.7-.95 1.45-.35.65-.55 1.4-.15.7-.25 1.4v.05q-.15 1.05-.35 2.05l-1.2 8.75v.1l-2.1 14.7H111.4l2.25-15.55h.05l.7-5.2 1.5-11.25 4.25 6.4v.05l.05.05q.25.3.55.6.55.7 1.45.7.6.05 1.1-.1l.45-.55.3-1.05 1.3-9.05h-.05l.7-5.05h-.05l.15-1.25h-.05l1.65-11.7h-16.25l-2.65 19.5m106.5-41.75q-2.25-2.25-5.5-2.25h-27.75q-3 0-5.75 2.25-1.3.95-2.05 2.1-.45.6-.7 1.2-.2.5-.35 1-.1.45-.15.95l-4.15 29.95h-.05l-.7 5.2h-.05l-.2 1.35h.05l-.05.3 5.85 1.15h-9.45q-2.1.05-3.95 1.6-1.9 1.55-2.25 3.55l-.5 3.5h-.05l-5.3 38.1h16.25l5-35.5 1.5-11.25q.35 3.85 3.5 6.4.65.5 1.4 1 3.3 2.05 8.9 3.1 2.6.5 5.7.75l1.75-11.25h-12.2l.4-2.95h-.05l.7-5.05h-.05q.1-.9.3-1.9.1-.75.2-1.6.85-5.9 2.15-14.9 0-.15.05-.25l.1-.9q.2-1.55.45-3.15h11.25l-3.1 20.8h16.5l4.1-28.05q.15-1.7-.4-3.15-.5-1.1-1.35-2.1m46.65 44.15q-.5.3-1 .65-.4.4-.8.85-.35.4-.7.85-.25.45-.45.9-.15.45-.3.95l-5.85 41.6h16.25l5-35.5 1.5-11.25 4.2 6.3q.7 1.55 3.25 1.8l.05-.1q.25-.4.35-.85l.3-1.05 1.8-14.05v-.05l5.35-37.45h-16.25l-6.15 44.3 5.85 1.15h-9.45q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45m5.4-38.9q.15-1.7-.4-3.15-.5-1.1-1.35-2.1-2.25-2.25-5.5-2.25h-27.75q-2.3 0-4.45 1.35-.65.35-1.3.9-1.3.95-2.05 2.1-.45.6-.7 1.2-.4.9-.5 1.95l-4.15 29.95h-.05l-.7 5.2h-.05l-.2 1.35h.05l-.05.3 5.85 1.15h-9.45q-2.1.05-3.95 1.6-1.9 1.55-2.25 3.55l-.5 3.5h-.05l-1.2 8.75v.1l-4.1 29.25h16.25l5-35.5 1.5-11.25q.3 3.25 2.6 5.6.4.4.9.8 4.75 3.9 16 4.85l1.75-11.25h-12.2l.4-2.95h-.05l.7-5.05h-.05q.15-.9.3-1.9.1-.75.25-1.6.15-1.25.35-2.65v-.05q.95-6.7 2.35-16.5h11.25l-3.1 20.8h16.5l4.1-28.05M345 66.35h-.05l1.15-8.2q.5-3-1.75-5.25-1.25-1.25-3-1.75-1-.5-2.25-.5h-27.95q-.65 0-1.3.1-2.5.35-4.7 2.15-2.75 2.25-3.25 5.25l-1.95 14.7v.05l-.05.3 5.85 1.15h-9.45q-1.9.05-3.6 1.35-.2.1-.35.25-1.9 1.55-2.25 3.55l-4.85 34.1q-.25 3 1.75 5.25 1.25 1.4 3 1.95 1.05.3 2.25.3H320q3.25 0 6-2.25 2.75-2 3.25-5l2.75-18.5h-16.5l-1.75 11H302.5l2.1-14.75h.05l.85-6 1.5-11.2q.35 3.8 3.5 6.35 4.6 3.8 15.4 4.8.3 0 .6.05h15.75L345 66.35m-16.4-.95-1.25 8.95h-11.3l.4-2.95h-.05l.7-5.05h-.1l.15-.95h11.45Z"/></g></svg> * The shadow template which is used to fill the actual Ruffle player element
<svg class="loading-animation" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 66 66"> * on the page.
<circle class="spinner" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle> *
</svg> */
<div class="loadbar"><div class="loadbar-inner"></div></div> export const ruffleShadowTemplate = document.createElement("template");
</div> const svgns = "http://www.w3.org/2000/svg";
const staticStyles = createElement("style", "static-styles");
const dynamicStyles = createElement("style", "dynamic-styles");
const container = createElement("div", "container");
<div id="save-manager" class="modal hidden"> // Play button elements
<div id="modal-area" class="modal-area"> const playButton = createElement("div", "play-button");
<span class="close-modal">&times;</span> const playIcon = createElement("div", undefined, "icon");
<div class="general-save-options"> const playSvg = createElement(
<span class="save-option" id="backup-saves"></span> "svg",
</div> undefined,
<table id="local-saves"></table> undefined,
</div> {
</div> xmlns: svgns,
"xmlns:xlink": "http://www.w3.org/1999/xlink",
preserveAspectRatio: "xMidYMid",
viewBox: "0 0 250 250",
width: "100%",
height: "100%",
},
svgns,
);
const playDefs = createElement("defs", undefined, undefined, undefined, svgns);
const playLinearGradient = createElement(
"linearGradient",
"a",
undefined,
{
gradientUnits: "userSpaceOnUse",
x1: "125",
y1: "0",
x2: "125",
y2: "250",
spreadMethod: "pad",
},
svgns,
);
const playStop0 = createElement(
"stop",
undefined,
undefined,
{
offset: "0%",
"stop-color": "#FDA138",
},
svgns,
);
const playStop100 = createElement(
"stop",
undefined,
undefined,
{
offset: "100%",
"stop-color": "#FD3A40",
},
svgns,
);
const playG = createElement("g", "b", undefined, undefined, svgns);
const playPath1 = createElement(
"path",
undefined,
undefined,
{
fill: "url(#a)",
d: "M250 125q0-52-37-88-36-37-88-37T37 37Q0 73 0 125t37 88q36 37 88 37t88-37q37-36 37-88M87 195V55l100 70-100 70z",
},
svgns,
);
const playPath2 = createElement(
"path",
undefined,
undefined,
{
fill: "#FFF",
d: "M87 55v140l100-70L87 55z",
},
svgns,
);
const playUse = document.createElementNS(svgns, "use");
playUse.href.baseVal = "#b";
<div id="video-modal" class="modal hidden"> // Unmute overlay elements
<div class="modal-area"> const unmuteOverlay = createElement("div", "unmute-overlay");
<span class="close-modal">&times;</span> const background = createElement("div", undefined, "background");
<div id="video-holder"></div> const unmuteIcon = createElement("div", undefined, "icon");
</div> const unmuteSvg = createElement(
</div> "svg",
"unmute-overlay-svg",
undefined,
{
xmlns: svgns,
"xmlns:xlink": "http://www.w3.org/1999/xlink",
preserveAspectRatio: "xMidYMid",
viewBox: "0 0 512 584",
width: "100%",
height: "100%",
scale: "0.8",
},
svgns,
);
const unmutePath1 = createElement(
"path",
undefined,
undefined,
{
fill: "#FFF",
stroke: "#FFF",
d: "m457.941 256 47.029-47.029c9.372-9.373 9.372-24.568 0-33.941-9.373-9.373-24.568-9.373-33.941 0l-47.029 47.029-47.029-47.029c-9.373-9.373-24.568-9.373-33.941 0-9.372 9.373-9.372 24.568 0 33.941l47.029 47.029-47.029 47.029c-9.372 9.373-9.372 24.568 0 33.941 4.686 4.687 10.827 7.03 16.97 7.03s12.284-2.343 16.971-7.029l47.029-47.03 47.029 47.029c4.687 4.687 10.828 7.03 16.971 7.03s12.284-2.343 16.971-7.029c9.372-9.373 9.372-24.568 0-33.941z",
},
svgns,
);
const unmutePath2 = createElement(
"path",
undefined,
undefined,
{
fill: "#FFF",
stroke: "#FFF",
d: "m99 160h-55c-24.301 0-44 19.699-44 44v104c0 24.301 19.699 44 44 44h55c2.761 0 5-2.239 5-5v-182c0-2.761-2.239-5-5-5z",
},
svgns,
);
const unmutePath3 = createElement(
"path",
undefined,
undefined,
{
fill: "#FFF",
stroke: "#FFF",
d: "m280 56h-24c-5.269 0-10.392 1.734-14.578 4.935l-103.459 79.116c-1.237.946-1.963 2.414-1.963 3.972v223.955c0 1.557.726 3.026 1.963 3.972l103.459 79.115c4.186 3.201 9.309 4.936 14.579 4.936h23.999c13.255 0 24-10.745 24-24v-352.001c0-13.255-10.745-24-24-24z",
},
svgns,
);
const unmuteText = createElement(
"text",
"unmute-text",
undefined,
{
x: "256",
y: "560",
"text-anchor": "middle",
"font-size": "60px",
fill: "#FFF",
stroke: "#FFF",
},
svgns,
);
<div id="context-menu-overlay" class="hidden"> // Virtual keyboard element
<ul id="context-menu"></ul> const virtualKeyboard = createElement("input", "virtual-keyboard", undefined, {
</div> type: "text",
`; autocapitalize: "off",
autocomplete: "off",
autocorrect: "off",
});
// Splash screen elements
const splashScreen = createElement("div", "splash-screen", "hidden");
const splashScreenSvg = createElement(
"svg",
undefined,
"logo",
{
xmlns: svgns,
"xmlns:xlink": "http://www.w3.org/1999/xlink",
preserveAspectRatio: "xMidYMid",
viewBox: "0 0 380 150",
},
svgns,
);
const splashScreenG = createElement(
"g",
undefined,
undefined,
undefined,
svgns,
);
const splashScreenPath1 = createElement(
"path",
undefined,
undefined,
{
fill: "#966214",
d: "M58.75 85.6q.75-.1 1.5-.35.85-.25 1.65-.75.55-.35 1.05-.8.5-.45.95-1 .5-.5.75-1.2-.05.05-.15.1-.1.15-.25.25l-.1.2q-.15.05-.25.1-.4 0-.8.05-.5-.25-.9-.5-.3-.1-.55-.3l-.6-.6-4.25-6.45-1.5 11.25h3.45m83.15-.2h3.45q.75-.1 1.5-.35.25-.05.45-.15.35-.15.65-.3l.5-.3q.25-.15.5-.35.45-.35.9-.75.45-.35.75-.85l.1-.1q.1-.2.2-.35.2-.3.35-.6l-.3.4-.15.15q-.5.15-1.1.1-.25 0-.4-.05-.5-.15-.8-.4-.15-.1-.25-.25-.3-.3-.55-.6l-.05-.05v-.05l-4.25-6.4-1.5 11.25m-21.15-3.95q-.3-.3-.55-.6l-.05-.05v-.05l-4.25-6.4-1.5 11.25h3.45q.75-.1 1.5-.35.85-.25 1.6-.75.75-.5 1.4-1.1.45-.35.75-.85.35-.5.65-1.05l-.45.55q-.5.15-1.1.1-.9 0-1.45-.7m59.15.3q-.75-.5-1.4-1-3.15-2.55-3.5-6.4l-1.5 11.25h21q-3.1-.25-5.7-.75-5.6-1.05-8.9-3.1m94.2 3.85h3.45q.6-.1 1.2-.3.4-.1.75-.2.35-.15.65-.3.7-.35 1.35-.8.75-.55 1.3-1.25.1-.15.25-.3-2.55-.25-3.25-1.8l-4.2-6.3-1.5 11.25m-45.3-4.85q-.5-.4-.9-.8-2.3-2.35-2.6-5.6l-1.5 11.25h21q-11.25-.95-16-4.85m97.7 4.85q-.3-.05-.6-.05-10.8-1-15.4-4.8-3.15-2.55-3.5-6.35l-1.5 11.2h21Z",
},
svgns,
);
const splashScreenPath2 = createElement(
"path",
undefined,
undefined,
{
fill: "var(--ruffle-orange)",
d: "M92.6 54.8q-1.95-1.4-4.5-1.4H60.35q-1.35 0-2.6.45-1.65.55-3.15 1.8-2.75 2.25-3.25 5.25l-1.65 12h.05v.3l5.85 1.15h-9.5q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45-.5.3-.95.7-.45.35-.85.8-.35.4-.65.85-.3.45-.5.9-.15.45-.3.95l-5.85 41.6H50.3l5-35.5 1.5-11.25 4.25 6.45.6.6q.25.2.55.3.4.25.9.5.4-.05.8-.05.1-.05.25-.1l.1-.2q.15-.1.25-.25.1-.05.15-.1l.3-1.05 1.75-12.3h11.15L75.8 82.6h16.5l2.3-16.25h-.05l.8-5.7q.4-2.45-1-4.2-.35-.4-.75-.8-.25-.25-.55-.5-.2-.2-.45-.35m16.2 18.1h.05l-.05.3 5.85 1.15H105.2q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45-.5.3-1 .65-.4.4-.8.85-.25.3-.55.65-.05.1-.15.2-.25.45-.4.9-.2.45-.3.95-.1.65-.2 1.25-.2 1.15-.4 2.25l-4.3 30.6q-.25 3 1.75 5.25 1.6 1.8 4 2.15.6.1 1.25.1h27.35q3.25 0 6-2.25.35-.35.7-.55l.3-.2q2-2 2.25-4.5l1.65-11.6q.05-.05.1-.05l1.65-11.35h.05l.7-5.2 1.5-11.25 4.25 6.4v.05l.05.05q.25.3.55.6.1.15.25.25.3.25.8.4.15.05.4.05.6.05 1.1-.1l.15-.15.3-.4.3-1.05 1.3-9.05h-.05l.7-5.05h-.05l.15-1.25h-.05l1.65-11.7h-16.25l-2.65 19.5h.05v.2l-.05.1h.05l5.8 1.15H132.7q-.5.05-1 .15-.5.15-1 .35-.15.05-.3.15-.3.1-.55.25-.05 0-.1.05-.5.3-1 .65-.4.35-.7.7-.55.7-.95 1.45-.35.65-.55 1.4-.15.7-.25 1.4v.05q-.15 1.05-.35 2.05l-1.2 8.75v.1l-2.1 14.7H111.4l2.25-15.55h.05l.7-5.2 1.5-11.25 4.25 6.4v.05l.05.05q.25.3.55.6.55.7 1.45.7.6.05 1.1-.1l.45-.55.3-1.05 1.3-9.05h-.05l.7-5.05h-.05l.15-1.25h-.05l1.65-11.7h-16.25l-2.65 19.5m106.5-41.75q-2.25-2.25-5.5-2.25h-27.75q-3 0-5.75 2.25-1.3.95-2.05 2.1-.45.6-.7 1.2-.2.5-.35 1-.1.45-.15.95l-4.15 29.95h-.05l-.7 5.2h-.05l-.2 1.35h.05l-.05.3 5.85 1.15h-9.45q-2.1.05-3.95 1.6-1.9 1.55-2.25 3.55l-.5 3.5h-.05l-5.3 38.1h16.25l5-35.5 1.5-11.25q.35 3.85 3.5 6.4.65.5 1.4 1 3.3 2.05 8.9 3.1 2.6.5 5.7.75l1.75-11.25h-12.2l.4-2.95h-.05l.7-5.05h-.05q.1-.9.3-1.9.1-.75.2-1.6.85-5.9 2.15-14.9 0-.15.05-.25l.1-.9q.2-1.55.45-3.15h11.25l-3.1 20.8h16.5l4.1-28.05q.15-1.7-.4-3.15-.5-1.1-1.35-2.1m46.65 44.15q-.5.3-1 .65-.4.4-.8.85-.35.4-.7.85-.25.45-.45.9-.15.45-.3.95l-5.85 41.6h16.25l5-35.5 1.5-11.25 4.2 6.3q.7 1.55 3.25 1.8l.05-.1q.25-.4.35-.85l.3-1.05 1.8-14.05v-.05l5.35-37.45h-16.25l-6.15 44.3 5.85 1.15h-9.45q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45m5.4-38.9q.15-1.7-.4-3.15-.5-1.1-1.35-2.1-2.25-2.25-5.5-2.25h-27.75q-2.3 0-4.45 1.35-.65.35-1.3.9-1.3.95-2.05 2.1-.45.6-.7 1.2-.4.9-.5 1.95l-4.15 29.95h-.05l-.7 5.2h-.05l-.2 1.35h.05l-.05.3 5.85 1.15h-9.45q-2.1.05-3.95 1.6-1.9 1.55-2.25 3.55l-.5 3.5h-.05l-1.2 8.75v.1l-4.1 29.25h16.25l5-35.5 1.5-11.25q.3 3.25 2.6 5.6.4.4.9.8 4.75 3.9 16 4.85l1.75-11.25h-12.2l.4-2.95h-.05l.7-5.05h-.05q.15-.9.3-1.9.1-.75.25-1.6.15-1.25.35-2.65v-.05q.95-6.7 2.35-16.5h11.25l-3.1 20.8h16.5l4.1-28.05M345 66.35h-.05l1.15-8.2q.5-3-1.75-5.25-1.25-1.25-3-1.75-1-.5-2.25-.5h-27.95q-.65 0-1.3.1-2.5.35-4.7 2.15-2.75 2.25-3.25 5.25l-1.95 14.7v.05l-.05.3 5.85 1.15h-9.45q-1.9.05-3.6 1.35-.2.1-.35.25-1.9 1.55-2.25 3.55l-4.85 34.1q-.25 3 1.75 5.25 1.25 1.4 3 1.95 1.05.3 2.25.3H320q3.25 0 6-2.25 2.75-2 3.25-5l2.75-18.5h-16.5l-1.75 11H302.5l2.1-14.75h.05l.85-6 1.5-11.2q.35 3.8 3.5 6.35 4.6 3.8 15.4 4.8.3 0 .6.05h15.75L345 66.35m-16.4-.95-1.25 8.95h-11.3l.4-2.95h-.05l.7-5.05h-.1l.15-.95h11.45Z",
},
svgns,
);
const loadingAnimation = createElement(
"svg",
undefined,
"loading-animation",
{
xmlns: svgns,
viewBox: "0 0 66 66",
},
svgns,
);
const spinner = createElement(
"circle",
undefined,
"spinner",
{
fill: "none",
"stroke-width": "6",
"stroke-linecap": "round",
cx: "33",
cy: "33",
r: "30",
},
svgns,
);
const loadbar = createElement("div", undefined, "loadbar");
const loadbarInner = createElement("div", undefined, "loadbar-inner");
// Save manager elements
const saveManager = createElement("div", "save-manager", "modal hidden");
const saveModalArea = createElement("div", "modal-area", "modal-area");
const saveModalClose = createElement("span", undefined, "close-modal");
saveModalClose.textContent = "\u00D7";
const generalSaveOptions = createElement(
"div",
undefined,
"general-save-options",
);
const backupSaves = createElement("span", "backup-saves", "save-option");
const localSaves = createElement("table", "local-saves");
// Video modal elements
const videoModal = createElement("div", "video-modal", "modal hidden");
const videoModalArea = createElement("div", undefined, "modal-area");
const videoModalClose = createElement("span", undefined, "close-modal");
videoModalClose.textContent = "\u00D7";
const videoHolder = createElement("div", "video-holder");
// Context menu overlay elements
const contextMenuOverlay = createElement(
"div",
"context-menu-overlay",
"hidden",
);
const contextMenu = createElement("ul", "context-menu");
appendElement(ruffleShadowTemplate.content, staticStyles);
appendElement(ruffleShadowTemplate.content, dynamicStyles);
appendElement(ruffleShadowTemplate.content, container);
// Play button append
appendElement(container, playButton);
appendElement(playButton, playIcon);
appendElement(playIcon, playSvg);
appendElement(playSvg, playDefs);
appendElement(playDefs, playLinearGradient);
appendElement(playLinearGradient, playStop0);
appendElement(playLinearGradient, playStop100);
appendElement(playDefs, playG);
appendElement(playG, playPath1);
appendElement(playG, playPath2);
appendElement(playSvg, playUse);
// Unmute overlay append
appendElement(container, unmuteOverlay);
appendElement(unmuteOverlay, background);
appendElement(unmuteOverlay, unmuteIcon);
appendElement(unmuteIcon, unmuteSvg);
appendElement(unmuteSvg, unmutePath1);
appendElement(unmuteSvg, unmutePath2);
appendElement(unmuteSvg, unmutePath3);
appendElement(unmuteSvg, unmuteText);
// Virtual keyboard append
appendElement(container, virtualKeyboard);
// Splash screen append
appendElement(ruffleShadowTemplate.content, splashScreen);
appendElement(splashScreen, splashScreenSvg);
appendElement(splashScreenSvg, splashScreenG);
appendElement(splashScreenG, splashScreenPath1);
appendElement(splashScreenG, splashScreenPath2);
appendElement(splashScreen, loadingAnimation);
appendElement(loadingAnimation, spinner);
appendElement(splashScreen, loadbar);
appendElement(loadbar, loadbarInner);
// Save manager append
appendElement(ruffleShadowTemplate.content, saveManager);
appendElement(saveManager, saveModalArea);
appendElement(saveModalArea, saveModalClose);
appendElement(saveModalArea, generalSaveOptions);
appendElement(generalSaveOptions, backupSaves);
appendElement(saveModalArea, localSaves);
// Video modal append
appendElement(ruffleShadowTemplate.content, videoModal);
appendElement(videoModal, videoModalArea);
appendElement(videoModalArea, videoModalClose);
appendElement(videoModalArea, videoHolder);
// Context menu overlay append
appendElement(ruffleShadowTemplate.content, contextMenuOverlay);
appendElement(contextMenuOverlay, contextMenu);