@vaadin/vaadin-themable-mixin
Advanced tools
Comparing version 24.4.0-alpha2 to 24.4.0-alpha20
{ | ||
"name": "@vaadin/vaadin-themable-mixin", | ||
"version": "24.4.0-alpha2", | ||
"version": "24.4.0-alpha20", | ||
"publishConfig": { | ||
@@ -43,3 +43,3 @@ "access": "public" | ||
}, | ||
"gitHead": "f303ead58d27e15d81a55db0559611fb77c0e421" | ||
"gitHead": "9d2eacc494eb27658ba9298be6656815912637be" | ||
} |
@@ -8,3 +8,2 @@ # vaadin-themable-mixin | ||
[![npm version](https://badgen.net/npm/v/@vaadin/vaadin-themable-mixin)](https://www.npmjs.com/package/@vaadin/vaadin-themable-mixin) | ||
[![Discord](https://img.shields.io/discord/732335336448852018?label=discord)](https://discord.gg/PHmkCKC) | ||
@@ -11,0 +10,0 @@ ## License |
@@ -6,3 +6,3 @@ /** | ||
*/ | ||
import { css, CSSResult, unsafeCSS } from 'lit'; | ||
import { adoptStyles, css, CSSResult, LitElement, unsafeCSS } from 'lit'; | ||
import { ThemePropertyMixin } from './vaadin-theme-property-mixin.js'; | ||
@@ -28,2 +28,12 @@ | ||
/** | ||
* @type {WeakRef<HTMLElement>[]} | ||
*/ | ||
const themableInstances = new Set(); | ||
/** | ||
* @type {string[]} | ||
*/ | ||
const themableTagNames = new Set(); | ||
/** | ||
* Check if the custom element type has themes applied. | ||
@@ -63,2 +73,125 @@ * @param {Function} elementClass | ||
/** | ||
* Returns true if the themeFor string matches the tag name | ||
* @param {string} themeFor | ||
* @param {string} tagName | ||
* @returns {boolean} | ||
*/ | ||
function matchesThemeFor(themeFor, tagName) { | ||
return (themeFor || '').split(' ').some((themeForToken) => { | ||
return new RegExp(`^${themeForToken.split('*').join('.*')}$`, 'u').test(tagName); | ||
}); | ||
} | ||
/** | ||
* Returns the CSS text content from an array of CSSResults | ||
* @param {CSSResult[]} styles | ||
* @returns {string} | ||
*/ | ||
function getCssText(styles) { | ||
return styles.map((style) => style.cssText).join('\n'); | ||
} | ||
const STYLE_ID = 'vaadin-themable-mixin-style'; | ||
/** | ||
* Includes the styles to the template. | ||
* @param {CSSResult[]} styles | ||
* @param {HTMLTemplateElement} template | ||
*/ | ||
function addStylesToTemplate(styles, template) { | ||
const styleEl = document.createElement('style'); | ||
styleEl.id = STYLE_ID; | ||
styleEl.textContent = getCssText(styles); | ||
template.content.appendChild(styleEl); | ||
} | ||
/** | ||
* Dynamically updates the styles of the given component instance. | ||
* @param {HTMLElement} instance | ||
*/ | ||
function updateInstanceStyles(instance) { | ||
if (!instance.shadowRoot) { | ||
return; | ||
} | ||
const componentClass = instance.constructor; | ||
if (instance instanceof LitElement) { | ||
// LitElement | ||
// The adoptStyles function may fall back to appending style elements to shadow root. | ||
// Remove them first to avoid duplicates. | ||
[...instance.shadowRoot.querySelectorAll('style')].forEach((style) => style.remove()); | ||
// Adopt the updated styles | ||
adoptStyles(instance.shadowRoot, componentClass.elementStyles); | ||
} else { | ||
// PolymerElement | ||
// Update style element content in the shadow root | ||
const style = instance.shadowRoot.getElementById(STYLE_ID); | ||
const template = componentClass.prototype._template; | ||
style.textContent = template.content.getElementById(STYLE_ID).textContent; | ||
} | ||
} | ||
/** | ||
* Dynamically updates the styles of the instances matching the given component type. | ||
* @param {Function} componentClass | ||
*/ | ||
function updateInstanceStylesOfType(componentClass) { | ||
// Iterate over component instances and update their styles if needed | ||
themableInstances.forEach((ref) => { | ||
const instance = ref.deref(); | ||
if (instance instanceof componentClass) { | ||
updateInstanceStyles(instance); | ||
} else if (!instance) { | ||
// Clean up the weak reference to a GC'd instance | ||
themableInstances.delete(ref); | ||
} | ||
}); | ||
} | ||
/** | ||
* Dynamically updates the styles of the given component type. | ||
* @param {Function} componentClass | ||
*/ | ||
function updateComponentStyles(componentClass) { | ||
if (componentClass.prototype instanceof LitElement) { | ||
// Update LitElement-based component's elementStyles | ||
componentClass.elementStyles = componentClass.finalizeStyles(componentClass.styles); | ||
} else { | ||
// Update Polymer-based component's template | ||
const template = componentClass.prototype._template; | ||
template.content.getElementById(STYLE_ID).textContent = getCssText(componentClass.getStylesForThis()); | ||
} | ||
// Update the styles of inheriting types | ||
themableTagNames.forEach((inheritingTagName) => { | ||
const inheritingClass = customElements.get(inheritingTagName); | ||
if (inheritingClass !== componentClass && inheritingClass.prototype instanceof componentClass) { | ||
updateComponentStyles(inheritingClass); | ||
} | ||
}); | ||
} | ||
/** | ||
* Check if the component type already has a style matching the given styles. | ||
* | ||
* @param {Function} componentClass | ||
* @param {CSSResultGroup} styles | ||
* @returns {boolean} | ||
*/ | ||
function hasMatchingStyle(componentClass, styles) { | ||
const themes = componentClass.__themes; | ||
if (!themes || !styles) { | ||
return false; | ||
} | ||
return themes.some((theme) => | ||
theme.styles.some((themeStyle) => styles.some((style) => style.cssText === themeStyle.cssText)), | ||
); | ||
} | ||
/** | ||
* Registers CSS styles for a component type. Make sure to register the styles before | ||
@@ -74,11 +207,2 @@ * the first instance of a component of the type is attached to DOM. | ||
export function registerStyles(themeFor, styles, options = {}) { | ||
if (themeFor) { | ||
if (hasThemes(themeFor)) { | ||
console.warn(`The custom element definition for "${themeFor}" | ||
was finalized before a style module was registered. | ||
Make sure to add component specific style modules before | ||
importing the corresponding custom element.`); | ||
} | ||
} | ||
styles = flattenStyles(styles); | ||
@@ -96,2 +220,30 @@ | ||
} | ||
if (themeFor) { | ||
// Update styles of the component types that match themeFor and have already been finalized | ||
themableTagNames.forEach((tagName) => { | ||
if (matchesThemeFor(themeFor, tagName) && hasThemes(tagName)) { | ||
const componentClass = customElements.get(tagName); | ||
if (hasMatchingStyle(componentClass, styles)) { | ||
// Show a warning if the component type already has some of the given styles | ||
console.warn(`Registering styles that already exist for ${tagName}`); | ||
} else if (!window.Vaadin || !window.Vaadin.suppressPostFinalizeStylesWarning) { | ||
// Show a warning if the component type has already been finalized | ||
console.warn( | ||
`The custom element definition for "${tagName}" ` + | ||
`was finalized before a style module was registered. ` + | ||
`Ideally, import component specific style modules before ` + | ||
`importing the corresponding custom element. ` + | ||
`This warning can be suppressed by setting "window.Vaadin.suppressPostFinalizeStylesWarning = true".`, | ||
); | ||
} | ||
// Update the styles of the component type | ||
updateComponentStyles(componentClass); | ||
// Update the styles of the component instances matching the component type | ||
updateInstanceStylesOfType(componentClass); | ||
} | ||
}); | ||
} | ||
} | ||
@@ -112,14 +264,2 @@ | ||
/** | ||
* Returns true if the themeFor string matches the tag name | ||
* @param {string} themeFor | ||
* @param {string} tagName | ||
* @returns {boolean} | ||
*/ | ||
function matchesThemeFor(themeFor, tagName) { | ||
return (themeFor || '').split(' ').some((themeForToken) => { | ||
return new RegExp(`^${themeForToken.split('*').join('.*')}$`, 'u').test(tagName); | ||
}); | ||
} | ||
/** | ||
* Maps the moduleName to an include priority number which is used for | ||
@@ -161,13 +301,2 @@ * determining the order in which styles are applied. | ||
/** | ||
* Includes the styles to the template. | ||
* @param {CSSResult[]} styles | ||
* @param {HTMLTemplateElement} template | ||
*/ | ||
function addStylesToTemplate(styles, template) { | ||
const styleEl = document.createElement('style'); | ||
styleEl.innerHTML = styles.map((style) => style.cssText).join('\n'); | ||
template.content.appendChild(styleEl); | ||
} | ||
/** | ||
* Returns an array of themes that should be used for styling a component matching | ||
@@ -207,2 +336,8 @@ * the tag name. The array is sorted by the include order. | ||
class VaadinThemableMixin extends ThemePropertyMixin(superClass) { | ||
constructor() { | ||
super(); | ||
// Store a weak reference to the instance | ||
themableInstances.add(new WeakRef(this)); | ||
} | ||
/** | ||
@@ -215,2 +350,6 @@ * Covers PolymerElement based component styling | ||
if (this.is) { | ||
themableTagNames.add(this.is); | ||
} | ||
// Make sure not to run the logic intended for PolymerElement when LitElement is used. | ||
@@ -239,3 +378,3 @@ if (this.elementStyles) { | ||
const themeStyles = this.getStylesForThis(); | ||
return styles ? [...super.finalizeStyles(styles), ...themeStyles] : themeStyles; | ||
return styles ? [...[styles].flat(Infinity), ...themeStyles] : themeStyles; | ||
} | ||
@@ -249,5 +388,6 @@ | ||
static getStylesForThis() { | ||
const superClassThemes = superClass.__themes || []; | ||
const parent = Object.getPrototypeOf(this.prototype); | ||
const inheritedThemes = (parent ? parent.constructor.__themes : []) || []; | ||
this.__themes = [...inheritedThemes, ...getThemes(this.is)]; | ||
this.__themes = [...superClassThemes, ...inheritedThemes, ...getThemes(this.is)]; | ||
const themeStyles = this.__themes.flatMap((theme) => theme.styles); | ||
@@ -254,0 +394,0 @@ // Remove duplicates |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
29477
484
12