@lithiumjs/ngx-material-theming
Version:
Dynamic and customizable Angular Material theming made easy.
227 lines (221 loc) • 10.8 kB
JavaScript
import chroma from 'chroma-js';
import darkThemeTemplate from '@lithiumjs/ngx-material-theming-templates/standard-light-theme.json';
// @dynamic
class ThemeLoader {
static { this.THEME_NAME_PARSER = /\[li-theme="([^\]\s]+)"\]/; }
/**
* @description Loads the given compiled theme in for use.
* @param compiledThemeData The pre-compiled theme data.
* @param labelElement [Optional] If `true`, will attach a `data-theme-name` attribute to the `<style>` element set to the theme's name.
*/
static loadCompiled(compiledThemeData, labelElement) {
const element = document.createElement("style");
element.type = "text/css";
if (labelElement === undefined ? true : labelElement) {
const nameParse = this.THEME_NAME_PARSER.exec(compiledThemeData);
if (nameParse && nameParse.length > 1) {
element.setAttribute("data-theme-name", "" + nameParse?.[1]);
}
}
element.innerHTML = compiledThemeData;
this.getHeadElement().appendChild(element);
return element;
}
/**
* @description Unloads the given compiled theme from the DOM.
* @param name The name of the theme to unload.
*/
static unloadCompiled(name) {
const head = this.getHeadElement();
head.querySelectorAll(`style[data-theme-name=${name}]`)
.forEach(style => head.removeChild(style));
}
static getHeadElement() {
const heads = document.getElementsByTagName("head");
if (!heads || heads.length === 0) {
const head = document.createElement("head");
document.appendChild(head);
return head;
}
else {
return heads[0];
}
}
}
var ThemeTemplateOptions;
(function (ThemeTemplateOptions) {
ThemeTemplateOptions.defaultValues = {
nameMatcher: /%theme-name%/g,
primaryHexMatcher: /#a099(\d{2})/g,
primaryRgbaMatcher: /rgba\s?\(\s?160\s?,\s?153\s?,\s?(\d+)\s?,\s?([\d.]+)\s?\)/g,
accentHexMatcher: /#b099(\d{2})/g,
accentRgbaMatcher: /rgba\s?\(\s?176\s?,\s?153\s?,\s?(\d+)\s?,\s?([\d.]+)\s?\)/g,
warnHexMatcher: /#c099(\d{2})/g,
warnRgbaMatcher: /rgba\s?\(\s?192\s?,\s?153\s?,\s?(\d+)\s?,\s?([\d.]+)\s?\)/g,
primaryContrastHexMatcher: /#ac99(\d{2})/g,
primaryContrastRgbaMatcher: /rgba\s?\(\s?172\s?,\s?153\s?,\s?(\d+)\s?,\s?([\d.]+)\s?\)/g,
accentContrastHexMatcher: /#bc99(\d{2})/g,
accentContrastRgbaMatcher: /rgba\s?\(\s?188\s?,\s?153\s?,\s?(\d+)\s?,\s?([\d.]+)\s?\)/g,
warnContrastHexMatcher: /#cc99(\d{2})/g,
warnContrastRgbaMatcher: /rgba\s?\(\s?204\s?,\s?153\s?,\s?(\d+)\s?,\s?([\d.]+)\s?\)/g
};
})(ThemeTemplateOptions || (ThemeTemplateOptions = {}));
class ThemeCreator {
/**
* @description Creates a new theme from the given template theme CSS and theme palettes and loads it for use with the given name.
* @param options The options to use to create the new theme.
* @return The compiled theme CSS.
*/
static createFromTemplate(options) {
// Generate the theme data from the given template data:
let themeData = options.templateData;
let templateOptions = Object.assign({}, ThemeTemplateOptions.defaultValues, options.templateOptions || {});
themeData = themeData.replace(templateOptions.nameMatcher, options.name);
themeData = this.substituteThemeHex(themeData, templateOptions.primaryHexMatcher, options.primaryPalette);
themeData = this.substituteThemeHex(themeData, templateOptions.accentHexMatcher, options.accentPalette);
themeData = this.substituteThemeHex(themeData, templateOptions.warnHexMatcher, options.warnPalette);
themeData = this.substituteThemeRgba(themeData, templateOptions.primaryRgbaMatcher, options.primaryPalette);
themeData = this.substituteThemeRgba(themeData, templateOptions.accentRgbaMatcher, options.accentPalette);
themeData = this.substituteThemeRgba(themeData, templateOptions.warnRgbaMatcher, options.warnPalette);
themeData = this.substituteThemeHex(themeData, templateOptions.primaryContrastHexMatcher, options.primaryPalette.contrast);
themeData = this.substituteThemeHex(themeData, templateOptions.accentContrastHexMatcher, options.accentPalette.contrast);
themeData = this.substituteThemeHex(themeData, templateOptions.warnContrastHexMatcher, options.warnPalette.contrast);
themeData = this.substituteThemeRgba(themeData, templateOptions.primaryContrastRgbaMatcher, options.primaryPalette.contrast);
themeData = this.substituteThemeRgba(themeData, templateOptions.accentContrastRgbaMatcher, options.accentPalette.contrast);
themeData = this.substituteThemeRgba(themeData, templateOptions.warnContrastRgbaMatcher, options.warnPalette.contrast);
const themeElement = ThemeLoader.loadCompiled(themeData);
// Store the color info for external programmatic use
themeElement.setAttribute("data-color-primary", options.primaryPalette[500]);
themeElement.setAttribute("data-color-accent", options.accentPalette[500]);
themeElement.setAttribute("data-color-warn", options.warnPalette[500]);
return themeData;
}
static substituteThemeHex(themeData, matcher, palette) {
return themeData.replace(matcher, (_match, $1) => palette[this.hexOffset($1)]);
}
static substituteThemeRgba(themeData, matcher, palette) {
return themeData.replace(matcher, (_match, $1, $2) => {
return `rgba(${palette[this.rgbaOffset($1)]},${this.rgbaAlpha($2)})`;
});
}
/** @description Compute the palette offset from an associated hex color regex match. */
static hexOffset(match) {
match = match.toLowerCase();
return String(Number.parseInt(`${match}${match.startsWith("a") ? "00" : "0"}`));
}
/** @description Compute the palette offset from an associated rgba color regex match. */
static rgbaOffset(match) {
const decOffset = Number.parseInt(match);
return this.hexOffset(decOffset.toString(16));
}
/** @description Compute the alpha channel from an associated rgba alpha regex match. */
static rgbaAlpha(match) {
return Number.parseFloat(match);
}
}
class ThemeGenerator {
/**
* @see ThemeCreator.createFromTemplate
*/
static createFromTemplate(options) {
return ThemeCreator.createFromTemplate(options);
}
/**
* @description Creates a new theme from the given theme palettes and loads it for use with the given name.
* @param options The options to use to create the new theme.
* @return The compiled theme.
*/
static create(options) {
return this.createFromTemplate({
templateData: this.loadStandardThemeTemplate(!!options.isDark),
...options
});
}
/**
* @description Computes a Material color palette for the given `baseColor`.
* @param baseColor The color that the palette will be based off of.
* @requires chroma-js
*/
static createPalette(baseColor) {
const basePalette = {
"50": this.paletteColor(baseColor, "50"),
"100": this.paletteColor(baseColor, "100"),
"200": this.paletteColor(baseColor, "200"),
"300": this.paletteColor(baseColor, "300"),
"400": this.paletteColor(baseColor, "400"),
"500": this.paletteColor(baseColor, "500"),
"600": this.paletteColor(baseColor, "600"),
"700": this.paletteColor(baseColor, "700"),
"800": this.paletteColor(baseColor, "800"),
"900": this.paletteColor(baseColor, "900"),
"A100": this.paletteColor(baseColor, "A100"),
"A200": this.paletteColor(baseColor, "A200"),
"A400": this.paletteColor(baseColor, "A400"),
"A700": this.paletteColor(baseColor, "A700"),
};
return Object.assign(basePalette, {
contrast: {
"50": this.contrastColor(basePalette["50"]),
"100": this.contrastColor(basePalette["100"]),
"200": this.contrastColor(basePalette["200"]),
"300": this.contrastColor(basePalette["300"]),
"400": this.contrastColor(basePalette["400"]),
"500": this.contrastColor(basePalette["500"]),
"600": this.contrastColor(basePalette["600"]),
"700": this.contrastColor(basePalette["700"]),
"800": this.contrastColor(basePalette["800"]),
"900": this.contrastColor(basePalette["900"]),
"A100": this.contrastColor(basePalette["A100"]),
"A200": this.contrastColor(basePalette["A200"]),
"A400": this.contrastColor(basePalette["A400"]),
"A700": this.contrastColor(basePalette["A700"]),
}
});
}
/**
* @description Computes a color for a Material theme palette using `baseColor` and the given `paletteOffset`.
* @param baseColor The base color.
* @param paletteOffset The offset in the palette.
* @return The new color to be used for the given palette offset.
* @requires chroma-js
*/
static paletteColor(baseColor, paletteOffset) {
const graduation = 0.3;
const aColor = paletteOffset.startsWith("A");
const numericOffset = Number.parseInt(aColor ? paletteOffset.slice(1) : paletteOffset);
const offset = (numericOffset < 500) ? (500 - numericOffset) : (numericOffset - 500);
const graduatedOffset = 0.2 + (offset * 0.01 * graduation);
// Darken or lighten the base color based on the palette offset number
if (numericOffset < 500) {
return chroma(baseColor).brighten(graduatedOffset ** 2).hex();
}
else if (numericOffset > 500) {
return chroma(baseColor).darken(graduatedOffset ** 2).hex();
}
else {
return baseColor;
}
}
/**
* @description Computes an appropriate contrast color (either black or white) that contrasts the given `color`.
* @param color The color to be contrasted.
* @return The contrast color.
* @requires chroma-js
*/
static contrastColor(color) {
if (chroma(color).luminance() < 0.45) {
return "white";
}
else {
return "black";
}
}
static loadStandardThemeTemplate(isDark) {
return isDark ? darkThemeTemplate.data : darkThemeTemplate.data;
}
}
/**
* Generated bundle index. Do not edit.
*/
export { ThemeGenerator };
//# sourceMappingURL=lithiumjs-ngx-material-theming-dynamic.mjs.map