| // Copyright 2007 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. |
| // All Rights Reserved. |
| |
| /** |
| * @fileoverview Provides utility routines for copying modified |
| * {@code CSSRule} objects from the parent document into iframes so that any |
| * content in the iframe will be styled as if it was inline in the parent |
| * document. |
| * |
| * <p> |
| * For example, you might have this CSS rule: |
| * |
| * #content .highlighted { background-color: yellow; } |
| * |
| * And this DOM structure: |
| * |
| * <div id="content"> |
| * <iframe /> |
| * </div> |
| * |
| * Then inside the iframe you have: |
| * |
| * <body> |
| * <div class="highlighted"> |
| * </body> |
| * |
| * If you copied the CSS rule directly into the iframe, it wouldn't match the |
| * .highlighted div. So we rewrite the original stylesheets based on the |
| * context where the iframe is going to be inserted. In this case the CSS |
| * selector would be rewritten to: |
| * |
| * body .highlighted { background-color: yellow; } |
| * </p> |
| * |
| */ |
| |
| |
| goog.provide('goog.cssom.iframe.style'); |
| |
| goog.require('goog.asserts'); |
| goog.require('goog.cssom'); |
| goog.require('goog.dom'); |
| goog.require('goog.dom.NodeType'); |
| goog.require('goog.dom.TagName'); |
| goog.require('goog.dom.classlist'); |
| goog.require('goog.string'); |
| goog.require('goog.style'); |
| goog.require('goog.userAgent'); |
| |
| |
| /** |
| * Regexp that matches "a", "a:link", "a:visited", etc. |
| * @type {RegExp} |
| * @private |
| */ |
| goog.cssom.iframe.style.selectorPartAnchorRegex_ = |
| /a(:(link|visited|active|hover))?/; |
| |
| |
| /** |
| * Delimiter between selectors (h1, h2) |
| * @type {string} |
| * @private |
| */ |
| goog.cssom.iframe.style.SELECTOR_DELIMITER_ = ','; |
| |
| |
| /** |
| * Delimiter between selector parts (.main h1) |
| * @type {string} |
| * @private |
| */ |
| goog.cssom.iframe.style.SELECTOR_PART_DELIMITER_ = ' '; |
| |
| |
| /** |
| * Delimiter marking the start of a css rules section ( h1 { ) |
| * @type {string} |
| * @private |
| */ |
| goog.cssom.iframe.style.DECLARATION_START_DELIMITER_ = '{'; |
| |
| |
| /** |
| * Delimiter marking the end of a css rules section ( } ) |
| * @type {string} |
| * @private |
| */ |
| goog.cssom.iframe.style.DECLARATION_END_DELIMITER_ = '}\n'; |
| |
| |
| |
| /** |
| * Class representing a CSS rule set. A rule set is something like this: |
| * h1, h2 { font-family: Arial; color: red; } |
| * @constructor |
| * @private |
| */ |
| goog.cssom.iframe.style.CssRuleSet_ = function() { |
| /** |
| * Text of the declarations inside the rule set. |
| * For example: 'font-family: Arial; color: red;' |
| * @type {string} |
| */ |
| this.declarationText = ''; |
| |
| /** |
| * Array of CssSelector objects, one for each selector. |
| * Example: [h1, h2] |
| * @type {Array<goog.cssom.iframe.style.CssSelector_>} |
| */ |
| this.selectors = []; |
| }; |
| |
| |
| /** |
| * Initializes the rule set from a {@code CSSRule}. |
| * |
| * @param {CSSRule} cssRule The {@code CSSRule} to initialize from. |
| * @return {boolean} True if initialization succeeded. We only support |
| * {@code CSSStyleRule} and {@code CSSFontFaceRule} objects. |
| */ |
| goog.cssom.iframe.style.CssRuleSet_.prototype.initializeFromCssRule = |
| function(cssRule) { |
| var ruleStyle = cssRule.style; // Cache object for performance. |
| if (!ruleStyle) { |
| return false; |
| } |
| var selector; |
| var declarations = ''; |
| if (ruleStyle && |
| (selector = cssRule.selectorText) && |
| (declarations = ruleStyle.cssText)) { |
| // IE get confused about cssText context if a stylesheet uses the |
| // mid-pass hack, and it ends up with an open comment (/*) but no |
| // closing comment. This will effectively comment out large parts |
| // of generated stylesheets later. This errs on the safe side by |
| // always tacking on an empty comment to force comments to be closed |
| // We used to check for a troublesome open comment using a regular |
| // expression, but it's faster not to check and always do this. |
| if (goog.userAgent.IE) { |
| declarations += '/* */'; |
| } |
| } else if (cssRule.cssText) { |
| var cssSelectorMatch = /([^\{]+)\{/; |
| var endTagMatch = /\}[^\}]*$/g; |
| // cssRule.cssText contains both selector and declarations: |
| // parse them out. |
| selector = cssSelectorMatch.exec(cssRule.cssText)[1]; |
| // Remove selector, {, and trailing }. |
| declarations = cssRule.cssText.replace(cssSelectorMatch, '').replace( |
| endTagMatch, ''); |
| } |
| if (selector) { |
| this.setSelectorsFromString(selector); |
| this.declarationText = declarations; |
| return true; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Parses a selectors string (which may contain multiple comma-delimited |
| * selectors) and loads the results into this.selectors. |
| * @param {string} selectorsString String containing selectors. |
| */ |
| goog.cssom.iframe.style.CssRuleSet_.prototype.setSelectorsFromString = |
| function(selectorsString) { |
| this.selectors = []; |
| var selectors = selectorsString.split(/,\s*/gm); |
| for (var i = 0; i < selectors.length; i++) { |
| var selector = selectors[i]; |
| if (selector.length > 0) { |
| this.selectors.push(new goog.cssom.iframe.style.CssSelector_(selector)); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Make a copy of this ruleset. |
| * @return {!goog.cssom.iframe.style.CssRuleSet_} A new CssRuleSet containing |
| * the same data as this one. |
| */ |
| goog.cssom.iframe.style.CssRuleSet_.prototype.clone = function() { |
| var newRuleSet = new goog.cssom.iframe.style.CssRuleSet_(); |
| newRuleSet.selectors = this.selectors.concat(); |
| newRuleSet.declarationText = this.declarationText; |
| return newRuleSet; |
| }; |
| |
| |
| /** |
| * Set the declaration text with properties from a given object. |
| * @param {Object} sourceObject Object whose properties and values should |
| * be used to generate the declaration text. |
| * @param {boolean=} opt_important Whether !important should be added to each |
| * declaration. |
| */ |
| goog.cssom.iframe.style.CssRuleSet_.prototype.setDeclarationTextFromObject = |
| function(sourceObject, opt_important) { |
| var stringParts = []; |
| // TODO(user): for ... in is costly in IE6 (extra garbage collection). |
| for (var prop in sourceObject) { |
| var value = sourceObject[prop]; |
| if (value) { |
| stringParts.push(prop, |
| ':', |
| value, (opt_important ? ' !important' : ''), |
| ';'); |
| } |
| } |
| this.declarationText = stringParts.join(''); |
| }; |
| |
| |
| /** |
| * Serializes this CssRuleSet_ into an array as a series of strings. |
| * The array can then be join()-ed to get a string representation |
| * of this ruleset. |
| * @param {Array<string>} array The array to which to append strings. |
| */ |
| goog.cssom.iframe.style.CssRuleSet_.prototype.writeToArray = function(array) { |
| var selectorCount = this.selectors.length; |
| var matchesAnchorTag = false; |
| for (var i = 0; i < selectorCount; i++) { |
| var selectorParts = this.selectors[i].parts; |
| var partCount = selectorParts.length; |
| for (var j = 0; j < partCount; j++) { |
| array.push(selectorParts[j].inputString_, |
| goog.cssom.iframe.style.SELECTOR_PART_DELIMITER_); |
| } |
| if (i < (selectorCount - 1)) { |
| array.push(goog.cssom.iframe.style.SELECTOR_DELIMITER_); |
| } |
| if (goog.userAgent.GECKO && |
| !goog.userAgent.isVersionOrHigher('1.9a')) { |
| // In Gecko pre-1.9 (Firefox 2 and lower) we need to add !important |
| // to rulesets that match "A" tags, otherwise Gecko's built-in |
| // stylesheet will take precedence when designMode is on. |
| matchesAnchorTag = matchesAnchorTag || |
| goog.cssom.iframe.style.selectorPartAnchorRegex_.test( |
| selectorParts[partCount - 1].inputString_); |
| } |
| } |
| var declarationText = this.declarationText; |
| if (matchesAnchorTag) { |
| declarationText = goog.cssom.iframe.style.makeColorRuleImportant_( |
| declarationText); |
| } |
| array.push(goog.cssom.iframe.style.DECLARATION_START_DELIMITER_, |
| declarationText, |
| goog.cssom.iframe.style.DECLARATION_END_DELIMITER_); |
| }; |
| |
| |
| /** |
| * Regexp that matches "color: value;". |
| * @type {RegExp} |
| * @private |
| */ |
| goog.cssom.iframe.style.colorImportantReplaceRegex_ = |
| /(^|;|{)\s*color:([^;]+);/g; |
| |
| |
| /** |
| * Adds !important to a css color: rule |
| * @param {string} cssText Text of the CSS rule(s) to modify. |
| * @return {string} Text with !important added to the color: rule if found. |
| * @private |
| */ |
| goog.cssom.iframe.style.makeColorRuleImportant_ = function(cssText) { |
| // Replace to insert a "! important" string. |
| return cssText.replace(goog.cssom.iframe.style.colorImportantReplaceRegex_, |
| '$1 color: $2 ! important; '); |
| }; |
| |
| |
| |
| /** |
| * Represents a single CSS selector, as described in |
| * http://www.w3.org/TR/REC-CSS2/selector.html |
| * Currently UNSUPPORTED are the following selector features: |
| * <ul> |
| * <li>pseudo-classes (:hover) |
| * <li>child selectors (div > h1) |
| * <li>adjacent sibling selectors (div + h1) |
| * <li>attribute selectors (input[type=submit]) |
| * </ul> |
| * @param {string=} opt_selectorString String containing selectors to parse. |
| * @constructor |
| * @private |
| */ |
| goog.cssom.iframe.style.CssSelector_ = function(opt_selectorString) { |
| |
| /** |
| * Object to track ancestry matches to speed up repeatedly testing this |
| * CssSelector against the same NodeAncestry object. |
| * @type {Object} |
| * @private |
| */ |
| this.ancestryMatchCache_ = {}; |
| if (opt_selectorString) { |
| this.setPartsFromString_(opt_selectorString); |
| } |
| }; |
| |
| |
| /** |
| * Parses a selector string into individual parts. |
| * @param {string} selectorString A string containing a CSS selector. |
| * @private |
| */ |
| goog.cssom.iframe.style.CssSelector_.prototype.setPartsFromString_ = |
| function(selectorString) { |
| var parts = []; |
| var selectorPartStrings = selectorString.split(/\s+/gm); |
| for (var i = 0; i < selectorPartStrings.length; i++) { |
| if (!selectorPartStrings[i]) { |
| continue; // Skip empty strings. |
| } |
| var part = new goog.cssom.iframe.style.CssSelectorPart_( |
| selectorPartStrings[i]); |
| parts.push(part); |
| } |
| this.parts = parts; |
| }; |
| |
| |
| /** |
| * Tests to see what part of a DOM element hierarchy would be matched by |
| * this selector, and returns the indexes of the matching element and matching |
| * selector part. |
| * <p> |
| * For example, given this hierarchy: |
| * document > html > body > div.content > div.sidebar > p |
| * and this CSS selector: |
| * body div.sidebar h1 |
| * This would return {elementIndex: 4, selectorPartIndex: 1}, |
| * indicating that the element at index 4 matched |
| * the css selector at index 1. |
| * </p> |
| * @param {goog.cssom.iframe.style.NodeAncestry_} elementAncestry Object |
| * representing an element and its ancestors. |
| * @return {Object} Object with the properties elementIndex and |
| * selectorPartIndex, or null if there was no match. |
| */ |
| goog.cssom.iframe.style.CssSelector_.prototype.matchElementAncestry = |
| function(elementAncestry) { |
| |
| var ancestryUid = elementAncestry.uid; |
| if (this.ancestryMatchCache_[ancestryUid]) { |
| return this.ancestryMatchCache_[ancestryUid]; |
| } |
| |
| // Walk through the selector parts and see how far down the element hierarchy |
| // we can go while matching the selector parts. |
| var elementIndex = 0; |
| var match = null; |
| var selectorPart = null; |
| var lastSelectorPart = null; |
| var ancestorNodes = elementAncestry.nodes; |
| var ancestorNodeCount = ancestorNodes.length; |
| |
| for (var i = 0; i <= this.parts.length; i++) { |
| selectorPart = this.parts[i]; |
| while (elementIndex < ancestorNodeCount) { |
| var currentElementInfo = ancestorNodes[elementIndex]; |
| if (selectorPart && |
| selectorPart.testElement(currentElementInfo)) { |
| match = { |
| elementIndex: elementIndex, |
| selectorPartIndex: i |
| }; |
| elementIndex++; |
| break; |
| } else if (lastSelectorPart && |
| lastSelectorPart.testElement(currentElementInfo)) { |
| match = { |
| elementIndex: elementIndex, |
| selectorPartIndex: i - 1 |
| }; |
| } |
| elementIndex++; |
| } |
| lastSelectorPart = selectorPart; |
| } |
| this.ancestryMatchCache_[ancestryUid] = match; |
| return match; |
| }; |
| |
| |
| |
| /** |
| * Represents one part of a CSS Selector. For example in the selector |
| * 'body #foo .bar', body, #foo, and .bar would be considered selector parts. |
| * In the official CSS spec these are called "simple selectors". |
| * @param {string} selectorPartString A string containing the selector part |
| * in css format. |
| * @constructor |
| * @private |
| */ |
| goog.cssom.iframe.style.CssSelectorPart_ = function(selectorPartString) { |
| // Only one CssSelectorPart instance should exist for a given string. |
| var cacheEntry = goog.cssom.iframe.style.CssSelectorPart_.instances_[ |
| selectorPartString]; |
| if (cacheEntry) { |
| return cacheEntry; |
| } |
| |
| // Optimization to avoid the more-expensive lookahead. |
| var identifiers; |
| if (selectorPartString.match(/[#\.]/)) { |
| // Lookahead regexp, won't work on IE 5.0. |
| identifiers = selectorPartString.split(/(?=[#\.])/); |
| } else { |
| identifiers = [selectorPartString]; |
| } |
| var properties = {}; |
| for (var i = 0; i < identifiers.length; i++) { |
| var identifier = identifiers[i]; |
| if (identifier.charAt(0) == '.') { |
| properties.className = identifier.substring(1, identifier.length); |
| } else if (identifier.charAt(0) == '#') { |
| properties.id = identifier.substring(1, identifier.length); |
| } else { |
| properties.tagName = identifier.toUpperCase(); |
| } |
| } |
| this.inputString_ = selectorPartString; |
| this.matchProperties_ = properties; |
| this.testedElements_ = {}; |
| goog.cssom.iframe.style.CssSelectorPart_.instances_[selectorPartString] = |
| this; |
| }; |
| |
| |
| /** |
| * Cache of existing CssSelectorPart_ instances. |
| * @type {Object} |
| * @private |
| */ |
| goog.cssom.iframe.style.CssSelectorPart_.instances_ = {}; |
| |
| |
| /** |
| * Test whether an element matches this selector part, considered in isolation. |
| * @param {Object} elementInfo Element properties to test. |
| * @return {boolean} Whether the element matched. |
| */ |
| goog.cssom.iframe.style.CssSelectorPart_.prototype.testElement = |
| function(elementInfo) { |
| |
| var elementUid = elementInfo.uid; |
| var cachedMatch = this.testedElements_[elementUid]; |
| if (typeof cachedMatch != 'undefined') { |
| return cachedMatch; |
| } |
| |
| var matchProperties = this.matchProperties_; |
| var testTag = matchProperties.tagName; |
| var testClass = matchProperties.className; |
| var testId = matchProperties.id; |
| |
| var matched = true; |
| if (testTag && testTag != '*' && testTag != elementInfo.nodeName) { |
| matched = false; |
| } else if (testId && testId != elementInfo.id) { |
| matched = false; |
| } else if (testClass && |
| !elementInfo.classNames[testClass]) { |
| matched = false; |
| } |
| |
| this.testedElements_[elementUid] = matched; |
| return matched; |
| }; |
| |
| |
| |
| /** |
| * Represents an element and all its parent/ancestor nodes. |
| * This class exists as an optimization so we run tests on an element |
| * hierarchy multiple times without walking the dom each time. |
| * @param {Element} el The DOM element whose ancestry should be stored. |
| * @constructor |
| * @private |
| */ |
| goog.cssom.iframe.style.NodeAncestry_ = function(el) { |
| var node = el; |
| var nodeUid = goog.getUid(node); |
| |
| // Return an existing object from the cache if one exits for this node. |
| var ancestry = goog.cssom.iframe.style.NodeAncestry_.instances_[nodeUid]; |
| if (ancestry) { |
| return ancestry; |
| } |
| |
| var nodes = []; |
| do { |
| var nodeInfo = { |
| id: node.id, |
| nodeName: node.nodeName |
| }; |
| nodeInfo.uid = goog.getUid(nodeInfo); |
| var className = node.className; |
| var classNamesLookup = {}; |
| if (className) { |
| var classNames = goog.dom.classlist.get(goog.asserts.assertElement(node)); |
| for (var i = 0; i < classNames.length; i++) { |
| classNamesLookup[classNames[i]] = 1; |
| } |
| } |
| nodeInfo.classNames = classNamesLookup; |
| nodes.unshift(nodeInfo); |
| } while (node = node.parentNode); |
| |
| /** |
| * Array of nodes in order of hierarchy from the top of the document |
| * to the node passed to the constructor |
| * @type {Array<Node>} |
| */ |
| this.nodes = nodes; |
| |
| this.uid = goog.getUid(this); |
| goog.cssom.iframe.style.NodeAncestry_.instances_[nodeUid] = this; |
| }; |
| |
| |
| /** |
| * Object for caching existing NodeAncestry instances. |
| * @private |
| */ |
| goog.cssom.iframe.style.NodeAncestry_.instances_ = {}; |
| |
| |
| /** |
| * Throw away all cached dom information. Call this if you've modified |
| * the structure or class/id attributes of your document and you want |
| * to recalculate the currently applied CSS rules. |
| */ |
| goog.cssom.iframe.style.resetDomCache = function() { |
| goog.cssom.iframe.style.NodeAncestry_.instances_ = {}; |
| }; |
| |
| |
| /** |
| * Inspects a document and returns all active rule sets |
| * @param {Document} doc The document from which to read CSS rules. |
| * @return {!Array<goog.cssom.iframe.style.CssRuleSet_>} An array of CssRuleSet |
| * objects representing all the active rule sets in the document. |
| * @private |
| */ |
| goog.cssom.iframe.style.getRuleSetsFromDocument_ = function(doc) { |
| var ruleSets = []; |
| var styleSheets = goog.cssom.getAllCssStyleSheets(doc.styleSheets); |
| for (var i = 0, styleSheet; styleSheet = styleSheets[i]; i++) { |
| var domRuleSets = goog.cssom.getCssRulesFromStyleSheet(styleSheet); |
| if (domRuleSets && domRuleSets.length) { |
| for (var j = 0, n = domRuleSets.length; j < n; j++) { |
| var ruleSet = new goog.cssom.iframe.style.CssRuleSet_(); |
| if (ruleSet.initializeFromCssRule(domRuleSets[j])) { |
| ruleSets.push(ruleSet); |
| } |
| } |
| } |
| } |
| return ruleSets; |
| }; |
| |
| |
| /** |
| * Static object to cache rulesets read from documents. Inspecting all |
| * active css rules is an expensive operation, so its best to only do |
| * it once and then cache the results. |
| * @type {Object} |
| * @private |
| */ |
| goog.cssom.iframe.style.ruleSetCache_ = {}; |
| |
| |
| /** |
| * Cache of ruleset objects keyed by document unique ID. |
| * @type {Object} |
| * @private |
| */ |
| goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_ = {}; |
| |
| |
| /** |
| * Loads ruleset definitions from a document. If the cache already |
| * has rulesets for this document the cached version will be replaced. |
| * @param {Document} doc The document from which to load rulesets. |
| */ |
| goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument = function(doc) { |
| var docUid = goog.getUid(doc); |
| goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_[docUid] = |
| goog.cssom.iframe.style.getRuleSetsFromDocument_(doc); |
| }; |
| |
| |
| /** |
| * Retrieves the array of css rulesets for this document. A cached |
| * version will be used when possible. |
| * @param {Document} doc The document for which to get rulesets. |
| * @return {!Array<goog.cssom.iframe.style.CssRuleSet_>} An array of CssRuleSet |
| * objects representing the css rule sets in the supplied document. |
| */ |
| goog.cssom.iframe.style.ruleSetCache_.getRuleSetsForDocument = function(doc) { |
| var docUid = goog.getUid(doc); |
| var cache = goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_; |
| if (!cache[docUid]) { |
| goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument(doc); |
| } |
| // Build a cloned copy of rulesets array, so if object in the returned array |
| // get modified future calls will still return the original unmodified |
| // versions. |
| var ruleSets = cache[docUid]; |
| var ruleSetsCopy = []; |
| for (var i = 0; i < ruleSets.length; i++) { |
| ruleSetsCopy.push(ruleSets[i].clone()); |
| } |
| return ruleSetsCopy; |
| }; |
| |
| |
| /** |
| * Array of CSS properties that are inherited by child nodes, according to |
| * the CSS 2.1 spec. Properties that may be set to relative values, such |
| * as font-size, and line-height, are omitted. |
| * @type {Array<string>} |
| * @private |
| */ |
| goog.cssom.iframe.style.inheritedProperties_ = [ |
| 'color', |
| 'visibility', |
| 'quotes', |
| 'list-style-type', |
| 'list-style-image', |
| 'list-style-position', |
| 'list-style', |
| 'page-break-inside', |
| 'orphans', |
| 'widows', |
| 'font-family', |
| 'font-style', |
| 'font-variant', |
| 'font-weight', |
| 'text-indent', |
| 'text-align', |
| 'text-transform', |
| 'white-space', |
| 'caption-side', |
| 'border-collapse', |
| 'border-spacing', |
| 'empty-cells', |
| 'cursor' |
| ]; |
| |
| |
| /** |
| * Array of CSS 2.1 properties that directly effect text nodes. |
| * @type {Array<string>} |
| * @private |
| */ |
| goog.cssom.iframe.style.textProperties_ = [ |
| 'font-family', |
| 'font-size', |
| 'font-weight', |
| 'font-variant', |
| 'font-style', |
| 'color', |
| 'text-align', |
| 'text-decoration', |
| 'text-indent', |
| 'text-transform', |
| 'letter-spacing', |
| 'white-space', |
| 'word-spacing' |
| ]; |
| |
| |
| /** |
| * Reads the current css rules from element's document, and returns them |
| * rewriting selectors so that any rules that formerly applied to element will |
| * be applied to doc.body. This makes it possible to replace a block in a page |
| * with an iframe and preserve the css styling of the contents. |
| * |
| * @param {Element} element The element for which context should be calculated. |
| * @param {boolean=} opt_forceRuleSetCacheUpdate Flag to force the internal |
| * cache of rulesets to refresh itself before we read the same. |
| * @param {boolean=} opt_copyBackgroundContext Flag indicating that if the |
| * {@code element} has a transparent background, background rules |
| * from the nearest ancestor element(s) that have background-color |
| * and/or background-image set should be copied. |
| * @return {string} String containing all CSS rules present in the original |
| * document, with modified selectors. |
| * @see goog.cssom.iframe.style.getBackgroundContext. |
| */ |
| goog.cssom.iframe.style.getElementContext = function( |
| element, |
| opt_forceRuleSetCacheUpdate, |
| opt_copyBackgroundContext) { |
| var sourceDocument = element.ownerDocument; |
| if (opt_forceRuleSetCacheUpdate) { |
| goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument( |
| sourceDocument); |
| } |
| var ruleSets = goog.cssom.iframe.style.ruleSetCache_. |
| getRuleSetsForDocument(sourceDocument); |
| |
| var elementAncestry = new goog.cssom.iframe.style.NodeAncestry_(element); |
| var bodySelectorPart = new goog.cssom.iframe.style.CssSelectorPart_('body'); |
| |
| for (var i = 0; i < ruleSets.length; i++) { |
| var ruleSet = ruleSets[i]; |
| var selectors = ruleSet.selectors; |
| // Cache selectors.length since we may be adding rules in the loop. |
| var ruleCount = selectors.length; |
| for (var j = 0; j < ruleCount; j++) { |
| var selector = selectors[j]; |
| // Test whether all or part of this selector would match |
| // this element or one of its ancestors |
| var match = selector.matchElementAncestry(elementAncestry); |
| if (match) { |
| var ruleIndex = match.selectorPartIndex; |
| var selectorParts = selector.parts; |
| var lastSelectorPartIndex = selectorParts.length - 1; |
| var selectorCopy; |
| if (match.elementIndex == elementAncestry.nodes.length - 1 || |
| ruleIndex < lastSelectorPartIndex) { |
| // Either the first part(s) of the selector matched this element, |
| // or the first part(s) of the selector matched a parent element |
| // and there are more parts of the selector that could target |
| // children of this element. |
| // So we inject a new selector, replacing the part that matched this |
| // element with 'body' so it will continue to match. |
| var selectorPartsCopy = selectorParts.concat(); |
| selectorPartsCopy.splice(0, |
| ruleIndex + 1, |
| bodySelectorPart); |
| selectorCopy = new goog.cssom.iframe.style.CssSelector_(); |
| selectorCopy.parts = selectorPartsCopy; |
| selectors.push(selectorCopy); |
| } else if (ruleIndex > 0 && ruleIndex == lastSelectorPartIndex) { |
| // The rule didn't match this element, but the entire rule did |
| // match an ancestor element. In this case we want to copy |
| // just the last part of the rule, to give it a chance to be applied |
| // to additional matching elements inside this element. |
| // Example DOM structure: body > div.funky > ul > li#editme |
| // Example CSS selector: .funky ul |
| // New CSS selector: body ul |
| selectorCopy = new goog.cssom.iframe.style.CssSelector_(); |
| selectorCopy.parts = [ |
| bodySelectorPart, |
| selectorParts[lastSelectorPartIndex] |
| ]; |
| selectors.push(selectorCopy); |
| } |
| } |
| } |
| } |
| |
| // Insert a new ruleset, setting the current inheritable styles of this |
| // element as the defaults for everything under in the frame. |
| var defaultPropertiesRuleSet = new goog.cssom.iframe.style.CssRuleSet_(); |
| var computedStyle = goog.cssom.iframe.style.getComputedStyleObject_(element); |
| |
| // Copy inheritable styles so they are applied to everything under HTML. |
| var htmlSelector = new goog.cssom.iframe.style.CssSelector_(); |
| htmlSelector.parts = [new goog.cssom.iframe.style.CssSelectorPart_('html')]; |
| defaultPropertiesRuleSet.selectors = [htmlSelector]; |
| var defaultProperties = {}; |
| for (var i = 0, prop; |
| prop = goog.cssom.iframe.style.inheritedProperties_[i]; |
| i++) { |
| defaultProperties[prop] = computedStyle[goog.string.toCamelCase(prop)]; |
| } |
| defaultPropertiesRuleSet.setDeclarationTextFromObject(defaultProperties); |
| ruleSets.push(defaultPropertiesRuleSet); |
| |
| var bodyRuleSet = new goog.cssom.iframe.style.CssRuleSet_(); |
| var bodySelector = new goog.cssom.iframe.style.CssSelector_(); |
| bodySelector.parts = [new goog.cssom.iframe.style.CssSelectorPart_('body')]; |
| // Core set of sane property values for BODY, to prevent copied |
| // styles from completely breaking the display. |
| var bodyProperties = { |
| position: 'relative', |
| top: '0', |
| left: '0', |
| right: 'auto', // Override any existing right value so 'left' works. |
| display: 'block', |
| visibility: 'visible' |
| }; |
| // Text formatting property values, to keep text nodes directly under BODY |
| // looking right. |
| for (i = 0; prop = goog.cssom.iframe.style.textProperties_[i]; i++) { |
| bodyProperties[prop] = computedStyle[goog.string.toCamelCase(prop)]; |
| } |
| if (opt_copyBackgroundContext && |
| goog.cssom.iframe.style.isTransparentValue_( |
| computedStyle['backgroundColor'])) { |
| // opt_useAncestorBackgroundRules means that, if the original element |
| // has a transparent backgorund, background properties rules should be |
| // added to explicitly make the body have the same background appearance |
| // as in the original element, even if its positioned somewhere else |
| // in the DOM. |
| var bgProperties = |
| goog.cssom.iframe.style.getBackgroundContext(element); |
| bodyProperties['background-color'] = bgProperties['backgroundColor']; |
| var elementBgImage = computedStyle['backgroundImage']; |
| if (!elementBgImage || elementBgImage == 'none') { |
| bodyProperties['background-image'] = bgProperties['backgroundImage']; |
| bodyProperties['background-repeat'] = bgProperties['backgroundRepeat']; |
| bodyProperties['background-position'] = |
| bgProperties['backgroundPosition']; |
| } |
| } |
| |
| bodyRuleSet.setDeclarationTextFromObject(bodyProperties, true); |
| bodyRuleSet.selectors = [bodySelector]; |
| ruleSets.push(bodyRuleSet); |
| |
| // Write outputTextParts to doc. |
| var ruleSetStrings = []; |
| ruleCount = ruleSets.length; |
| for (i = 0; i < ruleCount; i++) { |
| ruleSets[i].writeToArray(ruleSetStrings); |
| } |
| return ruleSetStrings.join(''); |
| }; |
| |
| |
| /** |
| * Tests whether a value is equivalent to 'transparent'. |
| * @param {string} colorValue The value to test. |
| * @return {boolean} Whether the value is transparent. |
| * @private |
| */ |
| goog.cssom.iframe.style.isTransparentValue_ = function(colorValue) { |
| return colorValue == 'transparent' || colorValue == 'rgba(0, 0, 0, 0)'; |
| }; |
| |
| |
| /** |
| * Returns an object containing the set of computedStyle/currentStyle |
| * values for the given element. Note that this should be used with |
| * caution as it ignores the fact that currentStyle and computedStyle |
| * are not the same for certain properties. |
| * |
| * @param {Element} element The element whose computed style to return. |
| * @return {Object} Object containing style properties and values. |
| * @private |
| */ |
| goog.cssom.iframe.style.getComputedStyleObject_ = function(element) { |
| // Return an object containing the element's computedStyle/currentStyle. |
| // The resulting object can be re-used to read multiple properties, which |
| // is faster than calling goog.style.getComputedStyle every time. |
| return element.currentStyle || |
| goog.dom.getOwnerDocument(element).defaultView.getComputedStyle( |
| element, '') || {}; |
| }; |
| |
| |
| /** |
| * RegExp that splits a value like "10px" or "-1em" into parts. |
| * @private |
| * @type {RegExp} |
| */ |
| goog.cssom.iframe.style.valueWithUnitsRegEx_ = /^(-?)([0-9]+)([a-z]*|%)/; |
| |
| |
| /** |
| * Given an object containing a set of styles, returns a two-element array |
| * containing the values of background-position-x and background-position-y. |
| * @param {Object} styleObject Object from which to read style properties. |
| * @return {Array<string>} The background-position values in the order [x, y]. |
| * @private |
| */ |
| goog.cssom.iframe.style.getBackgroundXYValues_ = function(styleObject) { |
| // Gecko only has backgroundPosition, containing both values. |
| // IE has only backgroundPositionX/backgroundPositionY. |
| // WebKit has both. |
| if (styleObject['backgroundPositionY']) { |
| return [styleObject['backgroundPositionX'], |
| styleObject['backgroundPositionY']]; |
| } else { |
| return (styleObject['backgroundPosition'] || '0 0').split(' '); |
| } |
| }; |
| |
| |
| /** |
| * Generates a set of CSS properties that can be used to make another |
| * element's background look like the background of a given element. |
| * This is useful when you want to copy the CSS context of an element, |
| * but the element's background is transparent. In the original context |
| * you would see the ancestor's backround color/image showing through, |
| * but in the new context there might be a something different underneath. |
| * Note that this assumes the element you're copying context from has a |
| * fairly standard positioning/layout - it assumes that when the element |
| * has a transparent background what you're going to see through it is its |
| * ancestors. |
| * @param {Element} element The element from which to copy background styles. |
| * @return {!Object} Object containing background* properties. |
| */ |
| goog.cssom.iframe.style.getBackgroundContext = function(element) { |
| var propertyValues = { |
| 'backgroundImage': 'none' |
| }; |
| var ancestor = element; |
| var currentIframeWindow; |
| // Walk up the DOM tree to find the ancestor nodes whose backgrounds |
| // may be visible underneath this element. Background-image and |
| // background-color don't have to come from the same node, but as soon |
| // an element with background-color is found there's no need to continue |
| // because backgrounds farther up the chain won't be visible. |
| // (This implementation is not sophisticated enough to handle opacity, |
| // or multple layered partially-transparent background images.) |
| while ((ancestor = ancestor.parentNode) && |
| ancestor.nodeType == goog.dom.NodeType.ELEMENT) { |
| var computedStyle = goog.cssom.iframe.style.getComputedStyleObject_( |
| /** @type {!Element} */ (ancestor)); |
| // Copy background color if a non-transparent value is found. |
| var backgroundColorValue = computedStyle['backgroundColor']; |
| if (!goog.cssom.iframe.style.isTransparentValue_(backgroundColorValue)) { |
| propertyValues['backgroundColor'] = backgroundColorValue; |
| } |
| // If a background image value is found, copy background-image, |
| // background-repeat, and background-position. |
| if (computedStyle['backgroundImage'] && |
| computedStyle['backgroundImage'] != 'none') { |
| propertyValues['backgroundImage'] = computedStyle['backgroundImage']; |
| propertyValues['backgroundRepeat'] = computedStyle['backgroundRepeat']; |
| // Calculate the offset between the original element and the element |
| // providing the background image, so the background position can be |
| // adjusted. |
| var relativePosition; |
| if (currentIframeWindow) { |
| relativePosition = goog.style.getFramedPageOffset( |
| element, currentIframeWindow); |
| var frameElement = currentIframeWindow.frameElement; |
| var iframeRelativePosition = goog.style.getRelativePosition( |
| /** @type {!Element} */ (frameElement), |
| /** @type {!Element} */ (ancestor)); |
| var iframeBorders = goog.style.getBorderBox(frameElement); |
| relativePosition.x += iframeRelativePosition.x + iframeBorders.left; |
| relativePosition.y += iframeRelativePosition.y + iframeBorders.top; |
| } else { |
| relativePosition = goog.style.getRelativePosition( |
| element, /** @type {Element} */ (ancestor)); |
| } |
| var backgroundXYValues = goog.cssom.iframe.style.getBackgroundXYValues_( |
| computedStyle); |
| // Parse background-repeat-* values in the form "10px", and adjust them. |
| for (var i = 0; i < 2; i++) { |
| var positionValue = backgroundXYValues[i]; |
| var coordinate = i == 0 ? 'X' : 'Y'; |
| var positionProperty = 'backgroundPosition' + coordinate; |
| // relative position to its ancestor. |
| var positionValueParts = |
| goog.cssom.iframe.style.valueWithUnitsRegEx_.exec(positionValue); |
| if (positionValueParts) { |
| var value = parseInt( |
| positionValueParts[1] + positionValueParts[2], 10); |
| var units = positionValueParts[3]; |
| // This only attempts to handle pixel values for now (plus |
| // '0anything', which is equivalent to 0px). |
| // TODO(user) Convert non-pixel values to pixels when possible. |
| if (value == 0 || units == 'px') { |
| value -= (coordinate == 'X' ? |
| relativePosition.x : relativePosition.y); |
| } |
| positionValue = value + units; |
| } |
| propertyValues[positionProperty] = positionValue; |
| } |
| propertyValues['backgroundPosition'] = |
| propertyValues['backgroundPositionX'] + ' ' + |
| propertyValues['backgroundPositionY']; |
| } |
| if (propertyValues['backgroundColor']) { |
| break; |
| } |
| if (ancestor.tagName == goog.dom.TagName.HTML) { |
| try { |
| currentIframeWindow = goog.dom.getWindow( |
| /** @type {Document} */ (ancestor.parentNode)); |
| // This could theoretically throw a security exception if the parent |
| // iframe is in a different domain. |
| ancestor = currentIframeWindow.frameElement; |
| if (!ancestor) { |
| // Loop has reached the top level window. |
| break; |
| } |
| } catch (e) { |
| // We don't have permission to go up to the parent window, stop here. |
| break; |
| } |
| } |
| } |
| return propertyValues; |
| }; |