CSSStyleSheet.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. //.CommonJS
  2. var CSSOM = {
  3. MediaList: require("./MediaList").MediaList,
  4. StyleSheet: require("./StyleSheet").StyleSheet,
  5. CSSRuleList: require("./CSSRuleList").CSSRuleList,
  6. CSSStyleRule: require("./CSSStyleRule").CSSStyleRule,
  7. };
  8. var errorUtils = require("./errorUtils").errorUtils;
  9. ///CommonJS
  10. /**
  11. * @constructor
  12. * @param {CSSStyleSheetInit} [opts] - CSSStyleSheetInit options.
  13. * @param {string} [opts.baseURL] - The base URL of the stylesheet.
  14. * @param {boolean} [opts.disabled] - The disabled attribute of the stylesheet.
  15. * @param {MediaList | string} [opts.media] - The media attribute of the stylesheet.
  16. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet
  17. */
  18. CSSOM.CSSStyleSheet = function CSSStyleSheet(opts) {
  19. CSSOM.StyleSheet.call(this);
  20. this.__constructed = true;
  21. this.__cssRules = new CSSOM.CSSRuleList();
  22. this.__ownerRule = null;
  23. if (opts && typeof opts === "object") {
  24. if (opts.baseURL && typeof opts.baseURL === "string") {
  25. this.__baseURL = opts.baseURL;
  26. }
  27. if (opts.media && typeof opts.media === "string") {
  28. this.media.mediaText = opts.media;
  29. }
  30. if (typeof opts.disabled === "boolean") {
  31. this.disabled = opts.disabled;
  32. }
  33. }
  34. };
  35. CSSOM.CSSStyleSheet.prototype = Object.create(CSSOM.StyleSheet.prototype);
  36. CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet;
  37. Object.setPrototypeOf(CSSOM.CSSStyleSheet, CSSOM.StyleSheet);
  38. Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "cssRules", {
  39. get: function() {
  40. return this.__cssRules;
  41. }
  42. });
  43. Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "rules", {
  44. get: function() {
  45. return this.__cssRules;
  46. }
  47. });
  48. Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "ownerRule", {
  49. get: function() {
  50. return this.__ownerRule;
  51. }
  52. });
  53. /**
  54. * Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade.
  55. *
  56. * sheet = new Sheet("body {margin: 0}")
  57. * sheet.toString()
  58. * -> "body{margin:0;}"
  59. * sheet.insertRule("img {border: none}", 0)
  60. * -> 0
  61. * sheet.toString()
  62. * -> "img{border:none;}body{margin:0;}"
  63. *
  64. * @param {string} rule
  65. * @param {number} [index=0]
  66. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-insertRule
  67. * @return {number} The index within the style sheet's rule collection of the newly inserted rule.
  68. */
  69. CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
  70. if (rule === undefined && index === undefined) {
  71. errorUtils.throwMissingArguments(this, 'insertRule', this.constructor.name);
  72. }
  73. if (index === void 0) {
  74. index = 0;
  75. }
  76. index = Number(index);
  77. if (index < 0) {
  78. index = 4294967296 + index;
  79. }
  80. if (index > this.cssRules.length) {
  81. errorUtils.throwIndexError(this, 'insertRule', this.constructor.name, index, this.cssRules.length);
  82. }
  83. var ruleToParse = String(rule);
  84. var parseErrors = [];
  85. var parsedSheet = CSSOM.parse(ruleToParse, undefined, function(err) {
  86. parseErrors.push(err);
  87. } );
  88. if (parsedSheet.cssRules.length !== 1) {
  89. errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
  90. }
  91. var cssRule = parsedSheet.cssRules[0];
  92. // Helper function to find the last index of a specific rule constructor
  93. function findLastIndexOfConstructor(rules, constructorName) {
  94. for (var i = rules.length - 1; i >= 0; i--) {
  95. if (rules[i].constructor.name === constructorName) {
  96. return i;
  97. }
  98. }
  99. return -1;
  100. }
  101. // Helper function to find the first index of a rule that's NOT of specified constructors
  102. function findFirstNonConstructorIndex(rules, constructorNames) {
  103. for (var i = 0; i < rules.length; i++) {
  104. if (constructorNames.indexOf(rules[i].constructor.name) === -1) {
  105. return i;
  106. }
  107. }
  108. return rules.length;
  109. }
  110. // Validate rule ordering based on CSS specification
  111. if (cssRule.constructor.name === 'CSSImportRule') {
  112. if (this.__constructed === true) {
  113. errorUtils.throwError(this, 'DOMException',
  114. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Can't insert @import rules into a constructed stylesheet.",
  115. 'SyntaxError');
  116. }
  117. // @import rules cannot be inserted after @layer rules that already exist
  118. // They can only be inserted at the beginning or after other @import rules
  119. var firstLayerIndex = findFirstNonConstructorIndex(this.cssRules, ['CSSImportRule']);
  120. if (firstLayerIndex < this.cssRules.length && this.cssRules[firstLayerIndex].constructor.name === 'CSSLayerStatementRule' && index > firstLayerIndex) {
  121. errorUtils.throwError(this, 'DOMException',
  122. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  123. 'HierarchyRequestError');
  124. }
  125. // Also cannot insert after @namespace or other rules
  126. var firstNonImportIndex = findFirstNonConstructorIndex(this.cssRules, ['CSSImportRule']);
  127. if (index > firstNonImportIndex && firstNonImportIndex < this.cssRules.length &&
  128. this.cssRules[firstNonImportIndex].constructor.name !== 'CSSLayerStatementRule') {
  129. errorUtils.throwError(this, 'DOMException',
  130. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  131. 'HierarchyRequestError');
  132. }
  133. } else if (cssRule.constructor.name === 'CSSNamespaceRule') {
  134. // @namespace rules can come after @layer and @import, but before any other rules
  135. // They cannot come before @import rules
  136. var firstImportIndex = -1;
  137. for (var i = 0; i < this.cssRules.length; i++) {
  138. if (this.cssRules[i].constructor.name === 'CSSImportRule') {
  139. firstImportIndex = i;
  140. break;
  141. }
  142. }
  143. var firstNonImportNamespaceIndex = findFirstNonConstructorIndex(this.cssRules, [
  144. 'CSSLayerStatementRule',
  145. 'CSSImportRule',
  146. 'CSSNamespaceRule'
  147. ]);
  148. // Cannot insert before @import rules
  149. if (firstImportIndex !== -1 && index <= firstImportIndex) {
  150. errorUtils.throwError(this, 'DOMException',
  151. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  152. 'HierarchyRequestError');
  153. }
  154. // Cannot insert if there are already non-special rules
  155. if (firstNonImportNamespaceIndex < this.cssRules.length) {
  156. errorUtils.throwError(this, 'DOMException',
  157. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  158. 'InvalidStateError');
  159. }
  160. // Cannot insert after other types of rules
  161. if (index > firstNonImportNamespaceIndex) {
  162. errorUtils.throwError(this, 'DOMException',
  163. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  164. 'HierarchyRequestError');
  165. }
  166. } else if (cssRule.constructor.name === 'CSSLayerStatementRule') {
  167. // @layer statement rules can be inserted anywhere before @import and @namespace
  168. // No additional restrictions beyond what's already handled
  169. } else {
  170. // Any other rule cannot be inserted before @import and @namespace
  171. var firstNonSpecialRuleIndex = findFirstNonConstructorIndex(this.cssRules, [
  172. 'CSSLayerStatementRule',
  173. 'CSSImportRule',
  174. 'CSSNamespaceRule'
  175. ]);
  176. if (index < firstNonSpecialRuleIndex) {
  177. errorUtils.throwError(this, 'DOMException',
  178. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  179. 'HierarchyRequestError');
  180. }
  181. if (parseErrors.filter(function(error) { return !error.isNested; }).length !== 0) {
  182. errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
  183. }
  184. }
  185. cssRule.__parentStyleSheet = this;
  186. this.cssRules.splice(index, 0, cssRule);
  187. return index;
  188. };
  189. CSSOM.CSSStyleSheet.prototype.addRule = function(selector, styleBlock, index) {
  190. if (index === void 0) {
  191. index = this.cssRules.length;
  192. }
  193. this.insertRule(selector + "{" + styleBlock + "}", index);
  194. return -1;
  195. };
  196. /**
  197. * Used to delete a rule from the style sheet.
  198. *
  199. * sheet = new Sheet("img{border:none} body{margin:0}")
  200. * sheet.toString()
  201. * -> "img{border:none;}body{margin:0;}"
  202. * sheet.deleteRule(0)
  203. * sheet.toString()
  204. * -> "body{margin:0;}"
  205. *
  206. * @param {number} index within the style sheet's rule list of the rule to remove.
  207. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-deleteRule
  208. */
  209. CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) {
  210. if (index === undefined) {
  211. errorUtils.throwMissingArguments(this, 'deleteRule', this.constructor.name);
  212. }
  213. index = Number(index);
  214. if (index < 0) {
  215. index = 4294967296 + index;
  216. }
  217. if (index >= this.cssRules.length) {
  218. errorUtils.throwIndexError(this, 'deleteRule', this.constructor.name, index, this.cssRules.length);
  219. }
  220. if (this.cssRules[index]) {
  221. if (this.cssRules[index].constructor.name == "CSSNamespaceRule") {
  222. var shouldContinue = this.cssRules.every(function (rule) {
  223. return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
  224. });
  225. if (!shouldContinue) {
  226. errorUtils.throwError(this, 'DOMException', "Failed to execute 'deleteRule' on '" + this.constructor.name + "': Failed to delete rule.", "InvalidStateError");
  227. }
  228. }
  229. if (this.cssRules[index].constructor.name == "CSSImportRule") {
  230. this.cssRules[index].styleSheet.__parentStyleSheet = null;
  231. }
  232. this.cssRules[index].__parentStyleSheet = null;
  233. }
  234. this.cssRules.splice(index, 1);
  235. };
  236. CSSOM.CSSStyleSheet.prototype.removeRule = function(index) {
  237. if (index === void 0) {
  238. index = 0;
  239. }
  240. this.deleteRule(index);
  241. };
  242. /**
  243. * Replaces the rules of a {@link CSSStyleSheet}
  244. *
  245. * @returns a promise
  246. * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replace
  247. */
  248. CSSOM.CSSStyleSheet.prototype.replace = function(text) {
  249. var _Promise;
  250. if (this.__globalObject && this.__globalObject['Promise']) {
  251. _Promise = this.__globalObject['Promise'];
  252. } else {
  253. _Promise = Promise;
  254. }
  255. var _setTimeout;
  256. if (this.__globalObject && this.__globalObject['setTimeout']) {
  257. _setTimeout = this.__globalObject['setTimeout'];
  258. } else {
  259. _setTimeout = setTimeout;
  260. }
  261. var sheet = this;
  262. return new _Promise(function (resolve, reject) {
  263. // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException.
  264. if (!sheet.__constructed || sheet.__disallowModification) {
  265. reject(errorUtils.createError(sheet, 'DOMException',
  266. "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.",
  267. 'NotAllowedError'));
  268. }
  269. // Set the disallow modification flag.
  270. sheet.__disallowModification = true;
  271. // In parallel, do these steps:
  272. _setTimeout(function() {
  273. // Let rules be the result of running parse a stylesheet's contents from text.
  274. var rules = new CSSOM.CSSRuleList();
  275. CSSOM.parse(text, { styleSheet: sheet, cssRules: rules });
  276. // If rules contains one or more @import rules, remove those rules from rules.
  277. var i = 0;
  278. while (i < rules.length) {
  279. if (rules[i].constructor.name === 'CSSImportRule') {
  280. rules.splice(i, 1);
  281. } else {
  282. i++;
  283. }
  284. }
  285. // Set sheet's CSS rules to rules.
  286. sheet.__cssRules.splice.apply(sheet.__cssRules, [0, sheet.__cssRules.length].concat(rules));
  287. // Unset sheet’s disallow modification flag.
  288. delete sheet.__disallowModification;
  289. // Resolve promise with sheet.
  290. resolve(sheet);
  291. })
  292. });
  293. }
  294. /**
  295. * Synchronously replaces the rules of a {@link CSSStyleSheet}
  296. *
  297. * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replacesync
  298. */
  299. CSSOM.CSSStyleSheet.prototype.replaceSync = function(text) {
  300. var sheet = this;
  301. // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException.
  302. if (!sheet.__constructed || sheet.__disallowModification) {
  303. errorUtils.throwError(sheet, 'DOMException',
  304. "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.",
  305. 'NotAllowedError');
  306. }
  307. // Let rules be the result of running parse a stylesheet's contents from text.
  308. var rules = new CSSOM.CSSRuleList();
  309. CSSOM.parse(text, { styleSheet: sheet, cssRules: rules });
  310. // If rules contains one or more @import rules, remove those rules from rules.
  311. var i = 0;
  312. while (i < rules.length) {
  313. if (rules[i].constructor.name === 'CSSImportRule') {
  314. rules.splice(i, 1);
  315. } else {
  316. i++;
  317. }
  318. }
  319. // Set sheet's CSS rules to rules.
  320. sheet.__cssRules.splice.apply(sheet.__cssRules, [0, sheet.__cssRules.length].concat(rules));
  321. }
  322. /**
  323. * NON-STANDARD
  324. * @return {string} serialize stylesheet
  325. */
  326. CSSOM.CSSStyleSheet.prototype.toString = function() {
  327. var result = "";
  328. var rules = this.cssRules;
  329. for (var i=0; i<rules.length; i++) {
  330. result += rules[i].cssText + "\n";
  331. }
  332. return result;
  333. };
  334. //.CommonJS
  335. exports.CSSStyleSheet = CSSOM.CSSStyleSheet;
  336. CSSOM.parse = require('./parse').parse; // Cannot be included sooner due to the mutual dependency between parse.js and CSSStyleSheet.js
  337. ///CommonJS