450 lines
16 KiB
JavaScript
450 lines
16 KiB
JavaScript
// 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 {
|
|
// IE is .rules, W3c is cssRules.
|
|
cssRuleList = styleSheet.rules || styleSheet.cssRules;
|
|
} 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.
|
|
// If on IE, use rules property, otherwise use cssRules property.
|
|
var rules = cssStyleSheet.rules || cssStyleSheet.cssRules;
|
|
index = rules.length;
|
|
}
|
|
if (cssStyleSheet.insertRule) {
|
|
// W3C.
|
|
cssStyleSheet.insertRule(cssText, index);
|
|
|
|
} else {
|
|
// IE: 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;
|
|
};
|
|
|