| // 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 Runtime development CSS Compiler emulation, via javascript. |
| * This class provides an approximation to CSSCompiler's functionality by |
| * hacking the live CSSOM. |
| * This code is designed to be inserted in the DOM immediately after the last |
| * style block in HEAD when in development mode, i.e. you are not using a |
| * running instance of a CSS Compiler to pass your CSS through. |
| */ |
| |
| |
| goog.provide('goog.debug.DevCss'); |
| goog.provide('goog.debug.DevCss.UserAgent'); |
| |
| goog.require('goog.asserts'); |
| goog.require('goog.cssom'); |
| goog.require('goog.dom.classlist'); |
| goog.require('goog.events'); |
| goog.require('goog.events.EventType'); |
| goog.require('goog.string'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * A class for solving development CSS issues/emulating the CSS Compiler. |
| * @param {goog.debug.DevCss.UserAgent=} opt_userAgent The user agent, if not |
| * passed in, will be determined using goog.userAgent. |
| * @param {number|string=} opt_userAgentVersion The user agent's version. |
| * If not passed in, will be determined using goog.userAgent. |
| * @throws {Error} When userAgent detection fails. |
| * @constructor |
| * @final |
| */ |
| goog.debug.DevCss = function(opt_userAgent, opt_userAgentVersion) { |
| if (!opt_userAgent) { |
| // Walks through the known goog.userAgents. |
| if (goog.userAgent.IE) { |
| opt_userAgent = goog.debug.DevCss.UserAgent.IE; |
| } else if (goog.userAgent.GECKO) { |
| opt_userAgent = goog.debug.DevCss.UserAgent.GECKO; |
| } else if (goog.userAgent.WEBKIT) { |
| opt_userAgent = goog.debug.DevCss.UserAgent.WEBKIT; |
| } else if (goog.userAgent.MOBILE) { |
| opt_userAgent = goog.debug.DevCss.UserAgent.MOBILE; |
| } else if (goog.userAgent.OPERA) { |
| opt_userAgent = goog.debug.DevCss.UserAgent.OPERA; |
| } |
| } |
| switch (opt_userAgent) { |
| case goog.debug.DevCss.UserAgent.OPERA: |
| case goog.debug.DevCss.UserAgent.IE: |
| case goog.debug.DevCss.UserAgent.GECKO: |
| case goog.debug.DevCss.UserAgent.FIREFOX: |
| case goog.debug.DevCss.UserAgent.WEBKIT: |
| case goog.debug.DevCss.UserAgent.SAFARI: |
| case goog.debug.DevCss.UserAgent.MOBILE: |
| break; |
| default: |
| throw Error('Could not determine the user agent from known UserAgents'); |
| } |
| |
| /** |
| * One of goog.debug.DevCss.UserAgent. |
| * @type {string} |
| * @private |
| */ |
| this.userAgent_ = opt_userAgent; |
| |
| /** |
| * @const @private |
| */ |
| this.userAgentTokens_ = {}; |
| |
| /** |
| * @type {number|string} |
| * @private |
| */ |
| this.userAgentVersion_ = opt_userAgentVersion || goog.userAgent.VERSION; |
| this.generateUserAgentTokens_(); |
| |
| /** |
| * @type {boolean} |
| * @private |
| */ |
| this.isIe6OrLess_ = this.userAgent_ == goog.debug.DevCss.UserAgent.IE && |
| goog.string.compareVersions('7', this.userAgentVersion_) > 0; |
| |
| if (this.isIe6OrLess_) { |
| /** |
| * @type {Array<{classNames,combinedClassName,els}>} |
| * @private |
| */ |
| this.ie6CombinedMatches_ = []; |
| } |
| }; |
| |
| |
| /** |
| * Rewrites the CSSOM as needed to activate any useragent-specific selectors. |
| * @param {boolean=} opt_enableIe6ReadyHandler If true(the default), and the |
| * userAgent is ie6, we set a document "ready" event handler to walk the DOM |
| * and make combined selector className changes. Having this parameter also |
| * aids unit testing. |
| */ |
| goog.debug.DevCss.prototype.activateBrowserSpecificCssRules = function( |
| opt_enableIe6ReadyHandler) { |
| var enableIe6EventHandler = goog.isDef(opt_enableIe6ReadyHandler) ? |
| opt_enableIe6ReadyHandler : true; |
| var cssRules = goog.cssom.getAllCssStyleRules(); |
| |
| for (var i = 0, cssRule; cssRule = cssRules[i]; i++) { |
| this.replaceBrowserSpecificClassNames_(cssRule); |
| } |
| |
| // Since we may have manipulated the rules above, we'll have to do a |
| // complete sweep again if we're in IE6. Luckily performance doesn't |
| // matter for this tool. |
| if (this.isIe6OrLess_) { |
| cssRules = goog.cssom.getAllCssStyleRules(); |
| for (var i = 0, cssRule; cssRule = cssRules[i]; i++) { |
| this.replaceIe6CombinedSelectors_(cssRule); |
| } |
| } |
| |
| // Add an event listener for document ready to rewrite any necessary |
| // combined classnames in IE6. |
| if (this.isIe6OrLess_ && enableIe6EventHandler) { |
| goog.events.listen(document, goog.events.EventType.LOAD, goog.bind( |
| this.addIe6CombinedClassNames_, this)); |
| } |
| }; |
| |
| |
| /** |
| * A list of possible user agent strings. |
| * @enum {string} |
| */ |
| goog.debug.DevCss.UserAgent = { |
| OPERA: 'OPERA', |
| IE: 'IE', |
| GECKO: 'GECKO', |
| FIREFOX: 'GECKO', |
| WEBKIT: 'WEBKIT', |
| SAFARI: 'WEBKIT', |
| MOBILE: 'MOBILE' |
| }; |
| |
| |
| /** |
| * A list of strings that may be used for matching in CSS files/development. |
| * @enum {string} |
| * @private |
| */ |
| goog.debug.DevCss.CssToken_ = { |
| USERAGENT: 'USERAGENT', |
| SEPARATOR: '-', |
| LESS_THAN: 'LT', |
| GREATER_THAN: 'GT', |
| LESS_THAN_OR_EQUAL: 'LTE', |
| GREATER_THAN_OR_EQUAL: 'GTE', |
| IE6_SELECTOR_TEXT: 'goog-ie6-selector', |
| IE6_COMBINED_GLUE: '_' |
| }; |
| |
| |
| /** |
| * Generates user agent token match strings with comparison and version bits. |
| * For example: |
| * userAgentTokens_.ANY will be like 'GECKO' |
| * userAgentTokens_.LESS_THAN will be like 'GECKO-LT3' etc... |
| * @private |
| */ |
| goog.debug.DevCss.prototype.generateUserAgentTokens_ = function() { |
| this.userAgentTokens_.ANY = goog.debug.DevCss.CssToken_.USERAGENT + |
| goog.debug.DevCss.CssToken_.SEPARATOR + this.userAgent_; |
| this.userAgentTokens_.EQUALS = this.userAgentTokens_.ANY + |
| goog.debug.DevCss.CssToken_.SEPARATOR; |
| this.userAgentTokens_.LESS_THAN = this.userAgentTokens_.ANY + |
| goog.debug.DevCss.CssToken_.SEPARATOR + |
| goog.debug.DevCss.CssToken_.LESS_THAN; |
| this.userAgentTokens_.LESS_THAN_OR_EQUAL = this.userAgentTokens_.ANY + |
| goog.debug.DevCss.CssToken_.SEPARATOR + |
| goog.debug.DevCss.CssToken_.LESS_THAN_OR_EQUAL; |
| this.userAgentTokens_.GREATER_THAN = this.userAgentTokens_.ANY + |
| goog.debug.DevCss.CssToken_.SEPARATOR + |
| goog.debug.DevCss.CssToken_.GREATER_THAN; |
| this.userAgentTokens_.GREATER_THAN_OR_EQUAL = this.userAgentTokens_.ANY + |
| goog.debug.DevCss.CssToken_.SEPARATOR + |
| goog.debug.DevCss.CssToken_.GREATER_THAN_OR_EQUAL; |
| }; |
| |
| |
| /** |
| * Gets the version number bit from a selector matching userAgentToken. |
| * @param {string} selectorText The selector text of a CSS rule. |
| * @param {string} userAgentToken Includes the LTE/GTE bit to see if it matches. |
| * @return {string|undefined} The version number. |
| * @private |
| */ |
| goog.debug.DevCss.prototype.getVersionNumberFromSelectorText_ = function( |
| selectorText, userAgentToken) { |
| var regex = new RegExp(userAgentToken + '([\\d\\.]+)'); |
| var matches = regex.exec(selectorText); |
| if (matches && matches.length == 2) { |
| return matches[1]; |
| } |
| }; |
| |
| |
| /** |
| * Extracts a rule version from the selector text, and if it finds one, calls |
| * compareVersions against it and the passed in token string to provide the |
| * value needed to determine if we have a match or not. |
| * @param {CSSRule} cssRule The rule to test against. |
| * @param {string} token The match token to test against the rule. |
| * @return {!Array|undefined} A tuple with the result of the compareVersions |
| * call and the matched ruleVersion. |
| * @private |
| */ |
| goog.debug.DevCss.prototype.getRuleVersionAndCompare_ = function(cssRule, |
| token) { |
| if (!cssRule.selectorText.match(token)) { |
| return; |
| } |
| var ruleVersion = this.getVersionNumberFromSelectorText_( |
| cssRule.selectorText, token); |
| if (!ruleVersion) { |
| return; |
| } |
| |
| var comparison = goog.string.compareVersions(this.userAgentVersion_, |
| ruleVersion); |
| return [comparison, ruleVersion]; |
| }; |
| |
| |
| /** |
| * Replaces a CSS selector if we have matches based on our useragent/version. |
| * Example: With a selector like ".USERAGENT-IE-LTE6 .class { prop: value }" if |
| * we are running IE6 we'll end up with ".class { prop: value }", thereby |
| * "activating" the selector. |
| * @param {CSSRule} cssRule The cssRule to potentially replace. |
| * @private |
| */ |
| goog.debug.DevCss.prototype.replaceBrowserSpecificClassNames_ = function( |
| cssRule) { |
| |
| // If we don't match the browser token, we can stop now. |
| if (!cssRule.selectorText.match(this.userAgentTokens_.ANY)) { |
| return; |
| } |
| |
| // We know it will begin as a classname. |
| var additionalRegexString; |
| |
| // Tests "Less than or equals". |
| var compared = this.getRuleVersionAndCompare_(cssRule, |
| this.userAgentTokens_.LESS_THAN_OR_EQUAL); |
| if (compared && compared.length) { |
| if (compared[0] > 0) { |
| return; |
| } |
| additionalRegexString = this.userAgentTokens_.LESS_THAN_OR_EQUAL + |
| compared[1]; |
| } |
| |
| // Tests "Less than". |
| compared = this.getRuleVersionAndCompare_(cssRule, |
| this.userAgentTokens_.LESS_THAN); |
| if (compared && compared.length) { |
| if (compared[0] > -1) { |
| return; |
| } |
| additionalRegexString = this.userAgentTokens_.LESS_THAN + compared[1]; |
| } |
| |
| // Tests "Greater than or equals". |
| compared = this.getRuleVersionAndCompare_(cssRule, |
| this.userAgentTokens_.GREATER_THAN_OR_EQUAL); |
| if (compared && compared.length) { |
| if (compared[0] < 0) { |
| return; |
| } |
| additionalRegexString = this.userAgentTokens_.GREATER_THAN_OR_EQUAL + |
| compared[1]; |
| } |
| |
| // Tests "Greater than". |
| compared = this.getRuleVersionAndCompare_(cssRule, |
| this.userAgentTokens_.GREATER_THAN); |
| if (compared && compared.length) { |
| if (compared[0] < 1) { |
| return; |
| } |
| additionalRegexString = this.userAgentTokens_.GREATER_THAN + compared[1]; |
| } |
| |
| // Tests "Equals". |
| compared = this.getRuleVersionAndCompare_(cssRule, |
| this.userAgentTokens_.EQUALS); |
| if (compared && compared.length) { |
| if (compared[0] != 0) { |
| return; |
| } |
| additionalRegexString = this.userAgentTokens_.EQUALS + compared[1]; |
| } |
| |
| // If we got to here without generating the additionalRegexString, then |
| // we did not match any of our comparison token strings, and we want a |
| // general browser token replacement. |
| if (!additionalRegexString) { |
| additionalRegexString = this.userAgentTokens_.ANY; |
| } |
| |
| // We need to match at least a single whitespace character to know that |
| // we are matching the entire useragent string token. |
| var regexString = '\\.' + additionalRegexString + '\\s+'; |
| var re = new RegExp(regexString, 'g'); |
| |
| var currentCssText = goog.cssom.getCssTextFromCssRule(cssRule); |
| |
| // Replacing the token with '' activates the selector for this useragent. |
| var newCssText = currentCssText.replace(re, ''); |
| |
| if (newCssText != currentCssText) { |
| goog.cssom.replaceCssRule(cssRule, newCssText); |
| } |
| }; |
| |
| |
| /** |
| * Replaces IE6 combined selector rules with a workable development alternative. |
| * IE6 actually parses .class1.class2 {} to simply .class2 {} which is nasty. |
| * To fully support combined selectors in IE6 this function needs to be paired |
| * with a call to replace the relevant DOM elements classNames as well. |
| * @see {this.addIe6CombinedClassNames_} |
| * @param {CSSRule} cssRule The rule to potentially fix. |
| * @private |
| */ |
| goog.debug.DevCss.prototype.replaceIe6CombinedSelectors_ = function(cssRule) { |
| // This match only ever works in IE because other UA's won't have our |
| // IE6_SELECTOR_TEXT in the cssText property. |
| if (cssRule.style.cssText && |
| cssRule.style.cssText.match( |
| goog.debug.DevCss.CssToken_.IE6_SELECTOR_TEXT)) { |
| var cssText = goog.cssom.getCssTextFromCssRule(cssRule); |
| var combinedSelectorText = this.getIe6CombinedSelectorText_(cssText); |
| if (combinedSelectorText) { |
| var newCssText = combinedSelectorText + '{' + cssRule.style.cssText + '}'; |
| goog.cssom.replaceCssRule(cssRule, newCssText); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Gets the appropriate new combined selector text for IE6. |
| * Also adds an entry onto ie6CombinedMatches_ with relevant info for the |
| * likely following call to walk the DOM and rewrite the class attribute. |
| * Example: With a selector like |
| * ".class2 { -goog-ie6-selector: .class1.class2; prop: value }". |
| * this function will return: |
| * ".class1_class2 { prop: value }". |
| * @param {string} cssText The CSS selector text and css rule text combined. |
| * @return {?string} The rewritten css rule text. |
| * @private |
| */ |
| goog.debug.DevCss.prototype.getIe6CombinedSelectorText_ = function(cssText) { |
| var regex = new RegExp(goog.debug.DevCss.CssToken_.IE6_SELECTOR_TEXT + |
| '\\s*:\\s*\\"([^\\"]+)\\"', 'gi'); |
| var matches = regex.exec(cssText); |
| if (matches) { |
| var combinedSelectorText = matches[1]; |
| // To aid in later fixing the DOM, we need to split up the possible |
| // selector groups by commas. |
| var groupedSelectors = combinedSelectorText.split(/\s*\,\s*/); |
| for (var i = 0, selector; selector = groupedSelectors[i]; i++) { |
| // Strips off the leading ".". |
| var combinedClassName = selector.substr(1); |
| var classNames = combinedClassName.split( |
| goog.debug.DevCss.CssToken_.IE6_COMBINED_GLUE); |
| var entry = { |
| classNames: classNames, |
| combinedClassName: combinedClassName, |
| els: [] |
| }; |
| this.ie6CombinedMatches_.push(entry); |
| } |
| return combinedSelectorText; |
| } |
| return null; |
| }; |
| |
| |
| /** |
| * Adds combined selectors with underscores to make them "work" in IE6. |
| * @see {this.replaceIe6CombinedSelectors_} |
| * @private |
| */ |
| goog.debug.DevCss.prototype.addIe6CombinedClassNames_ = function() { |
| if (!this.ie6CombinedMatches_.length) { |
| return; |
| } |
| var allEls = document.getElementsByTagName('*'); |
| // Match nodes for all classNames. |
| for (var i = 0, classNameEntry; classNameEntry = |
| this.ie6CombinedMatches_[i]; i++) { |
| for (var j = 0, el; el = allEls[j]; j++) { |
| var classNamesLength = classNameEntry.classNames.length; |
| for (var k = 0, className; className = classNameEntry.classNames[k]; |
| k++) { |
| if (!goog.dom.classlist.contains(goog.asserts.assert(el), className)) { |
| break; |
| } |
| if (k == classNamesLength - 1) { |
| classNameEntry.els.push(el); |
| } |
| } |
| } |
| // Walks over our matching nodes and fixes them. |
| if (classNameEntry.els.length) { |
| for (var j = 0, el; el = classNameEntry.els[j]; j++) { |
| goog.asserts.assert(el); |
| if (!goog.dom.classlist.contains(el, |
| classNameEntry.combinedClassName)) { |
| goog.dom.classlist.add(el, classNameEntry.combinedClassName); |
| } |
| } |
| } |
| } |
| }; |