Skip to content

Commit

Permalink
Make custom elements resilient to being moved between hosts
Browse files Browse the repository at this point in the history
Co-authored-by: fyorl <kim.mantas@gmail.com>
  • Loading branch information
Posnet and Fyorl authored Mar 1, 2024
1 parent 1420d1f commit af00d7b
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 182 deletions.
5 changes: 3 additions & 2 deletions module/applications/components/_module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import InventoryElement from "./inventory.mjs";
import ItemListControlsElement from "./item-list-controls.mjs";
import ProficiencyCycleElement from "./proficiency-cycle.mjs";
import SlideToggleElement from "./slide-toggle.mjs";
import AdoptedStyleSheetMixin from "./adopted-stylesheet-mixin.mjs";

window.customElements.define("dnd5e-effects", EffectsElement);
window.customElements.define("dnd5e-icon", IconElement);
Expand All @@ -15,6 +16,6 @@ window.customElements.define("proficiency-cycle", ProficiencyCycleElement);
window.customElements.define("slide-toggle", SlideToggleElement);

export {
EffectsElement, IconElement, InventoryElement, ItemListControlsElement, FiligreeBoxElement, ProficiencyCycleElement,
SlideToggleElement
AdoptedStyleSheetMixin, EffectsElement, IconElement, InventoryElement, ItemListControlsElement, FiligreeBoxElement,
ProficiencyCycleElement, SlideToggleElement
};
54 changes: 54 additions & 0 deletions module/applications/components/adopted-stylesheet-mixin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Adds functionality to a custom HTML element for caching its stylesheet and adopting it into its Shadow DOM, rather
* than having each stylesheet duplicated per element.
* @param {typeof HTMLElement} Base The base class being mixed.
* @returns {typeof AdoptedStyleSheetElement}
*/
export default function AdoptedStyleSheetMixin(Base) {
return class AdoptedStyleSheetElement extends Base {
/**
* A map of cached stylesheets per Document root.
* @type {WeakMap<WeakKey<Document>, CSSStyleSheet>}
* @protected
*/
static _stylesheets = new WeakMap();

/**
* The CSS content for this element.
* @type {string}
*/
static CSS = "";

/* -------------------------------------------- */

/** @inheritDoc */
adoptedCallback() {
this._adoptStyleSheet(this._getStyleSheet());
}

/* -------------------------------------------- */

/**
* Retrieves the cached stylesheet, or generates a new one.
* @protected
*/
_getStyleSheet() {
let sheet = this.constructor._stylesheets.get(this.ownerDocument);
if ( !sheet ) {
sheet = new this.ownerDocument.defaultView.CSSStyleSheet();
sheet.replaceSync(this.constructor.CSS);
this.constructor._stylesheets.set(this.ownerDocument, sheet);
}
return sheet;
}

/* -------------------------------------------- */

/**
* Adopt the stylesheet into the Shadow DOM.
* @param {CSSStyleSheet} sheet The sheet to adopt.
* @abstract
*/
_adoptStyleSheet(sheet) {}
}
}
138 changes: 64 additions & 74 deletions module/applications/components/filigree-box.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import AdoptedStyleSheetMixin from "./adopted-stylesheet-mixin.mjs";

