Make a mixin for loading a stylesheet for components, including HMR auto-update support (only in devMode)
This commit is contained in:
parent
fde3881653
commit
f2c169c077
4 changed files with 121 additions and 55 deletions
|
|
@ -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`);
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
80
module/components/mixins/Styles.mjs
Normal file
80
module/components/mixins/Styles.mjs
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue