/**
* 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 { getFills } from "./Color.js";
const DEFAULT_FONT_SIZE = "16px";
const DEFAULT_LINE_HEIGHT = "1.2";
/**
* 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);
target.setProperty(styleName, source.getPropertyValue(styleName));
}
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
}
/**
* An inert element that only keeps the style
* declaration used for merging other styleDeclarations.
*
* @type {HTMLDivElement|null}
*/
let inertElement = null
/**
* Resets the style declaration of the inert
* element.
*/
function resetInertElement() {
if (!inertElement) throw new Error('Invalid inert element');
resetStyleDeclaration(inertElement.style);
return inertElement;
}
/**
* Returns an instance of a
element used
* to keep style declarations.
*
* @returns {HTMLDivElement}
*/
function getInertElement() {
if (!inertElement) {
inertElement = document.createElement("div");
return inertElement;
}
resetInertElement();
return inertElement;
}
/**
* Computes the styles of an element the same way `window.getComputedStyle` does.
*
* @param {Element} element
* @returns {CSSStyleDeclaration}
*/
export function getComputedStyle(element) {
const inertElement = getInertElement();
let currentElement = element;
while (currentElement) {
// This is better but it doesn't work in JSDOM.
// for (const styleName of currentElement.style) {
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") {
const newValue = currentElement.style.getPropertyValue(styleName);
inertElement.style.setProperty(styleName, newValue);
}
} else {
inertElement.style.setProperty(
styleName,
currentElement.style.getPropertyValue(styleName)
);
}
}
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) {
const styleDeclaration = mergeStyleDeclarations(
styleDefaults,
getComputedStyle(node.parentElement)
);
// 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));
}
// 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.getPropertyPriority("--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 lineHeight = styleDeclaration.getPropertyValue("line-height");
if (!lineHeight || lineHeight === "") {
styleDeclaration.setProperty("line-height", DEFAULT_LINE_HEIGHT);
}
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"
) {
if (styleName === "--fills" && styleValue === null) debugger;
element.style.setProperty(styleName, JSON.stringify(styleValue));
} else {
element.style.setProperty(styleName, styleValue + (styleUnit ?? ""));
}
return element;
}
/**
* Returns the value of a style from a declaration.
*
* @param {CSSStyleDeclaration} style
* @param {string} styleName
* @param {string|undefined} [styleUnit]
* @returns {*}
*/
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);
}
return styleValue;
}
/**
* 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) {
for (const [styleName, styleUnit] of allowedStyles) {
if (!(styleName in styleObject)) {
continue;
}
const styleValue = styleObject[styleName];
if (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) {
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) {
mergedStyles[styleName] = newStyles[styleName];
} else {
mergedStyles[styleName] = getStyleFromDeclaration(styleDeclaration, styleName, styleUnit);
}
}
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";
}