/** * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) KALEIDOS INC */ import StyleDeclaration from '../../controllers/StyleDeclaration.js'; import { getFills } from "./Color.js"; const DEFAULT_FONT_SIZE = "16px"; const DEFAULT_FONT_SIZE_VALUE = parseFloat(DEFAULT_FONT_SIZE); const DEFAULT_LINE_HEIGHT = "1.2"; const DEFAULT_FONT_WEIGHT = "400"; /** Sanitizes font-family values to be quoted, so it handles multi-word font names * with numbers like "Font Awesome 7 Free" * * @param {string} value */ export function sanitizeFontFamily(value) { // NOTE: This is a fix for a bug introduced earlier that have might modified the font-family in the model // adding extra double quotes. if (value && value.startsWith('""')) { //remove the first and last quotes value = value.slice(1).replace(/"([^"]*)$/, "$1"); // remove quotes from font-family in 1-word font-families // and repeated values value = [ ...new Set( value .split(", ") .map((x) => (x.includes(" ") ? x : x.replace(/"/g, ""))), ), ].join(", "); } if (!value || value === "") { return "var(--fallback-families)"; } else if (value.endsWith(" var(--fallback-families)")) { return value; } else if (value.startsWith('"')) { return `${value}, var(--fallback-families)`; } else { return `"${value}", var(--fallback-families)`; } } /** * Merges two style declarations. `source` -> `target`. * * @param {CSSStyleDeclaration} target * @param {CSSStyleDeclaration} source * @returns {CSSStyleDeclaration} */ export function mergeStyleDeclarations(target, source) { // This is better but it doesn't work in JSDOM // for (const styleName of source) { for (let index = 0; index < source.length; index++) { const styleName = source.item(index); let styleValue = source.getPropertyValue(styleName); target.setProperty(styleName, styleValue); } return target; } /** * Resets the properties of a style declaration. * * @param {CSSStyleDeclaration} styleDeclaration * @returns {CSSStyleDeclaration} */ function resetStyleDeclaration(styleDeclaration) { for (let index = 0; index < styleDeclaration.length; index++) { const styleName = styleDeclaration.item(index); styleDeclaration.removeProperty(styleName); } return styleDeclaration; } /** * Resets the style declaration of the inert * element. */ export function resetInertElement() { const inertElement = getInertElement(); resetStyleDeclaration(inertElement.style); return inertElement; } /** * An inert element that only keeps the style * declaration used for merging other styleDeclarations. * * @type {HTMLDivElement|null} */ let globalInertElement = null; /** * Returns an instance of a
element used * to keep style declarations. * * @returns {HTMLDivElement} */ function getInertElement() { if (!globalInertElement) { globalInertElement = document.createElement("div"); return globalInertElement; } return globalInertElement; } /** * Returns a default declaration. * * @returns {CSSStyleDeclaration} */ function getStyleDefaultsDeclaration() { const inertElement = getInertElement(); resetInertElement(); return inertElement.style; } /** * Computes the styles of an element the same way `window.getComputedStyle` does. * * @param {Element} element * @returns {CSSStyleDeclaration} */ export function getComputedStyle(element) { if (typeof window !== "undefined" && window.getComputedStyle) { const inertElement = getInertElement(); resetInertElement(element); const computedStyle = window.getComputedStyle(element); inertElement.style = computedStyle; return inertElement.style; } return getComputedStylePolyfill(element); } /** * Returns a polyfilled version of a computed style. * * @param {Element} element * @returns {CSSStyleDeclaration} */ export function getComputedStylePolyfill(element) { const inertElement = getInertElement(); resetInertElement(element); let currentElement = element; while (currentElement) { for (let index = 0; index < currentElement.style.length; index++) { const styleName = currentElement.style.item(index); const currentValue = inertElement.style.getPropertyValue(styleName); if (currentValue) { const priority = currentElement.style.getPropertyPriority(styleName); if (priority === "important") { let newValue = currentElement.style.getPropertyValue(styleName); inertElement.style.setProperty(styleName, newValue); } } else { let newValue = currentElement.style.getPropertyValue(styleName); if (styleName === "font-family") { newValue = sanitizeFontFamily(newValue); } inertElement.style.setProperty(styleName, newValue); } } currentElement = currentElement.parentElement; } return inertElement.style; } /** * Normalizes style declaration. * * TODO: I think that this also needs to remove some "conflicting" * CSS properties like `font-family` or some CSS variables. * * @param {Node} node * @param {CSSStyleDeclaration} [styleDefaults] * @returns {CSSStyleDeclaration} */ export function normalizeStyles( node, styleDefaults = getStyleDefaultsDeclaration(), ) { const computedStyle = getComputedStyle(node.parentElement); const styleDeclaration = mergeStyleDeclarations(styleDefaults, computedStyle); // If there's a color property, we should convert it to // a --fills CSS variable property. const fills = styleDeclaration.getPropertyValue("--fills"); const color = styleDeclaration.getPropertyValue("color"); if (color && !fills) { styleDeclaration.removeProperty("color"); styleDeclaration.setProperty("--fills", getFills(color)); } else { styleDeclaration.setProperty("--fills", fills); } // If there's a font-family property and not a --font-id, then // we remove the font-family because it will not work. const fontFamily = styleDeclaration.getPropertyValue("font-family"); const fontId = styleDeclaration.getPropertyValue("--font-id"); if (fontFamily && !fontId) { styleDeclaration.removeProperty("font-family"); } const fontSize = styleDeclaration.getPropertyValue("font-size"); if (!fontSize || fontSize === "0px") { styleDeclaration.setProperty("font-size", DEFAULT_FONT_SIZE); } const fontWeight = styleDeclaration.getPropertyValue("font-weight"); if (!fontWeight || fontWeight === "0") { styleDeclaration.setProperty("font-weight", DEFAULT_FONT_WEIGHT); } const lineHeight = styleDeclaration.getPropertyValue("line-height"); if (!lineHeight || lineHeight === "" || !lineHeight.endsWith("px")) { // TODO: PodrĂ­amos convertir unidades en decimales. styleDeclaration.setProperty("line-height", DEFAULT_LINE_HEIGHT); } else if (lineHeight.endsWith("px")) { const fontSize = styleDeclaration.getPropertyValue("font-size"); styleDeclaration.setProperty( "line-height", parseFloat(lineHeight) / parseFloat(fontSize), ); } return styleDeclaration; } /** * Sets a single style property value of an element. * * @param {HTMLElement} element * @param {string} styleName * @param {*} styleValue * @param {string} [styleUnit] * @returns {HTMLElement} */ export function setStyle(element, styleName, styleValue, styleUnit) { if ( styleName.startsWith("--") && typeof styleValue !== "string" && typeof styleValue !== "number" ) { element.style.setProperty(styleName, JSON.stringify(styleValue)); } else { if (styleName === "font-family") { styleValue = sanitizeFontFamily(styleValue); } element.style.setProperty( styleName, styleValue + (styleUnit ? styleUnit : ""), ); } return element; } /** * Returns the value of the font size * * @param {number} styleValueAsNumber * @param {string} styleValue * @returns {string} */ function getStyleFontSize(styleValueAsNumber, styleValue) { if (styleValue.endsWith("pt")) { const baseSize = 1.3333; return (styleValueAsNumber * baseSize).toFixed(); } else if (styleValue.endsWith("em")) { const baseSize = DEFAULT_FONT_SIZE_VALUE; return (styleValueAsNumber * baseSize).toFixed(); } else if (styleValue.endsWith("%")) { const baseSize = DEFAULT_FONT_SIZE_VALUE; return ((styleValueAsNumber / 100) * baseSize).toFixed(); } return styleValueAsNumber.toFixed(); } /** * Returns the value of a style from a declaration. * * @param {CSSStyleDeclaration} style * @param {string} styleName * @param {string|undefined} [styleUnit] * @returns {string} */ export function getStyleFromDeclaration(style, styleName, styleUnit) { if (styleName.startsWith("--")) { return style.getPropertyValue(styleName); } const styleValue = style.getPropertyValue(styleName); if (styleValue.endsWith(styleUnit)) { return styleValue.slice(0, -styleUnit.length); } const styleValueAsNumber = parseFloat(styleValue); if (styleName === "font-size") { return getStyleFontSize(styleValueAsNumber, styleValue); } else if (styleName === "line-height") { return styleValue; } if (Number.isNaN(styleValueAsNumber)) { return styleValue; } return styleValueAsNumber.toFixed(); } /** * Returns the value of a style. * * @param {HTMLElement} element * @param {string} styleName * @param {string|undefined} [styleUnit] * @returns {*} */ export function getStyle(element, styleName, styleUnit) { return getStyleFromDeclaration(element.style, styleName, styleUnit); } /** * Sets the styles of an element using an object and a list of * allowed styles. * * @param {HTMLElement} element * @param {Array<[string,?string]>} allowedStyles * @param {Object.} styleObject * @returns {HTMLElement} */ export function setStylesFromObject(element, allowedStyles, styleObject) { if (element.tagName === "SPAN") for (const [styleName, styleUnit] of allowedStyles) { if (!(styleName in styleObject)) { continue; } let styleValue = styleObject[styleName]; if (!styleValue) continue; if (styleName === "font-family") { styleValue = sanitizeFontFamily(styleValue); } setStyle(element, styleName, styleValue, styleUnit); } return element; } /** * Sets the styles of an element using a CSS Style Declaration and a list * of allowed styles. * * @param {HTMLElement} element * @param {Array<[string,?string]>} allowedStyles * @param {CSSStyleDeclaration} styleDeclaration * @returns {HTMLElement} */ export function setStylesFromDeclaration( element, allowedStyles, styleDeclaration, ) { for (const [styleName, styleUnit] of allowedStyles) { const styleValue = getStyleFromDeclaration( styleDeclaration, styleName, styleUnit, ); if (styleValue) { setStyle(element, styleName, styleValue, styleUnit); } } return element; } /** * Sets the styles of an element using an Object or a CSS Style Declaration and * a list of allowed styles. * * @param {HTMLElement} element * @param {Array<[string,?string]} allowedStyles * @param {Object.|CSSStyleDeclaration} styleObjectOrDeclaration * @returns {HTMLElement} */ export function setStyles(element, allowedStyles, styleObjectOrDeclaration) { if (styleObjectOrDeclaration instanceof CSSStyleDeclaration || styleObjectOrDeclaration instanceof StyleDeclaration) { return setStylesFromDeclaration( element, allowedStyles, styleObjectOrDeclaration, ); } return setStylesFromObject(element, allowedStyles, styleObjectOrDeclaration); } /** * Gets the styles of an element using a list of allowed styles. * * @param {HTMLElement} element * @param {Array<[string,?string]} allowedStyles * @returns {Object.} */ export function getStyles(element, allowedStyles) { const styleObject = {}; for (const [styleName, styleUnit] of allowedStyles) { const styleValue = getStyle(element, styleName, styleUnit); if (styleValue) { styleObject[styleName] = styleValue; } } return styleObject; } /** * Returns a series of merged styles. * * @param {Array<[string,?string]} allowedStyles * @param {CSSStyleDeclaration} styleDeclaration * @param {Object.} newStyles * @returns {Object.} */ export function mergeStyles(allowedStyles, styleDeclaration, newStyles) { const mergedStyles = {}; for (const [styleName, styleUnit] of allowedStyles) { if (styleName in newStyles) { const styleValue = newStyles[styleName]; mergedStyles[styleName] = styleValue; } else { const styleValue = getStyleFromDeclaration( styleDeclaration, styleName, styleUnit, ); mergedStyles[styleName] = styleValue; } } return mergedStyles; } /** * Returns true if the specified style declaration has a display block. * * @param {CSSStyleDeclaration} style * @returns {boolean} */ export function isDisplayBlock(style) { return style.display === "block"; } /** * Returns true if the specified style declaration has a display inline * or inline-block. * * @param {CSSStyleDeclaration} style * @returns {boolean} */ export function isDisplayInline(style) { return style.display === "inline" || style.display === "inline-block"; }