Path: blob/trunk/third_party/closure/goog/debug/devcss/devcss.js
2868 views
// Copyright 2008 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Runtime development CSS Compiler emulation, via javascript.16* This class provides an approximation to CSSCompiler's functionality by17* hacking the live CSSOM.18* This code is designed to be inserted in the DOM immediately after the last19* style block in HEAD when in development mode, i.e. you are not using a20* running instance of a CSS Compiler to pass your CSS through.21*/222324goog.provide('goog.debug.DevCss');25goog.provide('goog.debug.DevCss.UserAgent');2627goog.require('goog.asserts');28goog.require('goog.cssom');29goog.require('goog.dom.classlist');30goog.require('goog.events');31goog.require('goog.events.EventType');32goog.require('goog.string');33goog.require('goog.userAgent');34353637/**38* A class for solving development CSS issues/emulating the CSS Compiler.39* @param {goog.debug.DevCss.UserAgent=} opt_userAgent The user agent, if not40* passed in, will be determined using goog.userAgent.41* @param {number|string=} opt_userAgentVersion The user agent's version.42* If not passed in, will be determined using goog.userAgent.43* @throws {Error} When userAgent detection fails.44* @constructor45* @final46*/47goog.debug.DevCss = function(opt_userAgent, opt_userAgentVersion) {48if (!opt_userAgent) {49// Walks through the known goog.userAgents.50if (goog.userAgent.IE) {51opt_userAgent = goog.debug.DevCss.UserAgent.IE;52} else if (goog.userAgent.GECKO) {53opt_userAgent = goog.debug.DevCss.UserAgent.GECKO;54} else if (goog.userAgent.WEBKIT) {55opt_userAgent = goog.debug.DevCss.UserAgent.WEBKIT;56} else if (goog.userAgent.MOBILE) {57opt_userAgent = goog.debug.DevCss.UserAgent.MOBILE;58} else if (goog.userAgent.OPERA) {59opt_userAgent = goog.debug.DevCss.UserAgent.OPERA;60} else if (goog.userAgent.EDGE) {61opt_userAgent = goog.debug.DevCss.UserAgent.EDGE;62}63}64switch (opt_userAgent) {65case goog.debug.DevCss.UserAgent.OPERA:66case goog.debug.DevCss.UserAgent.IE:67case goog.debug.DevCss.UserAgent.GECKO:68case goog.debug.DevCss.UserAgent.FIREFOX:69case goog.debug.DevCss.UserAgent.WEBKIT:70case goog.debug.DevCss.UserAgent.SAFARI:71case goog.debug.DevCss.UserAgent.MOBILE:72case goog.debug.DevCss.UserAgent.EDGE:73break;74default:75throw Error('Could not determine the user agent from known UserAgents');76}7778/**79* One of goog.debug.DevCss.UserAgent.80* @type {string}81* @private82*/83this.userAgent_ = opt_userAgent;8485/**86* @const @private87*/88this.userAgentTokens_ = {};8990/**91* @type {number|string}92* @private93*/94this.userAgentVersion_ = opt_userAgentVersion || goog.userAgent.VERSION;95this.generateUserAgentTokens_();9697/**98* @type {boolean}99* @private100*/101this.isIe6OrLess_ = this.userAgent_ == goog.debug.DevCss.UserAgent.IE &&102goog.string.compareVersions('7', this.userAgentVersion_) > 0;103104if (this.isIe6OrLess_) {105/**106* @type {Array<{classNames,combinedClassName,els}>}107* @private108*/109this.ie6CombinedMatches_ = [];110}111};112113114/**115* Rewrites the CSSOM as needed to activate any useragent-specific selectors.116* @param {boolean=} opt_enableIe6ReadyHandler If true(the default), and the117* userAgent is ie6, we set a document "ready" event handler to walk the DOM118* and make combined selector className changes. Having this parameter also119* aids unit testing.120*/121goog.debug.DevCss.prototype.activateBrowserSpecificCssRules = function(122opt_enableIe6ReadyHandler) {123var enableIe6EventHandler =124goog.isDef(opt_enableIe6ReadyHandler) ? opt_enableIe6ReadyHandler : true;125var cssRules = goog.cssom.getAllCssStyleRules();126127for (var i = 0, cssRule; cssRule = cssRules[i]; i++) {128this.replaceBrowserSpecificClassNames_(cssRule);129}130131// Since we may have manipulated the rules above, we'll have to do a132// complete sweep again if we're in IE6. Luckily performance doesn't133// matter for this tool.134if (this.isIe6OrLess_) {135cssRules = goog.cssom.getAllCssStyleRules();136for (var i = 0, cssRule; cssRule = cssRules[i]; i++) {137this.replaceIe6CombinedSelectors_(cssRule);138}139}140141// Add an event listener for document ready to rewrite any necessary142// combined classnames in IE6.143if (this.isIe6OrLess_ && enableIe6EventHandler) {144goog.events.listen(145document, goog.events.EventType.LOAD,146goog.bind(this.addIe6CombinedClassNames_, this));147}148};149150151/**152* A list of possible user agent strings.153* @enum {string}154*/155goog.debug.DevCss.UserAgent = {156OPERA: 'OPERA',157IE: 'IE',158GECKO: 'GECKO',159FIREFOX: 'GECKO',160WEBKIT: 'WEBKIT',161SAFARI: 'WEBKIT',162MOBILE: 'MOBILE',163EDGE: 'EDGE'164};165166167/**168* A list of strings that may be used for matching in CSS files/development.169* @enum {string}170* @private171*/172goog.debug.DevCss.CssToken_ = {173USERAGENT: 'USERAGENT',174SEPARATOR: '-',175LESS_THAN: 'LT',176GREATER_THAN: 'GT',177LESS_THAN_OR_EQUAL: 'LTE',178GREATER_THAN_OR_EQUAL: 'GTE',179IE6_SELECTOR_TEXT: 'goog-ie6-selector',180IE6_COMBINED_GLUE: '_'181};182183184/**185* Generates user agent token match strings with comparison and version bits.186* For example:187* userAgentTokens_.ANY will be like 'GECKO'188* userAgentTokens_.LESS_THAN will be like 'GECKO-LT3' etc...189* @private190*/191goog.debug.DevCss.prototype.generateUserAgentTokens_ = function() {192this.userAgentTokens_.ANY = goog.debug.DevCss.CssToken_.USERAGENT +193goog.debug.DevCss.CssToken_.SEPARATOR + this.userAgent_;194this.userAgentTokens_.EQUALS =195this.userAgentTokens_.ANY + goog.debug.DevCss.CssToken_.SEPARATOR;196this.userAgentTokens_.LESS_THAN = this.userAgentTokens_.ANY +197goog.debug.DevCss.CssToken_.SEPARATOR +198goog.debug.DevCss.CssToken_.LESS_THAN;199this.userAgentTokens_.LESS_THAN_OR_EQUAL = this.userAgentTokens_.ANY +200goog.debug.DevCss.CssToken_.SEPARATOR +201goog.debug.DevCss.CssToken_.LESS_THAN_OR_EQUAL;202this.userAgentTokens_.GREATER_THAN = this.userAgentTokens_.ANY +203goog.debug.DevCss.CssToken_.SEPARATOR +204goog.debug.DevCss.CssToken_.GREATER_THAN;205this.userAgentTokens_.GREATER_THAN_OR_EQUAL = this.userAgentTokens_.ANY +206goog.debug.DevCss.CssToken_.SEPARATOR +207goog.debug.DevCss.CssToken_.GREATER_THAN_OR_EQUAL;208};209210211/**212* Gets the version number bit from a selector matching userAgentToken.213* @param {string} selectorText The selector text of a CSS rule.214* @param {string} userAgentToken Includes the LTE/GTE bit to see if it matches.215* @return {string|undefined} The version number.216* @private217*/218goog.debug.DevCss.prototype.getVersionNumberFromSelectorText_ = function(219selectorText, userAgentToken) {220var regex = new RegExp(userAgentToken + '([\\d\\.]+)');221var matches = regex.exec(selectorText);222if (matches && matches.length == 2) {223return matches[1];224}225};226227228/**229* Extracts a rule version from the selector text, and if it finds one, calls230* compareVersions against it and the passed in token string to provide the231* value needed to determine if we have a match or not.232* @param {CSSRule} cssRule The rule to test against.233* @param {string} token The match token to test against the rule.234* @return {!Array|undefined} A tuple with the result of the compareVersions235* call and the matched ruleVersion.236* @private237*/238goog.debug.DevCss.prototype.getRuleVersionAndCompare_ = function(239cssRule, token) {240if (!cssRule.selectorText || !cssRule.selectorText.match(token)) {241return;242}243var ruleVersion =244this.getVersionNumberFromSelectorText_(cssRule.selectorText, token);245if (!ruleVersion) {246return;247}248249var comparison =250goog.string.compareVersions(this.userAgentVersion_, ruleVersion);251return [comparison, ruleVersion];252};253254255/**256* Replaces a CSS selector if we have matches based on our useragent/version.257* Example: With a selector like ".USERAGENT-IE-LTE6 .class { prop: value }" if258* we are running IE6 we'll end up with ".class { prop: value }", thereby259* "activating" the selector.260* @param {CSSRule} cssRule The cssRule to potentially replace.261* @private262*/263goog.debug.DevCss.prototype.replaceBrowserSpecificClassNames_ = function(264cssRule) {265266// If we don't match the browser token, we can stop now.267if (!cssRule.selectorText ||268!cssRule.selectorText.match(this.userAgentTokens_.ANY)) {269return;270}271272// We know it will begin as a classname.273var additionalRegexString;274275// Tests "Less than or equals".276var compared = this.getRuleVersionAndCompare_(277cssRule, this.userAgentTokens_.LESS_THAN_OR_EQUAL);278if (compared && compared.length) {279if (compared[0] > 0) {280return;281}282additionalRegexString =283this.userAgentTokens_.LESS_THAN_OR_EQUAL + compared[1];284}285286// Tests "Less than".287compared =288this.getRuleVersionAndCompare_(cssRule, this.userAgentTokens_.LESS_THAN);289if (compared && compared.length) {290if (compared[0] > -1) {291return;292}293additionalRegexString = this.userAgentTokens_.LESS_THAN + compared[1];294}295296// Tests "Greater than or equals".297compared = this.getRuleVersionAndCompare_(298cssRule, this.userAgentTokens_.GREATER_THAN_OR_EQUAL);299if (compared && compared.length) {300if (compared[0] < 0) {301return;302}303additionalRegexString =304this.userAgentTokens_.GREATER_THAN_OR_EQUAL + compared[1];305}306307// Tests "Greater than".308compared = this.getRuleVersionAndCompare_(309cssRule, this.userAgentTokens_.GREATER_THAN);310if (compared && compared.length) {311if (compared[0] < 1) {312return;313}314additionalRegexString = this.userAgentTokens_.GREATER_THAN + compared[1];315}316317// Tests "Equals".318compared =319this.getRuleVersionAndCompare_(cssRule, this.userAgentTokens_.EQUALS);320if (compared && compared.length) {321if (compared[0] != 0) {322return;323}324additionalRegexString = this.userAgentTokens_.EQUALS + compared[1];325}326327// If we got to here without generating the additionalRegexString, then328// we did not match any of our comparison token strings, and we want a329// general browser token replacement.330if (!additionalRegexString) {331additionalRegexString = this.userAgentTokens_.ANY;332}333334// We need to match at least a single whitespace character to know that335// we are matching the entire useragent string token.336var regexString = '\\.' + additionalRegexString + '\\s+';337var re = new RegExp(regexString, 'g');338339var currentCssText = goog.cssom.getCssTextFromCssRule(cssRule);340341// Replacing the token with '' activates the selector for this useragent.342var newCssText = currentCssText.replace(re, '');343344if (newCssText != currentCssText) {345goog.cssom.replaceCssRule(cssRule, newCssText);346}347};348349350/**351* Replaces IE6 combined selector rules with a workable development alternative.352* IE6 actually parses .class1.class2 {} to simply .class2 {} which is nasty.353* To fully support combined selectors in IE6 this function needs to be paired354* with a call to replace the relevant DOM elements classNames as well.355* @see {this.addIe6CombinedClassNames_}356* @param {CSSRule} cssRule The rule to potentially fix.357* @private358*/359goog.debug.DevCss.prototype.replaceIe6CombinedSelectors_ = function(cssRule) {360// This match only ever works in IE because other UA's won't have our361// IE6_SELECTOR_TEXT in the cssText property.362if (cssRule.style && cssRule.style.cssText &&363cssRule.style.cssText.match(364goog.debug.DevCss.CssToken_.IE6_SELECTOR_TEXT)) {365var cssText = goog.cssom.getCssTextFromCssRule(cssRule);366var combinedSelectorText = this.getIe6CombinedSelectorText_(cssText);367if (combinedSelectorText) {368var newCssText = combinedSelectorText + '{' + cssRule.style.cssText + '}';369goog.cssom.replaceCssRule(cssRule, newCssText);370}371}372};373374375/**376* Gets the appropriate new combined selector text for IE6.377* Also adds an entry onto ie6CombinedMatches_ with relevant info for the378* likely following call to walk the DOM and rewrite the class attribute.379* Example: With a selector like380* ".class2 { -goog-ie6-selector: .class1.class2; prop: value }".381* this function will return:382* ".class1_class2 { prop: value }".383* @param {string} cssText The CSS selector text and css rule text combined.384* @return {?string} The rewritten css rule text.385* @private386*/387goog.debug.DevCss.prototype.getIe6CombinedSelectorText_ = function(cssText) {388var regex = new RegExp(389goog.debug.DevCss.CssToken_.IE6_SELECTOR_TEXT +390'\\s*:\\s*\\"([^\\"]+)\\"',391'gi');392var matches = regex.exec(cssText);393if (matches) {394var combinedSelectorText = matches[1];395// To aid in later fixing the DOM, we need to split up the possible396// selector groups by commas.397var groupedSelectors = combinedSelectorText.split(/\s*\,\s*/);398for (var i = 0, selector; selector = groupedSelectors[i]; i++) {399// Strips off the leading ".".400var combinedClassName = selector.substr(1);401var classNames = combinedClassName.split(402goog.debug.DevCss.CssToken_.IE6_COMBINED_GLUE);403var entry = {404classNames: classNames,405combinedClassName: combinedClassName,406els: []407};408this.ie6CombinedMatches_.push(entry);409}410return combinedSelectorText;411}412return null;413};414415416/**417* Adds combined selectors with underscores to make them "work" in IE6.418* @see {this.replaceIe6CombinedSelectors_}419* @private420*/421goog.debug.DevCss.prototype.addIe6CombinedClassNames_ = function() {422if (!this.ie6CombinedMatches_.length) {423return;424}425var allEls = document.getElementsByTagName('*');426// Match nodes for all classNames.427for (var i = 0, classNameEntry; classNameEntry = this.ie6CombinedMatches_[i];428i++) {429for (var j = 0, el; el = allEls[j]; j++) {430var classNamesLength = classNameEntry.classNames.length;431for (var k = 0, className; className = classNameEntry.classNames[k];432k++) {433if (!goog.dom.classlist.contains(el, className)) {434break;435}436if (k == classNamesLength - 1) {437classNameEntry.els.push(el);438}439}440}441// Walks over our matching nodes and fixes them.442if (classNameEntry.els.length) {443for (var j = 0, el; el = classNameEntry.els[j]; j++) {444goog.asserts.assert(el);445if (!goog.dom.classlist.contains(446el, classNameEntry.combinedClassName)) {447goog.dom.classlist.add(el, classNameEntry.combinedClassName);448}449}450}451}452};453454455