CSSStyleDeclaration.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. /**
  2. * This is a fork from the CSS Style Declaration part of
  3. * https://github.com/NV/CSSOM
  4. */
  5. "use strict";
  6. const CSSOM = require("rrweb-cssom");
  7. const allExtraProperties = require("./allExtraProperties");
  8. const allProperties = require("./generated/allProperties");
  9. const implementedProperties = require("./generated/implementedProperties");
  10. const generatedProperties = require("./generated/properties");
  11. const { hasVarFunc, parseKeyword, parseShorthand, prepareValue, splitValue } = require("./parsers");
  12. const { dashedToCamelCase } = require("./utils/camelize");
  13. const { getPropertyDescriptor } = require("./utils/propertyDescriptors");
  14. const { asciiLowercase } = require("./utils/strings");
  15. /**
  16. * @see https://drafts.csswg.org/cssom/#the-cssstyledeclaration-interface
  17. */
  18. class CSSStyleDeclaration {
  19. /**
  20. * @param {Function} onChangeCallback
  21. * @param {object} [opt]
  22. * @param {object} [opt.context] - Window, Element or CSSRule.
  23. */
  24. constructor(onChangeCallback, opt = {}) {
  25. // Make constructor and internals non-enumerable.
  26. Object.defineProperties(this, {
  27. constructor: {
  28. enumerable: false,
  29. writable: true
  30. },
  31. // Window
  32. _global: {
  33. value: globalThis,
  34. enumerable: false,
  35. writable: true
  36. },
  37. // Element
  38. _ownerNode: {
  39. value: null,
  40. enumerable: false,
  41. writable: true
  42. },
  43. // CSSRule
  44. _parentNode: {
  45. value: null,
  46. enumerable: false,
  47. writable: true
  48. },
  49. _onChange: {
  50. value: null,
  51. enumerable: false,
  52. writable: true
  53. },
  54. _values: {
  55. value: new Map(),
  56. enumerable: false,
  57. writable: true
  58. },
  59. _priorities: {
  60. value: new Map(),
  61. enumerable: false,
  62. writable: true
  63. },
  64. _length: {
  65. value: 0,
  66. enumerable: false,
  67. writable: true
  68. },
  69. _computed: {
  70. value: false,
  71. enumerable: false,
  72. writable: true
  73. },
  74. _readonly: {
  75. value: false,
  76. enumerable: false,
  77. writable: true
  78. },
  79. _setInProgress: {
  80. value: false,
  81. enumerable: false,
  82. writable: true
  83. }
  84. });
  85. const { context } = opt;
  86. if (context) {
  87. if (typeof context.getComputedStyle === "function") {
  88. this._global = context;
  89. this._computed = true;
  90. this._readonly = true;
  91. } else if (context.nodeType === 1 && Object.hasOwn(context, "style")) {
  92. this._global = context.ownerDocument.defaultView;
  93. this._ownerNode = context;
  94. } else if (Object.hasOwn(context, "parentRule")) {
  95. this._parentRule = context;
  96. // Find Window from the owner node of the StyleSheet.
  97. const window = context?.parentStyleSheet?.ownerNode?.ownerDocument?.defaultView;
  98. if (window) {
  99. this._global = window;
  100. }
  101. }
  102. }
  103. if (typeof onChangeCallback === "function") {
  104. this._onChange = onChangeCallback;
  105. }
  106. }
  107. get cssText() {
  108. if (this._computed) {
  109. return "";
  110. }
  111. const properties = [];
  112. for (let i = 0; i < this._length; i++) {
  113. const property = this[i];
  114. const value = this.getPropertyValue(property);
  115. const priority = this.getPropertyPriority(property);
  116. if (priority === "important") {
  117. properties.push(`${property}: ${value} !${priority};`);
  118. } else {
  119. properties.push(`${property}: ${value};`);
  120. }
  121. }
  122. return properties.join(" ");
  123. }
  124. set cssText(value) {
  125. if (this._readonly) {
  126. const msg = "cssText can not be modified.";
  127. const name = "NoModificationAllowedError";
  128. throw new this._global.DOMException(msg, name);
  129. }
  130. Array.prototype.splice.call(this, 0, this._length);
  131. this._values.clear();
  132. this._priorities.clear();
  133. if (this._parentRule || (this._ownerNode && this._setInProgress)) {
  134. return;
  135. }
  136. this._setInProgress = true;
  137. let dummyRule;
  138. try {
  139. dummyRule = CSSOM.parse(`#bogus{${value}}`).cssRules[0].style;
  140. } catch {
  141. // Malformed css, just return.
  142. return;
  143. }
  144. for (let i = 0; i < dummyRule.length; i++) {
  145. const property = dummyRule[i];
  146. this.setProperty(
  147. property,
  148. dummyRule.getPropertyValue(property),
  149. dummyRule.getPropertyPriority(property)
  150. );
  151. }
  152. this._setInProgress = false;
  153. if (typeof this._onChange === "function") {
  154. this._onChange(this.cssText);
  155. }
  156. }
  157. get length() {
  158. return this._length;
  159. }
  160. // This deletes indices if the new length is less then the current length.
  161. // If the new length is more, it does nothing, the new indices will be
  162. // undefined until set.
  163. set length(len) {
  164. for (let i = len; i < this._length; i++) {
  165. delete this[i];
  166. }
  167. this._length = len;
  168. }
  169. // Readonly
  170. get parentRule() {
  171. return this._parentRule;
  172. }
  173. get cssFloat() {
  174. return this.getPropertyValue("float");
  175. }
  176. set cssFloat(value) {
  177. this._setProperty("float", value);
  178. }
  179. /**
  180. * @param {string} property
  181. */
  182. getPropertyPriority(property) {
  183. return this._priorities.get(property) || "";
  184. }
  185. /**
  186. * @param {string} property
  187. */
  188. getPropertyValue(property) {
  189. if (this._values.has(property)) {
  190. return this._values.get(property).toString();
  191. }
  192. return "";
  193. }
  194. /**
  195. * @param {...number} args
  196. */
  197. item(...args) {
  198. if (!args.length) {
  199. const msg = "1 argument required, but only 0 present.";
  200. throw new this._global.TypeError(msg);
  201. }
  202. let [index] = args;
  203. index = parseInt(index);
  204. if (Number.isNaN(index) || index < 0 || index >= this._length) {
  205. return "";
  206. }
  207. return this[index];
  208. }
  209. /**
  210. * @param {string} property
  211. */
  212. removeProperty(property) {
  213. if (this._readonly) {
  214. const msg = `Property ${property} can not be modified.`;
  215. const name = "NoModificationAllowedError";
  216. throw new this._global.DOMException(msg, name);
  217. }
  218. if (!this._values.has(property)) {
  219. return "";
  220. }
  221. const prevValue = this._values.get(property);
  222. this._values.delete(property);
  223. this._priorities.delete(property);
  224. const index = Array.prototype.indexOf.call(this, property);
  225. if (index >= 0) {
  226. Array.prototype.splice.call(this, index, 1);
  227. if (typeof this._onChange === "function") {
  228. this._onChange(this.cssText);
  229. }
  230. }
  231. return prevValue;
  232. }
  233. /**
  234. * @param {string} property
  235. * @param {string} value
  236. * @param {string?} [priority] - "important" or null
  237. */
  238. setProperty(property, value, priority = null) {
  239. if (this._readonly) {
  240. const msg = `Property ${property} can not be modified.`;
  241. const name = "NoModificationAllowedError";
  242. throw new this._global.DOMException(msg, name);
  243. }
  244. value = prepareValue(value, this._global);
  245. if (value === "") {
  246. this[property] = "";
  247. this.removeProperty(property);
  248. return;
  249. }
  250. const isCustomProperty = property.startsWith("--");
  251. if (isCustomProperty) {
  252. this._setProperty(property, value);
  253. return;
  254. }
  255. property = asciiLowercase(property);
  256. if (!allProperties.has(property) && !allExtraProperties.has(property)) {
  257. return;
  258. }
  259. this[property] = value;
  260. if (priority) {
  261. this._priorities.set(property, priority);
  262. } else {
  263. this._priorities.delete(property);
  264. }
  265. }
  266. }
  267. // Internal methods
  268. Object.defineProperties(CSSStyleDeclaration.prototype, {
  269. _shorthandGetter: {
  270. /**
  271. * @param {string} property
  272. * @param {object} shorthandFor
  273. */
  274. value(property, shorthandFor) {
  275. const parts = [];
  276. for (const key of shorthandFor.keys()) {
  277. const val = this.getPropertyValue(key);
  278. if (hasVarFunc(val)) {
  279. return "";
  280. }
  281. if (val !== "") {
  282. parts.push(val);
  283. }
  284. }
  285. if (parts.length) {
  286. return parts.join(" ");
  287. }
  288. if (this._values.has(property)) {
  289. return this.getPropertyValue(property);
  290. }
  291. return "";
  292. },
  293. enumerable: false
  294. },
  295. _implicitGetter: {
  296. /**
  297. * @param {string} property
  298. * @param {Array.<string>} positions
  299. */
  300. value(property, positions = []) {
  301. const parts = [];
  302. for (const position of positions) {
  303. const val = this.getPropertyValue(`${property}-${position}`);
  304. if (val === "" || hasVarFunc(val)) {
  305. return "";
  306. }
  307. parts.push(val);
  308. }
  309. if (!parts.length) {
  310. return "";
  311. }
  312. switch (positions.length) {
  313. case 4: {
  314. const [top, right, bottom, left] = parts;
  315. if (top === right && top === bottom && right === left) {
  316. return top;
  317. }
  318. if (top !== right && top === bottom && right === left) {
  319. return `${top} ${right}`;
  320. }
  321. if (top !== right && top !== bottom && right === left) {
  322. return `${top} ${right} ${bottom}`;
  323. }
  324. return `${top} ${right} ${bottom} ${left}`;
  325. }
  326. case 2: {
  327. const [x, y] = parts;
  328. if (x === y) {
  329. return x;
  330. }
  331. return `${x} ${y}`;
  332. }
  333. default:
  334. return "";
  335. }
  336. },
  337. enumerable: false
  338. },
  339. _setProperty: {
  340. /**
  341. * @param {string} property
  342. * @param {string} val
  343. * @param {string?} [priority]
  344. */
  345. value(property, val, priority = null) {
  346. if (typeof val !== "string") {
  347. return;
  348. }
  349. if (val === "") {
  350. this.removeProperty(property);
  351. return;
  352. }
  353. let originalText = "";
  354. if (typeof this._onChange === "function") {
  355. originalText = this.cssText;
  356. }
  357. if (this._values.has(property)) {
  358. const index = Array.prototype.indexOf.call(this, property);
  359. // The property already exists but is not indexed into `this` so add it.
  360. if (index < 0) {
  361. this[this._length] = property;
  362. this._length++;
  363. }
  364. } else {
  365. // New property.
  366. this[this._length] = property;
  367. this._length++;
  368. }
  369. this._values.set(property, val);
  370. if (priority) {
  371. this._priorities.set(property, priority);
  372. } else {
  373. this._priorities.delete(property);
  374. }
  375. if (
  376. typeof this._onChange === "function" &&
  377. this.cssText !== originalText &&
  378. !this._setInProgress
  379. ) {
  380. this._onChange(this.cssText);
  381. }
  382. },
  383. enumerable: false
  384. },
  385. _shorthandSetter: {
  386. /**
  387. * @param {string} property
  388. * @param {string} val
  389. * @param {object} shorthandFor
  390. */
  391. value(property, val, shorthandFor) {
  392. val = prepareValue(val, this._global);
  393. const obj = parseShorthand(val, shorthandFor);
  394. if (!obj) {
  395. return;
  396. }
  397. for (const subprop of Object.keys(obj)) {
  398. // In case subprop is an implicit property, this will clear *its*
  399. // subpropertiesX.
  400. const camel = dashedToCamelCase(subprop);
  401. this[camel] = obj[subprop];
  402. // In case it gets translated into something else (0 -> 0px).
  403. obj[subprop] = this[camel];
  404. this.removeProperty(subprop);
  405. // Don't add in empty properties.
  406. if (obj[subprop] !== "") {
  407. this._values.set(subprop, obj[subprop]);
  408. }
  409. }
  410. for (const [subprop] of shorthandFor) {
  411. if (!Object.hasOwn(obj, subprop)) {
  412. this.removeProperty(subprop);
  413. this._values.delete(subprop);
  414. }
  415. }
  416. // In case the value is something like 'none' that removes all values,
  417. // check that the generated one is not empty, first remove the property,
  418. // if it already exists, then call the shorthandGetter, if it's an empty
  419. // string, don't set the property.
  420. this.removeProperty(property);
  421. const calculated = this._shorthandGetter(property, shorthandFor);
  422. if (calculated !== "") {
  423. this._setProperty(property, calculated);
  424. }
  425. return obj;
  426. },
  427. enumerable: false
  428. },
  429. // Companion to shorthandSetter, but for the individual parts which takes
  430. // position value in the middle.
  431. _midShorthandSetter: {
  432. /**
  433. * @param {string} property
  434. * @param {string} val
  435. * @param {object} shorthandFor
  436. * @param {Array.<string>} positions
  437. */
  438. value(property, val, shorthandFor, positions = []) {
  439. val = prepareValue(val, this._global);
  440. const obj = this._shorthandSetter(property, val, shorthandFor);
  441. if (!obj) {
  442. return;
  443. }
  444. for (const position of positions) {
  445. this.removeProperty(`${property}-${position}`);
  446. this._values.set(`${property}-${position}`, val);
  447. }
  448. },
  449. enumerable: false
  450. },
  451. _implicitSetter: {
  452. /**
  453. * @param {string} prefix
  454. * @param {string} part
  455. * @param {string} val
  456. * @param {Function} isValid
  457. * @param {Function} parser
  458. * @param {Array.<string>} positions
  459. */
  460. value(prefix, part, val, isValid, parser, positions = []) {
  461. val = prepareValue(val, this._global);
  462. if (typeof val !== "string") {
  463. return;
  464. }
  465. part ||= "";
  466. if (part) {
  467. part = `-${part}`;
  468. }
  469. let parts = [];
  470. if (val === "") {
  471. parts.push(val);
  472. } else {
  473. const key = parseKeyword(val);
  474. if (key) {
  475. parts.push(key);
  476. } else {
  477. parts.push(...splitValue(val));
  478. }
  479. }
  480. if (!parts.length || parts.length > positions.length || !parts.every(isValid)) {
  481. return;
  482. }
  483. parts = parts.map((p) => parser(p));
  484. this._setProperty(`${prefix}${part}`, parts.join(" "));
  485. switch (positions.length) {
  486. case 4:
  487. if (parts.length === 1) {
  488. parts.push(parts[0], parts[0], parts[0]);
  489. } else if (parts.length === 2) {
  490. parts.push(parts[0], parts[1]);
  491. } else if (parts.length === 3) {
  492. parts.push(parts[1]);
  493. }
  494. break;
  495. case 2:
  496. if (parts.length === 1) {
  497. parts.push(parts[0]);
  498. }
  499. break;
  500. default:
  501. }
  502. for (let i = 0; i < positions.length; i++) {
  503. const property = `${prefix}-${positions[i]}${part}`;
  504. this.removeProperty(property);
  505. this._values.set(property, parts[i]);
  506. }
  507. },
  508. enumerable: false
  509. },
  510. // Companion to implicitSetter, but for the individual parts.
  511. // This sets the individual value, and checks to see if all sub-parts are
  512. // set. If so, it sets the shorthand version and removes the individual parts
  513. // from the cssText.
  514. _subImplicitSetter: {
  515. /**
  516. * @param {string} prefix
  517. * @param {string} part
  518. * @param {string} val
  519. * @param {Function} isValid
  520. * @param {Function} parser
  521. * @param {Array.<string>} positions
  522. */
  523. value(prefix, part, val, isValid, parser, positions = []) {
  524. val = prepareValue(val, this._global);
  525. if (typeof val !== "string" || !isValid(val)) {
  526. return;
  527. }
  528. val = parser(val);
  529. const property = `${prefix}-${part}`;
  530. this._setProperty(property, val);
  531. const combinedPriority = this.getPropertyPriority(prefix);
  532. const subparts = [];
  533. for (const position of positions) {
  534. subparts.push(`${prefix}-${position}`);
  535. }
  536. const parts = subparts.map((subpart) => this._values.get(subpart));
  537. const priorities = subparts.map((subpart) => this.getPropertyPriority(subpart));
  538. const [priority] = priorities;
  539. // Combine into a single property if all values are set and have the same
  540. // priority.
  541. if (
  542. priority === combinedPriority &&
  543. parts.every((p) => p) &&
  544. priorities.every((p) => p === priority)
  545. ) {
  546. for (let i = 0; i < subparts.length; i++) {
  547. this.removeProperty(subparts[i]);
  548. this._values.set(subparts[i], parts[i]);
  549. }
  550. this._setProperty(prefix, parts.join(" "), priority);
  551. } else {
  552. this.removeProperty(prefix);
  553. for (let i = 0; i < subparts.length; i++) {
  554. // The property we're setting won't be important, the rest will either
  555. // keep their priority or inherit it from the combined property
  556. const subPriority = subparts[i] === property ? "" : priorities[i] || combinedPriority;
  557. this._setProperty(subparts[i], parts[i], subPriority);
  558. }
  559. }
  560. },
  561. enumerable: false
  562. }
  563. });
  564. // Properties
  565. Object.defineProperties(CSSStyleDeclaration.prototype, generatedProperties);
  566. // Additional properties
  567. [...allProperties, ...allExtraProperties].forEach(function (property) {
  568. if (!implementedProperties.has(property)) {
  569. const declaration = getPropertyDescriptor(property);
  570. Object.defineProperty(CSSStyleDeclaration.prototype, property, declaration);
  571. const camel = dashedToCamelCase(property);
  572. Object.defineProperty(CSSStyleDeclaration.prototype, camel, declaration);
  573. if (/^webkit[A-Z]/.test(camel)) {
  574. const pascal = camel.replace(/^webkit/, "Webkit");
  575. Object.defineProperty(CSSStyleDeclaration.prototype, pascal, declaration);
  576. }
  577. }
  578. });
  579. exports.CSSStyleDeclaration = CSSStyleDeclaration;