/**
* Custom element that adds a filigree border that can be colored.
*/
export default class FiligreeBoxElement extends HTMLElement {
export default class FiligreeBoxElement extends AdoptedStyleSheetMixin(HTMLElement) {
constructor() {
super();
this.#shadowRoot = this.attachShadow({ mode: "closed" });
this.#buildStyle();
this._adoptStyleSheet(this._getStyleSheet());
const backdrop = document.createElement("div");
backdrop.classList.add("backdrop");
this.#shadowRoot.appendChild(backdrop);
Expand All @@ -21,82 +23,55 @@ export default class FiligreeBoxElement extends HTMLElement {
this.#shadowRoot.appendChild(slot);
}

/**
* Shadow root that contains the box shapes.
* @type {ShadowRoot}
*/
#shadowRoot;

/* -------------------------------------------- */
/** @inheritDoc */
static CSS = `
:host {
position: relative;
isolation: isolate;
min-height: 56px;
filter: var(--filigree-drop-shadow, drop-shadow(0 0 12px var(--dnd5e-shadow-15)));
}
.backdrop {
--chamfer: 12px;
position: absolute;
inset: 0;
background: var(--filigree-background-color, var(--dnd5e-color-card));
z-index: -2;
clip-path: polygon(
var(--chamfer) 0,
calc(100% - var(--chamfer)) 0,
100% var(--chamfer),
100% calc(100% - var(--chamfer)),
calc(100% - var(--chamfer)) 100%,
var(--chamfer) 100%,
0 calc(100% - var(--chamfer)),
0 var(--chamfer)
);
}
.filigree {
position: absolute;
fill: var(--filigree-border-color, var(--dnd5e-color-gold));
z-index: -1;
/**
* The stylesheet to attach to the element's shadow root.
* @type {CSSStyleSheet}
* @protected
*/
static _stylesheet;
&.top, &.bottom { height: 30px; }
&.top { top: 0; }
&.bottom { bottom: 0; scale: 1 -1; }
/* -------------------------------------------- */
&.left, &.right { width: 25px; }
&.left { left: 0; }
&.right { right: 0; scale: -1 1; }
/**
* Build the shadow DOM's styles.
*/
#buildStyle() {
if ( !this.constructor._stylesheet ) {
this.constructor._stylesheet = new CSSStyleSheet();
this.constructor._stylesheet.replaceSync(`
:host {
position: relative;
isolation: isolate;
min-height: 56px;
filter: var(--filigree-drop-shadow, drop-shadow(0 0 12px var(--dnd5e-shadow-15)));
}
.backdrop {
--chamfer: 12px;
position: absolute;
inset: 0;
background: var(--filigree-background-color, var(--dnd5e-color-card));
z-index: -2;
clip-path: polygon(
var(--chamfer) 0,
calc(100% - var(--chamfer)) 0,
100% var(--chamfer),
100% calc(100% - var(--chamfer)),
calc(100% - var(--chamfer)) 100%,
var(--chamfer) 100%,
0 calc(100% - var(--chamfer)),
0 var(--chamfer)
);
}
.filigree {
position: absolute;
fill: var(--filigree-border-color, var(--dnd5e-color-gold));
z-index: -1;
&.top, &.bottom { height: 30px; }
&.top { top: 0; }
&.bottom { bottom: 0; scale: 1 -1; }
&.left, &.right { width: 25px; }
&.left { left: 0; }
&.right { right: 0; scale: -1 1; }
&.bottom.right { scale: -1 -1; }
}
.filigree.block {
inline-size: calc(100% - 50px);
inset-inline: 25px;
}
.filigree.inline {
block-size: calc(100% - 60px);
inset-block: 30px;
}
`);
&.bottom.right { scale: -1 -1; }
}
this.#shadowRoot.adoptedStyleSheets = [this.constructor._stylesheet];
}

/* -------------------------------------------- */
.filigree.block {
inline-size: calc(100% - 50px);
inset-inline: 25px;
}
.filigree.inline {
block-size: calc(100% - 60px);
inset-block: 30px;
}
`;

/**
* Path definitions for the various box corners and edges.
Expand All @@ -108,6 +83,21 @@ export default class FiligreeBoxElement extends HTMLElement {
inline: "M 0 10 L 0 0 L 2.99 0 L 2.989 10 L 0 10 Z M 6.9 10 L 6.9 0 L 8.6 0 L 8.6 10 L 6.9 10 Z"
});

/**
* Shadow root that contains the box shapes.
* @type {ShadowRoot}
*/
#shadowRoot;

/* -------------------------------------------- */

/** @inheritDoc */
_adoptStyleSheet(sheet) {
this.#shadowRoot.adoptedStyleSheets = [sheet];
}

/* -------------------------------------------- */

/**
* Build an SVG element.
* @param {string} path SVG path to use.
Expand Down
56 changes: 26 additions & 30 deletions module/applications/components/icon.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import AdoptedStyleSheetMixin from "./adopted-stylesheet-mixin.mjs";

/**
* Custom element for displaying SVG icons that are cached and can be styled.
*/
export default class IconElement extends HTMLElement {
export default class IconElement extends AdoptedStyleSheetMixin(HTMLElement) {
constructor() {
super();
this.#internals = this.attachInternals();
this.#internals.role = "img";
this.#shadowRoot = this.attachShadow({ mode: "closed" });
}

/** @inheritDoc */
static CSS = `
:host {
display: contents;
}
svg {
fill: var(--icon-fill, #000);
width: var(--icon-width, var(--icon-size, 1em));
height: var(--icon-height, var(--icon-size, 1em));
}
`;

/**
* Cached SVG files by SRC.
* @type {Map<string, SVGElement|Promise<SVGElement>>}
*/
static #svgCache = new Map();

/**
* The custom element's form and accessibility internals.
* @type {ElementInternals}
Expand Down Expand Up @@ -37,40 +57,16 @@ export default class IconElement extends HTMLElement {

/* -------------------------------------------- */

/**
* Stylesheet that is shared among all icons.
* @type {CSSStyleSheet}
*/
static #stylesheet;

/* -------------------------------------------- */

/**
* Cached SVG files by SRC.
* @type {Map<string, SVGElement|Promise<SVGElement>>}
*/
static #svgCache = new Map();
/** @inheritDoc */
_adoptStyleSheet(sheet) {
this.#shadowRoot.adoptedStyleSheets = [sheet];
}

/* -------------------------------------------- */

/** @inheritDoc */
connectedCallback() {
// Create icon styles
if ( !this.constructor.#stylesheet ) {
this.constructor.#stylesheet = new CSSStyleSheet();
this.constructor.#stylesheet.replaceSync(`
:host {
display: contents;
}
svg {
fill: var(--icon-fill, #000);
width: var(--icon-width, var(--icon-size, 1em));
height: var(--icon-height, var(--icon-size, 1em));
}
`);
}
this.#shadowRoot.adoptedStyleSheets = [this.constructor.#stylesheet];

this._adoptStyleSheet(this._getStyleSheet());
const insertElement = element => {
if ( !element ) return;
const clone = element.cloneNode(true);
Expand Down
Loading

0 comments on commit af00d7b

Please sign in to comment.