UNPKG

@microsoft/load-themed-styles

Version:
338 lines 12.9 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; /// <reference lib="dom" /> /** * In sync mode, styles are registered as style elements synchronously with loadStyles() call. * In async mode, styles are buffered and registered as batch in async timer for performance purpose. */ export var Mode; (function (Mode) { Mode[Mode["sync"] = 0] = "sync"; Mode[Mode["async"] = 1] = "async"; })(Mode || (Mode = {})); /** * Themable styles and non-themable styles are tracked separately * Specify ClearStyleOptions when calling clearStyles API to specify which group of registered styles should be cleared. */ export var ClearStyleOptions; (function (ClearStyleOptions) { /** only themable styles will be cleared */ ClearStyleOptions[ClearStyleOptions["onlyThemable"] = 1] = "onlyThemable"; /** only non-themable styles will be cleared */ ClearStyleOptions[ClearStyleOptions["onlyNonThemable"] = 2] = "onlyNonThemable"; /** both themable and non-themable styles will be cleared */ ClearStyleOptions[ClearStyleOptions["all"] = 3] = "all"; })(ClearStyleOptions || (ClearStyleOptions = {})); // Store the theming state in __themeState__ global scope for reuse in the case of duplicate // load-themed-styles hosted on the page. var _root = typeof window === 'undefined' ? global : window; // eslint-disable-line @typescript-eslint/no-explicit-any // Nonce string to inject into script tag if one provided. This is used in CSP (Content Security Policy). var _styleNonce = _root && _root.CSPSettings && _root.CSPSettings.nonce; var _themeState = initializeThemeState(); /** * Matches theming tokens. For example, "[theme: themeSlotName, default: #FFF]" (including the quotes). */ var _themeTokenRegex = /[\'\"]\[theme:\s*(\w+)\s*(?:\,\s*default:\s*([\\"\']?[\.\,\(\)\#\-\s\w]*[\.\,\(\)\#\-\w][\"\']?))?\s*\][\'\"]/g; var now = function () { return typeof performance !== 'undefined' && !!performance.now ? performance.now() : Date.now(); }; function measure(func) { var start = now(); func(); var end = now(); _themeState.perf.duration += end - start; } /** * initialize global state object */ function initializeThemeState() { var state = _root.__themeState__ || { theme: undefined, lastStyleElement: undefined, registeredStyles: [] }; if (!state.runState) { state = __assign(__assign({}, state), { perf: { count: 0, duration: 0 }, runState: { flushTimer: 0, mode: Mode.sync, buffer: [] } }); } if (!state.registeredThemableStyles) { state = __assign(__assign({}, state), { registeredThemableStyles: [] }); } _root.__themeState__ = state; return state; } /** * Loads a set of style text. If it is registered too early, we will register it when the window.load * event is fired. * @param {string | ThemableArray} styles Themable style text to register. * @param {boolean} loadAsync When true, always load styles in async mode, irrespective of current sync mode. */ export function loadStyles(styles, loadAsync) { if (loadAsync === void 0) { loadAsync = false; } measure(function () { var styleParts = Array.isArray(styles) ? styles : splitStyles(styles); var _a = _themeState.runState, mode = _a.mode, buffer = _a.buffer, flushTimer = _a.flushTimer; if (loadAsync || mode === Mode.async) { buffer.push(styleParts); if (!flushTimer) { _themeState.runState.flushTimer = asyncLoadStyles(); } } else { applyThemableStyles(styleParts); } }); } /** * Allows for customizable loadStyles logic. e.g. for server side rendering application * @param {(processedStyles: string, rawStyles?: string | ThemableArray) => void} * a loadStyles callback that gets called when styles are loaded or reloaded */ export function configureLoadStyles(loadStylesFn) { _themeState.loadStyles = loadStylesFn; } /** * Configure run mode of load-themable-styles * @param mode load-themable-styles run mode, async or sync */ export function configureRunMode(mode) { _themeState.runState.mode = mode; } /** * external code can call flush to synchronously force processing of currently buffered styles */ export function flush() { measure(function () { var styleArrays = _themeState.runState.buffer.slice(); _themeState.runState.buffer = []; var mergedStyleArray = [].concat.apply([], styleArrays); if (mergedStyleArray.length > 0) { applyThemableStyles(mergedStyleArray); } }); } /** * register async loadStyles */ function asyncLoadStyles() { // Use "self" to distinguish conflicting global typings for setTimeout() from lib.dom.d.ts vs Jest's @types/node // https://github.com/jestjs/jest/issues/14418 return self.setTimeout(function () { _themeState.runState.flushTimer = 0; flush(); }, 0); } /** * Loads a set of style text. If it is registered too early, we will register it when the window.load event * is fired. * @param {string} styleText Style to register. * @param {IStyleRecord} styleRecord Existing style record to re-apply. */ function applyThemableStyles(stylesArray, styleRecord) { if (_themeState.loadStyles) { _themeState.loadStyles(resolveThemableArray(stylesArray).styleString, stylesArray); } else { registerStyles(stylesArray); } } /** * Registers a set theme tokens to find and replace. If styles were already registered, they will be * replaced. * @param {theme} theme JSON object of theme tokens to values. */ export function loadTheme(theme) { _themeState.theme = theme; var style = document.body.style; for (var key in theme) { if (theme.hasOwnProperty(key)) { style.setProperty("--".concat(key), theme[key]); } } // reload styles. reloadStyles(); } /** * Replaces theme tokens with CSS variable references. * @param styles - Raw css text with theme tokens * @returns A css string with theme tokens replaced with css variable references */ export function replaceTokensWithVariables(styles) { return styles.replace(_themeTokenRegex, function (match, themeSlot, defaultValue) { return typeof defaultValue === 'string' ? "var(--".concat(themeSlot, ", ").concat(defaultValue, ")") : "var(--".concat(themeSlot, ")"); }); } /** * Clear already registered style elements and style records in theme_State object * @param option - specify which group of registered styles should be cleared. * Default to be both themable and non-themable styles will be cleared */ export function clearStyles(option) { if (option === void 0) { option = ClearStyleOptions.all; } if (option === ClearStyleOptions.all || option === ClearStyleOptions.onlyNonThemable) { clearStylesInternal(_themeState.registeredStyles); _themeState.registeredStyles = []; } if (option === ClearStyleOptions.all || option === ClearStyleOptions.onlyThemable) { clearStylesInternal(_themeState.registeredThemableStyles); _themeState.registeredThemableStyles = []; } } function clearStylesInternal(records) { records.forEach(function (styleRecord) { var styleElement = styleRecord && styleRecord.styleElement; if (styleElement && styleElement.parentElement) { styleElement.parentElement.removeChild(styleElement); } }); } /** * Reloads styles. */ function reloadStyles() { if (_themeState.theme) { var themableStyles = []; for (var _i = 0, _a = _themeState.registeredThemableStyles; _i < _a.length; _i++) { var styleRecord = _a[_i]; themableStyles.push(styleRecord.themableStyle); } if (themableStyles.length > 0) { clearStyles(ClearStyleOptions.onlyThemable); applyThemableStyles([].concat.apply([], themableStyles)); } } } /** * Find theme tokens and replaces them with provided theme values. * @param {string} styles Tokenized styles to fix. */ export function detokenize(styles) { if (styles) { styles = resolveThemableArray(splitStyles(styles)).styleString; } return styles; } /** * Resolves ThemingInstruction objects in an array and joins the result into a string. * @param {ThemableArray} splitStyleArray ThemableArray to resolve and join. */ function resolveThemableArray(splitStyleArray) { var theme = _themeState.theme; var themable = false; // Resolve the array of theming instructions to an array of strings. // Then join the array to produce the final CSS string. var resolvedArray = (splitStyleArray || []).map(function (currentValue) { var themeSlot = currentValue.theme; if (themeSlot) { themable = true; // A theming annotation. Resolve it. var themedValue = theme ? theme[themeSlot] : undefined; var defaultValue = currentValue.defaultValue || 'inherit'; // Warn to console if we hit an unthemed value even when themes are provided, but only if "DEBUG" is true. // Allow the themedValue to be undefined to explicitly request the default value. if (theme && !themedValue && console && !(themeSlot in theme) && typeof DEBUG !== 'undefined' && DEBUG) { // eslint-disable-next-line no-console console.warn("Theming value not provided for \"".concat(themeSlot, "\". Falling back to \"").concat(defaultValue, "\".")); } return themedValue || defaultValue; } else { // A non-themable string. Preserve it. return currentValue.rawString; } }); return { styleString: resolvedArray.join(''), themable: themable }; } /** * Split tokenized CSS into an array of strings and theme specification objects * @param {string} styles Tokenized styles to split. */ export function splitStyles(styles) { var result = []; if (styles) { var pos = 0; // Current position in styles. var tokenMatch = void 0; while ((tokenMatch = _themeTokenRegex.exec(styles))) { var matchIndex = tokenMatch.index; if (matchIndex > pos) { result.push({ rawString: styles.substring(pos, matchIndex) }); } result.push({ theme: tokenMatch[1], defaultValue: tokenMatch[2] // May be undefined }); // index of the first character after the current match pos = _themeTokenRegex.lastIndex; } // Push the rest of the string after the last match. result.push({ rawString: styles.substring(pos) }); } return result; } /** * Registers a set of style text. If it is registered too early, we will register it when the * window.load event is fired. * @param {ThemableArray} styleArray Array of IThemingInstruction objects to register. * @param {IStyleRecord} styleRecord May specify a style Element to update. */ function registerStyles(styleArray) { if (typeof document === 'undefined') { return; } var head = document.getElementsByTagName('head')[0]; var styleElement = document.createElement('style'); var _a = resolveThemableArray(styleArray), styleString = _a.styleString, themable = _a.themable; styleElement.setAttribute('data-load-themed-styles', 'true'); if (_styleNonce) { styleElement.setAttribute('nonce', _styleNonce); } styleElement.appendChild(document.createTextNode(styleString)); _themeState.perf.count++; head.appendChild(styleElement); var ev = document.createEvent('HTMLEvents'); ev.initEvent('styleinsert', true /* bubbleEvent */, false /* cancelable */); ev.args = { newStyle: styleElement }; document.dispatchEvent(ev); var record = { styleElement: styleElement, themableStyle: styleArray }; if (themable) { _themeState.registeredThemableStyles.push(record); } else { _themeState.registeredStyles.push(record); } } //# sourceMappingURL=index.js.map