| // Copyright 2008 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview CSS Object Model helper functions. |
| * References: |
| * - W3C: http://dev.w3.org/csswg/cssom/ |
| * - MSDN: http://msdn.microsoft.com/en-us/library/ms531209(VS.85).aspx. |
| * @supported in FF3, IE6, IE7, Safari 3.1.2, Chrome |
| * TODO(user): Fix in Opera. |
| * TODO(user): Consider hacking page, media, etc.. to work. |
| * This would be pretty challenging. IE returns the text for any rule |
| * regardless of whether or not the media is correct or not. Firefox at |
| * least supports CSSRule.type to figure out if it's a media type and then |
| * we could do something interesting, but IE offers no way for us to tell. |
| */ |
| |
| goog.provide('goog.cssom'); |
| goog.provide('goog.cssom.CssRuleType'); |
| |
| goog.require('goog.array'); |
| goog.require('goog.dom'); |
| |
| |
| /** |
| * Enumeration of {@code CSSRule} types. |
| * @enum {number} |
| */ |
| goog.cssom.CssRuleType = { |
| STYLE: 1, |
| IMPORT: 3, |
| MEDIA: 4, |
| FONT_FACE: 5, |
| PAGE: 6, |
| NAMESPACE: 7 |
| }; |
| |
| |
| /** |
| * Recursively gets all CSS as text, optionally starting from a given |
| * CSSStyleSheet. |
| * @param {(CSSStyleSheet|StyleSheetList)=} opt_styleSheet The CSSStyleSheet. |
| * @return {string} css text. |
| */ |
| goog.cssom.getAllCssText = function(opt_styleSheet) { |
| var styleSheet = opt_styleSheet || document.styleSheets; |
| return /** @type {string} */ (goog.cssom.getAllCss_(styleSheet, true)); |
| }; |
| |
| |
| /** |
| * Recursively gets all CSSStyleRules, optionally starting from a given |
| * CSSStyleSheet. |
| * Note that this excludes any CSSImportRules, CSSMediaRules, etc.. |
| * @param {(CSSStyleSheet|StyleSheetList)=} opt_styleSheet The CSSStyleSheet. |
| * @return {Array<CSSStyleRule>} A list of CSSStyleRules. |
| */ |
| goog.cssom.getAllCssStyleRules = function(opt_styleSheet) { |
| var styleSheet = opt_styleSheet || document.styleSheets; |
| return /** @type {!Array<CSSStyleRule>} */ ( |
| goog.cssom.getAllCss_(styleSheet, false)); |
| }; |
| |
| |
| /** |
| * Returns the CSSRules from a styleSheet. |
| * Worth noting here is that IE and FF differ in terms of what they will return. |
| * Firefox will return styleSheet.cssRules, which includes ImportRules and |
| * anything which implements the CSSRules interface. IE returns simply a list of |
| * CSSRules. |
| * @param {CSSStyleSheet} styleSheet The CSSStyleSheet. |
| * @throws {Error} If we cannot access the rules on a stylesheet object - this |
| * can happen if a stylesheet object's rules are accessed before the rules |
| * have been downloaded and parsed and are "ready". |
| * @return {CSSRuleList} An array of CSSRules or null. |
| */ |
| goog.cssom.getCssRulesFromStyleSheet = function(styleSheet) { |
| var cssRuleList = null; |
| try { |
| // Select cssRules unless it isn't present. For pre-IE9 IE, use the rules |
| // collection instead. |
| // It's important to be consistent in using only the W3C or IE apis on |
| // IE9+ where both are present to ensure that there is no indexing |
| // mismatches - the collections are subtly different in what the include or |
| // exclude which can lead to one collection being longer than the other |
| // depending on the page's construction. |
| cssRuleList = styleSheet.cssRules /* W3C */ || styleSheet.rules /* IE */; |
| } catch (e) { |
| // This can happen if we try to access the CSSOM before it's "ready". |
| if (e.code == 15) { |
| // Firefox throws an NS_ERROR_DOM_INVALID_ACCESS_ERR error if a stylesheet |
| // is read before it has been fully parsed. Let the caller know which |
| // stylesheet failed. |
| e.styleSheet = styleSheet; |
| throw e; |
| } |
| } |
| return cssRuleList; |
| }; |
| |
| |
| /** |
| * Gets all CSSStyleSheet objects starting from some CSSStyleSheet. Note that we |
| * want to return the sheets in the order of the cascade, therefore if we |
| * encounter an import, we will splice that CSSStyleSheet object in front of |
| * the CSSStyleSheet that contains it in the returned array of CSSStyleSheets. |
| * @param {(CSSStyleSheet|StyleSheetList)=} opt_styleSheet A CSSStyleSheet. |
| * @param {boolean=} opt_includeDisabled If true, includes disabled stylesheets, |
| * defaults to false. |
| * @return {!Array<CSSStyleSheet>} A list of CSSStyleSheet objects. |
| */ |
| goog.cssom.getAllCssStyleSheets = function(opt_styleSheet, |
| opt_includeDisabled) { |
| var styleSheetsOutput = []; |
| var styleSheet = opt_styleSheet || document.styleSheets; |
| var includeDisabled = goog.isDef(opt_includeDisabled) ? opt_includeDisabled : |
| false; |
| |
| // Imports need to go first. |
| if (styleSheet.imports && styleSheet.imports.length) { |
| for (var i = 0, n = styleSheet.imports.length; i < n; i++) { |
| goog.array.extend(styleSheetsOutput, |
| goog.cssom.getAllCssStyleSheets(styleSheet.imports[i])); |
| } |
| |
| } else if (styleSheet.length) { |
| // In case we get a StyleSheetList object. |
| // http://dev.w3.org/csswg/cssom/#the-stylesheetlist |
| for (var i = 0, n = styleSheet.length; i < n; i++) { |
| goog.array.extend(styleSheetsOutput, |
| goog.cssom.getAllCssStyleSheets(styleSheet[i])); |
| } |
| } else { |
| // We need to walk through rules in browsers which implement .cssRules |
| // to see if there are styleSheets buried in there. |
| // If we have a CSSStyleSheet within CssRules. |
| var cssRuleList = goog.cssom.getCssRulesFromStyleSheet( |
| /** @type {!CSSStyleSheet} */ (styleSheet)); |
| if (cssRuleList && cssRuleList.length) { |
| // Chrome does not evaluate cssRuleList[i] to undefined when i >=n; |
| // so we use a (i < n) check instead of cssRuleList[i] in the loop below |
| // and in other places where we iterate over a rules list. |
| // See issue # 5917 in Chromium. |
| for (var i = 0, n = cssRuleList.length, cssRule; i < n; i++) { |
| cssRule = cssRuleList[i]; |
| // There are more stylesheets to get on this object.. |
| if (cssRule.styleSheet) { |
| goog.array.extend(styleSheetsOutput, |
| goog.cssom.getAllCssStyleSheets(cssRule.styleSheet)); |
| } |
| } |
| } |
| } |
| |
| // This is a CSSStyleSheet. (IE uses .rules, W3c and Opera cssRules.) |
| if ((styleSheet.type || styleSheet.rules || styleSheet.cssRules) && |
| (!styleSheet.disabled || includeDisabled)) { |
| styleSheetsOutput.push(styleSheet); |
| } |
| |
| return styleSheetsOutput; |
| }; |
| |
| |
| /** |
| * Gets the cssText from a CSSRule object cross-browserly. |
| * @param {CSSRule} cssRule A CSSRule. |
| * @return {string} cssText The text for the rule, including the selector. |
| */ |
| goog.cssom.getCssTextFromCssRule = function(cssRule) { |
| var cssText = ''; |
| |
| if (cssRule.cssText) { |
| // W3C. |
| cssText = cssRule.cssText; |
| } else if (cssRule.style && cssRule.style.cssText && cssRule.selectorText) { |
| // IE: The spacing here is intended to make the result consistent with |
| // FF and Webkit. |
| // We also remove the special properties that we may have added in |
| // getAllCssStyleRules since IE includes those in the cssText. |
| var styleCssText = cssRule.style.cssText. |
| replace(/\s*-closure-parent-stylesheet:\s*\[object\];?\s*/gi, ''). |
| replace(/\s*-closure-rule-index:\s*[\d]+;?\s*/gi, ''); |
| var thisCssText = cssRule.selectorText + ' { ' + styleCssText + ' }'; |
| cssText = thisCssText; |
| } |
| |
| return cssText; |
| }; |
| |
| |
| /** |
| * Get the index of the CSSRule in it's CSSStyleSheet. |
| * @param {CSSRule} cssRule A CSSRule. |
| * @param {CSSStyleSheet=} opt_parentStyleSheet A reference to the stylesheet |
| * object this cssRule belongs to. |
| * @throws {Error} When we cannot get the parentStyleSheet. |
| * @return {number} The index of the CSSRule, or -1. |
| */ |
| goog.cssom.getCssRuleIndexInParentStyleSheet = function(cssRule, |
| opt_parentStyleSheet) { |
| // Look for our special style.ruleIndex property from getAllCss. |
| if (cssRule.style && cssRule.style['-closure-rule-index']) { |
| return cssRule.style['-closure-rule-index']; |
| } |
| |
| var parentStyleSheet = opt_parentStyleSheet || |
| goog.cssom.getParentStyleSheet(cssRule); |
| |
| if (!parentStyleSheet) { |
| // We could call getAllCssStyleRules() here to get our special indexes on |
| // the style object, but that seems like it could be wasteful. |
| throw Error('Cannot find a parentStyleSheet.'); |
| } |
| |
| var cssRuleList = goog.cssom.getCssRulesFromStyleSheet(parentStyleSheet); |
| if (cssRuleList && cssRuleList.length) { |
| for (var i = 0, n = cssRuleList.length, thisCssRule; i < n; i++) { |
| thisCssRule = cssRuleList[i]; |
| if (thisCssRule == cssRule) { |
| return i; |
| } |
| } |
| } |
| return -1; |
| }; |
| |
| |
| /** |
| * We do some trickery in getAllCssStyleRules that hacks this in for IE. |
| * If the cssRule object isn't coming from a result of that function call, this |
| * method will return undefined in IE. |
| * @param {CSSRule} cssRule The CSSRule. |
| * @return {CSSStyleSheet} A styleSheet object. |
| */ |
| goog.cssom.getParentStyleSheet = function(cssRule) { |
| return cssRule.parentStyleSheet || |
| cssRule.style && |
| cssRule.style['-closure-parent-stylesheet']; |
| }; |
| |
| |
| /** |
| * Replace a cssRule with some cssText for a new rule. |
| * If the cssRule object is not one of objects returned by |
| * getAllCssStyleRules, then you'll need to provide both the styleSheet and |
| * possibly the index, since we can't infer them from the standard cssRule |
| * object in IE. We do some trickery in getAllCssStyleRules to hack this in. |
| * @param {CSSRule} cssRule A CSSRule. |
| * @param {string} cssText The text for the new CSSRule. |
| * @param {CSSStyleSheet=} opt_parentStyleSheet A reference to the stylesheet |
| * object this cssRule belongs to. |
| * @param {number=} opt_index The index of the cssRule in its parentStylesheet. |
| * @throws {Error} If we cannot find a parentStyleSheet. |
| * @throws {Error} If we cannot find a css rule index. |
| */ |
| goog.cssom.replaceCssRule = function(cssRule, cssText, opt_parentStyleSheet, |
| opt_index) { |
| var parentStyleSheet = opt_parentStyleSheet || |
| goog.cssom.getParentStyleSheet(cssRule); |
| if (parentStyleSheet) { |
| var index = opt_index >= 0 ? opt_index : |
| goog.cssom.getCssRuleIndexInParentStyleSheet(cssRule, parentStyleSheet); |
| if (index >= 0) { |
| goog.cssom.removeCssRule(parentStyleSheet, index); |
| goog.cssom.addCssRule(parentStyleSheet, cssText, index); |
| } else { |
| throw Error('Cannot proceed without the index of the cssRule.'); |
| } |
| } else { |
| throw Error('Cannot proceed without the parentStyleSheet.'); |
| } |
| }; |
| |
| |
| /** |
| * Cross browser function to add a CSSRule into a CSSStyleSheet, optionally |
| * at a given index. |
| * @param {CSSStyleSheet} cssStyleSheet The CSSRule's parentStyleSheet. |
| * @param {string} cssText The text for the new CSSRule. |
| * @param {number=} opt_index The index of the cssRule in its parentStylesheet. |
| * @throws {Error} If the css rule text appears to be ill-formatted. |
| * TODO(bowdidge): Inserting at index 0 fails on Firefox 2 and 3 with an |
| * exception warning "Node cannot be inserted at the specified point in |
| * the hierarchy." |
| */ |
| goog.cssom.addCssRule = function(cssStyleSheet, cssText, opt_index) { |
| var index = opt_index; |
| if (index < 0 || index == undefined) { |
| // If no index specified, insert at the end of the current list |
| // of rules. |
| var rules = goog.cssom.getCssRulesFromStyleSheet(cssStyleSheet); |
| index = rules.length; |
| } |
| if (cssStyleSheet.insertRule) { |
| // W3C (including IE9+). |
| cssStyleSheet.insertRule(cssText, index); |
| |
| } else { |
| // IE, pre 9: We have to parse the cssRule text to get the selector |
| // separated from the style text. |
| // aka Everything that isn't a colon, followed by a colon, then |
| // the rest is the style part. |
| var matches = /^([^\{]+)\{([^\{]+)\}/.exec(cssText); |
| if (matches.length == 3) { |
| var selector = matches[1]; |
| var style = matches[2]; |
| cssStyleSheet.addRule(selector, style, index); |
| } else { |
| throw Error('Your CSSRule appears to be ill-formatted.'); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Cross browser function to remove a CSSRule in a CSSStyleSheet at an index. |
| * @param {CSSStyleSheet} cssStyleSheet The CSSRule's parentStyleSheet. |
| * @param {number} index The CSSRule's index in the parentStyleSheet. |
| */ |
| goog.cssom.removeCssRule = function(cssStyleSheet, index) { |
| if (cssStyleSheet.deleteRule) { |
| // W3C. |
| cssStyleSheet.deleteRule(index); |
| |
| } else { |
| // IE. |
| cssStyleSheet.removeRule(index); |
| } |
| }; |
| |
| |
| /** |
| * Appends a DOM node to HEAD containing the css text that's passed in. |
| * @param {string} cssText CSS to add to the end of the document. |
| * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper user for |
| * document interactions. |
| * @return {!Element} The newly created STYLE element. |
| */ |
| goog.cssom.addCssText = function(cssText, opt_domHelper) { |
| var document = opt_domHelper ? opt_domHelper.getDocument() : |
| goog.dom.getDocument(); |
| var cssNode = document.createElement('style'); |
| cssNode.type = 'text/css'; |
| var head = document.getElementsByTagName('head')[0]; |
| head.appendChild(cssNode); |
| if (cssNode.styleSheet) { |
| // IE. |
| cssNode.styleSheet.cssText = cssText; |
| } else { |
| // W3C. |
| var cssTextNode = document.createTextNode(cssText); |
| cssNode.appendChild(cssTextNode); |
| } |
| return cssNode; |
| }; |
| |
| |
| /** |
| * Cross browser method to get the filename from the StyleSheet's href. |
| * Explorer only returns the filename in the href, while other agents return |
| * the full path. |
| * @param {!StyleSheet} styleSheet Any valid StyleSheet object with an href. |
| * @throws {Error} When there's no href property found. |
| * @return {?string} filename The filename, or null if not an external |
| * styleSheet. |
| */ |
| goog.cssom.getFileNameFromStyleSheet = function(styleSheet) { |
| var href = styleSheet.href; |
| |
| // Another IE/FF difference. IE returns an empty string, while FF and others |
| // return null for CSSStyleSheets not from an external file. |
| if (!href) { |
| return null; |
| } |
| |
| // We need the regexp to ensure we get the filename minus any query params. |
| var matches = /([^\/\?]+)[^\/]*$/.exec(href); |
| var filename = matches[1]; |
| return filename; |
| }; |
| |
| |
| /** |
| * Recursively gets all CSS text or rules. |
| * @param {CSSStyleSheet|StyleSheetList} styleSheet The CSSStyleSheet. |
| * @param {boolean} isTextOutput If true, output is cssText, otherwise cssRules. |
| * @return {string|!Array<CSSRule>} cssText or cssRules. |
| * @private |
| */ |
| goog.cssom.getAllCss_ = function(styleSheet, isTextOutput) { |
| var cssOut = []; |
| var styleSheets = goog.cssom.getAllCssStyleSheets(styleSheet); |
| |
| for (var i = 0; styleSheet = styleSheets[i]; i++) { |
| var cssRuleList = goog.cssom.getCssRulesFromStyleSheet(styleSheet); |
| |
| if (cssRuleList && cssRuleList.length) { |
| |
| // We're going to track cssRule index if we want rule output. |
| if (!isTextOutput) { |
| var ruleIndex = 0; |
| } |
| |
| for (var j = 0, n = cssRuleList.length, cssRule; j < n; j++) { |
| cssRule = cssRuleList[j]; |
| // Gets cssText output, ignoring CSSImportRules. |
| if (isTextOutput && !cssRule.href) { |
| var res = goog.cssom.getCssTextFromCssRule(cssRule); |
| cssOut.push(res); |
| |
| } else if (!cssRule.href) { |
| // Gets cssRules output, ignoring CSSImportRules. |
| if (cssRule.style) { |
| // This is a fun little hack to get parentStyleSheet into the rule |
| // object for IE since it failed to implement rule.parentStyleSheet. |
| // We can later read this property when doing things like hunting |
| // for indexes in order to delete a given CSSRule. |
| // Unfortunately we have to use the style object to store these |
| // pieces of info since the rule object is read-only. |
| if (!cssRule.parentStyleSheet) { |
| cssRule.style['-closure-parent-stylesheet'] = styleSheet; |
| } |
| |
| // This is a hack to help with possible removal of the rule later, |
| // where we just append the rule's index in its parentStyleSheet |
| // onto the style object as a property. |
| // Unfortunately we have to use the style object to store these |
| // pieces of info since the rule object is read-only. |
| cssRule.style['-closure-rule-index'] = ruleIndex; |
| } |
| cssOut.push(cssRule); |
| } |
| |
| if (!isTextOutput) { |
| ruleIndex++; |
| } |
| } |
| } |
| } |
| return isTextOutput ? cssOut.join(' ') : cssOut; |
| }; |
| |