Rename the elements folder to components
This commit is contained in:
parent
e489da9666
commit
e49fa03fed
6 changed files with 1 additions and 1 deletions
125
module/Apps/components/Icon.mjs
Normal file
125
module/Apps/components/Icon.mjs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { Logger } from "../../utils/Logger.mjs";
|
||||
import { StyledShadowElement } from "./mixins/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 RipCryptIcon extends StyledShadowElement(HTMLElement) {
|
||||
static elementName = `rc-icon`;
|
||||
static formAssociated = false;
|
||||
|
||||
/* Stuff for the mixin to use */
|
||||
static _stylePath = `css/components/icon.css`;
|
||||
|
||||
|
||||
static _cache = new Map();
|
||||
#container;
|
||||
/** @type {null | string} */
|
||||
_name;
|
||||
/** @type {null | string} */
|
||||
_path;
|
||||
|
||||
/* Stored IDs for all of the hooks that are in this component */
|
||||
#svgHmr;
|
||||
|
||||
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(`./systems/${game.system.id}/assets/${this._name}.svg`);
|
||||
};
|
||||
|
||||
if (this._path && !content) {
|
||||
content = await this.#getIcon(this._path);
|
||||
};
|
||||
|
||||
if (content) {
|
||||
this.#container.appendChild(content.cloneNode(true));
|
||||
};
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
if (game.settings.get(`ripcrypt`, `devMode`)) {
|
||||
this.#svgHmr = Hooks.on(`${game.system.id}-hmr:svg`, (iconName, data) => {
|
||||
if (this._name === iconName || this._path?.endsWith(data.path)) {
|
||||
const svg = this.#parseSVG(data.content);
|
||||
this.constructor._cache.set(iconName, svg);
|
||||
this.#container.replaceChildren(svg.cloneNode(true));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
this._mounted = true;
|
||||
};
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (!this._mounted) { return };
|
||||
|
||||
Hooks.off(`${game.system.id}-hmr:svg`, this.#svgHmr);
|
||||
|
||||
this._mounted = false;
|
||||
};
|
||||
|
||||
async #getIcon(path) {
|
||||
// Cache hit!
|
||||
if (this.constructor._cache.has(path)) {
|
||||
Logger.debug(`Image ${path} cache hit`);
|
||||
return this.constructor._cache.get(path);
|
||||
};
|
||||
|
||||
const r = await fetch(path);
|
||||
switch (r.status) {
|
||||
case 200:
|
||||
case 201:
|
||||
break;
|
||||
default:
|
||||
Logger.error(`Failed to fetch icon: ${path}`);
|
||||
return;
|
||||
};
|
||||
|
||||
Logger.debug(`Adding image ${path} to the cache`);
|
||||
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`);
|
||||
};
|
||||
};
|
||||
55
module/Apps/components/RipCryptBorder.mjs
Normal file
55
module/Apps/components/RipCryptBorder.mjs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { StyledShadowElement } from "./mixins/StyledShadowElement.mjs";
|
||||
|
||||
/**
|
||||
Attributes:
|
||||
*/
|
||||
export class RipCryptBorder extends StyledShadowElement(HTMLElement) {
|
||||
static elementName = `rc-border`;
|
||||
static formAssociated = false;
|
||||
|
||||
/* Stuff for the mixin to use */
|
||||
static _stylePath = `css/components/rc-border.css`;
|
||||
#container;
|
||||
|
||||
_mounted = false;
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._mounted) { return };
|
||||
|
||||
/*
|
||||
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);
|
||||
};
|
||||
};
|
||||
|
||||
this.#container = document.createElement(`div`);
|
||||
this.#container.classList = `rc-border`;
|
||||
|
||||
const titleContainer = document.createElement(`div`);
|
||||
titleContainer.classList = `title`;
|
||||
const titleSlot = document.createElement(`slot`);
|
||||
titleSlot.innerHTML = `No Title`;
|
||||
titleSlot.name = `title`;
|
||||
titleContainer.appendChild(titleSlot.cloneNode(true));
|
||||
this.#container.appendChild(titleContainer.cloneNode(true));
|
||||
|
||||
const contentSlot = document.createElement(`slot`);
|
||||
contentSlot.name = `content`;
|
||||
this.#container.appendChild(contentSlot.cloneNode(true));
|
||||
|
||||
this._shadow.appendChild(this.#container);
|
||||
|
||||
this._mounted = true;
|
||||
};
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (!this._mounted) { return };
|
||||
this._mounted = false;
|
||||
};
|
||||
};
|
||||
26
module/Apps/components/_index.mjs
Normal file
26
module/Apps/components/_index.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Logger } from "../../utils/Logger.mjs";
|
||||
import { RipCryptBorder } from "./RipCryptBorder.mjs";
|
||||
import { RipCryptIcon } from "./Icon.mjs";
|
||||
import { RipCryptSVGLoader } from "./svgLoader.mjs";
|
||||
|
||||
const components = [
|
||||
RipCryptIcon,
|
||||
RipCryptSVGLoader,
|
||||
RipCryptBorder,
|
||||
];
|
||||
|
||||
export function registerCustomComponents() {
|
||||
(CONFIG.CACHE ??= {}).componentListeners ??= [];
|
||||
for (const component of components) {
|
||||
if (!window.customElements.get(component.elementName)) {
|
||||
Logger.debug(`Registering component "${component.elementName}"`);
|
||||
window.customElements.define(
|
||||
component.elementName,
|
||||
component,
|
||||
);
|
||||
if (component.formAssociated) {
|
||||
CONFIG.CACHE.componentListeners.push(component.elementName);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
64
module/Apps/components/mixins/StyledShadowElement.mjs
Normal file
64
module/Apps/components/mixins/StyledShadowElement.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* @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<string, string>}
|
||||
*/
|
||||
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(`./systems/${game.system.id}/templates/${stylePath}`)
|
||||
.then(r => r.text())
|
||||
.then(t => {
|
||||
this.constructor._styles.set(stylePath, t);
|
||||
this._style.innerHTML = t;
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
11
module/Apps/components/svgLoader.mjs
Normal file
11
module/Apps/components/svgLoader.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { RipCryptIcon } from "./Icon.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 RipCryptSVGLoader extends RipCryptIcon {
|
||||
static elementName = `rc-svg`;
|
||||
static _stylePath = `css/components/svg-loader.css`;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue