devcss.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Runtime development CSS Compiler emulation, via javascript.
  16. * This class provides an approximation to CSSCompiler's functionality by
  17. * hacking the live CSSOM.
  18. * This code is designed to be inserted in the DOM immediately after the last
  19. * style block in HEAD when in development mode, i.e. you are not using a
  20. * running instance of a CSS Compiler to pass your CSS through.
  21. */
  22. goog.provide('goog.debug.DevCss');
  23. goog.provide('goog.debug.DevCss.UserAgent');
  24. goog.require('goog.asserts');
  25. goog.require('goog.cssom');
  26. goog.require('goog.dom.classlist');
  27. goog.require('goog.events');
  28. goog.require('goog.events.EventType');
  29. goog.require('goog.string');
  30. goog.require('goog.userAgent');
  31. /**
  32. * A class for solving development CSS issues/emulating the CSS Compiler.
  33. * @param {goog.debug.DevCss.UserAgent=} opt_userAgent The user agent, if not
  34. * passed in, will be determined using goog.userAgent.
  35. * @param {number|string=} opt_userAgentVersion The user agent's version.
  36. * If not passed in, will be determined using goog.userAgent.
  37. * @throws {Error} When userAgent detection fails.
  38. * @constructor
  39. * @final
  40. */
  41. goog.debug.DevCss = function(opt_userAgent, opt_userAgentVersion) {
  42. if (!opt_userAgent) {
  43. // Walks through the known goog.userAgents.
  44. if (goog.userAgent.IE) {
  45. opt_userAgent = goog.debug.DevCss.UserAgent.IE;
  46. } else if (goog.userAgent.GECKO) {
  47. opt_userAgent = goog.debug.DevCss.UserAgent.GECKO;
  48. } else if (goog.userAgent.WEBKIT) {
  49. opt_userAgent = goog.debug.DevCss.UserAgent.WEBKIT;
  50. } else if (goog.userAgent.MOBILE) {
  51. opt_userAgent = goog.debug.DevCss.UserAgent.MOBILE;
  52. } else if (goog.userAgent.OPERA) {
  53. opt_userAgent = goog.debug.DevCss.UserAgent.OPERA;
  54. } else if (goog.userAgent.EDGE) {
  55. opt_userAgent = goog.debug.DevCss.UserAgent.EDGE;
  56. }
  57. }
  58. switch (opt_userAgent) {
  59. case goog.debug.DevCss.UserAgent.OPERA:
  60. case goog.debug.DevCss.UserAgent.IE:
  61. case goog.debug.DevCss.UserAgent.GECKO:
  62. case goog.debug.DevCss.UserAgent.FIREFOX:
  63. case goog.debug.DevCss.UserAgent.WEBKIT:
  64. case goog.debug.DevCss.UserAgent.SAFARI:
  65. case goog.debug.DevCss.UserAgent.MOBILE:
  66. case goog.debug.DevCss.UserAgent.EDGE:
  67. break;
  68. default:
  69. throw Error('Could not determine the user agent from known UserAgents');
  70. }
  71. /**
  72. * One of goog.debug.DevCss.UserAgent.
  73. * @type {string}
  74. * @private
  75. */
  76. this.userAgent_ = opt_userAgent;
  77. /**
  78. * @const @private
  79. */
  80. this.userAgentTokens_ = {};
  81. /**
  82. * @type {number|string}
  83. * @private
  84. */
  85. this.userAgentVersion_ = opt_userAgentVersion || goog.userAgent.VERSION;
  86. this.generateUserAgentTokens_();
  87. /**
  88. * @type {boolean}
  89. * @private
  90. */
  91. this.isIe6OrLess_ = this.userAgent_ == goog.debug.DevCss.UserAgent.IE &&
  92. goog.string.compareVersions('7', this.userAgentVersion_) > 0;
  93. if (this.isIe6OrLess_) {
  94. /**
  95. * @type {Array<{classNames,combinedClassName,els}>}
  96. * @private
  97. */
  98. this.ie6CombinedMatches_ = [];
  99. }
  100. };
  101. /**
  102. * Rewrites the CSSOM as needed to activate any useragent-specific selectors.
  103. * @param {boolean=} opt_enableIe6ReadyHandler If true(the default), and the
  104. * userAgent is ie6, we set a document "ready" event handler to walk the DOM
  105. * and make combined selector className changes. Having this parameter also
  106. * aids unit testing.
  107. */
  108. goog.debug.DevCss.prototype.activateBrowserSpecificCssRules = function(
  109. opt_enableIe6ReadyHandler) {
  110. var enableIe6EventHandler =
  111. goog.isDef(opt_enableIe6ReadyHandler) ? opt_enableIe6ReadyHandler : true;
  112. var cssRules = goog.cssom.getAllCssStyleRules();
  113. for (var i = 0, cssRule; cssRule = cssRules[i]; i++) {
  114. this.replaceBrowserSpecificClassNames_(cssRule);
  115. }
  116. // Since we may have manipulated the rules above, we'll have to do a
  117. // complete sweep again if we're in IE6. Luckily performance doesn't
  118. // matter for this tool.
  119. if (this.isIe6OrLess_) {
  120. cssRules = goog.cssom.getAllCssStyleRules();
  121. for (var i = 0, cssRule; cssRule = cssRules[i]; i++) {
  122. this.replaceIe6CombinedSelectors_(cssRule);
  123. }
  124. }
  125. // Add an event listener for document ready to rewrite any necessary
  126. // combined classnames in IE6.
  127. if (this.isIe6OrLess_ && enableIe6EventHandler) {
  128. goog.events.listen(
  129. document, goog.events.EventType.LOAD,
  130. goog.bind(this.addIe6CombinedClassNames_, this));
  131. }
  132. };
  133. /**
  134. * A list of possible user agent strings.
  135. * @enum {string}
  136. */
  137. goog.debug.DevCss.UserAgent = {
  138. OPERA: 'OPERA',
  139. IE: 'IE',
  140. GECKO: 'GECKO',
  141. FIREFOX: 'GECKO',
  142. WEBKIT: 'WEBKIT',
  143. SAFARI: 'WEBKIT',
  144. MOBILE: 'MOBILE',
  145. EDGE: 'EDGE'
  146. };
  147. /**
  148. * A list of strings that may be used for matching in CSS files/development.
  149. * @enum {string}
  150. * @private
  151. */
  152. goog.debug.DevCss.CssToken_ = {
  153. USERAGENT: 'USERAGENT',
  154. SEPARATOR: '-',
  155. LESS_THAN: 'LT',
  156. GREATER_THAN: 'GT',
  157. LESS_THAN_OR_EQUAL: 'LTE',
  158. GREATER_THAN_OR_EQUAL: 'GTE',
  159. IE6_SELECTOR_TEXT: 'goog-ie6-selector',
  160. IE6_COMBINED_GLUE: '_'
  161. };
  162. /**
  163. * Generates user agent token match strings with comparison and version bits.
  164. * For example:
  165. * userAgentTokens_.ANY will be like 'GECKO'
  166. * userAgentTokens_.LESS_THAN will be like 'GECKO-LT3' etc...
  167. * @private
  168. */
  169. goog.debug.DevCss.prototype.generateUserAgentTokens_ = function() {
  170. this.userAgentTokens_.ANY = goog.debug.DevCss.CssToken_.USERAGENT +
  171. goog.debug.DevCss.CssToken_.SEPARATOR + this.userAgent_;
  172. this.userAgentTokens_.EQUALS =
  173. this.userAgentTokens_.ANY + goog.debug.DevCss.CssToken_.SEPARATOR;
  174. this.userAgentTokens_.LESS_THAN = this.userAgentTokens_.ANY +
  175. goog.debug.DevCss.CssToken_.SEPARATOR +
  176. goog.debug.DevCss.CssToken_.LESS_THAN;
  177. this.userAgentTokens_.LESS_THAN_OR_EQUAL = this.userAgentTokens_.ANY +
  178. goog.debug.DevCss.CssToken_.SEPARATOR +
  179. goog.debug.DevCss.CssToken_.LESS_THAN_OR_EQUAL;
  180. this.userAgentTokens_.GREATER_THAN = this.userAgentTokens_.ANY +
  181. goog.debug.DevCss.CssToken_.SEPARATOR +
  182. goog.debug.DevCss.CssToken_.GREATER_THAN;
  183. this.userAgentTokens_.GREATER_THAN_OR_EQUAL = this.userAgentTokens_.ANY +
  184. goog.debug.DevCss.CssToken_.SEPARATOR +
  185. goog.debug.DevCss.CssToken_.GREATER_THAN_OR_EQUAL;
  186. };
  187. /**
  188. * Gets the version number bit from a selector matching userAgentToken.
  189. * @param {string} selectorText The selector text of a CSS rule.
  190. * @param {string} userAgentToken Includes the LTE/GTE bit to see if it matches.
  191. * @return {string|undefined} The version number.
  192. * @private
  193. */
  194. goog.debug.DevCss.prototype.getVersionNumberFromSelectorText_ = function(
  195. selectorText, userAgentToken) {
  196. var regex = new RegExp(userAgentToken + '([\\d\\.]+)');
  197. var matches = regex.exec(selectorText);
  198. if (matches && matches.length == 2) {
  199. return matches[1];
  200. }
  201. };
  202. /**
  203. * Extracts a rule version from the selector text, and if it finds one, calls
  204. * compareVersions against it and the passed in token string to provide the
  205. * value needed to determine if we have a match or not.
  206. * @param {CSSRule} cssRule The rule to test against.
  207. * @param {string} token The match token to test against the rule.
  208. * @return {!Array|undefined} A tuple with the result of the compareVersions
  209. * call and the matched ruleVersion.
  210. * @private
  211. */
  212. goog.debug.DevCss.prototype.getRuleVersionAndCompare_ = function(
  213. cssRule, token) {
  214. if (!cssRule.selectorText || !cssRule.selectorText.match(token)) {
  215. return;
  216. }
  217. var ruleVersion =
  218. this.getVersionNumberFromSelectorText_(cssRule.selectorText, token);
  219. if (!ruleVersion) {
  220. return;
  221. }
  222. var comparison =
  223. goog.string.compareVersions(this.userAgentVersion_, ruleVersion);
  224. return [comparison, ruleVersion];
  225. };
  226. /**
  227. * Replaces a CSS selector if we have matches based on our useragent/version.
  228. * Example: With a selector like ".USERAGENT-IE-LTE6 .class { prop: value }" if
  229. * we are running IE6 we'll end up with ".class { prop: value }", thereby
  230. * "activating" the selector.
  231. * @param {CSSRule} cssRule The cssRule to potentially replace.
  232. * @private
  233. */
  234. goog.debug.DevCss.prototype.replaceBrowserSpecificClassNames_ = function(
  235. cssRule) {
  236. // If we don't match the browser token, we can stop now.
  237. if (!cssRule.selectorText ||
  238. !cssRule.selectorText.match(this.userAgentTokens_.ANY)) {
  239. return;
  240. }
  241. // We know it will begin as a classname.
  242. var additionalRegexString;
  243. // Tests "Less than or equals".
  244. var compared = this.getRuleVersionAndCompare_(
  245. cssRule, this.userAgentTokens_.LESS_THAN_OR_EQUAL);
  246. if (compared && compared.length) {
  247. if (compared[0] > 0) {
  248. return;
  249. }
  250. additionalRegexString =
  251. this.userAgentTokens_.LESS_THAN_OR_EQUAL + compared[1];
  252. }
  253. // Tests "Less than".
  254. compared =
  255. this.getRuleVersionAndCompare_(cssRule, this.userAgentTokens_.LESS_THAN);
  256. if (compared && compared.length) {
  257. if (compared[0] > -1) {
  258. return;
  259. }
  260. additionalRegexString = this.userAgentTokens_.LESS_THAN + compared[1];
  261. }
  262. // Tests "Greater than or equals".
  263. compared = this.getRuleVersionAndCompare_(
  264. cssRule, this.userAgentTokens_.GREATER_THAN_OR_EQUAL);
  265. if (compared && compared.length) {
  266. if (compared[0] < 0) {
  267. return;
  268. }
  269. additionalRegexString =
  270. this.userAgentTokens_.GREATER_THAN_OR_EQUAL + compared[1];
  271. }
  272. // Tests "Greater than".
  273. compared = this.getRuleVersionAndCompare_(
  274. cssRule, this.userAgentTokens_.GREATER_THAN);
  275. if (compared && compared.length) {
  276. if (compared[0] < 1) {
  277. return;
  278. }
  279. additionalRegexString = this.userAgentTokens_.GREATER_THAN + compared[1];
  280. }
  281. // Tests "Equals".
  282. compared =
  283. this.getRuleVersionAndCompare_(cssRule, this.userAgentTokens_.EQUALS);
  284. if (compared && compared.length) {
  285. if (compared[0] != 0) {
  286. return;
  287. }
  288. additionalRegexString = this.userAgentTokens_.EQUALS + compared[1];
  289. }
  290. // If we got to here without generating the additionalRegexString, then
  291. // we did not match any of our comparison token strings, and we want a
  292. // general browser token replacement.
  293. if (!additionalRegexString) {
  294. additionalRegexString = this.userAgentTokens_.ANY;
  295. }
  296. // We need to match at least a single whitespace character to know that
  297. // we are matching the entire useragent string token.
  298. var regexString = '\\.' + additionalRegexString + '\\s+';
  299. var re = new RegExp(regexString, 'g');
  300. var currentCssText = goog.cssom.getCssTextFromCssRule(cssRule);
  301. // Replacing the token with '' activates the selector for this useragent.
  302. var newCssText = currentCssText.replace(re, '');
  303. if (newCssText != currentCssText) {
  304. goog.cssom.replaceCssRule(cssRule, newCssText);
  305. }
  306. };
  307. /**
  308. * Replaces IE6 combined selector rules with a workable development alternative.
  309. * IE6 actually parses .class1.class2 {} to simply .class2 {} which is nasty.
  310. * To fully support combined selectors in IE6 this function needs to be paired
  311. * with a call to replace the relevant DOM elements classNames as well.
  312. * @see {this.addIe6CombinedClassNames_}
  313. * @param {CSSRule} cssRule The rule to potentially fix.
  314. * @private
  315. */
  316. goog.debug.DevCss.prototype.replaceIe6CombinedSelectors_ = function(cssRule) {
  317. // This match only ever works in IE because other UA's won't have our
  318. // IE6_SELECTOR_TEXT in the cssText property.
  319. if (cssRule.style && cssRule.style.cssText &&
  320. cssRule.style.cssText.match(
  321. goog.debug.DevCss.CssToken_.IE6_SELECTOR_TEXT)) {
  322. var cssText = goog.cssom.getCssTextFromCssRule(cssRule);
  323. var combinedSelectorText = this.getIe6CombinedSelectorText_(cssText);
  324. if (combinedSelectorText) {
  325. var newCssText = combinedSelectorText + '{' + cssRule.style.cssText + '}';
  326. goog.cssom.replaceCssRule(cssRule, newCssText);
  327. }
  328. }
  329. };
  330. /**
  331. * Gets the appropriate new combined selector text for IE6.
  332. * Also adds an entry onto ie6CombinedMatches_ with relevant info for the
  333. * likely following call to walk the DOM and rewrite the class attribute.
  334. * Example: With a selector like
  335. * ".class2 { -goog-ie6-selector: .class1.class2; prop: value }".
  336. * this function will return:
  337. * ".class1_class2 { prop: value }".
  338. * @param {string} cssText The CSS selector text and css rule text combined.
  339. * @return {?string} The rewritten css rule text.
  340. * @private
  341. */
  342. goog.debug.DevCss.prototype.getIe6CombinedSelectorText_ = function(cssText) {
  343. var regex = new RegExp(
  344. goog.debug.DevCss.CssToken_.IE6_SELECTOR_TEXT +
  345. '\\s*:\\s*\\"([^\\"]+)\\"',
  346. 'gi');
  347. var matches = regex.exec(cssText);
  348. if (matches) {
  349. var combinedSelectorText = matches[1];
  350. // To aid in later fixing the DOM, we need to split up the possible
  351. // selector groups by commas.
  352. var groupedSelectors = combinedSelectorText.split(/\s*\,\s*/);
  353. for (var i = 0, selector; selector = groupedSelectors[i]; i++) {
  354. // Strips off the leading ".".
  355. var combinedClassName = selector.substr(1);
  356. var classNames = combinedClassName.split(
  357. goog.debug.DevCss.CssToken_.IE6_COMBINED_GLUE);
  358. var entry = {
  359. classNames: classNames,
  360. combinedClassName: combinedClassName,
  361. els: []
  362. };
  363. this.ie6CombinedMatches_.push(entry);
  364. }
  365. return combinedSelectorText;
  366. }
  367. return null;
  368. };
  369. /**
  370. * Adds combined selectors with underscores to make them "work" in IE6.
  371. * @see {this.replaceIe6CombinedSelectors_}
  372. * @private
  373. */
  374. goog.debug.DevCss.prototype.addIe6CombinedClassNames_ = function() {
  375. if (!this.ie6CombinedMatches_.length) {
  376. return;
  377. }
  378. var allEls = document.getElementsByTagName('*');
  379. // Match nodes for all classNames.
  380. for (var i = 0, classNameEntry; classNameEntry = this.ie6CombinedMatches_[i];
  381. i++) {
  382. for (var j = 0, el; el = allEls[j]; j++) {
  383. var classNamesLength = classNameEntry.classNames.length;
  384. for (var k = 0, className; className = classNameEntry.classNames[k];
  385. k++) {
  386. if (!goog.dom.classlist.contains(el, className)) {
  387. break;
  388. }
  389. if (k == classNamesLength - 1) {
  390. classNameEntry.els.push(el);
  391. }
  392. }
  393. }
  394. // Walks over our matching nodes and fixes them.
  395. if (classNameEntry.els.length) {
  396. for (var j = 0, el; el = classNameEntry.els[j]; j++) {
  397. goog.asserts.assert(el);
  398. if (!goog.dom.classlist.contains(
  399. el, classNameEntry.combinedClassName)) {
  400. goog.dom.classlist.add(el, classNameEntry.combinedClassName);
  401. }
  402. }
  403. }
  404. }
  405. };