445 lines
14 KiB
JavaScript
445 lines
14 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 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.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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|