CSSKeyframesRule.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. //.CommonJS
  2. var CSSOM = {
  3. CSSRule: require("./CSSRule").CSSRule,
  4. CSSRuleList: require("./CSSRuleList").CSSRuleList,
  5. parse: require("./parse").parse
  6. };
  7. var errorUtils = require("./errorUtils").errorUtils;
  8. ///CommonJS
  9. /**
  10. * @constructor
  11. * @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframesRule
  12. */
  13. CSSOM.CSSKeyframesRule = function CSSKeyframesRule() {
  14. CSSOM.CSSRule.call(this);
  15. this.name = '';
  16. this.cssRules = new CSSOM.CSSRuleList();
  17. // Set up initial indexed access
  18. this._setupIndexedAccess();
  19. // Override cssRules methods after initial setup, store references as non-enumerable properties
  20. var self = this;
  21. var originalPush = this.cssRules.push;
  22. var originalSplice = this.cssRules.splice;
  23. // Create non-enumerable method overrides
  24. Object.defineProperty(this.cssRules, 'push', {
  25. value: function() {
  26. var result = originalPush.apply(this, arguments);
  27. self._setupIndexedAccess();
  28. return result;
  29. },
  30. writable: true,
  31. enumerable: false,
  32. configurable: true
  33. });
  34. Object.defineProperty(this.cssRules, 'splice', {
  35. value: function() {
  36. var result = originalSplice.apply(this, arguments);
  37. self._setupIndexedAccess();
  38. return result;
  39. },
  40. writable: true,
  41. enumerable: false,
  42. configurable: true
  43. });
  44. };
  45. CSSOM.CSSKeyframesRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  46. CSSOM.CSSKeyframesRule.prototype.constructor = CSSOM.CSSKeyframesRule;
  47. Object.setPrototypeOf(CSSOM.CSSKeyframesRule, CSSOM.CSSRule);
  48. Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "type", {
  49. value: 7,
  50. writable: false
  51. });
  52. // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframesRule.cpp
  53. Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "cssText", {
  54. get: function() {
  55. var values = "";
  56. var valuesArr = [" {"];
  57. if (this.cssRules.length) {
  58. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  59. if (rule.cssText !== "") {
  60. acc.push(rule.cssText);
  61. }
  62. return acc;
  63. }, []).join("\n "));
  64. }
  65. values = valuesArr.join("\n ") + "\n}";
  66. var cssWideKeywords = ['initial', 'inherit', 'revert', 'revert-layer', 'unset', 'none'];
  67. var processedName = cssWideKeywords.includes(this.name) ? '"' + this.name + '"' : this.name;
  68. return "@" + (this._vendorPrefix || '') + "keyframes " + processedName + values;
  69. }
  70. });
  71. /**
  72. * Appends a new keyframe rule to the list of keyframes.
  73. *
  74. * @param {string} rule - The keyframe rule string to append (e.g., "50% { opacity: 0.5; }")
  75. * @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-appendrule
  76. */
  77. CSSOM.CSSKeyframesRule.prototype.appendRule = function appendRule(rule) {
  78. if (arguments.length === 0) {
  79. errorUtils.throwMissingArguments(this, 'appendRule', 'CSSKeyframesRule');
  80. }
  81. var parsedRule;
  82. try {
  83. // Parse the rule string as a keyframe rule
  84. var tempStyleSheet = CSSOM.parse("@keyframes temp { " + rule + " }");
  85. if (tempStyleSheet.cssRules.length > 0 && tempStyleSheet.cssRules[0].cssRules.length > 0) {
  86. parsedRule = tempStyleSheet.cssRules[0].cssRules[0];
  87. } else {
  88. throw new Error("Failed to parse keyframe rule");
  89. }
  90. } catch (e) {
  91. errorUtils.throwParseError(this, 'appendRule', 'CSSKeyframesRule', rule);
  92. }
  93. parsedRule.__parentRule = this;
  94. this.cssRules.push(parsedRule);
  95. };
  96. /**
  97. * Deletes a keyframe rule that matches the specified key.
  98. *
  99. * @param {string} select - The keyframe selector to delete (e.g., "50%", "from", "to")
  100. * @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-deleterule
  101. */
  102. CSSOM.CSSKeyframesRule.prototype.deleteRule = function deleteRule(select) {
  103. if (arguments.length === 0) {
  104. errorUtils.throwMissingArguments(this, 'deleteRule', 'CSSKeyframesRule');
  105. }
  106. var normalizedSelect = this._normalizeKeyText(select);
  107. for (var i = 0; i < this.cssRules.length; i++) {
  108. var rule = this.cssRules[i];
  109. if (this._normalizeKeyText(rule.keyText) === normalizedSelect) {
  110. rule.__parentRule = null;
  111. this.cssRules.splice(i, 1);
  112. return;
  113. }
  114. }
  115. };
  116. /**
  117. * Finds and returns the keyframe rule that matches the specified key.
  118. * When multiple rules have the same key, returns the last one.
  119. *
  120. * @param {string} select - The keyframe selector to find (e.g., "50%", "from", "to")
  121. * @return {CSSKeyframeRule|null} The matching keyframe rule, or null if not found
  122. * @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-findrule
  123. */
  124. CSSOM.CSSKeyframesRule.prototype.findRule = function findRule(select) {
  125. if (arguments.length === 0) {
  126. errorUtils.throwMissingArguments(this, 'findRule', 'CSSKeyframesRule');
  127. }
  128. var normalizedSelect = this._normalizeKeyText(select);
  129. // Iterate backwards to find the last matching rule
  130. for (var i = this.cssRules.length - 1; i >= 0; i--) {
  131. var rule = this.cssRules[i];
  132. if (this._normalizeKeyText(rule.keyText) === normalizedSelect) {
  133. return rule;
  134. }
  135. }
  136. return null;
  137. };
  138. /**
  139. * Normalizes keyframe selector text for comparison.
  140. * Handles "from" -> "0%" and "to" -> "100%" conversions and trims whitespace.
  141. *
  142. * @private
  143. * @param {string} keyText - The keyframe selector text to normalize
  144. * @return {string} The normalized keyframe selector text
  145. */
  146. CSSOM.CSSKeyframesRule.prototype._normalizeKeyText = function _normalizeKeyText(keyText) {
  147. if (!keyText) return '';
  148. var normalized = keyText.toString().trim().toLowerCase();
  149. // Convert keywords to percentages for comparison
  150. if (normalized === 'from') {
  151. return '0%';
  152. } else if (normalized === 'to') {
  153. return '100%';
  154. }
  155. return normalized;
  156. };
  157. /**
  158. * Makes CSSKeyframesRule iterable over its cssRules.
  159. * Allows for...of loops and other iterable methods.
  160. */
  161. if (typeof Symbol !== 'undefined' && Symbol.iterator) {
  162. CSSOM.CSSKeyframesRule.prototype[Symbol.iterator] = function() {
  163. var index = 0;
  164. var cssRules = this.cssRules;
  165. return {
  166. next: function() {
  167. if (index < cssRules.length) {
  168. return { value: cssRules[index++], done: false };
  169. } else {
  170. return { done: true };
  171. }
  172. }
  173. };
  174. };
  175. }
  176. /**
  177. * Adds indexed getters for direct access to cssRules by index.
  178. * This enables rule[0], rule[1], etc. access patterns.
  179. * Works in environments where Proxy is not available (like jsdom).
  180. */
  181. CSSOM.CSSKeyframesRule.prototype._setupIndexedAccess = function() {
  182. // Remove any existing indexed properties
  183. for (var i = 0; i < 1000; i++) { // reasonable upper limit
  184. if (this.hasOwnProperty(i)) {
  185. delete this[i];
  186. } else {
  187. break;
  188. }
  189. }
  190. // Add indexed getters for current cssRules
  191. for (var i = 0; i < this.cssRules.length; i++) {
  192. (function(index) {
  193. Object.defineProperty(this, index, {
  194. get: function() {
  195. return this.cssRules[index];
  196. },
  197. enumerable: false,
  198. configurable: true
  199. });
  200. }.call(this, i));
  201. }
  202. // Update length property
  203. Object.defineProperty(this, 'length', {
  204. get: function() {
  205. return this.cssRules.length;
  206. },
  207. enumerable: false,
  208. configurable: true
  209. });
  210. };
  211. //.CommonJS
  212. exports.CSSKeyframesRule = CSSOM.CSSKeyframesRule;
  213. ///CommonJS