Make a mixin for loading a stylesheet for components, including HMR auto-update support (only in devMode)

This commit is contained in:
Oliver-Akins 2024-04-13 12:18:36 -06:00
parent fde3881653
commit f2c169c077
4 changed files with 121 additions and 55 deletions

View file

@ -1,34 +1,39 @@
import { StyledShadowElement } from "./mixins/Styles.mjs";
/** /**
Attributes: Attributes:
@property {string} name - The name of the icon, takes precedence over the path @property {string} name - The name of the icon, takes precedence over the path
@property {string} path - The path of the icon file @property {string} path - The path of the icon file
*/ */
export class DotDungeonIcon extends HTMLElement { export class DotDungeonIcon extends StyledShadowElement(HTMLElement) {
static elementName = `dd-icon`; static elementName = `dd-icon`;
static formAssociated = false; static formAssociated = false;
#shadow; /* Stuff for the mixin to use */
static _stylePath = `v3/components/icon.css`;
static #styles = ``;
static _cache = new Map(); static _cache = new Map();
#style;
#container; #container;
/** @type {null | string} */ /** @type {null | string} */
_name; _name;
/** @type {null | string} */ /** @type {null | string} */
_path; _path;
/* Stored IDs for all of the hooks that are in this component */
#svgHmr;
constructor() { constructor() {
super(); super();
this.#shadow = this.attachShadow({ mode: `open`, delegatesFocus: true }); // this._shadow = this.attachShadow({ mode: `open`, delegatesFocus: true });
if (DotDungeonIcon.#styles) this.#embedStyles();
this.#container = document.createElement(`div`); this.#container = document.createElement(`div`);
this.#shadow.appendChild(this.#container); this._shadow.appendChild(this.#container);
}; };
_mounted = false; _mounted = false;
async connectedCallback() { async connectedCallback() {
super.connectedCallback();
if (this._mounted) return; if (this._mounted) return;
this._name = this.getAttribute(`name`); this._name = this.getAttribute(`name`);
@ -45,18 +50,6 @@ export class DotDungeonIcon extends HTMLElement {
}; };
}; };
/*
Style fetching if we haven't gotten them yet
*/
if (!DotDungeonIcon.#styles) {
fetch(`./systems/dotdungeon/.styles/v3/components/icon.css`)
.then(r => r.text())
.then(t => {
DotDungeonIcon.#styles = t;
this.#embedStyles();
});
};
/* /*
Try to retrieve the icon if it isn't present, try the path then default to 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 the slot content, as then we can have a default per-icon usage
@ -78,28 +71,33 @@ export class DotDungeonIcon extends HTMLElement {
This is so that when we get an HMR event from Foundry we can appropriately This is so that when we get an HMR event from Foundry we can appropriately
handle it using our logic to update the component and the icon cache. handle it using our logic to update the component and the icon cache.
*/ */
Hooks.on(`dd-hmr:svg`, (iconName, data) => { if (game.settings.get(`dotdungeon`, `devMode`)) {
this.#svgHmr = Hooks.on(`dd-hmr:svg`, (iconName, data) => {
if (this._name === iconName || this._path?.endsWith(data.path)) { if (this._name === iconName || this._path?.endsWith(data.path)) {
const svg = this.#parseSVG(data.content); const svg = this.#parseSVG(data.content);
DotDungeonIcon._cache.set(iconName, svg); this.constructor._cache.set(iconName, svg);
this.#container.replaceChildren(svg.cloneNode(true)); this.#container.replaceChildren(svg.cloneNode(true));
}; };
}); });
};
this._mounted = true; this._mounted = true;
}; };
#embedStyles() { disconnectedCallback() {
this.#style = document.createElement(`style`); super.disconnectedCallback();
this.#style.innerHTML = DotDungeonIcon.#styles; if (!this._mounted) return;
this.#shadow.appendChild(this.#style);
Hooks.off(`dd-hmr:svg`, this.#svgHmr);
this._mounted = false;
}; };
async #getIcon(path) { async #getIcon(path) {
// Cache hit! // Cache hit!
if (DotDungeonIcon._cache.has(path)) { if (this.constructor._cache.has(path)) {
console.debug(`.dungeon | Icon ${path} cache hit`); console.debug(`.dungeon | Icon ${path} cache hit`);
return DotDungeonIcon._cache.get(path); return this.constructor._cache.get(path);
}; };
const r = await fetch(path); const r = await fetch(path);
@ -114,7 +112,7 @@ export class DotDungeonIcon extends HTMLElement {
console.debug(`.dungeon | Adding icon ${path} to the cache`); console.debug(`.dungeon | Adding icon ${path} to the cache`);
const svg = this.#parseSVG(await r.text()); const svg = this.#parseSVG(await r.text());
DotDungeonIcon._cache.set(path, svg); this.constructor._cache.set(path, svg);
return svg; return svg;
}; };
@ -123,5 +121,5 @@ export class DotDungeonIcon extends HTMLElement {
const temp = document.createElement(`div`); const temp = document.createElement(`div`);
temp.innerHTML = content; temp.innerHTML = content;
return temp.querySelector(`svg`); return temp.querySelector(`svg`);
} };
}; };

