// 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. * 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'); goog.require('goog.dom.TagName'); /** * 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} A list of CSSStyleRules. */ goog.cssom.getAllCssStyleRules = function(opt_styleSheet) { var styleSheet = opt_styleSheet || document.styleSheets; return /** @type {!Array} */ ( 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} 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( /** @type {CSSStyleSheet} */ (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( /** @type {!CSSStyleSheet} */ (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 && /** @type {!Object} */ (cssRule.style)['-closure-rule-index']) { return (/** @type {!Object} */ (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 && (/** @type {!Object} */ (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 = Number(opt_index) >= 0 ? Number(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 == undefined || index < 0) { // 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 domHelper = opt_domHelper || goog.dom.getDomHelper(); var document = domHelper.getDocument(); var cssNode = domHelper.createElement(goog.dom.TagName.STYLE); cssNode.type = 'text/css'; var head = domHelper.getElementsByTagName(goog.dom.TagName.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} 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) { 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) { (/** @type {!Object} */ (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. (/** @type {!Object} */ (cssRule.style))['-closure-rule-index'] = isTextOutput ? undefined : ruleIndex; } cssOut.push(cssRule); } if (!isTextOutput) { ruleIndex++; } } } } return isTextOutput ? cssOut.join(' ') : cssOut; };