cssom.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview CSS Object Model helper functions.
  16. * References:
  17. * - W3C: http://dev.w3.org/csswg/cssom/
  18. * - MSDN: http://msdn.microsoft.com/en-us/library/ms531209(VS.85).aspx.
  19. * TODO(user): Consider hacking page, media, etc.. to work.
  20. * This would be pretty challenging. IE returns the text for any rule
  21. * regardless of whether or not the media is correct or not. Firefox at
  22. * least supports CSSRule.type to figure out if it's a media type and then
  23. * we could do something interesting, but IE offers no way for us to tell.
  24. */
  25. goog.provide('goog.cssom');
  26. goog.provide('goog.cssom.CssRuleType');
  27. goog.require('goog.array');
  28. goog.require('goog.dom');
  29. goog.require('goog.dom.TagName');
  30. /**
  31. * Enumeration of {@code CSSRule} types.
  32. * @enum {number}
  33. */
  34. goog.cssom.CssRuleType = {
  35. STYLE: 1,
  36. IMPORT: 3,
  37. MEDIA: 4,
  38. FONT_FACE: 5,
  39. PAGE: 6,
  40. NAMESPACE: 7
  41. };
  42. /**
  43. * Recursively gets all CSS as text, optionally starting from a given
  44. * CSSStyleSheet.
  45. * @param {(CSSStyleSheet|StyleSheetList)=} opt_styleSheet The CSSStyleSheet.
  46. * @return {string} css text.
  47. */
  48. goog.cssom.getAllCssText = function(opt_styleSheet) {
  49. var styleSheet = opt_styleSheet || document.styleSheets;
  50. return /** @type {string} */ (goog.cssom.getAllCss_(styleSheet, true));
  51. };
  52. /**
  53. * Recursively gets all CSSStyleRules, optionally starting from a given
  54. * CSSStyleSheet.
  55. * Note that this excludes any CSSImportRules, CSSMediaRules, etc..
  56. * @param {(CSSStyleSheet|StyleSheetList)=} opt_styleSheet The CSSStyleSheet.
  57. * @return {Array<CSSStyleRule>} A list of CSSStyleRules.
  58. */
  59. goog.cssom.getAllCssStyleRules = function(opt_styleSheet) {
  60. var styleSheet = opt_styleSheet || document.styleSheets;
  61. return /** @type {!Array<CSSStyleRule>} */ (
  62. goog.cssom.getAllCss_(styleSheet, false));
  63. };
  64. /**
  65. * Returns the CSSRules from a styleSheet.
  66. * Worth noting here is that IE and FF differ in terms of what they will return.
  67. * Firefox will return styleSheet.cssRules, which includes ImportRules and
  68. * anything which implements the CSSRules interface. IE returns simply a list of
  69. * CSSRules.
  70. * @param {CSSStyleSheet} styleSheet The CSSStyleSheet.
  71. * @throws {Error} If we cannot access the rules on a stylesheet object - this
  72. * can happen if a stylesheet object's rules are accessed before the rules
  73. * have been downloaded and parsed and are "ready".
  74. * @return {CSSRuleList} An array of CSSRules or null.
  75. */
  76. goog.cssom.getCssRulesFromStyleSheet = function(styleSheet) {
  77. var cssRuleList = null;
  78. try {
  79. // Select cssRules unless it isn't present. For pre-IE9 IE, use the rules
  80. // collection instead.
  81. // It's important to be consistent in using only the W3C or IE apis on
  82. // IE9+ where both are present to ensure that there is no indexing
  83. // mismatches - the collections are subtly different in what the include or
  84. // exclude which can lead to one collection being longer than the other
  85. // depending on the page's construction.
  86. cssRuleList = styleSheet.cssRules /* W3C */ || styleSheet.rules /* IE */;
  87. } catch (e) {
  88. // This can happen if we try to access the CSSOM before it's "ready".
  89. if (e.code == 15) {
  90. // Firefox throws an NS_ERROR_DOM_INVALID_ACCESS_ERR error if a stylesheet
  91. // is read before it has been fully parsed. Let the caller know which
  92. // stylesheet failed.
  93. e.styleSheet = styleSheet;
  94. throw e;
  95. }
  96. }
  97. return cssRuleList;
  98. };
  99. /**
  100. * Gets all CSSStyleSheet objects starting from some CSSStyleSheet. Note that we
  101. * want to return the sheets in the order of the cascade, therefore if we
  102. * encounter an import, we will splice that CSSStyleSheet object in front of
  103. * the CSSStyleSheet that contains it in the returned array of CSSStyleSheets.
  104. * @param {(CSSStyleSheet|StyleSheetList)=} opt_styleSheet A CSSStyleSheet.
  105. * @param {boolean=} opt_includeDisabled If true, includes disabled stylesheets,
  106. * defaults to false.
  107. * @return {!Array<CSSStyleSheet>} A list of CSSStyleSheet objects.
  108. */
  109. goog.cssom.getAllCssStyleSheets = function(
  110. opt_styleSheet, opt_includeDisabled) {
  111. var styleSheetsOutput = [];
  112. var styleSheet = opt_styleSheet || document.styleSheets;
  113. var includeDisabled =
  114. goog.isDef(opt_includeDisabled) ? opt_includeDisabled : false;
  115. // Imports need to go first.
  116. if (styleSheet.imports && styleSheet.imports.length) {
  117. for (var i = 0, n = styleSheet.imports.length; i < n; i++) {
  118. goog.array.extend(
  119. styleSheetsOutput,
  120. goog.cssom.getAllCssStyleSheets(
  121. /** @type {CSSStyleSheet} */ (styleSheet.imports[i])));
  122. }
  123. } else if (styleSheet.length) {
  124. // In case we get a StyleSheetList object.
  125. // http://dev.w3.org/csswg/cssom/#the-stylesheetlist
  126. for (var i = 0, n = styleSheet.length; i < n; i++) {
  127. goog.array.extend(
  128. styleSheetsOutput, goog.cssom.getAllCssStyleSheets(
  129. /** @type {!CSSStyleSheet} */ (styleSheet[i])));
  130. }
  131. } else {
  132. // We need to walk through rules in browsers which implement .cssRules
  133. // to see if there are styleSheets buried in there.
  134. // If we have a CSSStyleSheet within CssRules.
  135. var cssRuleList = goog.cssom.getCssRulesFromStyleSheet(
  136. /** @type {!CSSStyleSheet} */ (styleSheet));
  137. if (cssRuleList && cssRuleList.length) {
  138. // Chrome does not evaluate cssRuleList[i] to undefined when i >=n;
  139. // so we use a (i < n) check instead of cssRuleList[i] in the loop below
  140. // and in other places where we iterate over a rules list.
  141. // See issue # 5917 in Chromium.
  142. for (var i = 0, n = cssRuleList.length, cssRule; i < n; i++) {
  143. cssRule = cssRuleList[i];
  144. // There are more stylesheets to get on this object..
  145. if (cssRule.styleSheet) {
  146. goog.array.extend(
  147. styleSheetsOutput,
  148. goog.cssom.getAllCssStyleSheets(cssRule.styleSheet));
  149. }
  150. }
  151. }
  152. }
  153. // This is a CSSStyleSheet. (IE uses .rules, W3c and Opera cssRules.)
  154. if ((styleSheet.type || styleSheet.rules || styleSheet.cssRules) &&
  155. (!styleSheet.disabled || includeDisabled)) {
  156. styleSheetsOutput.push(styleSheet);
  157. }
  158. return styleSheetsOutput;
  159. };
  160. /**
  161. * Gets the cssText from a CSSRule object cross-browserly.
  162. * @param {CSSRule} cssRule A CSSRule.
  163. * @return {string} cssText The text for the rule, including the selector.
  164. */
  165. goog.cssom.getCssTextFromCssRule = function(cssRule) {
  166. var cssText = '';
  167. if (cssRule.cssText) {
  168. // W3C.
  169. cssText = cssRule.cssText;
  170. } else if (cssRule.style && cssRule.style.cssText && cssRule.selectorText) {
  171. // IE: The spacing here is intended to make the result consistent with
  172. // FF and Webkit.
  173. // We also remove the special properties that we may have added in
  174. // getAllCssStyleRules since IE includes those in the cssText.
  175. var styleCssText =
  176. cssRule.style.cssText
  177. .replace(/\s*-closure-parent-stylesheet:\s*\[object\];?\s*/gi, '')
  178. .replace(/\s*-closure-rule-index:\s*[\d]+;?\s*/gi, '');
  179. var thisCssText = cssRule.selectorText + ' { ' + styleCssText + ' }';
  180. cssText = thisCssText;
  181. }
  182. return cssText;
  183. };
  184. /**
  185. * Get the index of the CSSRule in it's CSSStyleSheet.
  186. * @param {CSSRule} cssRule A CSSRule.
  187. * @param {CSSStyleSheet=} opt_parentStyleSheet A reference to the stylesheet
  188. * object this cssRule belongs to.
  189. * @throws {Error} When we cannot get the parentStyleSheet.
  190. * @return {number} The index of the CSSRule, or -1.
  191. */
  192. goog.cssom.getCssRuleIndexInParentStyleSheet = function(
  193. cssRule, opt_parentStyleSheet) {
  194. // Look for our special style.ruleIndex property from getAllCss.
  195. if (cssRule.style && /** @type {!Object} */ (cssRule.style)['-closure-rule-index']) {
  196. return (/** @type {!Object} */ (cssRule.style))['-closure-rule-index'];
  197. }
  198. var parentStyleSheet =
  199. opt_parentStyleSheet || goog.cssom.getParentStyleSheet(cssRule);
  200. if (!parentStyleSheet) {
  201. // We could call getAllCssStyleRules() here to get our special indexes on
  202. // the style object, but that seems like it could be wasteful.
  203. throw Error('Cannot find a parentStyleSheet.');
  204. }
  205. var cssRuleList = goog.cssom.getCssRulesFromStyleSheet(parentStyleSheet);
  206. if (cssRuleList && cssRuleList.length) {
  207. for (var i = 0, n = cssRuleList.length, thisCssRule; i < n; i++) {
  208. thisCssRule = cssRuleList[i];
  209. if (thisCssRule == cssRule) {
  210. return i;
  211. }
  212. }
  213. }
  214. return -1;
  215. };
  216. /**
  217. * We do some trickery in getAllCssStyleRules that hacks this in for IE.
  218. * If the cssRule object isn't coming from a result of that function call, this
  219. * method will return undefined in IE.
  220. * @param {CSSRule} cssRule The CSSRule.
  221. * @return {CSSStyleSheet} A styleSheet object.
  222. */
  223. goog.cssom.getParentStyleSheet = function(cssRule) {
  224. return cssRule.parentStyleSheet ||
  225. cssRule.style &&
  226. (/** @type {!Object} */ (cssRule.style))['-closure-parent-stylesheet'];
  227. };
  228. /**
  229. * Replace a cssRule with some cssText for a new rule.
  230. * If the cssRule object is not one of objects returned by
  231. * getAllCssStyleRules, then you'll need to provide both the styleSheet and
  232. * possibly the index, since we can't infer them from the standard cssRule
  233. * object in IE. We do some trickery in getAllCssStyleRules to hack this in.
  234. * @param {CSSRule} cssRule A CSSRule.
  235. * @param {string} cssText The text for the new CSSRule.
  236. * @param {CSSStyleSheet=} opt_parentStyleSheet A reference to the stylesheet
  237. * object this cssRule belongs to.
  238. * @param {number=} opt_index The index of the cssRule in its parentStylesheet.
  239. * @throws {Error} If we cannot find a parentStyleSheet.
  240. * @throws {Error} If we cannot find a css rule index.
  241. */
  242. goog.cssom.replaceCssRule = function(
  243. cssRule, cssText, opt_parentStyleSheet, opt_index) {
  244. var parentStyleSheet =
  245. opt_parentStyleSheet || goog.cssom.getParentStyleSheet(cssRule);
  246. if (parentStyleSheet) {
  247. var index = Number(opt_index) >= 0 ?
  248. Number(opt_index) :
  249. goog.cssom.getCssRuleIndexInParentStyleSheet(cssRule, parentStyleSheet);
  250. if (index >= 0) {
  251. goog.cssom.removeCssRule(parentStyleSheet, index);
  252. goog.cssom.addCssRule(parentStyleSheet, cssText, index);
  253. } else {
  254. throw Error('Cannot proceed without the index of the cssRule.');
  255. }
  256. } else {
  257. throw Error('Cannot proceed without the parentStyleSheet.');
  258. }
  259. };
  260. /**
  261. * Cross browser function to add a CSSRule into a CSSStyleSheet, optionally
  262. * at a given index.
  263. * @param {CSSStyleSheet} cssStyleSheet The CSSRule's parentStyleSheet.
  264. * @param {string} cssText The text for the new CSSRule.
  265. * @param {number=} opt_index The index of the cssRule in its parentStylesheet.
  266. * @throws {Error} If the css rule text appears to be ill-formatted.
  267. * TODO(bowdidge): Inserting at index 0 fails on Firefox 2 and 3 with an
  268. * exception warning "Node cannot be inserted at the specified point in
  269. * the hierarchy."
  270. */
  271. goog.cssom.addCssRule = function(cssStyleSheet, cssText, opt_index) {
  272. var index = opt_index;
  273. if (index == undefined || index < 0) {
  274. // If no index specified, insert at the end of the current list
  275. // of rules.
  276. var rules = goog.cssom.getCssRulesFromStyleSheet(cssStyleSheet);
  277. index = rules.length;
  278. }
  279. if (cssStyleSheet.insertRule) {
  280. // W3C (including IE9+).
  281. cssStyleSheet.insertRule(cssText, index);
  282. } else {
  283. // IE, pre 9: We have to parse the cssRule text to get the selector
  284. // separated from the style text.
  285. // aka Everything that isn't a colon, followed by a colon, then
  286. // the rest is the style part.
  287. var matches = /^([^\{]+)\{([^\{]+)\}/.exec(cssText);
  288. if (matches.length == 3) {
  289. var selector = matches[1];
  290. var style = matches[2];
  291. cssStyleSheet.addRule(selector, style, index);
  292. } else {
  293. throw Error('Your CSSRule appears to be ill-formatted.');
  294. }
  295. }
  296. };
  297. /**
  298. * Cross browser function to remove a CSSRule in a CSSStyleSheet at an index.
  299. * @param {CSSStyleSheet} cssStyleSheet The CSSRule's parentStyleSheet.
  300. * @param {number} index The CSSRule's index in the parentStyleSheet.
  301. */
  302. goog.cssom.removeCssRule = function(cssStyleSheet, index) {
  303. if (cssStyleSheet.deleteRule) {
  304. // W3C.
  305. cssStyleSheet.deleteRule(index);
  306. } else {
  307. // IE.
  308. cssStyleSheet.removeRule(index);
  309. }
  310. };
  311. /**
  312. * Appends a DOM node to HEAD containing the css text that's passed in.
  313. * @param {string} cssText CSS to add to the end of the document.
  314. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper user for
  315. * document interactions.
  316. * @return {!Element} The newly created STYLE element.
  317. */
  318. goog.cssom.addCssText = function(cssText, opt_domHelper) {
  319. var domHelper = opt_domHelper || goog.dom.getDomHelper();
  320. var document = domHelper.getDocument();
  321. var cssNode = domHelper.createElement(goog.dom.TagName.STYLE);
  322. cssNode.type = 'text/css';
  323. var head = domHelper.getElementsByTagName(goog.dom.TagName.HEAD)[0];
  324. head.appendChild(cssNode);
  325. if (cssNode.styleSheet) {
  326. // IE.
  327. cssNode.styleSheet.cssText = cssText;
  328. } else {
  329. // W3C.
  330. var cssTextNode = document.createTextNode(cssText);
  331. cssNode.appendChild(cssTextNode);
  332. }
  333. return cssNode;
  334. };
  335. /**
  336. * Cross browser method to get the filename from the StyleSheet's href.
  337. * Explorer only returns the filename in the href, while other agents return
  338. * the full path.
  339. * @param {!StyleSheet} styleSheet Any valid StyleSheet object with an href.
  340. * @throws {Error} When there's no href property found.
  341. * @return {?string} filename The filename, or null if not an external
  342. * styleSheet.
  343. */
  344. goog.cssom.getFileNameFromStyleSheet = function(styleSheet) {
  345. var href = styleSheet.href;
  346. // Another IE/FF difference. IE returns an empty string, while FF and others
  347. // return null for CSSStyleSheets not from an external file.
  348. if (!href) {
  349. return null;
  350. }
  351. // We need the regexp to ensure we get the filename minus any query params.
  352. var matches = /([^\/\?]+)[^\/]*$/.exec(href);
  353. var filename = matches[1];
  354. return filename;
  355. };
  356. /**
  357. * Recursively gets all CSS text or rules.
  358. * @param {CSSStyleSheet|StyleSheetList} styleSheet The CSSStyleSheet.
  359. * @param {boolean} isTextOutput If true, output is cssText, otherwise cssRules.
  360. * @return {string|!Array<CSSRule>} cssText or cssRules.
  361. * @private
  362. */
  363. goog.cssom.getAllCss_ = function(styleSheet, isTextOutput) {
  364. var cssOut = [];
  365. var styleSheets = goog.cssom.getAllCssStyleSheets(styleSheet);
  366. for (var i = 0; styleSheet = styleSheets[i]; i++) {
  367. var cssRuleList = goog.cssom.getCssRulesFromStyleSheet(styleSheet);
  368. if (cssRuleList && cssRuleList.length) {
  369. var ruleIndex = 0;
  370. for (var j = 0, n = cssRuleList.length, cssRule; j < n; j++) {
  371. cssRule = cssRuleList[j];
  372. // Gets cssText output, ignoring CSSImportRules.
  373. if (isTextOutput && !cssRule.href) {
  374. var res = goog.cssom.getCssTextFromCssRule(cssRule);
  375. cssOut.push(res);
  376. } else if (!cssRule.href) {
  377. // Gets cssRules output, ignoring CSSImportRules.
  378. if (cssRule.style) {
  379. // This is a fun little hack to get parentStyleSheet into the rule
  380. // object for IE since it failed to implement rule.parentStyleSheet.
  381. // We can later read this property when doing things like hunting
  382. // for indexes in order to delete a given CSSRule.
  383. // Unfortunately we have to use the style object to store these
  384. // pieces of info since the rule object is read-only.
  385. if (!cssRule.parentStyleSheet) {
  386. (/** @type {!Object} */ (cssRule.style))[
  387. '-closure-parent-stylesheet'] = styleSheet;
  388. }
  389. // This is a hack to help with possible removal of the rule later,
  390. // where we just append the rule's index in its parentStyleSheet
  391. // onto the style object as a property.
  392. // Unfortunately we have to use the style object to store these
  393. // pieces of info since the rule object is read-only.
  394. (/** @type {!Object} */ (cssRule.style))['-closure-rule-index'] =
  395. isTextOutput ? undefined : ruleIndex;
  396. }
  397. cssOut.push(cssRule);
  398. }
  399. if (!isTextOutput) {
  400. ruleIndex++;
  401. }
  402. }
  403. }
  404. }
  405. return isTextOutput ? cssOut.join(' ') : cssOut;
  406. };