/** * This is a fork from the CSS Style Declaration part of * https://github.com/NV/CSSOM */ "use strict"; const allProperties = require("./generated/allProperties"); const implementedProperties = require("./generated/implementedProperties"); const generatedProperties = require("./generated/properties"); const { borderProperties, getPositionValue, normalizeProperties, prepareBorderProperties, prepareProperties, shorthandProperties } = require("./normalize"); const { hasVarFunc, isGlobalKeyword, parseCSS, parsePropertyValue, prepareValue } = require("./parsers"); const allExtraProperties = require("./utils/allExtraProperties"); const { dashedToCamelCase } = require("./utils/camelize"); const { getPropertyDescriptor } = require("./utils/propertyDescriptors"); const { asciiLowercase } = require("./utils/strings"); /** * @see https://drafts.csswg.org/cssom/#the-cssstyledeclaration-interface */ class CSSStyleDeclaration { /** * @param {Function} onChangeCallback * @param {object} [opt] * @param {object} [opt.context] - Window, Element or CSSRule. */ constructor(onChangeCallback, opt = {}) { // Make constructor and internals non-enumerable. Object.defineProperties(this, { constructor: { enumerable: false, writable: true }, // Window _global: { value: globalThis, enumerable: false, writable: true }, // Element _ownerNode: { value: null, enumerable: false, writable: true }, // CSSRule _parentNode: { value: null, enumerable: false, writable: true }, _onChange: { value: null, enumerable: false, writable: true }, _values: { value: new Map(), enumerable: false, writable: true }, _priorities: { value: new Map(), enumerable: false, writable: true }, _length: { value: 0, enumerable: false, writable: true }, _computed: { value: false, enumerable: false, writable: true }, _readonly: { value: false, enumerable: false, writable: true }, _setInProgress: { value: false, enumerable: false, writable: true } }); const { context } = opt; if (context) { if (typeof context.getComputedStyle === "function") { this._global = context; this._computed = true; this._readonly = true; } else if (context.nodeType === 1 && Object.hasOwn(context, "style")) { this._global = context.ownerDocument.defaultView; this._ownerNode = context; } else if (Object.hasOwn(context, "parentRule")) { this._parentRule = context; // Find Window from the owner node of the StyleSheet. const window = context?.parentStyleSheet?.ownerNode?.ownerDocument?.defaultView; if (window) { this._global = window; } } } if (typeof onChangeCallback === "function") { this._onChange = onChangeCallback; } } get cssText() { if (this._computed) { return ""; } const properties = new Map(); for (let i = 0; i < this._length; i++) { const property = this[i]; const value = this.getPropertyValue(property); const priority = this._priorities.get(property) ?? ""; if (shorthandProperties.has(property)) { const { shorthandFor } = shorthandProperties.get(property); for (const [longhand] of shorthandFor) { if (priority || !this._priorities.get(longhand)) { properties.delete(longhand); } } } properties.set(property, { property, value, priority }); } const normalizedProperties = normalizeProperties(properties); const parts = []; for (const { property, value, priority } of normalizedProperties.values()) { if (priority) { parts.push(`${property}: ${value} !${priority};`); } else { parts.push(`${property}: ${value};`); } } return parts.join(" "); } set cssText(val) { if (this._readonly) { const msg = "cssText can not be modified."; const name = "NoModificationAllowedError"; throw new this._global.DOMException(msg, name); } Array.prototype.splice.call(this, 0, this._length); this._values.clear(); this._priorities.clear(); if (this._parentRule || (this._ownerNode && this._setInProgress)) { return; } try { this._setInProgress = true; const valueObj = parseCSS( val, { context: "declarationList", parseValue: false }, true ); if (valueObj?.children) { const properties = new Map(); let shouldSkipNext = false; for (const item of valueObj.children) { if (item.type === "Atrule") { continue; } if (item.type === "Rule") { shouldSkipNext = true; continue; } if (shouldSkipNext === true) { shouldSkipNext = false; continue; } const { important, property, value: { value } } = item; if (typeof property === "string" && typeof value === "string") { const priority = important ? "important" : ""; const isCustomProperty = property.startsWith("--"); if (isCustomProperty || hasVarFunc(value)) { if (properties.has(property)) { const { priority: itemPriority } = properties.get(property); if (!itemPriority) { properties.set(property, { property, value, priority }); } } else { properties.set(property, { property, value, priority }); } } else { const parsedValue = parsePropertyValue(property, value, { globalObject: this._global }); if (parsedValue) { if (properties.has(property)) { const { priority: itemPriority } = properties.get(property); if (!itemPriority) { properties.set(property, { property, value, priority }); } } else { properties.set(property, { property, value, priority }); } } else { this.removeProperty(property); } } } } const parsedProperties = prepareProperties(properties, { globalObject: this._global }); for (const [property, item] of parsedProperties) { const { priority, value } = item; this._priorities.set(property, priority); this.setProperty(property, value, priority); } } } catch { return; } finally { this._setInProgress = false; } if (typeof this._onChange === "function") { this._onChange(this.cssText); } } get length() { return this._length; } // This deletes indices if the new length is less then the current length. // If the new length is more, it does nothing, the new indices will be // undefined until set. set length(len) { for (let i = len; i < this._length; i++) { delete this[i]; } this._length = len; } // Readonly get parentRule() { return this._parentRule; } get cssFloat() { return this.getPropertyValue("float"); } set cssFloat(value) { this._setProperty("float", value); } /** * @param {string} property */ getPropertyPriority(property) { return this._priorities.get(property) || ""; } /** * @param {string} property */ getPropertyValue(property) { if (this._values.has(property)) { return this._values.get(property).toString(); } return ""; } /** * @param {...number} args */ item(...args) { if (!args.length) { const msg = "1 argument required, but only 0 present."; throw new this._global.TypeError(msg); } const [value] = args; const index = parseInt(value); if (Number.isNaN(index) || index < 0 || index >= this._length) { return ""; } return this[index]; } /** * @param {string} property */ removeProperty(property) { if (this._readonly) { const msg = `Property ${property} can not be modified.`; const name = "NoModificationAllowedError"; throw new this._global.DOMException(msg, name); } if (!this._values.has(property)) { return ""; } const prevValue = this._values.get(property); this._values.delete(property); this._priorities.delete(property); const index = Array.prototype.indexOf.call(this, property); if (index >= 0) { Array.prototype.splice.call(this, index, 1); if (typeof this._onChange === "function") { this._onChange(this.cssText); } } return prevValue; } /** * @param {string} prop * @param {string} val * @param {string} prior */ setProperty(prop, val, prior) { if (this._readonly) { const msg = `Property ${prop} can not be modified.`; const name = "NoModificationAllowedError"; throw new this._global.DOMException(msg, name); } const value = prepareValue(val); if (value === "") { this[prop] = ""; this.removeProperty(prop); return; } const priority = prior === "important" ? "important" : ""; const isCustomProperty = prop.startsWith("--"); if (isCustomProperty) { this._setProperty(prop, value, priority); return; } const property = asciiLowercase(prop); if (!allProperties.has(property) && !allExtraProperties.has(property)) { return; } if (priority) { this._priorities.set(property, priority); } else { this._priorities.delete(property); } this[property] = value; } } // Internal methods Object.defineProperties(CSSStyleDeclaration.prototype, { _setProperty: { /** * @param {string} property * @param {string} val * @param {string} priority */ value(property, val, priority) { if (typeof val !== "string") { return; } if (val === "") { this.removeProperty(property); return; } let originalText = ""; if (typeof this._onChange === "function" && !this._setInProgress) { originalText = this.cssText; } if (this._values.has(property)) { const index = Array.prototype.indexOf.call(this, property); // The property already exists but is not indexed into `this` so add it. if (index < 0) { this[this._length] = property; this._length++; } } else { // New property. this[this._length] = property; this._length++; } if (priority === "important") { this._priorities.set(property, priority); } else { this._priorities.delete(property); } this._values.set(property, val); if ( typeof this._onChange === "function" && !this._setInProgress && this.cssText !== originalText ) { this._onChange(this.cssText); } }, enumerable: false }, _borderSetter: { /** * @param {string} prop * @param {object|Array|string} val * @param {string} prior */ value(prop, val, prior) { const properties = new Map(); if (prop === "border") { let priority = ""; if (typeof prior === "string") { priority = prior; } else { priority = this._priorities.get(prop) ?? ""; } properties.set(prop, { propery: prop, value: val, priority }); } else { for (let i = 0; i < this._length; i++) { const property = this[i]; if (borderProperties.has(property)) { const value = this.getPropertyValue(property); const longhandPriority = this._priorities.get(property) ?? ""; let priority = longhandPriority; if (prop === property && typeof prior === "string") { priority = prior; } properties.set(property, { property, value, priority }); } } } const parsedProperties = prepareBorderProperties(prop, val, prior, properties, { globalObject: this._global }); for (const [property, item] of parsedProperties) { const { priority, value } = item; this._setProperty(property, value, priority); } }, enumerable: false }, _flexBoxSetter: { /** * @param {string} prop * @param {string} val * @param {string} prior * @param {string} shorthandProperty */ value(prop, val, prior, shorthandProperty) { if (!shorthandProperty || !shorthandProperties.has(shorthandProperty)) { return; } const shorthandPriority = this._priorities.get(shorthandProperty); this.removeProperty(shorthandProperty); let priority = ""; if (typeof prior === "string") { priority = prior; } else { priority = this._priorities.get(prop) ?? ""; } this.removeProperty(prop); if (shorthandPriority && priority) { this._setProperty(prop, val); } else { this._setProperty(prop, val, priority); } if (val && !hasVarFunc(val)) { const longhandValues = []; const shorthandItem = shorthandProperties.get(shorthandProperty); let hasGlobalKeyword = false; for (const [longhandProperty] of shorthandItem.shorthandFor) { if (longhandProperty === prop) { if (isGlobalKeyword(val)) { hasGlobalKeyword = true; } longhandValues.push(val); } else { const longhandValue = this.getPropertyValue(longhandProperty); const longhandPriority = this._priorities.get(longhandProperty) ?? ""; if (!longhandValue || longhandPriority !== priority) { break; } if (isGlobalKeyword(longhandValue)) { hasGlobalKeyword = true; } longhandValues.push(longhandValue); } } if (longhandValues.length === shorthandItem.shorthandFor.size) { if (hasGlobalKeyword) { const [firstValue, ...restValues] = longhandValues; if (restValues.every((value) => value === firstValue)) { this._setProperty(shorthandProperty, firstValue, priority); } } else { const parsedValue = shorthandItem.parse(longhandValues.join(" ")); const shorthandValue = Object.values(parsedValue).join(" "); this._setProperty(shorthandProperty, shorthandValue, priority); } } } }, enumerable: false }, _positionShorthandSetter: { /** * @param {string} prop * @param {Array|string} val * @param {string} prior */ value(prop, val, prior) { if (!shorthandProperties.has(prop)) { return; } const shorthandValues = []; if (Array.isArray(val)) { shorthandValues.push(...val); } else if (typeof val === "string") { shorthandValues.push(val); } else { return; } let priority = ""; if (typeof prior === "string") { priority = prior; } else { priority = this._priorities.get(prop) ?? ""; } const { position, shorthandFor } = shorthandProperties.get(prop); let hasPriority = false; for (const [longhandProperty, longhandItem] of shorthandFor) { const { position: longhandPosition } = longhandItem; const longhandValue = getPositionValue(shorthandValues, longhandPosition); if (priority) { this._setProperty(longhandProperty, longhandValue, priority); } else { const longhandPriority = this._priorities.get(longhandProperty) ?? ""; if (longhandPriority) { hasPriority = true; } else { this._setProperty(longhandProperty, longhandValue, priority); } } } if (hasPriority) { this.removeProperty(prop); } else { const shorthandValue = getPositionValue(shorthandValues, position); this._setProperty(prop, shorthandValue, priority); } }, enumerable: false }, _positionLonghandSetter: { /** * @param {string} prop * @param {string} val * @param {string} prior * @param {string} shorthandProperty */ value(prop, val, prior, shorthandProperty) { if (!shorthandProperty || !shorthandProperties.has(shorthandProperty)) { return; } const shorthandPriority = this._priorities.get(shorthandProperty); this.removeProperty(shorthandProperty); let priority = ""; if (typeof prior === "string") { priority = prior; } else { priority = this._priorities.get(prop) ?? ""; } this.removeProperty(prop); if (shorthandPriority && priority) { this._setProperty(prop, val); } else { this._setProperty(prop, val, priority); } if (val && !hasVarFunc(val)) { const longhandValues = []; const { shorthandFor, position: shorthandPosition } = shorthandProperties.get(shorthandProperty); for (const [longhandProperty] of shorthandFor) { const longhandValue = this.getPropertyValue(longhandProperty); const longhandPriority = this._priorities.get(longhandProperty) ?? ""; if (!longhandValue || longhandPriority !== priority) { return; } longhandValues.push(longhandValue); } if (longhandValues.length === shorthandFor.size) { const replacedValue = getPositionValue(longhandValues, shorthandPosition); this._setProperty(shorthandProperty, replacedValue); } } }, enumerable: false } }); // Properties Object.defineProperties(CSSStyleDeclaration.prototype, generatedProperties); // Additional properties [...allProperties, ...allExtraProperties].forEach((property) => { if (!implementedProperties.has(property)) { const declaration = getPropertyDescriptor(property); Object.defineProperty(CSSStyleDeclaration.prototype, property, declaration); const camel = dashedToCamelCase(property); Object.defineProperty(CSSStyleDeclaration.prototype, camel, declaration); if (/^webkit[A-Z]/.test(camel)) { const pascal = camel.replace(/^webkit/, "Webkit"); Object.defineProperty(CSSStyleDeclaration.prototype, pascal, declaration); } } }); module.exports = { CSSStyleDeclaration, propertyList: Object.fromEntries(implementedProperties) };