View file

@ -1,4 +1,5 @@
import { DotDungeonIcon } from "./icon.mjs"; import { DotDungeonIcon } from "./icon.mjs";
import { StyledShadowElement } from "./mixins/Styles.mjs";
/** /**
Attributes: Attributes:
@ -13,15 +14,14 @@ Styling:
- `--height`: Controls the height of the element + the width of the buttons (default: 1.25rem) - `--height`: Controls the height of the element + the width of the buttons (default: 1.25rem)
- `--width`: Controls the width of the number input (default 50px) - `--width`: Controls the width of the number input (default 50px)
*/ */
export class DotDungeonIncrementer extends HTMLElement { export class DotDungeonIncrementer extends StyledShadowElement(HTMLElement) {
static elementName = `dd-incrementer`; static elementName = `dd-incrementer`;
static formAssociated = true; static formAssociated = true;
static #styles = ``; static _stylePath = `v3/components/incrementer.css`;
_internals; _internals;
_shadow;
#input; #input;
#style;
_min; _min;
_max; _max;
@ -30,8 +30,6 @@ export class DotDungeonIncrementer extends HTMLElement {
constructor() { constructor() {
super(); super();
this._shadow = this.attachShadow({ mode: `open`, delegatesFocus: true });
if (DotDungeonIncrementer.#styles) this.#embedStyles();
// Form internals // Form internals
this._internals = this.attachInternals(); this._internals = this.attachInternals();
@ -61,6 +59,7 @@ export class DotDungeonIncrementer extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
super.connectedCallback();
this.replaceChildren(); this.replaceChildren();
// Attribute parsing / registration // Attribute parsing / registration
@ -119,21 +118,6 @@ export class DotDungeonIncrementer extends HTMLElement {
this.style.setProperty(`--` + prop, attrVar.value); this.style.setProperty(`--` + prop, attrVar.value);
}; };
}; };
if (!DotDungeonIncrementer.#styles) {
fetch(`./systems/dotdungeon/.styles/v3/components/incrementer.css`)
.then(r => r.text())
.then(t => {
DotDungeonIncrementer.#styles = t;
this.#embedStyles();
});
};
};
#embedStyles() {
this.#style = document.createElement(`style`);
this.#style.innerHTML = DotDungeonIncrementer.#styles;
this._shadow.appendChild(this.#style);
}; };
#updateValue() { #updateValue() {

View file

@ -0,0 +1,80 @@
/**
* @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 {string}
*/
static _styles;
/**
* The HTML element of the stylesheet
* @type {HTMLStyleElement}
*/
_style;
/** @type {ShadowRoot} */
_shadow;
/**
* The hook ID for this element's CSS hot reload
* @type {number}
*/
#cssHmr;
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();
if (game.settings.get(`dotdungeon`, `devMode`)) {
this.#cssHmr = Hooks.on(`dd-hmr:css`, (data) => {
if (data.path.endsWith(this.constructor._stylePath)) {
this._style.innerHTML = data.content;
};
});
};
this.#mounted = true;
};
disconnectedCallback() {
if (!this.#mounted) return;
if (this.#cssHmr != null) {
Hooks.off(`dd-hmr:css`, this.#cssHmr);
this.#cssHmr = null;
};
this.#mounted = false;
};
_getStyles() {
if (this.constructor._styles) {
this._style.innerHTML = this.constructor._styles;
} else {
fetch(`./systems/dotdungeon/.styles/${this.constructor._stylePath}`)
.then(r => r.text())
.then(t => {
this.constructor._styles = t;
this._style.innerHTML = t;
});
}
};
};
};

View file

@ -35,6 +35,10 @@ const loaders = {
}, },
js() {window.location.reload()}, js() {window.location.reload()},
mjs() {window.location.reload()}, mjs() {window.location.reload()},
css(data) {
console.debug(`.dungeon | Hot-reloading CSS: ${data.path}`);
Hooks.call(`dd-hmr:css`, data);
},
}; };
Hooks.on(`hotReload`, async (data) => { Hooks.on(`hotReload`, async (data) => {