From e8bf1354242b28fd3f4aedc392741c370cd13ec1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 10 Feb 2026 23:03:43 -0700 Subject: [PATCH] Add custom elements --- module/apps/elements/Icon.mjs | 11 ++ module/apps/elements/SVGLoader.mjs | 102 +++++++++++++++++++ module/apps/elements/StyledShadowElement.mjs | 66 ++++++++++++ module/apps/elements/_index.mjs | 22 ++++ module/hooks/init.mjs | 5 +- 5 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 module/apps/elements/Icon.mjs create mode 100644 module/apps/elements/SVGLoader.mjs create mode 100644 module/apps/elements/StyledShadowElement.mjs create mode 100644 module/apps/elements/_index.mjs diff --git a/module/apps/elements/Icon.mjs b/module/apps/elements/Icon.mjs new file mode 100644 index 0000000..e7861c9 --- /dev/null +++ b/module/apps/elements/Icon.mjs @@ -0,0 +1,11 @@ +import { OFTSVGLoader } from "./SVGLoader.mjs"; + +/** +Attributes: +@property {string} name - The name of the icon, takes precedence over the path +@property {string} path - The path of the icon file +*/ +export class OFTIcon extends OFTSVGLoader { + static elementName = `oft-icon`; + static _stylePath = `icon.css`; +}; diff --git a/module/apps/elements/SVGLoader.mjs b/module/apps/elements/SVGLoader.mjs new file mode 100644 index 0000000..323bc42 --- /dev/null +++ b/module/apps/elements/SVGLoader.mjs @@ -0,0 +1,102 @@ +import { filePath } from "../../consts.mjs"; +import { StyledShadowElement } from "./StyledShadowElement.mjs"; + +/** +Attributes: +@property {string} name - The name of the icon, takes precedence over the path +@property {string} path - The path of the icon file +*/ +export class OFTSVGLoader extends StyledShadowElement(HTMLElement) { + static elementName = `oft-svg`; + static formAssociated = false; + + /* Stuff for the mixin to use */ + static _stylePath = `svg-loader.css`; + + + static _cache = new Map(); + #container; + /** @type {null | string} */ + _name; + /** @type {null | string} */ + _path; + + constructor() { + super(); + + this.#container = document.createElement(`div`); + this._shadow.appendChild(this.#container); + }; + + _mounted = false; + async connectedCallback() { + super.connectedCallback(); + if (this._mounted) { return }; + + this._name = this.getAttribute(`name`); + this._path = this.getAttribute(`path`); + + /* + This converts all of the double-dash prefixed properties on the element to + CSS variables so that they don't all need to be provided by doing style="" + */ + for (const attrVar of this.attributes) { + if (attrVar.name?.startsWith(`var:`)) { + const prop = attrVar.name.replace(`var:`, ``); + this.style.setProperty(`--` + prop, attrVar.value); + }; + }; + + /* + Try to retrieve the icon if it isn't present, try the path then default to + the slot content, as then we can have a default per-icon usage + */ + let content; + if (this._name) { + content = await this.#getIcon(filePath(`assets/${this._name}.svg`)); + }; + + if (this._path && !content) { + content = await this.#getIcon(this._path); + }; + + if (content) { + this.#container.appendChild(content.cloneNode(true)); + }; + + this._mounted = true; + }; + + disconnectedCallback() { + super.disconnectedCallback(); + if (!this._mounted) { return }; + + this._mounted = false; + }; + + async #getIcon(path) { + // Cache hit! + if (this.constructor._cache.has(path)) { + return this.constructor._cache.get(path); + }; + + const r = await fetch(path); + switch (r.status) { + case 200: + case 201: + break; + default: return; + }; + + const svg = this.#parseSVG(await r.text()); + this.constructor._cache.set(path, svg); + return svg; + }; + + /** Takes an SVG string and returns it as a DOM node */ + #parseSVG(content) { + const temp = document.createElement(`div`); + temp.innerHTML = content; + return temp.querySelector(`svg`); + }; +}; diff --git a/module/apps/elements/StyledShadowElement.mjs b/module/apps/elements/StyledShadowElement.mjs new file mode 100644 index 0000000..4929868 --- /dev/null +++ b/module/apps/elements/StyledShadowElement.mjs @@ -0,0 +1,66 @@ +import { filePath } from "../../consts.mjs"; + +/** + * @param {HTMLElement} Base + */ +export function StyledShadowElement(Base) { + return class extends Base { + /** + * The path to the CSS that is loaded + * @type {string} + */ + static _stylePath; + + /** + * The stringified CSS to use + * @type {Map} + */ + static _styles = new Map(); + + /** + * The HTML element of the stylesheet + * @type {HTMLStyleElement} + */ + _style; + + /** @type {ShadowRoot} */ + _shadow; + + constructor() { + super(); + + this._shadow = this.attachShadow({ mode: `open` }); + this._style = document.createElement(`style`); + this._shadow.appendChild(this._style); + }; + + #mounted = false; + connectedCallback() { + if (this.#mounted) { return }; + + this._getStyles(); + + this.#mounted = true; + }; + + disconnectedCallback() { + if (!this.#mounted) { return }; + this.#mounted = false; + }; + + _getStyles() { + // TODO: Cache the CSS content in a more sane way that doesn't break + const stylePath = this.constructor._stylePath; + if (this.constructor._styles.has(stylePath)) { + this._style.innerHTML = this.constructor._styles.get(stylePath); + } else { + fetch(filePath(`styles/components/${stylePath}`)) + .then(r => r.text()) + .then(t => { + this.constructor._styles.set(stylePath, t); + this._style.innerHTML = t; + }); + } + }; + }; +}; diff --git a/module/apps/elements/_index.mjs b/module/apps/elements/_index.mjs new file mode 100644 index 0000000..9ec5bcc --- /dev/null +++ b/module/apps/elements/_index.mjs @@ -0,0 +1,22 @@ +import { OFTIcon } from "./Icon.mjs"; +import { OFTSVGLoader } from "./SVGLoader.mjs"; + +const components = [ + OFTSVGLoader, + OFTIcon, +]; + +export function registerCustomComponents() { + (CONFIG.CACHE ??= {}).componentListeners ??= []; + for (const component of components) { + if (!window.customElements.get(component.elementName)) { + window.customElements.define( + component.elementName, + component, + ); + if (component.formAssociated) { + CONFIG.CACHE.componentListeners.push(component.elementName); + } + }; + } +}; diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 729a004..01bc44c 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -1,9 +1,10 @@ -// Settings +// Tweaks import { preventMovementHistory } from "../tweaks/preventMovementHistory.mjs"; import { toggleMouseBroadcast } from "../tweaks/toggleMouseBroadcast.mjs"; // Utils import { Logger } from "../utils/Logger.mjs"; +import { registerCustomComponents } from "../apps/elements/_index.mjs"; /* This is only here for setting that **require** being registered during @@ -14,6 +15,8 @@ where they ideally should be implemented. Hooks.on(`init`, () => { Logger.log(`Initializing`); + registerCustomComponents(); + preventMovementHistory(); toggleMouseBroadcast(); });