UNPKG

devextreme

Version:

JavaScript/TypeScript Component Suite for Responsive Web Development

414 lines (403 loc) • 12.7 kB
/** * DevExtreme (esm/__internal/ui/themes.js) * Version: 25.2.7 * Build date: Tue May 05 2026 * * Copyright (c) 2012 - 2026 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import devices from "../../core/devices"; import domAdapter from "../../core/dom_adapter"; import $ from "../../core/renderer"; import { Deferred, when } from "../../core/utils/deferred"; import { parseHTML } from "../../core/utils/html_parser"; import { each } from "../../core/utils/iterator"; import readyCallbacks from "../../core/utils/ready_callbacks"; import { getOuterHeight } from "../../core/utils/size"; import { changeCallback, originalViewPort, value as viewPortValue } from "../../core/utils/view_port"; import { getWindow, hasWindow } from "../../core/utils/window"; import errors from "../../ui/widget/ui.errors"; import { uiLayerInitialized } from "../core/utils/m_common"; import { themeReadyCallback } from "../ui/m_themes_callback"; const window = getWindow(); const ready = readyCallbacks.add; const viewPort = viewPortValue; const viewPortChanged = changeCallback; let initDeferred = new Deferred; const DX_LINK_SELECTOR = "link[rel=dx-theme]"; const THEME_ATTR = "data-theme"; const ACTIVE_ATTR = "data-active"; const DX_HAIRLINES_CLASS = "dx-hairlines"; const ANY_THEME = "any"; let context; let $activeThemeLink; let knownThemes; let currentThemeName; let pendingThemeName; let defaultTimeout = 15e3; const THEME_MARKER_PREFIX = "dx."; function readThemeMarker() { if (!hasWindow()) { return null } const element = $("<div>", context).addClass("dx-theme-marker").appendTo(context.documentElement); let result; try { if (!(null !== window && void 0 !== window && window.getComputedStyle)) { return null } result = window.getComputedStyle(element.get(0)).fontFamily; if (!result) { return null } result = result.replace(/["']/g, ""); if ("dx." !== result.substr(0, 3)) { return null } return result.substr(3) } finally { element.remove() } } export function isPendingThemeLoaded() { if (!pendingThemeName) { return true } const anyThemePending = "any" === pendingThemeName; if ("resolved" === initDeferred.state() && anyThemePending) { return true } const themeMarker = readThemeMarker(); if (themeMarker && anyThemePending) { return true } return themeMarker === pendingThemeName } export function waitForThemeLoad(themeName) { let waitStartTime; let timerId; let intervalCleared = true; pendingThemeName = themeName; function handleLoaded() { pendingThemeName = null; clearInterval(timerId); intervalCleared = true; themeReadyCallback.fire(); themeReadyCallback.empty(); initDeferred.resolve() } if (isPendingThemeLoaded() || !defaultTimeout) { handleLoaded() } else { if (!intervalCleared) { if (pendingThemeName) { pendingThemeName = themeName } return } waitStartTime = Date.now(); intervalCleared = false; timerId = setInterval(() => { const isLoaded = isPendingThemeLoaded(); const isTimeout = !isLoaded && Date.now() - waitStartTime > defaultTimeout; if (isTimeout) { errors.log("W0004", pendingThemeName) } if (isLoaded || isTimeout) { handleLoaded() } }, 10) } } function processMarkup() { const $allThemeLinks = $(DX_LINK_SELECTOR, context); if (!$allThemeLinks.length) { return } knownThemes = {}; $activeThemeLink = $(parseHTML("<link rel=stylesheet>"), context); $allThemeLinks.each(function() { const link = $(this, context); const fullThemeName = link.attr(THEME_ATTR); const url = link.attr("href"); const isActive = "true" === link.attr(ACTIVE_ATTR); knownThemes[fullThemeName] = { url: url, isActive: isActive } }); $allThemeLinks.last().after($activeThemeLink); $allThemeLinks.remove() } function resolveFullThemeName(desiredThemeName) { const desiredThemeParts = desiredThemeName ? desiredThemeName.split(".") : []; let result = null; if (knownThemes) { if (desiredThemeName in knownThemes) { return desiredThemeName } each(knownThemes, (knownThemeName, themeData) => { const knownThemeParts = knownThemeName.split("."); if (desiredThemeParts[0] && knownThemeParts[0] !== desiredThemeParts[0]) { return } if (desiredThemeParts[1] && desiredThemeParts[1] !== knownThemeParts[1]) { return } if (desiredThemeParts[2] && desiredThemeParts[2] !== knownThemeParts[2]) { return } if (!result || themeData.isActive) { result = knownThemeName } if (themeData.isActive) { return false } }) } return result } function initContext(newContext) { try { if (newContext !== context) { knownThemes = null } } catch (x) { knownThemes = null } context = newContext } function getCssClasses(themeName) { var _themeName; themeName = themeName || current(); const result = []; const themeNameParts = null === (_themeName = themeName) || void 0 === _themeName ? void 0 : _themeName.split("."); if (themeNameParts) { result.push(`dx-theme-${themeNameParts[0]}`, `dx-theme-${themeNameParts[0]}-typography`); if (themeNameParts.length > 1) { result.push(`dx-color-scheme-${themeNameParts[1]}${isMaterialBased(themeName)?`-${themeNameParts[2]}`:""}`) } } return result } let themeClasses; function _attachCssClasses(element, themeName) { themeClasses = getCssClasses(themeName).join(" "); $(element).addClass(themeClasses); (() => { const pixelRatio = hasWindow() && window.devicePixelRatio; if (!pixelRatio || pixelRatio < 2) { return } const $tester = $("<div>"); $tester.css("border", ".5px solid transparent"); $("body").append($tester); if (1 === getOuterHeight($tester)) { $(element).addClass("dx-hairlines"); themeClasses += " dx-hairlines" } $tester.remove() })() } export function attachCssClasses(element, themeName) { when(uiLayerInitialized).done(() => { _attachCssClasses(element, themeName) }) } export function detachCssClasses(element) { when(uiLayerInitialized).done(() => { $(element).removeClass(themeClasses) }) } export function current(options) { if (!arguments.length) { currentThemeName = currentThemeName || readThemeMarker(); return currentThemeName } detachCssClasses(viewPort()); options = options || {}; if ("string" === typeof options) { options = { theme: options } } const isAutoInit = options._autoInit; const { loadCallback: loadCallback } = options; let currentThemeData; currentThemeName = resolveFullThemeName(options.theme || currentThemeName); if (currentThemeName) { currentThemeData = knownThemes[currentThemeName] } if (loadCallback) { themeReadyCallback.add(loadCallback) } if (currentThemeData) { $activeThemeLink.attr("href", knownThemes[currentThemeName].url); if (themeReadyCallback.has() || "resolved" !== initDeferred.state() || options._forceTimeout) { waitForThemeLoad(currentThemeName) } } else if (isAutoInit) { if (hasWindow()) { waitForThemeLoad("any") } themeReadyCallback.fire(); themeReadyCallback.empty() } else { throw errors.Error("E0021", currentThemeName) } initDeferred.done(() => attachCssClasses(originalViewPort(), currentThemeName)) } export function init(options) { options = options || {}; initContext(options.context || domAdapter.getDocument()); if (!context) { return } processMarkup(); currentThemeName = void 0; current(options) } function isTheme(themeRegExp, themeName) { if (!themeName) { themeName = currentThemeName || readThemeMarker() } return new RegExp(themeRegExp).test(themeName) } export function isMaterial(themeName) { return isTheme("material", themeName) } export function isFluent(themeName) { return isTheme("fluent", themeName) } export function isMaterialBased(themeName) { return isMaterial(themeName) || isFluent(themeName) } export function isGeneric(themeName) { return isTheme("generic", themeName) } export function isDark(themeName) { return isTheme("dark", themeName) } export function isCompact(themeName) { return isTheme("compact", themeName) } function themeReady(callback) { themeReadyCallback.add(callback) } export function isWebFontLoaded(text, fontWeight) { var _testElement$parentNo; const document = domAdapter.getDocument(); const testElement = document.createElement("span"); testElement.style.position = "absolute"; testElement.style.top = "-9999px"; testElement.style.left = "-9999px"; testElement.style.visibility = "hidden"; testElement.style.fontFamily = "arial"; testElement.style.fontSize = "250px"; testElement.style.fontWeight = fontWeight; testElement.innerHTML = text; document.body.appendChild(testElement); const etalonFontWidth = testElement.offsetWidth; testElement.style.fontFamily = "roboto, 'roboto fallback', arial"; const testedFontWidth = testElement.offsetWidth; null === (_testElement$parentNo = testElement.parentNode) || void 0 === _testElement$parentNo || _testElement$parentNo.removeChild(testElement); return etalonFontWidth !== testedFontWidth } export function waitWebFont(text, fontWeight) { return new Promise(resolve => { const clear = () => { clearInterval(intervalId); clearTimeout(timeoutId); resolve() }; const intervalId = setInterval(() => { if (isWebFontLoaded(text, fontWeight)) { clear() } }, 15); const timeoutId = setTimeout(clear, 2e3) }) } function autoInit() { init({ _autoInit: true, _forceTimeout: true }); if ($(DX_LINK_SELECTOR, context).length) { throw errors.Error("E0022") } } if (hasWindow()) { autoInit() } else { ready(autoInit) } viewPortChanged.add((viewPort, prevViewPort) => { initDeferred.done(() => { detachCssClasses(prevViewPort); attachCssClasses(viewPort) }) }); devices.changed.add(() => { init({ _autoInit: true }) }); export { themeReady as ready }; export function resetTheme() { var _$activeThemeLink; null === (_$activeThemeLink = $activeThemeLink) || void 0 === _$activeThemeLink || _$activeThemeLink.attr("href", "about:blank"); currentThemeName = null; pendingThemeName = null; initDeferred = new Deferred } export function initialized(callback) { initDeferred.done(callback) } export function setDefaultTimeout(timeout) { defaultTimeout = timeout } export default { setDefaultTimeout: setDefaultTimeout, init: init, initialized: initialized, resetTheme: resetTheme, ready: themeReady, waitWebFont: waitWebFont, isWebFontLoaded: isWebFontLoaded, isCompact: isCompact, isDark: isDark, isGeneric: isGeneric, isMaterial: isMaterial, isFluent: isFluent, isMaterialBased: isMaterialBased, detachCssClasses: detachCssClasses, attachCssClasses: attachCssClasses, current: current, waitForThemeLoad: waitForThemeLoad, isPendingThemeLoaded: isPendingThemeLoaded };