123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- /**
- * This is a fork from the CSS Style Declaration part of
- * https://github.com/NV/CSSOM
- */
- "use strict";
- const CSSOM = require("rrweb-cssom");
- const allExtraProperties = require("./allExtraProperties");
- const allProperties = require("./generated/allProperties");
- const implementedProperties = require("./generated/implementedProperties");
- const generatedProperties = require("./generated/properties");
- const { hasVarFunc, parseKeyword, parseShorthand, prepareValue, splitValue } = require("./parsers");
- 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 = [];
- for (let i = 0; i < this._length; i++) {
- const property = this[i];
- const value = this.getPropertyValue(property);
- const priority = this.getPropertyPriority(property);
- if (priority === "important") {
- properties.push(`${property}: ${value} !${priority};`);
- } else {
- properties.push(`${property}: ${value};`);
- }
- }
- return properties.join(" ");
- }
- set cssText(value) {
- 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;
- }
- this._setInProgress = true;
- let dummyRule;
- try {
- dummyRule = CSSOM.parse(`#bogus{${value}}`).cssRules[0].style;
- } catch {
- // Malformed css, just return.
- return;
- }
- for (let i = 0; i < dummyRule.length; i++) {
- const property = dummyRule[i];
- this.setProperty(
- property,
- dummyRule.getPropertyValue(property),
- dummyRule.getPropertyPriority(property)
- );
- }
- 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);
- }
- let [index] = args;
- index = parseInt(index);
- 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} property
- * @param {string} value
- * @param {string?} [priority] - "important" or null
- */
- setProperty(property, value, priority = null) {
- if (this._readonly) {
- const msg = `Property ${property} can not be modified.`;
- const name = "NoModificationAllowedError";
- throw new this._global.DOMException(msg, name);
- }
- value = prepareValue(value, this._global);
- if (value === "") {
- this[property] = "";
- this.removeProperty(property);
- return;
- }
- const isCustomProperty = property.startsWith("--");
- if (isCustomProperty) {
- this._setProperty(property, value);
- return;
- }
- property = asciiLowercase(property);
- if (!allProperties.has(property) && !allExtraProperties.has(property)) {
- return;
- }
- this[property] = value;
- if (priority) {
- this._priorities.set(property, priority);
- } else {
- this._priorities.delete(property);
- }
- }
- }
- // Internal methods
- Object.defineProperties(CSSStyleDeclaration.prototype, {
- _shorthandGetter: {
- /**
- * @param {string} property
- * @param {object} shorthandFor
- */
- value(property, shorthandFor) {
- const parts = [];
- for (const key of shorthandFor.keys()) {
- const val = this.getPropertyValue(key);
- if (hasVarFunc(val)) {
- return "";
- }
- if (val !== "") {
- parts.push(val);
- }
- }
- if (parts.length) {
- return parts.join(" ");
- }
- if (this._values.has(property)) {
- return this.getPropertyValue(property);
- }
- return "";
- },
- enumerable: false
- },
- _implicitGetter: {
- /**
- * @param {string} property
- * @param {Array.<string>} positions
- */
- value(property, positions = []) {
- const parts = [];
- for (const position of positions) {
- const val = this.getPropertyValue(`${property}-${position}`);
- if (val === "" || hasVarFunc(val)) {
- return "";
- }
- parts.push(val);
- }
- if (!parts.length) {
- return "";
- }
- switch (positions.length) {
- case 4: {
- const [top, right, bottom, left] = parts;
- if (top === right && top === bottom && right === left) {
- return top;
- }
- if (top !== right && top === bottom && right === left) {
- return `${top} ${right}`;
- }
- if (top !== right && top !== bottom && right === left) {
- return `${top} ${right} ${bottom}`;
- }
- return `${top} ${right} ${bottom} ${left}`;
- }
- case 2: {
- const [x, y] = parts;
- if (x === y) {
- return x;
- }
- return `${x} ${y}`;
- }
- default:
- return "";
- }
- },
- enumerable: false
- },
- _setProperty: {
- /**
- * @param {string} property
- * @param {string} val
- * @param {string?} [priority]
- */
- value(property, val, priority = null) {
- if (typeof val !== "string") {
- return;
- }
- if (val === "") {
- this.removeProperty(property);
- return;
- }
- let originalText = "";
- if (typeof this._onChange === "function") {
- 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++;
- }
- this._values.set(property, val);
- if (priority) {
- this._priorities.set(property, priority);
- } else {
- this._priorities.delete(property);
- }
- if (
- typeof this._onChange === "function" &&
- this.cssText !== originalText &&
- !this._setInProgress
- ) {
- this._onChange(this.cssText);
- }
- },
- enumerable: false
- },
- _shorthandSetter: {
- /**
- * @param {string} property
- * @param {string} val
- * @param {object} shorthandFor
- */
- value(property, val, shorthandFor) {
- val = prepareValue(val, this._global);
- const obj = parseShorthand(val, shorthandFor);
- if (!obj) {
- return;
- }
- for (const subprop of Object.keys(obj)) {
- // In case subprop is an implicit property, this will clear *its*
- // subpropertiesX.
- const camel = dashedToCamelCase(subprop);
- this[camel] = obj[subprop];
- // In case it gets translated into something else (0 -> 0px).
- obj[subprop] = this[camel];
- this.removeProperty(subprop);
- // Don't add in empty properties.
- if (obj[subprop] !== "") {
- this._values.set(subprop, obj[subprop]);
- }
- }
- for (const [subprop] of shorthandFor) {
- if (!Object.hasOwn(obj, subprop)) {
- this.removeProperty(subprop);
- this._values.delete(subprop);
- }
- }
- // In case the value is something like 'none' that removes all values,
- // check that the generated one is not empty, first remove the property,
- // if it already exists, then call the shorthandGetter, if it's an empty
- // string, don't set the property.
- this.removeProperty(property);
- const calculated = this._shorthandGetter(property, shorthandFor);
- if (calculated !== "") {
- this._setProperty(property, calculated);
- }
- return obj;
- },
- enumerable: false
- },
- // Companion to shorthandSetter, but for the individual parts which takes
- // position value in the middle.
- _midShorthandSetter: {
- /**
- * @param {string} property
- * @param {string} val
- * @param {object} shorthandFor
- * @param {Array.<string>} positions
- */
- value(property, val, shorthandFor, positions = []) {
- val = prepareValue(val, this._global);
- const obj = this._shorthandSetter(property, val, shorthandFor);
- if (!obj) {
- return;
- }
- for (const position of positions) {
- this.removeProperty(`${property}-${position}`);
- this._values.set(`${property}-${position}`, val);
- }
- },
- enumerable: false
- },
- _implicitSetter: {
- /**
- * @param {string} prefix
- * @param {string} part
- * @param {string} val
- * @param {Function} isValid
- * @param {Function} parser
- * @param {Array.<string>} positions
- */
- value(prefix, part, val, isValid, parser, positions = []) {
- val = prepareValue(val, this._global);
- if (typeof val !== "string") {
- return;
- }
- part ||= "";
- if (part) {
- part = `-${part}`;
- }
- let parts = [];
- if (val === "") {
- parts.push(val);
- } else {
- const key = parseKeyword(val);
- if (key) {
- parts.push(key);
- } else {
- parts.push(...splitValue(val));
- }
- }
- if (!parts.length || parts.length > positions.length || !parts.every(isValid)) {
- return;
- }
- parts = parts.map((p) => parser(p));
- this._setProperty(`${prefix}${part}`, parts.join(" "));
- switch (positions.length) {
- case 4:
- if (parts.length === 1) {
- parts.push(parts[0], parts[0], parts[0]);
- } else if (parts.length === 2) {
- parts.push(parts[0], parts[1]);
- } else if (parts.length === 3) {
- parts.push(parts[1]);
- }
- break;
- case 2:
- if (parts.length === 1) {
- parts.push(parts[0]);
- }
- break;
- default:
- }
- for (let i = 0; i < positions.length; i++) {
- const property = `${prefix}-${positions[i]}${part}`;
- this.removeProperty(property);
- this._values.set(property, parts[i]);
- }
- },
- enumerable: false
- },
- // Companion to implicitSetter, but for the individual parts.
- // This sets the individual value, and checks to see if all sub-parts are
- // set. If so, it sets the shorthand version and removes the individual parts
- // from the cssText.
- _subImplicitSetter: {
- /**
- * @param {string} prefix
- * @param {string} part
- * @param {string} val
- * @param {Function} isValid
- * @param {Function} parser
- * @param {Array.<string>} positions
- */
- value(prefix, part, val, isValid, parser, positions = []) {
- val = prepareValue(val, this._global);
- if (typeof val !== "string" || !isValid(val)) {
- return;
- }
- val = parser(val);
- const property = `${prefix}-${part}`;
- this._setProperty(property, val);
- const combinedPriority = this.getPropertyPriority(prefix);
- const subparts = [];
- for (const position of positions) {
- subparts.push(`${prefix}-${position}`);
- }
- const parts = subparts.map((subpart) => this._values.get(subpart));
- const priorities = subparts.map((subpart) => this.getPropertyPriority(subpart));
- const [priority] = priorities;
- // Combine into a single property if all values are set and have the same
- // priority.
- if (
- priority === combinedPriority &&
- parts.every((p) => p) &&
- priorities.every((p) => p === priority)
- ) {
- for (let i = 0; i < subparts.length; i++) {
- this.removeProperty(subparts[i]);
- this._values.set(subparts[i], parts[i]);
- }
- this._setProperty(prefix, parts.join(" "), priority);
- } else {
- this.removeProperty(prefix);
- for (let i = 0; i < subparts.length; i++) {
- // The property we're setting won't be important, the rest will either
- // keep their priority or inherit it from the combined property
- const subPriority = subparts[i] === property ? "" : priorities[i] || combinedPriority;
- this._setProperty(subparts[i], parts[i], subPriority);
- }
- }
- },
- enumerable: false
- }
- });
- // Properties
- Object.defineProperties(CSSStyleDeclaration.prototype, generatedProperties);
- // Additional properties
- [...allProperties, ...allExtraProperties].forEach(function (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);
- }
- }
- });
- exports.CSSStyleDeclaration = CSSStyleDeclaration;
|