// 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. goog.provide('goog.debug.DevCss'); goog.provide('goog.debug.DevCss.UserAgent'); goog.require('goog.cssom'); goog.require('goog.dom.classes'); 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 */ 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; /** * @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)); } }; /** * @type {Object} * @private */ goog.debug.DevCss.prototype.userAgentTokens_ = {}; /** * 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('*'); var matches = []; // 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.classes.has(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++) { if (!goog.dom.classes.has(el, classNameEntry.combinedClassName)) { goog.dom.classes.add(el, classNameEntry.combinedClassName); } } } } };