UNPKG

antd-theme

Version:

Provide runtime dynamic theme for ant design.

429 lines (380 loc) 13.4 kB
/** * An IThemingInstruction can specify a rawString to be preserved or a theme slot and a default value * to use if that slot is not specified by the theme. */ /* eslint-disable @typescript-eslint/no-use-before-define */ // Declaring a global here in case that the execution environment is Node.js (without importing the // entire node.js d.ts for now) declare let global: any; // eslint-disable-line @typescript-eslint/no-explicit-any export interface IThemingInstruction { name?: string, theme?: string; defaultValue?: string; semi?: string; rawString?: string; } export type ThemableArray = IThemingInstruction[]; export type ITheme = Map<string,string>; interface IStyleSheet { cssText: string; } interface IExtendedHtmlStyleElement extends HTMLStyleElement { styleSheet: IStyleSheet; } /** * Performance Measurement of loading styles */ interface IMeasurement { /** * Count of style element injected, which is the slow operation in IE */ count: number; /** * Total duration of all loadStyles exections */ duration: number; } interface IRunState { mode: Mode; buffer: ThemableArray[]; flushTimer: number; } interface IThemeState { theme: ITheme | undefined; lastStyleElement: IExtendedHtmlStyleElement; registeredStyles: IStyleRecord[]; // records of already registered non-themable styles registeredThemableStyles: IStyleRecord[]; // records of already registered themable styles loadStyles: ((processedStyles: string, rawStyles?: string | ThemableArray) => void) | undefined; perf: IMeasurement; runState: IRunState; } interface IStyleRecord { styleElement: Element; themableStyle: ThemableArray; } interface ICustomEvent<T> extends Event { args?: T; } /** * object returned from resolveThemableArray function */ interface IThemableArrayResolveResult { /** this string is the processed styles in string */ styleString: string; /** this boolean indicates if this style array is themable */ themable: boolean; } /** * 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 const enum Mode { sync, async } /** * 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 const enum ClearStyleOptions { /** only themable styles will be cleared */ onlyThemable = 1, /** only non-themable styles will be cleared */ onlyNonThemable = 2, /** both themable and non-themable styles will be cleared */ all = 3 } // Store the theming state in __themeState__ global scope for reuse in the case of duplicate // load-themed-styles hosted on the page. const _root: any = (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). const _styleNonce: string = _root && _root.CSPSettings && _root.CSPSettings.nonce; const _themeState: IThemeState = initializeThemeState(); /** * Matches theming tokens. For example, "[theme: themeSlotName, default: #FFF]" (including the quotes). */ const _themeTokenRegex: RegExp = /([-\w]+\s*:\s*)?[\'\"]\[theme:\s*(\w+)\s*(?:\,\s*default:\s*([\\"\']?[\.\,\(\)\#\-\s\w]*[\"\']?))?\s*\][\'\"](\s*;)?/g; const now: () => number = () => ((typeof performance !== 'undefined' && !!performance.now) ? performance.now() : Date.now()); function measure(func: () => void): void { const start: number = now(); func(); const end: number = now(); _themeState.perf.duration += end - start; } /** * initialize global state object */ function initializeThemeState(): IThemeState { let state: IThemeState = _root.__themeState__ || { theme: undefined, lastStyleElement: undefined, registeredStyles: [], }; if (!state.runState) { state = { ...(state), perf: { count: 0, duration: 0, }, runState: { flushTimer: 0, mode: Mode.sync, buffer: [], }, }; } if (!state.registeredThemableStyles) { state = { ...(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: string | ThemableArray, loadAsync: boolean = false): void { measure(() => { const styleParts: ThemableArray = Array.isArray(styles) ? styles : splitStyles(styles); const { mode, buffer, flushTimer, } = _themeState.runState; 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: ((processedStyles: string, rawStyles?: string | ThemableArray) => void) | undefined ): void { _themeState.loadStyles = loadStylesFn; } /** * Configure run mode of load-themable-styles * @param mode load-themable-styles run mode, async or sync */ export function configureRunMode(mode: Mode): void { _themeState.runState.mode = mode; } /** * external code can call flush to synchronously force processing of currently buffered styles */ export function flush(): void { measure(() => { const styleArrays: ThemableArray[] = _themeState.runState.buffer.slice(); _themeState.runState.buffer = []; const mergedStyleArray: ThemableArray = ([] as ThemableArray).concat.apply([], styleArrays); if (mergedStyleArray.length > 0) { applyThemableStyles(mergedStyleArray); } }); } /** * register async loadStyles */ function asyncLoadStyles(): number { return window.setTimeout(() => { _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: ThemableArray, styleRecord?: IStyleRecord): void { 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: ITheme | undefined): void { _themeState.theme = theme; // reload styles. reloadStyles(); } /** * 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: ClearStyleOptions = ClearStyleOptions.all): void { 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: IStyleRecord[]): void { records.forEach((styleRecord: IStyleRecord) => { const styleElement: HTMLStyleElement = styleRecord && styleRecord.styleElement as HTMLStyleElement; if (styleElement && styleElement.parentElement) { styleElement.parentElement.removeChild(styleElement); } }); } /** * Reloads styles. */ function reloadStyles(): void { if (_themeState.theme) { const themableStyles: ThemableArray[] = []; for (const styleRecord of _themeState.registeredThemableStyles) { themableStyles.push(styleRecord.themableStyle); } if (themableStyles.length > 0) { clearStyles(ClearStyleOptions.onlyThemable); applyThemableStyles(([] as ThemableArray).concat.apply([], themableStyles)); } } } /** * Find theme tokens and replaces them with provided theme values. * @param {string} styles Tokenized styles to fix. */ export function detokenize(styles: string | undefined): string | undefined { 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: ThemableArray): IThemableArrayResolveResult { const { theme }: IThemeState = _themeState; let themable: boolean = false; // Resolve the array of theming instructions to an array of strings. // Then join the array to produce the final CSS string. const resolvedArray: (string | undefined)[] = (splitStyleArray || []).map((currentValue: IThemingInstruction) => { const themeSlot: string | undefined = currentValue.theme; if (themeSlot) { themable = true; // A theming annotation. Resolve it. const themedValue: string | undefined = theme ? theme.get(themeSlot) : undefined; // const defaultValue: string = 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 (currentValue.name && currentValue.semi) { return themedValue ? `${currentValue.name}${themedValue || currentValue.defaultValue}${currentValue.semi}` : ''; } if (currentValue.name) { return `${currentValue.name}${themedValue || currentValue.defaultValue}`; } if (currentValue.semi) { return `${themedValue || currentValue.defaultValue}${currentValue.semi}`; } return themedValue; } // A non-themable string. Preserve it. return currentValue.rawString; }); return { styleString: resolvedArray.join(''), themable, }; } /** * Split tokenized CSS into an array of strings and theme specification objects * @param {string} styles Tokenized styles to split. */ export function splitStyles(styles: string): ThemableArray { const result: ThemableArray = []; if (styles) { let pos: number = 0; // Current position in styles. let tokenMatch: RegExpExecArray | null; // eslint-disable-line @rushstack/no-null while ((tokenMatch = _themeTokenRegex.exec(styles))) { const matchIndex: number = tokenMatch.index; if (matchIndex > pos) { result.push({ rawString: styles.substring(pos, matchIndex), }); } result.push({ name: tokenMatch[1], theme: tokenMatch[2], defaultValue: tokenMatch[3], // May be undefined semi: tokenMatch[4], }); // 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: ThemableArray): void { if (typeof document === 'undefined') { return; } const head: HTMLHeadElement = document.getElementsByTagName('head')[0]; const styleElement: HTMLStyleElement = document.createElement('style'); const { styleString, themable, } = resolveThemableArray(styleArray); styleElement.setAttribute('data-load-themed-styles', 'true'); if (_styleNonce) { styleElement.setAttribute('nonce', _styleNonce); } styleElement.appendChild(document.createTextNode(styleString)); _themeState.perf.count++; head.appendChild(styleElement); const ev: ICustomEvent<{ newStyle: HTMLStyleElement}> = document.createEvent('HTMLEvents'); ev.initEvent('styleinsert', true /* bubbleEvent */, false /* cancelable */); ev.args = { newStyle: styleElement, }; document.dispatchEvent(ev); const record: IStyleRecord = { styleElement, themableStyle: styleArray, }; if (themable) { _themeState.registeredThemableStyles.push(record); } else { _themeState.registeredStyles.push(record); } }