CSSStyleDeclaration.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  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 allProperties = require("./generated/allProperties");
  7. const implementedProperties = require("./generated/implementedProperties");
  8. const generatedProperties = require("./generated/properties");
  9. const {
  10. borderProperties,
  11. getPositionValue,
  12. normalizeProperties,
  13. prepareBorderProperties,
  14. prepareProperties,
  15. shorthandProperties
  16. } = require("./normalize");
  17. const {
  18. hasVarFunc,
  19. isGlobalKeyword,
  20. parseCSS,
  21. parsePropertyValue,
  22. prepareValue
  23. } = require("./parsers");
  24. const allExtraProperties = require("./utils/allExtraProperties");
  25. const { dashedToCamelCase } = require("./utils/camelize");
  26. const { getPropertyDescriptor } = require("./utils/propertyDescriptors");
  27. const { asciiLowercase } = require("./utils/strings");
  28. /**
  29. * @see https://drafts.csswg.org/cssom/#the-cssstyledeclaration-interface
  30. */
  31. class CSSStyleDeclaration {
  32. /**
  33. * @param {Function} onChangeCallback
  34. * @param {object} [opt]
  35. * @param {object} [opt.context] - Window, Element or CSSRule.
  36. */
  37. constructor(onChangeCallback, opt = {}) {
  38. // Make constructor and internals non-enumerable.
  39. Object.defineProperties(this, {
  40. constructor: {
  41. enumerable: false,
  42. writable: true
  43. },
  44. // Window
  45. _global: {
  46. value: globalThis,
  47. enumerable: false,
  48. writable: true
  49. },
  50. // Element
  51. _ownerNode: {
  52. value: null,
  53. enumerable: false,
  54. writable: true
  55. },
  56. // CSSRule
  57. _parentNode: {
  58. value: null,
  59. enumerable: false,
  60. writable: true
  61. },
  62. _onChange: {
  63. value: null,
  64. enumerable: false,
  65. writable: true
  66. },
  67. _values: {
  68. value: new Map(),
  69. enumerable: false,
  70. writable: true
  71. },
  72. _priorities: {
  73. value: new Map(),
  74. enumerable: false,
  75. writable: true
  76. },
  77. _length: {
  78. value: 0,
  79. enumerable: false,
  80. writable: true
  81. },
  82. _computed: {
  83. value: false,
  84. enumerable: false,
  85. writable: true
  86. },
  87. _readonly: {
  88. value: false,
  89. enumerable: false,
  90. writable: true
  91. },
  92. _setInProgress: {
  93. value: false,
  94. enumerable: false,
  95. writable: true
  96. }
  97. });
  98. const { context } = opt;
  99. if (context) {
  100. if (typeof context.getComputedStyle === "function") {
  101. this._global = context;
  102. this._computed = true;
  103. this._readonly = true;
  104. } else if (context.nodeType === 1 && Object.hasOwn(context, "style")) {
  105. this._global = context.ownerDocument.defaultView;
  106. this._ownerNode = context;
  107. } else if (Object.hasOwn(context, "parentRule")) {
  108. this._parentRule = context;
  109. // Find Window from the owner node of the StyleSheet.
  110. const window = context?.parentStyleSheet?.ownerNode?.ownerDocument?.defaultView;
  111. if (window) {
  112. this._global = window;
  113. }
  114. }
  115. }
  116. if (typeof onChangeCallback === "function") {
  117. this._onChange = onChangeCallback;
  118. }
  119. }
  120. get cssText() {
  121. if (this._computed) {
  122. return "";
  123. }
  124. const properties = new Map();
  125. for (let i = 0; i < this._length; i++) {
  126. const property = this[i];
  127. const value = this.getPropertyValue(property);
  128. const priority = this._priorities.get(property) ?? "";
  129. if (shorthandProperties.has(property)) {
  130. const { shorthandFor } = shorthandProperties.get(property);
  131. for (const [longhand] of shorthandFor) {
  132. if (priority || !this._priorities.get(longhand)) {
  133. properties.delete(longhand);
  134. }
  135. }
  136. }
  137. properties.set(property, { property, value, priority });
  138. }
  139. const normalizedProperties = normalizeProperties(properties);
  140. const parts = [];
  141. for (const { property, value, priority } of normalizedProperties.values()) {
  142. if (priority) {
  143. parts.push(`${property}: ${value} !${priority};`);
  144. } else {
  145. parts.push(`${property}: ${value};`);
  146. }
  147. }
  148. return parts.join(" ");
  149. }
  150. set cssText(val) {
  151. if (this._readonly) {
  152. const msg = "cssText can not be modified.";
  153. const name = "NoModificationAllowedError";
  154. throw new this._global.DOMException(msg, name);
  155. }
  156. Array.prototype.splice.call(this, 0, this._length);
  157. this._values.clear();
  158. this._priorities.clear();
  159. if (this._parentRule || (this._ownerNode && this._setInProgress)) {
  160. return;
  161. }
  162. try {
  163. this._setInProgress = true;
  164. const valueObj = parseCSS(
  165. val,
  166. {
  167. context: "declarationList",
  168. parseValue: false
  169. },
  170. true
  171. );
  172. if (valueObj?.children) {
  173. const properties = new Map();
  174. let shouldSkipNext = false;
  175. for (const item of valueObj.children) {
  176. if (item.type === "Atrule") {
  177. continue;
  178. }
  179. if (item.type === "Rule") {
  180. shouldSkipNext = true;
  181. continue;
  182. }
  183. if (shouldSkipNext === true) {
  184. shouldSkipNext = false;
  185. continue;
  186. }
  187. const {
  188. important,
  189. property,
  190. value: { value }
  191. } = item;
  192. if (typeof property === "string" && typeof value === "string") {
  193. const priority = important ? "important" : "";
  194. const isCustomProperty = property.startsWith("--");
  195. if (isCustomProperty || hasVarFunc(value)) {
  196. if (properties.has(property)) {
  197. const { priority: itemPriority } = properties.get(property);
  198. if (!itemPriority) {
  199. properties.set(property, { property, value, priority });
  200. }
  201. } else {
  202. properties.set(property, { property, value, priority });
  203. }
  204. } else {
  205. const parsedValue = parsePropertyValue(property, value, {
  206. globalObject: this._global
  207. });
  208. if (parsedValue) {
  209. if (properties.has(property)) {
  210. const { priority: itemPriority } = properties.get(property);
  211. if (!itemPriority) {
  212. properties.set(property, { property, value, priority });
  213. }
  214. } else {
  215. properties.set(property, { property, value, priority });
  216. }
  217. } else {
  218. this.removeProperty(property);
  219. }
  220. }
  221. }
  222. }
  223. const parsedProperties = prepareProperties(properties, {
  224. globalObject: this._global
  225. });
  226. for (const [property, item] of parsedProperties) {
  227. const { priority, value } = item;
  228. this._priorities.set(property, priority);
  229. this.setProperty(property, value, priority);
  230. }
  231. }
  232. } catch {
  233. return;
  234. } finally {
  235. this._setInProgress = false;
  236. }
  237. if (typeof this._onChange === "function") {
  238. this._onChange(this.cssText);
  239. }
  240. }
  241. get length() {
  242. return this._length;
  243. }
  244. // This deletes indices if the new length is less then the current length.
  245. // If the new length is more, it does nothing, the new indices will be
  246. // undefined until set.
  247. set length(len) {
  248. for (let i = len; i < this._length; i++) {
  249. delete this[i];
  250. }
  251. this._length = len;
  252. }
  253. // Readonly
  254. get parentRule() {
  255. return this._parentRule;
  256. }
  257. get cssFloat() {
  258. return this.getPropertyValue("float");
  259. }
  260. set cssFloat(value) {
  261. this._setProperty("float", value);
  262. }
  263. /**
  264. * @param {string} property
  265. */
  266. getPropertyPriority(property) {
  267. return this._priorities.get(property) || "";
  268. }
  269. /**
  270. * @param {string} property
  271. */
  272. getPropertyValue(property) {
  273. if (this._values.has(property)) {
  274. return this._values.get(property).toString();
  275. }
  276. return "";
  277. }
  278. /**
  279. * @param {...number} args
  280. */
  281. item(...args) {
  282. if (!args.length) {
  283. const msg = "1 argument required, but only 0 present.";
  284. throw new this._global.TypeError(msg);
  285. }
  286. const [value] = args;
  287. const index = parseInt(value);
  288. if (Number.isNaN(index) || index < 0 || index >= this._length) {
  289. return "";
  290. }
  291. return this[index];
  292. }
  293. /**
  294. * @param {string} property
  295. */
  296. removeProperty(property) {
  297. if (this._readonly) {
  298. const msg = `Property ${property} can not be modified.`;
  299. const name = "NoModificationAllowedError";
  300. throw new this._global.DOMException(msg, name);
  301. }
  302. if (!this._values.has(property)) {
  303. return "";
  304. }
  305. const prevValue = this._values.get(property);
  306. this._values.delete(property);
  307. this._priorities.delete(property);
  308. const index = Array.prototype.indexOf.call(this, property);
  309. if (index >= 0) {
  310. Array.prototype.splice.call(this, index, 1);
  311. if (typeof this._onChange === "function") {
  312. this._onChange(this.cssText);
  313. }
  314. }
  315. return prevValue;
  316. }
  317. /**
  318. * @param {string} prop
  319. * @param {string} val
  320. * @param {string} prior
  321. */
  322. setProperty(prop, val, prior) {
  323. if (this._readonly) {
  324. const msg = `Property ${prop} can not be modified.`;
  325. const name = "NoModificationAllowedError";
  326. throw new this._global.DOMException(msg, name);
  327. }
  328. const value = prepareValue(val);
  329. if (value === "") {
  330. this[prop] = "";
  331. this.removeProperty(prop);
  332. return;
  333. }
  334. const priority = prior === "important" ? "important" : "";
  335. const isCustomProperty = prop.startsWith("--");
  336. if (isCustomProperty) {
  337. this._setProperty(prop, value, priority);
  338. return;
  339. }
  340. const property = asciiLowercase(prop);
  341. if (!allProperties.has(property) && !allExtraProperties.has(property)) {
  342. return;
  343. }
  344. if (priority) {
  345. this._priorities.set(property, priority);
  346. } else {
  347. this._priorities.delete(property);
  348. }
  349. this[property] = value;
  350. }
  351. }
  352. // Internal methods
  353. Object.defineProperties(CSSStyleDeclaration.prototype, {
  354. _setProperty: {
  355. /**
  356. * @param {string} property
  357. * @param {string} val
  358. * @param {string} priority
  359. */
  360. value(property, val, priority) {
  361. if (typeof val !== "string") {
  362. return;
  363. }
  364. if (val === "") {
  365. this.removeProperty(property);
  366. return;
  367. }
  368. let originalText = "";
  369. if (typeof this._onChange === "function" && !this._setInProgress) {
  370. originalText = this.cssText;
  371. }
  372. if (this._values.has(property)) {
  373. const index = Array.prototype.indexOf.call(this, property);
  374. // The property already exists but is not indexed into `this` so add it.
  375. if (index < 0) {
  376. this[this._length] = property;
  377. this._length++;
  378. }
  379. } else {
  380. // New property.
  381. this[this._length] = property;
  382. this._length++;
  383. }
  384. if (priority === "important") {
  385. this._priorities.set(property, priority);
  386. } else {
  387. this._priorities.delete(property);
  388. }
  389. this._values.set(property, val);
  390. if (
  391. typeof this._onChange === "function" &&
  392. !this._setInProgress &&
  393. this.cssText !== originalText
  394. ) {
  395. this._onChange(this.cssText);
  396. }
  397. },
  398. enumerable: false
  399. },
  400. _borderSetter: {
  401. /**
  402. * @param {string} prop
  403. * @param {object|Array|string} val
  404. * @param {string} prior
  405. */
  406. value(prop, val, prior) {
  407. const properties = new Map();
  408. if (prop === "border") {
  409. let priority = "";
  410. if (typeof prior === "string") {
  411. priority = prior;
  412. } else {
  413. priority = this._priorities.get(prop) ?? "";
  414. }
  415. properties.set(prop, { propery: prop, value: val, priority });
  416. } else {
  417. for (let i = 0; i < this._length; i++) {
  418. const property = this[i];
  419. if (borderProperties.has(property)) {
  420. const value = this.getPropertyValue(property);
  421. const longhandPriority = this._priorities.get(property) ?? "";
  422. let priority = longhandPriority;
  423. if (prop === property && typeof prior === "string") {
  424. priority = prior;
  425. }
  426. properties.set(property, { property, value, priority });
  427. }
  428. }
  429. }
  430. const parsedProperties = prepareBorderProperties(prop, val, prior, properties, {
  431. globalObject: this._global
  432. });
  433. for (const [property, item] of parsedProperties) {
  434. const { priority, value } = item;
  435. this._setProperty(property, value, priority);
  436. }
  437. },
  438. enumerable: false
  439. },
  440. _flexBoxSetter: {
  441. /**
  442. * @param {string} prop
  443. * @param {string} val
  444. * @param {string} prior
  445. * @param {string} shorthandProperty
  446. */
  447. value(prop, val, prior, shorthandProperty) {
  448. if (!shorthandProperty || !shorthandProperties.has(shorthandProperty)) {
  449. return;
  450. }
  451. const shorthandPriority = this._priorities.get(shorthandProperty);
  452. this.removeProperty(shorthandProperty);
  453. let priority = "";
  454. if (typeof prior === "string") {
  455. priority = prior;
  456. } else {
  457. priority = this._priorities.get(prop) ?? "";
  458. }
  459. this.removeProperty(prop);
  460. if (shorthandPriority && priority) {
  461. this._setProperty(prop, val);
  462. } else {
  463. this._setProperty(prop, val, priority);
  464. }
  465. if (val && !hasVarFunc(val)) {
  466. const longhandValues = [];
  467. const shorthandItem = shorthandProperties.get(shorthandProperty);
  468. let hasGlobalKeyword = false;
  469. for (const [longhandProperty] of shorthandItem.shorthandFor) {
  470. if (longhandProperty === prop) {
  471. if (isGlobalKeyword(val)) {
  472. hasGlobalKeyword = true;
  473. }
  474. longhandValues.push(val);
  475. } else {
  476. const longhandValue = this.getPropertyValue(longhandProperty);
  477. const longhandPriority = this._priorities.get(longhandProperty) ?? "";
  478. if (!longhandValue || longhandPriority !== priority) {
  479. break;
  480. }
  481. if (isGlobalKeyword(longhandValue)) {
  482. hasGlobalKeyword = true;
  483. }
  484. longhandValues.push(longhandValue);
  485. }
  486. }
  487. if (longhandValues.length === shorthandItem.shorthandFor.size) {
  488. if (hasGlobalKeyword) {
  489. const [firstValue, ...restValues] = longhandValues;
  490. if (restValues.every((value) => value === firstValue)) {
  491. this._setProperty(shorthandProperty, firstValue, priority);
  492. }
  493. } else {
  494. const parsedValue = shorthandItem.parse(longhandValues.join(" "));
  495. const shorthandValue = Object.values(parsedValue).join(" ");
  496. this._setProperty(shorthandProperty, shorthandValue, priority);
  497. }
  498. }
  499. }
  500. },
  501. enumerable: false
  502. },
  503. _positionShorthandSetter: {
  504. /**
  505. * @param {string} prop
  506. * @param {Array|string} val
  507. * @param {string} prior
  508. */
  509. value(prop, val, prior) {
  510. if (!shorthandProperties.has(prop)) {
  511. return;
  512. }
  513. const shorthandValues = [];
  514. if (Array.isArray(val)) {
  515. shorthandValues.push(...val);
  516. } else if (typeof val === "string") {
  517. shorthandValues.push(val);
  518. } else {
  519. return;
  520. }
  521. let priority = "";
  522. if (typeof prior === "string") {
  523. priority = prior;
  524. } else {
  525. priority = this._priorities.get(prop) ?? "";
  526. }
  527. const { position, shorthandFor } = shorthandProperties.get(prop);
  528. let hasPriority = false;
  529. for (const [longhandProperty, longhandItem] of shorthandFor) {
  530. const { position: longhandPosition } = longhandItem;
  531. const longhandValue = getPositionValue(shorthandValues, longhandPosition);
  532. if (priority) {
  533. this._setProperty(longhandProperty, longhandValue, priority);
  534. } else {
  535. const longhandPriority = this._priorities.get(longhandProperty) ?? "";
  536. if (longhandPriority) {
  537. hasPriority = true;
  538. } else {
  539. this._setProperty(longhandProperty, longhandValue, priority);
  540. }
  541. }
  542. }
  543. if (hasPriority) {
  544. this.removeProperty(prop);
  545. } else {
  546. const shorthandValue = getPositionValue(shorthandValues, position);
  547. this._setProperty(prop, shorthandValue, priority);
  548. }
  549. },
  550. enumerable: false
  551. },
  552. _positionLonghandSetter: {
  553. /**
  554. * @param {string} prop
  555. * @param {string} val
  556. * @param {string} prior
  557. * @param {string} shorthandProperty
  558. */
  559. value(prop, val, prior, shorthandProperty) {
  560. if (!shorthandProperty || !shorthandProperties.has(shorthandProperty)) {
  561. return;
  562. }
  563. const shorthandPriority = this._priorities.get(shorthandProperty);
  564. this.removeProperty(shorthandProperty);
  565. let priority = "";
  566. if (typeof prior === "string") {
  567. priority = prior;
  568. } else {
  569. priority = this._priorities.get(prop) ?? "";
  570. }
  571. this.removeProperty(prop);
  572. if (shorthandPriority && priority) {
  573. this._setProperty(prop, val);
  574. } else {
  575. this._setProperty(prop, val, priority);
  576. }
  577. if (val && !hasVarFunc(val)) {
  578. const longhandValues = [];
  579. const { shorthandFor, position: shorthandPosition } =
  580. shorthandProperties.get(shorthandProperty);
  581. for (const [longhandProperty] of shorthandFor) {
  582. const longhandValue = this.getPropertyValue(longhandProperty);
  583. const longhandPriority = this._priorities.get(longhandProperty) ?? "";
  584. if (!longhandValue || longhandPriority !== priority) {
  585. return;
  586. }
  587. longhandValues.push(longhandValue);
  588. }
  589. if (longhandValues.length === shorthandFor.size) {
  590. const replacedValue = getPositionValue(longhandValues, shorthandPosition);
  591. this._setProperty(shorthandProperty, replacedValue);
  592. }
  593. }
  594. },
  595. enumerable: false
  596. }
  597. });
  598. // Properties
  599. Object.defineProperties(CSSStyleDeclaration.prototype, generatedProperties);
  600. // Additional properties
  601. [...allProperties, ...allExtraProperties].forEach((property) => {
  602. if (!implementedProperties.has(property)) {
  603. const declaration = getPropertyDescriptor(property);
  604. Object.defineProperty(CSSStyleDeclaration.prototype, property, declaration);
  605. const camel = dashedToCamelCase(property);
  606. Object.defineProperty(CSSStyleDeclaration.prototype, camel, declaration);
  607. if (/^webkit[A-Z]/.test(camel)) {
  608. const pascal = camel.replace(/^webkit/, "Webkit");
  609. Object.defineProperty(CSSStyleDeclaration.prototype, pascal, declaration);
  610. }
  611. }
  612. });
  613. module.exports = {
  614. CSSStyleDeclaration,
  615. propertyList: Object.fromEntries(implementedProperties)
  616. };