123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- // 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;
- } else if (goog.userAgent.EDGE) {
- opt_userAgent = goog.debug.DevCss.UserAgent.EDGE;
- }
- }
- 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:
- case goog.debug.DevCss.UserAgent.EDGE:
- 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',
- EDGE: 'EDGE'
- };
- /**
- * 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 || !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 ||
- !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 && 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(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);
- }
- }
- }
- }
- };
|