style.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. // Copyright 2007 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. // All Rights Reserved.
  15. /**
  16. * @fileoverview Provides utility routines for copying modified
  17. * {@code CSSRule} objects from the parent document into iframes so that any
  18. * content in the iframe will be styled as if it was inline in the parent
  19. * document.
  20. *
  21. * <p>
  22. * For example, you might have this CSS rule:
  23. *
  24. * #content .highlighted { background-color: yellow; }
  25. *
  26. * And this DOM structure:
  27. *
  28. * <div id="content">
  29. * <iframe />
  30. * </div>
  31. *
  32. * Then inside the iframe you have:
  33. *
  34. * <body>
  35. * <div class="highlighted">
  36. * </body>
  37. *
  38. * If you copied the CSS rule directly into the iframe, it wouldn't match the
  39. * .highlighted div. So we rewrite the original stylesheets based on the
  40. * context where the iframe is going to be inserted. In this case the CSS
  41. * selector would be rewritten to:
  42. *
  43. * body .highlighted { background-color: yellow; }
  44. * </p>
  45. *
  46. */
  47. goog.provide('goog.cssom.iframe.style');
  48. goog.require('goog.asserts');
  49. goog.require('goog.cssom');
  50. goog.require('goog.dom');
  51. goog.require('goog.dom.NodeType');
  52. goog.require('goog.dom.TagName');
  53. goog.require('goog.dom.classlist');
  54. goog.require('goog.string');
  55. goog.require('goog.style');
  56. goog.require('goog.userAgent');
  57. /**
  58. * Regexp that matches "a", "a:link", "a:visited", etc.
  59. * @type {RegExp}
  60. * @private
  61. */
  62. goog.cssom.iframe.style.selectorPartAnchorRegex_ =
  63. /a(:(link|visited|active|hover))?/;
  64. /**
  65. * Delimiter between selectors (h1, h2)
  66. * @type {string}
  67. * @private
  68. */
  69. goog.cssom.iframe.style.SELECTOR_DELIMITER_ = ',';
  70. /**
  71. * Delimiter between selector parts (.main h1)
  72. * @type {string}
  73. * @private
  74. */
  75. goog.cssom.iframe.style.SELECTOR_PART_DELIMITER_ = ' ';
  76. /**
  77. * Delimiter marking the start of a css rules section ( h1 { )
  78. * @type {string}
  79. * @private
  80. */
  81. goog.cssom.iframe.style.DECLARATION_START_DELIMITER_ = '{';
  82. /**
  83. * Delimiter marking the end of a css rules section ( } )
  84. * @type {string}
  85. * @private
  86. */
  87. goog.cssom.iframe.style.DECLARATION_END_DELIMITER_ = '}\n';
  88. /**
  89. * Class representing a CSS rule set. A rule set is something like this:
  90. * h1, h2 { font-family: Arial; color: red; }
  91. * @constructor
  92. * @private
  93. */
  94. goog.cssom.iframe.style.CssRuleSet_ = function() {
  95. /**
  96. * Text of the declarations inside the rule set.
  97. * For example: 'font-family: Arial; color: red;'
  98. * @type {string}
  99. */
  100. this.declarationText = '';
  101. /**
  102. * Array of CssSelector objects, one for each selector.
  103. * Example: [h1, h2]
  104. * @type {Array<goog.cssom.iframe.style.CssSelector_>}
  105. */
  106. this.selectors = [];
  107. };
  108. /**
  109. * Initializes the rule set from a {@code CSSRule}.
  110. *
  111. * @param {CSSRule} cssRule The {@code CSSRule} to initialize from.
  112. * @return {boolean} True if initialization succeeded. We only support
  113. * {@code CSSStyleRule} and {@code CSSFontFaceRule} objects.
  114. */
  115. goog.cssom.iframe.style.CssRuleSet_.prototype.initializeFromCssRule = function(
  116. cssRule) {
  117. var ruleStyle = cssRule.style; // Cache object for performance.
  118. if (!ruleStyle) {
  119. return false;
  120. }
  121. var selector;
  122. var declarations = '';
  123. if (ruleStyle && (selector = cssRule.selectorText) &&
  124. (declarations = ruleStyle.cssText)) {
  125. // IE get confused about cssText context if a stylesheet uses the
  126. // mid-pass hack, and it ends up with an open comment (/*) but no
  127. // closing comment. This will effectively comment out large parts
  128. // of generated stylesheets later. This errs on the safe side by
  129. // always tacking on an empty comment to force comments to be closed
  130. // We used to check for a troublesome open comment using a regular
  131. // expression, but it's faster not to check and always do this.
  132. if (goog.userAgent.IE) {
  133. declarations += '/* */';
  134. }
  135. } else if (cssRule.cssText) {
  136. var cssSelectorMatch = /([^\{]+)\{/;
  137. var endTagMatch = /\}[^\}]*$/g;
  138. // cssRule.cssText contains both selector and declarations:
  139. // parse them out.
  140. selector = cssSelectorMatch.exec(cssRule.cssText)[1];
  141. // Remove selector, {, and trailing }.
  142. declarations =
  143. cssRule.cssText.replace(cssSelectorMatch, '').replace(endTagMatch, '');
  144. }
  145. if (selector) {
  146. this.setSelectorsFromString(selector);
  147. this.declarationText = declarations;
  148. return true;
  149. }
  150. return false;
  151. };
  152. /**
  153. * Parses a selectors string (which may contain multiple comma-delimited
  154. * selectors) and loads the results into this.selectors.
  155. * @param {string} selectorsString String containing selectors.
  156. */
  157. goog.cssom.iframe.style.CssRuleSet_.prototype.setSelectorsFromString = function(
  158. selectorsString) {
  159. this.selectors = [];
  160. var selectors = selectorsString.split(/,\s*/gm);
  161. for (var i = 0; i < selectors.length; i++) {
  162. var selector = selectors[i];
  163. if (selector.length > 0) {
  164. this.selectors.push(new goog.cssom.iframe.style.CssSelector_(selector));
  165. }
  166. }
  167. };
  168. /**
  169. * Make a copy of this ruleset.
  170. * @return {!goog.cssom.iframe.style.CssRuleSet_} A new CssRuleSet containing
  171. * the same data as this one.
  172. */
  173. goog.cssom.iframe.style.CssRuleSet_.prototype.clone = function() {
  174. var newRuleSet = new goog.cssom.iframe.style.CssRuleSet_();
  175. newRuleSet.selectors = this.selectors.concat();
  176. newRuleSet.declarationText = this.declarationText;
  177. return newRuleSet;
  178. };
  179. /**
  180. * Set the declaration text with properties from a given object.
  181. * @param {Object} sourceObject Object whose properties and values should
  182. * be used to generate the declaration text.
  183. * @param {boolean=} opt_important Whether !important should be added to each
  184. * declaration.
  185. */
  186. goog.cssom.iframe.style.CssRuleSet_.prototype.setDeclarationTextFromObject =
  187. function(sourceObject, opt_important) {
  188. var stringParts = [];
  189. // TODO(user): for ... in is costly in IE6 (extra garbage collection).
  190. for (var prop in sourceObject) {
  191. var value = sourceObject[prop];
  192. if (value) {
  193. stringParts.push(
  194. prop, ':', value, (opt_important ? ' !important' : ''), ';');
  195. }
  196. }
  197. this.declarationText = stringParts.join('');
  198. };
  199. /**
  200. * Serializes this CssRuleSet_ into an array as a series of strings.
  201. * The array can then be join()-ed to get a string representation
  202. * of this ruleset.
  203. * @param {Array<string>} array The array to which to append strings.
  204. */
  205. goog.cssom.iframe.style.CssRuleSet_.prototype.writeToArray = function(array) {
  206. var selectorCount = this.selectors.length;
  207. var matchesAnchorTag = false;
  208. for (var i = 0; i < selectorCount; i++) {
  209. var selectorParts = this.selectors[i].parts;
  210. var partCount = selectorParts.length;
  211. for (var j = 0; j < partCount; j++) {
  212. array.push(
  213. selectorParts[j].inputString_,
  214. goog.cssom.iframe.style.SELECTOR_PART_DELIMITER_);
  215. }
  216. if (i < (selectorCount - 1)) {
  217. array.push(goog.cssom.iframe.style.SELECTOR_DELIMITER_);
  218. }
  219. if (goog.userAgent.GECKO && !goog.userAgent.isVersionOrHigher('1.9a')) {
  220. // In Gecko pre-1.9 (Firefox 2 and lower) we need to add !important
  221. // to rulesets that match "A" tags, otherwise Gecko's built-in
  222. // stylesheet will take precedence when designMode is on.
  223. matchesAnchorTag = matchesAnchorTag ||
  224. goog.cssom.iframe.style.selectorPartAnchorRegex_.test(
  225. selectorParts[partCount - 1].inputString_);
  226. }
  227. }
  228. var declarationText = this.declarationText;
  229. if (matchesAnchorTag) {
  230. declarationText =
  231. goog.cssom.iframe.style.makeColorRuleImportant_(declarationText);
  232. }
  233. array.push(
  234. goog.cssom.iframe.style.DECLARATION_START_DELIMITER_, declarationText,
  235. goog.cssom.iframe.style.DECLARATION_END_DELIMITER_);
  236. };
  237. /**
  238. * Regexp that matches "color: value;".
  239. * @type {RegExp}
  240. * @private
  241. */
  242. goog.cssom.iframe.style.colorImportantReplaceRegex_ =
  243. /(^|;|{)\s*color:([^;]+);/g;
  244. /**
  245. * Adds !important to a css color: rule
  246. * @param {string} cssText Text of the CSS rule(s) to modify.
  247. * @return {string} Text with !important added to the color: rule if found.
  248. * @private
  249. */
  250. goog.cssom.iframe.style.makeColorRuleImportant_ = function(cssText) {
  251. // Replace to insert a "! important" string.
  252. return cssText.replace(
  253. goog.cssom.iframe.style.colorImportantReplaceRegex_,
  254. '$1 color: $2 ! important; ');
  255. };
  256. /**
  257. * Represents a single CSS selector, as described in
  258. * http://www.w3.org/TR/REC-CSS2/selector.html
  259. * Currently UNSUPPORTED are the following selector features:
  260. * <ul>
  261. * <li>pseudo-classes (:hover)
  262. * <li>child selectors (div > h1)
  263. * <li>adjacent sibling selectors (div + h1)
  264. * <li>attribute selectors (input[type=submit])
  265. * </ul>
  266. * @param {string=} opt_selectorString String containing selectors to parse.
  267. * @constructor
  268. * @private
  269. */
  270. goog.cssom.iframe.style.CssSelector_ = function(opt_selectorString) {
  271. /**
  272. * Object to track ancestry matches to speed up repeatedly testing this
  273. * CssSelector against the same NodeAncestry object.
  274. * @type {Object}
  275. * @private
  276. */
  277. this.ancestryMatchCache_ = {};
  278. if (opt_selectorString) {
  279. this.setPartsFromString_(opt_selectorString);
  280. }
  281. };
  282. /**
  283. * Parses a selector string into individual parts.
  284. * @param {string} selectorString A string containing a CSS selector.
  285. * @private
  286. */
  287. goog.cssom.iframe.style.CssSelector_.prototype.setPartsFromString_ = function(
  288. selectorString) {
  289. var parts = [];
  290. var selectorPartStrings = selectorString.split(/\s+/gm);
  291. for (var i = 0; i < selectorPartStrings.length; i++) {
  292. if (!selectorPartStrings[i]) {
  293. continue; // Skip empty strings.
  294. }
  295. var part =
  296. new goog.cssom.iframe.style.CssSelectorPart_(selectorPartStrings[i]);
  297. parts.push(part);
  298. }
  299. this.parts = parts;
  300. };
  301. /**
  302. * Tests to see what part of a DOM element hierarchy would be matched by
  303. * this selector, and returns the indexes of the matching element and matching
  304. * selector part.
  305. * <p>
  306. * For example, given this hierarchy:
  307. * document > html > body > div.content > div.sidebar > p
  308. * and this CSS selector:
  309. * body div.sidebar h1
  310. * This would return {elementIndex: 4, selectorPartIndex: 1},
  311. * indicating that the element at index 4 matched
  312. * the css selector at index 1.
  313. * </p>
  314. * @param {goog.cssom.iframe.style.NodeAncestry_} elementAncestry Object
  315. * representing an element and its ancestors.
  316. * @return {Object} Object with the properties elementIndex and
  317. * selectorPartIndex, or null if there was no match.
  318. */
  319. goog.cssom.iframe.style.CssSelector_.prototype.matchElementAncestry = function(
  320. elementAncestry) {
  321. var ancestryUid = elementAncestry.uid;
  322. if (this.ancestryMatchCache_[ancestryUid]) {
  323. return this.ancestryMatchCache_[ancestryUid];
  324. }
  325. // Walk through the selector parts and see how far down the element hierarchy
  326. // we can go while matching the selector parts.
  327. var elementIndex = 0;
  328. var match = null;
  329. var selectorPart = null;
  330. var lastSelectorPart = null;
  331. var ancestorNodes = elementAncestry.nodes;
  332. var ancestorNodeCount = ancestorNodes.length;
  333. for (var i = 0; i <= this.parts.length; i++) {
  334. selectorPart = this.parts[i];
  335. while (elementIndex < ancestorNodeCount) {
  336. var currentElementInfo = ancestorNodes[elementIndex];
  337. if (selectorPart && selectorPart.testElement(currentElementInfo)) {
  338. match = {elementIndex: elementIndex, selectorPartIndex: i};
  339. elementIndex++;
  340. break;
  341. } else if (
  342. lastSelectorPart &&
  343. lastSelectorPart.testElement(currentElementInfo)) {
  344. match = {elementIndex: elementIndex, selectorPartIndex: i - 1};
  345. }
  346. elementIndex++;
  347. }
  348. lastSelectorPart = selectorPart;
  349. }
  350. this.ancestryMatchCache_[ancestryUid] = match;
  351. return match;
  352. };
  353. /**
  354. * Represents one part of a CSS Selector. For example in the selector
  355. * 'body #foo .bar', body, #foo, and .bar would be considered selector parts.
  356. * In the official CSS spec these are called "simple selectors".
  357. * @param {string} selectorPartString A string containing the selector part
  358. * in css format.
  359. * @constructor
  360. * @private
  361. */
  362. goog.cssom.iframe.style.CssSelectorPart_ = function(selectorPartString) {
  363. // Only one CssSelectorPart instance should exist for a given string.
  364. var cacheEntry =
  365. goog.cssom.iframe.style.CssSelectorPart_.instances_[selectorPartString];
  366. if (cacheEntry) {
  367. return cacheEntry;
  368. }
  369. // Optimization to avoid the more-expensive lookahead.
  370. var identifiers;
  371. if (selectorPartString.match(/[#\.]/)) {
  372. // Lookahead regexp, won't work on IE 5.0.
  373. identifiers = selectorPartString.split(/(?=[#\.])/);
  374. } else {
  375. identifiers = [selectorPartString];
  376. }
  377. var properties = {};
  378. for (var i = 0; i < identifiers.length; i++) {
  379. var identifier = identifiers[i];
  380. if (identifier.charAt(0) == '.') {
  381. properties.className = identifier.substring(1, identifier.length);
  382. } else if (identifier.charAt(0) == '#') {
  383. properties.id = identifier.substring(1, identifier.length);
  384. } else {
  385. properties.tagName = identifier.toUpperCase();
  386. }
  387. }
  388. this.inputString_ = selectorPartString;
  389. this.matchProperties_ = properties;
  390. this.testedElements_ = {};
  391. goog.cssom.iframe.style.CssSelectorPart_.instances_[selectorPartString] =
  392. this;
  393. };
  394. /**
  395. * Cache of existing CssSelectorPart_ instances.
  396. * @type {Object}
  397. * @private
  398. */
  399. goog.cssom.iframe.style.CssSelectorPart_.instances_ = {};
  400. /**
  401. * Test whether an element matches this selector part, considered in isolation.
  402. * @param {Object} elementInfo Element properties to test.
  403. * @return {boolean} Whether the element matched.
  404. */
  405. goog.cssom.iframe.style.CssSelectorPart_.prototype.testElement = function(
  406. elementInfo) {
  407. var elementUid = elementInfo.uid;
  408. var cachedMatch = this.testedElements_[elementUid];
  409. if (typeof cachedMatch != 'undefined') {
  410. return cachedMatch;
  411. }
  412. var matchProperties = this.matchProperties_;
  413. var testTag = matchProperties.tagName;
  414. var testClass = matchProperties.className;
  415. var testId = matchProperties.id;
  416. var matched = true;
  417. if (testTag && testTag != '*' && testTag != elementInfo.nodeName) {
  418. matched = false;
  419. } else if (testId && testId != elementInfo.id) {
  420. matched = false;
  421. } else if (testClass && !elementInfo.classNames[testClass]) {
  422. matched = false;
  423. }
  424. this.testedElements_[elementUid] = matched;
  425. return matched;
  426. };
  427. /**
  428. * Represents an element and all its parent/ancestor nodes.
  429. * This class exists as an optimization so we run tests on an element
  430. * hierarchy multiple times without walking the dom each time.
  431. * @param {Element} el The DOM element whose ancestry should be stored.
  432. * @constructor
  433. * @private
  434. */
  435. goog.cssom.iframe.style.NodeAncestry_ = function(el) {
  436. var node = el;
  437. var nodeUid = goog.getUid(node);
  438. // Return an existing object from the cache if one exits for this node.
  439. var ancestry = goog.cssom.iframe.style.NodeAncestry_.instances_[nodeUid];
  440. if (ancestry) {
  441. return ancestry;
  442. }
  443. var nodes = [];
  444. do {
  445. var nodeInfo = {id: node.id, nodeName: node.nodeName};
  446. nodeInfo.uid = goog.getUid(nodeInfo);
  447. var className = node.className;
  448. var classNamesLookup = {};
  449. if (className) {
  450. var classNames = goog.dom.classlist.get(goog.asserts.assertElement(node));
  451. for (var i = 0; i < classNames.length; i++) {
  452. classNamesLookup[classNames[i]] = 1;
  453. }
  454. }
  455. nodeInfo.classNames = classNamesLookup;
  456. nodes.unshift(nodeInfo);
  457. } while (node = node.parentNode);
  458. /**
  459. * Array of nodes in order of hierarchy from the top of the document
  460. * to the node passed to the constructor
  461. * @type {Array<Node>}
  462. */
  463. this.nodes = nodes;
  464. this.uid = goog.getUid(this);
  465. goog.cssom.iframe.style.NodeAncestry_.instances_[nodeUid] = this;
  466. };
  467. /**
  468. * Object for caching existing NodeAncestry instances.
  469. * @private
  470. */
  471. goog.cssom.iframe.style.NodeAncestry_.instances_ = {};
  472. /**
  473. * Throw away all cached dom information. Call this if you've modified
  474. * the structure or class/id attributes of your document and you want
  475. * to recalculate the currently applied CSS rules.
  476. */
  477. goog.cssom.iframe.style.resetDomCache = function() {
  478. goog.cssom.iframe.style.NodeAncestry_.instances_ = {};
  479. };
  480. /**
  481. * Inspects a document and returns all active rule sets
  482. * @param {Document} doc The document from which to read CSS rules.
  483. * @return {!Array<goog.cssom.iframe.style.CssRuleSet_>} An array of CssRuleSet
  484. * objects representing all the active rule sets in the document.
  485. * @private
  486. */
  487. goog.cssom.iframe.style.getRuleSetsFromDocument_ = function(doc) {
  488. var ruleSets = [];
  489. var styleSheets = goog.cssom.getAllCssStyleSheets(doc.styleSheets);
  490. for (var i = 0, styleSheet; styleSheet = styleSheets[i]; i++) {
  491. var domRuleSets = goog.cssom.getCssRulesFromStyleSheet(styleSheet);
  492. if (domRuleSets && domRuleSets.length) {
  493. for (var j = 0, n = domRuleSets.length; j < n; j++) {
  494. var ruleSet = new goog.cssom.iframe.style.CssRuleSet_();
  495. if (ruleSet.initializeFromCssRule(domRuleSets[j])) {
  496. ruleSets.push(ruleSet);
  497. }
  498. }
  499. }
  500. }
  501. return ruleSets;
  502. };
  503. /**
  504. * Static object to cache rulesets read from documents. Inspecting all
  505. * active css rules is an expensive operation, so its best to only do
  506. * it once and then cache the results.
  507. * @type {Object}
  508. * @private
  509. */
  510. goog.cssom.iframe.style.ruleSetCache_ = {};
  511. /**
  512. * Cache of ruleset objects keyed by document unique ID.
  513. * @type {Object}
  514. * @private
  515. */
  516. goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_ = {};
  517. /**
  518. * Loads ruleset definitions from a document. If the cache already
  519. * has rulesets for this document the cached version will be replaced.
  520. * @param {Document} doc The document from which to load rulesets.
  521. */
  522. goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument = function(doc) {
  523. var docUid = goog.getUid(doc);
  524. goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_[docUid] =
  525. goog.cssom.iframe.style.getRuleSetsFromDocument_(doc);
  526. };
  527. /**
  528. * Retrieves the array of css rulesets for this document. A cached
  529. * version will be used when possible.
  530. * @param {Document} doc The document for which to get rulesets.
  531. * @return {!Array<goog.cssom.iframe.style.CssRuleSet_>} An array of CssRuleSet
  532. * objects representing the css rule sets in the supplied document.
  533. */
  534. goog.cssom.iframe.style.ruleSetCache_.getRuleSetsForDocument = function(doc) {
  535. var docUid = goog.getUid(doc);
  536. var cache = goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_;
  537. if (!cache[docUid]) {
  538. goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument(doc);
  539. }
  540. // Build a cloned copy of rulesets array, so if object in the returned array
  541. // get modified future calls will still return the original unmodified
  542. // versions.
  543. var ruleSets = cache[docUid];
  544. var ruleSetsCopy = [];
  545. for (var i = 0; i < ruleSets.length; i++) {
  546. ruleSetsCopy.push(ruleSets[i].clone());
  547. }
  548. return ruleSetsCopy;
  549. };
  550. /**
  551. * Array of CSS properties that are inherited by child nodes, according to
  552. * the CSS 2.1 spec. Properties that may be set to relative values, such
  553. * as font-size, and line-height, are omitted.
  554. * @type {Array<string>}
  555. * @private
  556. */
  557. goog.cssom.iframe.style.inheritedProperties_ = [
  558. 'color',
  559. 'visibility',
  560. 'quotes',
  561. 'list-style-type',
  562. 'list-style-image',
  563. 'list-style-position',
  564. 'list-style',
  565. 'page-break-inside',
  566. 'orphans',
  567. 'widows',
  568. 'font-family',
  569. 'font-style',
  570. 'font-variant',
  571. 'font-weight',
  572. 'text-indent',
  573. 'text-align',
  574. 'text-transform',
  575. 'white-space',
  576. 'caption-side',
  577. 'border-collapse',
  578. 'border-spacing',
  579. 'empty-cells',
  580. 'cursor'
  581. ];
  582. /**
  583. * Array of CSS 2.1 properties that directly effect text nodes.
  584. * @type {Array<string>}
  585. * @private
  586. */
  587. goog.cssom.iframe.style.textProperties_ = [
  588. 'font-family', 'font-size', 'font-weight', 'font-variant', 'font-style',
  589. 'color', 'text-align', 'text-decoration', 'text-indent', 'text-transform',
  590. 'letter-spacing', 'white-space', 'word-spacing'
  591. ];
  592. /**
  593. * Reads the current css rules from element's document, and returns them
  594. * rewriting selectors so that any rules that formerly applied to element will
  595. * be applied to doc.body. This makes it possible to replace a block in a page
  596. * with an iframe and preserve the css styling of the contents.
  597. *
  598. * @param {Element} element The element for which context should be calculated.
  599. * @param {boolean=} opt_forceRuleSetCacheUpdate Flag to force the internal
  600. * cache of rulesets to refresh itself before we read the same.
  601. * @param {boolean=} opt_copyBackgroundContext Flag indicating that if the
  602. * {@code element} has a transparent background, background rules
  603. * from the nearest ancestor element(s) that have background-color
  604. * and/or background-image set should be copied.
  605. * @return {string} String containing all CSS rules present in the original
  606. * document, with modified selectors.
  607. * @see goog.cssom.iframe.style.getBackgroundContext.
  608. */
  609. goog.cssom.iframe.style.getElementContext = function(
  610. element, opt_forceRuleSetCacheUpdate, opt_copyBackgroundContext) {
  611. var sourceDocument = element.ownerDocument;
  612. if (opt_forceRuleSetCacheUpdate) {
  613. goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument(
  614. sourceDocument);
  615. }
  616. var ruleSets = goog.cssom.iframe.style.ruleSetCache_.getRuleSetsForDocument(
  617. sourceDocument);
  618. var elementAncestry = new goog.cssom.iframe.style.NodeAncestry_(element);
  619. var bodySelectorPart = new goog.cssom.iframe.style.CssSelectorPart_('body');
  620. for (var i = 0; i < ruleSets.length; i++) {
  621. var ruleSet = ruleSets[i];
  622. var selectors = ruleSet.selectors;
  623. // Cache selectors.length since we may be adding rules in the loop.
  624. var ruleCount = selectors.length;
  625. for (var j = 0; j < ruleCount; j++) {
  626. var selector = selectors[j];
  627. // Test whether all or part of this selector would match
  628. // this element or one of its ancestors
  629. var match = selector.matchElementAncestry(elementAncestry);
  630. if (match) {
  631. var ruleIndex = match.selectorPartIndex;
  632. var selectorParts = selector.parts;
  633. var lastSelectorPartIndex = selectorParts.length - 1;
  634. var selectorCopy;
  635. if (match.elementIndex == elementAncestry.nodes.length - 1 ||
  636. ruleIndex < lastSelectorPartIndex) {
  637. // Either the first part(s) of the selector matched this element,
  638. // or the first part(s) of the selector matched a parent element
  639. // and there are more parts of the selector that could target
  640. // children of this element.
  641. // So we inject a new selector, replacing the part that matched this
  642. // element with 'body' so it will continue to match.
  643. var selectorPartsCopy = selectorParts.concat();
  644. selectorPartsCopy.splice(0, ruleIndex + 1, bodySelectorPart);
  645. selectorCopy = new goog.cssom.iframe.style.CssSelector_();
  646. selectorCopy.parts = selectorPartsCopy;
  647. selectors.push(selectorCopy);
  648. } else if (ruleIndex > 0 && ruleIndex == lastSelectorPartIndex) {
  649. // The rule didn't match this element, but the entire rule did
  650. // match an ancestor element. In this case we want to copy
  651. // just the last part of the rule, to give it a chance to be applied
  652. // to additional matching elements inside this element.
  653. // Example DOM structure: body > div.funky > ul > li#editme
  654. // Example CSS selector: .funky ul
  655. // New CSS selector: body ul
  656. selectorCopy = new goog.cssom.iframe.style.CssSelector_();
  657. selectorCopy.parts =
  658. [bodySelectorPart, selectorParts[lastSelectorPartIndex]];
  659. selectors.push(selectorCopy);
  660. }
  661. }
  662. }
  663. }
  664. // Insert a new ruleset, setting the current inheritable styles of this
  665. // element as the defaults for everything under in the frame.
  666. var defaultPropertiesRuleSet = new goog.cssom.iframe.style.CssRuleSet_();
  667. var computedStyle = goog.cssom.iframe.style.getComputedStyleObject_(element);
  668. // Copy inheritable styles so they are applied to everything under HTML.
  669. var htmlSelector = new goog.cssom.iframe.style.CssSelector_();
  670. htmlSelector.parts = [new goog.cssom.iframe.style.CssSelectorPart_('html')];
  671. defaultPropertiesRuleSet.selectors = [htmlSelector];
  672. var defaultProperties = {};
  673. for (var i = 0, prop; prop = goog.cssom.iframe.style.inheritedProperties_[i];
  674. i++) {
  675. defaultProperties[prop] = computedStyle[goog.string.toCamelCase(prop)];
  676. }
  677. defaultPropertiesRuleSet.setDeclarationTextFromObject(defaultProperties);
  678. ruleSets.push(defaultPropertiesRuleSet);
  679. var bodyRuleSet = new goog.cssom.iframe.style.CssRuleSet_();
  680. var bodySelector = new goog.cssom.iframe.style.CssSelector_();
  681. bodySelector.parts = [new goog.cssom.iframe.style.CssSelectorPart_('body')];
  682. // Core set of sane property values for BODY, to prevent copied
  683. // styles from completely breaking the display.
  684. var bodyProperties = {
  685. position: 'relative',
  686. top: '0',
  687. left: '0',
  688. right: 'auto', // Override any existing right value so 'left' works.
  689. display: 'block',
  690. visibility: 'visible'
  691. };
  692. // Text formatting property values, to keep text nodes directly under BODY
  693. // looking right.
  694. for (i = 0; prop = goog.cssom.iframe.style.textProperties_[i]; i++) {
  695. bodyProperties[prop] = computedStyle[goog.string.toCamelCase(prop)];
  696. }
  697. if (opt_copyBackgroundContext &&
  698. goog.cssom.iframe.style.isTransparentValue_(
  699. computedStyle['backgroundColor'])) {
  700. // opt_useAncestorBackgroundRules means that, if the original element
  701. // has a transparent background, background properties rules should be
  702. // added to explicitly make the body have the same background appearance
  703. // as in the original element, even if its positioned somewhere else
  704. // in the DOM.
  705. var bgProperties = goog.cssom.iframe.style.getBackgroundContext(element);
  706. bodyProperties['background-color'] = bgProperties['backgroundColor'];
  707. var elementBgImage = computedStyle['backgroundImage'];
  708. if (!elementBgImage || elementBgImage == 'none') {
  709. bodyProperties['background-image'] = bgProperties['backgroundImage'];
  710. bodyProperties['background-repeat'] = bgProperties['backgroundRepeat'];
  711. bodyProperties['background-position'] =
  712. bgProperties['backgroundPosition'];
  713. }
  714. }
  715. bodyRuleSet.setDeclarationTextFromObject(bodyProperties, true);
  716. bodyRuleSet.selectors = [bodySelector];
  717. ruleSets.push(bodyRuleSet);
  718. // Write outputTextParts to doc.
  719. var ruleSetStrings = [];
  720. ruleCount = ruleSets.length;
  721. for (i = 0; i < ruleCount; i++) {
  722. ruleSets[i].writeToArray(ruleSetStrings);
  723. }
  724. return ruleSetStrings.join('');
  725. };
  726. /**
  727. * Tests whether a value is equivalent to 'transparent'.
  728. * @param {string} colorValue The value to test.
  729. * @return {boolean} Whether the value is transparent.
  730. * @private
  731. */
  732. goog.cssom.iframe.style.isTransparentValue_ = function(colorValue) {
  733. return colorValue == 'transparent' || colorValue == 'rgba(0, 0, 0, 0)';
  734. };
  735. /**
  736. * Returns an object containing the set of computedStyle/currentStyle
  737. * values for the given element. Note that this should be used with
  738. * caution as it ignores the fact that currentStyle and computedStyle
  739. * are not the same for certain properties.
  740. *
  741. * @param {Element} element The element whose computed style to return.
  742. * @return {Object} Object containing style properties and values.
  743. * @private
  744. */
  745. goog.cssom.iframe.style.getComputedStyleObject_ = function(element) {
  746. // Return an object containing the element's computedStyle/currentStyle.
  747. // The resulting object can be re-used to read multiple properties, which
  748. // is faster than calling goog.style.getComputedStyle every time.
  749. return element.currentStyle ||
  750. goog.dom.getOwnerDocument(element).defaultView.getComputedStyle(
  751. element, '') ||
  752. {};
  753. };
  754. /**
  755. * RegExp that splits a value like "10px" or "-1em" into parts.
  756. * @private
  757. * @type {RegExp}
  758. */
  759. goog.cssom.iframe.style.valueWithUnitsRegEx_ = /^(-?)([0-9]+)([a-z]*|%)/;
  760. /**
  761. * Given an object containing a set of styles, returns a two-element array
  762. * containing the values of background-position-x and background-position-y.
  763. * @param {Object} styleObject Object from which to read style properties.
  764. * @return {Array<string>} The background-position values in the order [x, y].
  765. * @private
  766. */
  767. goog.cssom.iframe.style.getBackgroundXYValues_ = function(styleObject) {
  768. // Gecko only has backgroundPosition, containing both values.
  769. // IE has only backgroundPositionX/backgroundPositionY.
  770. // WebKit has both.
  771. if (styleObject['backgroundPositionY']) {
  772. return [
  773. styleObject['backgroundPositionX'], styleObject['backgroundPositionY']
  774. ];
  775. } else {
  776. return (styleObject['backgroundPosition'] || '0 0').split(' ');
  777. }
  778. };
  779. /**
  780. * Generates a set of CSS properties that can be used to make another
  781. * element's background look like the background of a given element.
  782. * This is useful when you want to copy the CSS context of an element,
  783. * but the element's background is transparent. In the original context
  784. * you would see the ancestor's backround color/image showing through,
  785. * but in the new context there might be a something different underneath.
  786. * Note that this assumes the element you're copying context from has a
  787. * fairly standard positioning/layout - it assumes that when the element
  788. * has a transparent background what you're going to see through it is its
  789. * ancestors.
  790. * @param {Element} element The element from which to copy background styles.
  791. * @return {!Object} Object containing background* properties.
  792. */
  793. goog.cssom.iframe.style.getBackgroundContext = function(element) {
  794. var propertyValues = {'backgroundImage': 'none'};
  795. var ancestor = element;
  796. var currentIframeWindow;
  797. // Walk up the DOM tree to find the ancestor nodes whose backgrounds
  798. // may be visible underneath this element. Background-image and
  799. // background-color don't have to come from the same node, but as soon
  800. // an element with background-color is found there's no need to continue
  801. // because backgrounds farther up the chain won't be visible.
  802. // (This implementation is not sophisticated enough to handle opacity,
  803. // or multple layered partially-transparent background images.)
  804. while ((ancestor = /** @type {!Element} */ (ancestor.parentNode)) &&
  805. ancestor.nodeType == goog.dom.NodeType.ELEMENT) {
  806. var computedStyle =
  807. goog.cssom.iframe.style.getComputedStyleObject_(ancestor);
  808. // Copy background color if a non-transparent value is found.
  809. var backgroundColorValue = computedStyle['backgroundColor'];
  810. if (!goog.cssom.iframe.style.isTransparentValue_(backgroundColorValue)) {
  811. propertyValues['backgroundColor'] = backgroundColorValue;
  812. }
  813. // If a background image value is found, copy background-image,
  814. // background-repeat, and background-position.
  815. if (computedStyle['backgroundImage'] &&
  816. computedStyle['backgroundImage'] != 'none') {
  817. propertyValues['backgroundImage'] = computedStyle['backgroundImage'];
  818. propertyValues['backgroundRepeat'] = computedStyle['backgroundRepeat'];
  819. // Calculate the offset between the original element and the element
  820. // providing the background image, so the background position can be
  821. // adjusted.
  822. var relativePosition;
  823. if (currentIframeWindow) {
  824. relativePosition =
  825. goog.style.getFramedPageOffset(element, currentIframeWindow);
  826. var frameElement = currentIframeWindow.frameElement;
  827. var iframeRelativePosition = goog.style.getRelativePosition(
  828. /** @type {!Element} */ (frameElement), ancestor);
  829. var iframeBorders = goog.style.getBorderBox(frameElement);
  830. relativePosition.x += iframeRelativePosition.x + iframeBorders.left;
  831. relativePosition.y += iframeRelativePosition.y + iframeBorders.top;
  832. } else {
  833. relativePosition = goog.style.getRelativePosition(element, ancestor);
  834. }
  835. var backgroundXYValues =
  836. goog.cssom.iframe.style.getBackgroundXYValues_(computedStyle);
  837. // Parse background-repeat-* values in the form "10px", and adjust them.
  838. for (var i = 0; i < 2; i++) {
  839. var positionValue = backgroundXYValues[i];
  840. var coordinate = i == 0 ? 'X' : 'Y';
  841. var positionProperty = 'backgroundPosition' + coordinate;
  842. // relative position to its ancestor.
  843. var positionValueParts =
  844. goog.cssom.iframe.style.valueWithUnitsRegEx_.exec(positionValue);
  845. if (positionValueParts) {
  846. var value =
  847. parseInt(positionValueParts[1] + positionValueParts[2], 10);
  848. var units = positionValueParts[3];
  849. // This only attempts to handle pixel values for now (plus
  850. // '0anything', which is equivalent to 0px).
  851. // TODO(user) Convert non-pixel values to pixels when possible.
  852. if (value == 0 || units == 'px') {
  853. value -=
  854. (coordinate == 'X' ? relativePosition.x : relativePosition.y);
  855. }
  856. positionValue = value + units;
  857. }
  858. propertyValues[positionProperty] = positionValue;
  859. }
  860. propertyValues['backgroundPosition'] =
  861. propertyValues['backgroundPositionX'] + ' ' +
  862. propertyValues['backgroundPositionY'];
  863. }
  864. if (propertyValues['backgroundColor']) {
  865. break;
  866. }
  867. if (ancestor.tagName == goog.dom.TagName.HTML) {
  868. try {
  869. currentIframeWindow = goog.dom.getWindow(
  870. /** @type {Document} */ (ancestor.parentNode));
  871. // This could theoretically throw a security exception if the parent
  872. // iframe is in a different domain.
  873. ancestor = currentIframeWindow.frameElement;
  874. if (!ancestor) {
  875. // Loop has reached the top level window.
  876. break;
  877. }
  878. } catch (e) {
  879. // We don't have permission to go up to the parent window, stop here.
  880. break;
  881. }
  882. }
  883. }
  884. return propertyValues;
  885. };