Path: blob/trunk/third_party/closure/goog/cssom/iframe/style.js
2868 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.13// All Rights Reserved.1415/**16* @fileoverview Provides utility routines for copying modified17* {@code CSSRule} objects from the parent document into iframes so that any18* content in the iframe will be styled as if it was inline in the parent19* 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 the39* .highlighted div. So we rewrite the original stylesheets based on the40* context where the iframe is going to be inserted. In this case the CSS41* selector would be rewritten to:42*43* body .highlighted { background-color: yellow; }44* </p>45*46*/474849goog.provide('goog.cssom.iframe.style');5051goog.require('goog.asserts');52goog.require('goog.cssom');53goog.require('goog.dom');54goog.require('goog.dom.NodeType');55goog.require('goog.dom.TagName');56goog.require('goog.dom.classlist');57goog.require('goog.string');58goog.require('goog.style');59goog.require('goog.userAgent');606162/**63* Regexp that matches "a", "a:link", "a:visited", etc.64* @type {RegExp}65* @private66*/67goog.cssom.iframe.style.selectorPartAnchorRegex_ =68/a(:(link|visited|active|hover))?/;697071/**72* Delimiter between selectors (h1, h2)73* @type {string}74* @private75*/76goog.cssom.iframe.style.SELECTOR_DELIMITER_ = ',';777879/**80* Delimiter between selector parts (.main h1)81* @type {string}82* @private83*/84goog.cssom.iframe.style.SELECTOR_PART_DELIMITER_ = ' ';858687/**88* Delimiter marking the start of a css rules section ( h1 { )89* @type {string}90* @private91*/92goog.cssom.iframe.style.DECLARATION_START_DELIMITER_ = '{';939495/**96* Delimiter marking the end of a css rules section ( } )97* @type {string}98* @private99*/100goog.cssom.iframe.style.DECLARATION_END_DELIMITER_ = '}\n';101102103104/**105* Class representing a CSS rule set. A rule set is something like this:106* h1, h2 { font-family: Arial; color: red; }107* @constructor108* @private109*/110goog.cssom.iframe.style.CssRuleSet_ = function() {111/**112* Text of the declarations inside the rule set.113* For example: 'font-family: Arial; color: red;'114* @type {string}115*/116this.declarationText = '';117118/**119* Array of CssSelector objects, one for each selector.120* Example: [h1, h2]121* @type {Array<goog.cssom.iframe.style.CssSelector_>}122*/123this.selectors = [];124};125126127/**128* Initializes the rule set from a {@code CSSRule}.129*130* @param {CSSRule} cssRule The {@code CSSRule} to initialize from.131* @return {boolean} True if initialization succeeded. We only support132* {@code CSSStyleRule} and {@code CSSFontFaceRule} objects.133*/134goog.cssom.iframe.style.CssRuleSet_.prototype.initializeFromCssRule = function(135cssRule) {136var ruleStyle = cssRule.style; // Cache object for performance.137if (!ruleStyle) {138return false;139}140var selector;141var declarations = '';142if (ruleStyle && (selector = cssRule.selectorText) &&143(declarations = ruleStyle.cssText)) {144// IE get confused about cssText context if a stylesheet uses the145// mid-pass hack, and it ends up with an open comment (/*) but no146// closing comment. This will effectively comment out large parts147// of generated stylesheets later. This errs on the safe side by148// always tacking on an empty comment to force comments to be closed149// We used to check for a troublesome open comment using a regular150// expression, but it's faster not to check and always do this.151if (goog.userAgent.IE) {152declarations += '/* */';153}154} else if (cssRule.cssText) {155var cssSelectorMatch = /([^\{]+)\{/;156var endTagMatch = /\}[^\}]*$/g;157// cssRule.cssText contains both selector and declarations:158// parse them out.159selector = cssSelectorMatch.exec(cssRule.cssText)[1];160// Remove selector, {, and trailing }.161declarations =162cssRule.cssText.replace(cssSelectorMatch, '').replace(endTagMatch, '');163}164if (selector) {165this.setSelectorsFromString(selector);166this.declarationText = declarations;167return true;168}169return false;170};171172173/**174* Parses a selectors string (which may contain multiple comma-delimited175* selectors) and loads the results into this.selectors.176* @param {string} selectorsString String containing selectors.177*/178goog.cssom.iframe.style.CssRuleSet_.prototype.setSelectorsFromString = function(179selectorsString) {180this.selectors = [];181var selectors = selectorsString.split(/,\s*/gm);182for (var i = 0; i < selectors.length; i++) {183var selector = selectors[i];184if (selector.length > 0) {185this.selectors.push(new goog.cssom.iframe.style.CssSelector_(selector));186}187}188};189190191/**192* Make a copy of this ruleset.193* @return {!goog.cssom.iframe.style.CssRuleSet_} A new CssRuleSet containing194* the same data as this one.195*/196goog.cssom.iframe.style.CssRuleSet_.prototype.clone = function() {197var newRuleSet = new goog.cssom.iframe.style.CssRuleSet_();198newRuleSet.selectors = this.selectors.concat();199newRuleSet.declarationText = this.declarationText;200return newRuleSet;201};202203204/**205* Set the declaration text with properties from a given object.206* @param {Object} sourceObject Object whose properties and values should207* be used to generate the declaration text.208* @param {boolean=} opt_important Whether !important should be added to each209* declaration.210*/211goog.cssom.iframe.style.CssRuleSet_.prototype.setDeclarationTextFromObject =212function(sourceObject, opt_important) {213var stringParts = [];214// TODO(user): for ... in is costly in IE6 (extra garbage collection).215for (var prop in sourceObject) {216var value = sourceObject[prop];217if (value) {218stringParts.push(219prop, ':', value, (opt_important ? ' !important' : ''), ';');220}221}222this.declarationText = stringParts.join('');223};224225226/**227* Serializes this CssRuleSet_ into an array as a series of strings.228* The array can then be join()-ed to get a string representation229* of this ruleset.230* @param {Array<string>} array The array to which to append strings.231*/232goog.cssom.iframe.style.CssRuleSet_.prototype.writeToArray = function(array) {233var selectorCount = this.selectors.length;234var matchesAnchorTag = false;235for (var i = 0; i < selectorCount; i++) {236var selectorParts = this.selectors[i].parts;237var partCount = selectorParts.length;238for (var j = 0; j < partCount; j++) {239array.push(240selectorParts[j].inputString_,241goog.cssom.iframe.style.SELECTOR_PART_DELIMITER_);242}243if (i < (selectorCount - 1)) {244array.push(goog.cssom.iframe.style.SELECTOR_DELIMITER_);245}246if (goog.userAgent.GECKO && !goog.userAgent.isVersionOrHigher('1.9a')) {247// In Gecko pre-1.9 (Firefox 2 and lower) we need to add !important248// to rulesets that match "A" tags, otherwise Gecko's built-in249// stylesheet will take precedence when designMode is on.250matchesAnchorTag = matchesAnchorTag ||251goog.cssom.iframe.style.selectorPartAnchorRegex_.test(252selectorParts[partCount - 1].inputString_);253}254}255var declarationText = this.declarationText;256if (matchesAnchorTag) {257declarationText =258goog.cssom.iframe.style.makeColorRuleImportant_(declarationText);259}260array.push(261goog.cssom.iframe.style.DECLARATION_START_DELIMITER_, declarationText,262goog.cssom.iframe.style.DECLARATION_END_DELIMITER_);263};264265266/**267* Regexp that matches "color: value;".268* @type {RegExp}269* @private270*/271goog.cssom.iframe.style.colorImportantReplaceRegex_ =272/(^|;|{)\s*color:([^;]+);/g;273274275/**276* Adds !important to a css color: rule277* @param {string} cssText Text of the CSS rule(s) to modify.278* @return {string} Text with !important added to the color: rule if found.279* @private280*/281goog.cssom.iframe.style.makeColorRuleImportant_ = function(cssText) {282// Replace to insert a "! important" string.283return cssText.replace(284goog.cssom.iframe.style.colorImportantReplaceRegex_,285'$1 color: $2 ! important; ');286};287288289290/**291* Represents a single CSS selector, as described in292* http://www.w3.org/TR/REC-CSS2/selector.html293* Currently UNSUPPORTED are the following selector features:294* <ul>295* <li>pseudo-classes (:hover)296* <li>child selectors (div > h1)297* <li>adjacent sibling selectors (div + h1)298* <li>attribute selectors (input[type=submit])299* </ul>300* @param {string=} opt_selectorString String containing selectors to parse.301* @constructor302* @private303*/304goog.cssom.iframe.style.CssSelector_ = function(opt_selectorString) {305306/**307* Object to track ancestry matches to speed up repeatedly testing this308* CssSelector against the same NodeAncestry object.309* @type {Object}310* @private311*/312this.ancestryMatchCache_ = {};313if (opt_selectorString) {314this.setPartsFromString_(opt_selectorString);315}316};317318319/**320* Parses a selector string into individual parts.321* @param {string} selectorString A string containing a CSS selector.322* @private323*/324goog.cssom.iframe.style.CssSelector_.prototype.setPartsFromString_ = function(325selectorString) {326var parts = [];327var selectorPartStrings = selectorString.split(/\s+/gm);328for (var i = 0; i < selectorPartStrings.length; i++) {329if (!selectorPartStrings[i]) {330continue; // Skip empty strings.331}332var part =333new goog.cssom.iframe.style.CssSelectorPart_(selectorPartStrings[i]);334parts.push(part);335}336this.parts = parts;337};338339340/**341* Tests to see what part of a DOM element hierarchy would be matched by342* this selector, and returns the indexes of the matching element and matching343* selector part.344* <p>345* For example, given this hierarchy:346* document > html > body > div.content > div.sidebar > p347* and this CSS selector:348* body div.sidebar h1349* This would return {elementIndex: 4, selectorPartIndex: 1},350* indicating that the element at index 4 matched351* the css selector at index 1.352* </p>353* @param {goog.cssom.iframe.style.NodeAncestry_} elementAncestry Object354* representing an element and its ancestors.355* @return {Object} Object with the properties elementIndex and356* selectorPartIndex, or null if there was no match.357*/358goog.cssom.iframe.style.CssSelector_.prototype.matchElementAncestry = function(359elementAncestry) {360361var ancestryUid = elementAncestry.uid;362if (this.ancestryMatchCache_[ancestryUid]) {363return this.ancestryMatchCache_[ancestryUid];364}365366// Walk through the selector parts and see how far down the element hierarchy367// we can go while matching the selector parts.368var elementIndex = 0;369var match = null;370var selectorPart = null;371var lastSelectorPart = null;372var ancestorNodes = elementAncestry.nodes;373var ancestorNodeCount = ancestorNodes.length;374375for (var i = 0; i <= this.parts.length; i++) {376selectorPart = this.parts[i];377while (elementIndex < ancestorNodeCount) {378var currentElementInfo = ancestorNodes[elementIndex];379if (selectorPart && selectorPart.testElement(currentElementInfo)) {380match = {elementIndex: elementIndex, selectorPartIndex: i};381elementIndex++;382break;383} else if (384lastSelectorPart &&385lastSelectorPart.testElement(currentElementInfo)) {386match = {elementIndex: elementIndex, selectorPartIndex: i - 1};387}388elementIndex++;389}390lastSelectorPart = selectorPart;391}392this.ancestryMatchCache_[ancestryUid] = match;393return match;394};395396397398/**399* Represents one part of a CSS Selector. For example in the selector400* 'body #foo .bar', body, #foo, and .bar would be considered selector parts.401* In the official CSS spec these are called "simple selectors".402* @param {string} selectorPartString A string containing the selector part403* in css format.404* @constructor405* @private406*/407goog.cssom.iframe.style.CssSelectorPart_ = function(selectorPartString) {408// Only one CssSelectorPart instance should exist for a given string.409var cacheEntry =410goog.cssom.iframe.style.CssSelectorPart_.instances_[selectorPartString];411if (cacheEntry) {412return cacheEntry;413}414415// Optimization to avoid the more-expensive lookahead.416var identifiers;417if (selectorPartString.match(/[#\.]/)) {418// Lookahead regexp, won't work on IE 5.0.419identifiers = selectorPartString.split(/(?=[#\.])/);420} else {421identifiers = [selectorPartString];422}423var properties = {};424for (var i = 0; i < identifiers.length; i++) {425var identifier = identifiers[i];426if (identifier.charAt(0) == '.') {427properties.className = identifier.substring(1, identifier.length);428} else if (identifier.charAt(0) == '#') {429properties.id = identifier.substring(1, identifier.length);430} else {431properties.tagName = identifier.toUpperCase();432}433}434this.inputString_ = selectorPartString;435this.matchProperties_ = properties;436this.testedElements_ = {};437goog.cssom.iframe.style.CssSelectorPart_.instances_[selectorPartString] =438this;439};440441442/**443* Cache of existing CssSelectorPart_ instances.444* @type {Object}445* @private446*/447goog.cssom.iframe.style.CssSelectorPart_.instances_ = {};448449450/**451* Test whether an element matches this selector part, considered in isolation.452* @param {Object} elementInfo Element properties to test.453* @return {boolean} Whether the element matched.454*/455goog.cssom.iframe.style.CssSelectorPart_.prototype.testElement = function(456elementInfo) {457458var elementUid = elementInfo.uid;459var cachedMatch = this.testedElements_[elementUid];460if (typeof cachedMatch != 'undefined') {461return cachedMatch;462}463464var matchProperties = this.matchProperties_;465var testTag = matchProperties.tagName;466var testClass = matchProperties.className;467var testId = matchProperties.id;468469var matched = true;470if (testTag && testTag != '*' && testTag != elementInfo.nodeName) {471matched = false;472} else if (testId && testId != elementInfo.id) {473matched = false;474} else if (testClass && !elementInfo.classNames[testClass]) {475matched = false;476}477478this.testedElements_[elementUid] = matched;479return matched;480};481482483484/**485* Represents an element and all its parent/ancestor nodes.486* This class exists as an optimization so we run tests on an element487* hierarchy multiple times without walking the dom each time.488* @param {Element} el The DOM element whose ancestry should be stored.489* @constructor490* @private491*/492goog.cssom.iframe.style.NodeAncestry_ = function(el) {493var node = el;494var nodeUid = goog.getUid(node);495496// Return an existing object from the cache if one exits for this node.497var ancestry = goog.cssom.iframe.style.NodeAncestry_.instances_[nodeUid];498if (ancestry) {499return ancestry;500}501502var nodes = [];503do {504var nodeInfo = {id: node.id, nodeName: node.nodeName};505nodeInfo.uid = goog.getUid(nodeInfo);506var className = node.className;507var classNamesLookup = {};508if (className) {509var classNames = goog.dom.classlist.get(goog.asserts.assertElement(node));510for (var i = 0; i < classNames.length; i++) {511classNamesLookup[classNames[i]] = 1;512}513}514nodeInfo.classNames = classNamesLookup;515nodes.unshift(nodeInfo);516} while (node = node.parentNode);517518/**519* Array of nodes in order of hierarchy from the top of the document520* to the node passed to the constructor521* @type {Array<Node>}522*/523this.nodes = nodes;524525this.uid = goog.getUid(this);526goog.cssom.iframe.style.NodeAncestry_.instances_[nodeUid] = this;527};528529530/**531* Object for caching existing NodeAncestry instances.532* @private533*/534goog.cssom.iframe.style.NodeAncestry_.instances_ = {};535536537/**538* Throw away all cached dom information. Call this if you've modified539* the structure or class/id attributes of your document and you want540* to recalculate the currently applied CSS rules.541*/542goog.cssom.iframe.style.resetDomCache = function() {543goog.cssom.iframe.style.NodeAncestry_.instances_ = {};544};545546547/**548* Inspects a document and returns all active rule sets549* @param {Document} doc The document from which to read CSS rules.550* @return {!Array<goog.cssom.iframe.style.CssRuleSet_>} An array of CssRuleSet551* objects representing all the active rule sets in the document.552* @private553*/554goog.cssom.iframe.style.getRuleSetsFromDocument_ = function(doc) {555var ruleSets = [];556var styleSheets = goog.cssom.getAllCssStyleSheets(doc.styleSheets);557for (var i = 0, styleSheet; styleSheet = styleSheets[i]; i++) {558var domRuleSets = goog.cssom.getCssRulesFromStyleSheet(styleSheet);559if (domRuleSets && domRuleSets.length) {560for (var j = 0, n = domRuleSets.length; j < n; j++) {561var ruleSet = new goog.cssom.iframe.style.CssRuleSet_();562if (ruleSet.initializeFromCssRule(domRuleSets[j])) {563ruleSets.push(ruleSet);564}565}566}567}568return ruleSets;569};570571572/**573* Static object to cache rulesets read from documents. Inspecting all574* active css rules is an expensive operation, so its best to only do575* it once and then cache the results.576* @type {Object}577* @private578*/579goog.cssom.iframe.style.ruleSetCache_ = {};580581582/**583* Cache of ruleset objects keyed by document unique ID.584* @type {Object}585* @private586*/587goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_ = {};588589590/**591* Loads ruleset definitions from a document. If the cache already592* has rulesets for this document the cached version will be replaced.593* @param {Document} doc The document from which to load rulesets.594*/595goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument = function(doc) {596var docUid = goog.getUid(doc);597goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_[docUid] =598goog.cssom.iframe.style.getRuleSetsFromDocument_(doc);599};600601602/**603* Retrieves the array of css rulesets for this document. A cached604* version will be used when possible.605* @param {Document} doc The document for which to get rulesets.606* @return {!Array<goog.cssom.iframe.style.CssRuleSet_>} An array of CssRuleSet607* objects representing the css rule sets in the supplied document.608*/609goog.cssom.iframe.style.ruleSetCache_.getRuleSetsForDocument = function(doc) {610var docUid = goog.getUid(doc);611var cache = goog.cssom.iframe.style.ruleSetCache_.ruleSetCache_;612if (!cache[docUid]) {613goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument(doc);614}615// Build a cloned copy of rulesets array, so if object in the returned array616// get modified future calls will still return the original unmodified617// versions.618var ruleSets = cache[docUid];619var ruleSetsCopy = [];620for (var i = 0; i < ruleSets.length; i++) {621ruleSetsCopy.push(ruleSets[i].clone());622}623return ruleSetsCopy;624};625626627/**628* Array of CSS properties that are inherited by child nodes, according to629* the CSS 2.1 spec. Properties that may be set to relative values, such630* as font-size, and line-height, are omitted.631* @type {Array<string>}632* @private633*/634goog.cssom.iframe.style.inheritedProperties_ = [635'color',636'visibility',637'quotes',638'list-style-type',639'list-style-image',640'list-style-position',641'list-style',642'page-break-inside',643'orphans',644'widows',645'font-family',646'font-style',647'font-variant',648'font-weight',649'text-indent',650'text-align',651'text-transform',652'white-space',653'caption-side',654'border-collapse',655'border-spacing',656'empty-cells',657'cursor'658];659660661/**662* Array of CSS 2.1 properties that directly effect text nodes.663* @type {Array<string>}664* @private665*/666goog.cssom.iframe.style.textProperties_ = [667'font-family', 'font-size', 'font-weight', 'font-variant', 'font-style',668'color', 'text-align', 'text-decoration', 'text-indent', 'text-transform',669'letter-spacing', 'white-space', 'word-spacing'670];671672673/**674* Reads the current css rules from element's document, and returns them675* rewriting selectors so that any rules that formerly applied to element will676* be applied to doc.body. This makes it possible to replace a block in a page677* with an iframe and preserve the css styling of the contents.678*679* @param {Element} element The element for which context should be calculated.680* @param {boolean=} opt_forceRuleSetCacheUpdate Flag to force the internal681* cache of rulesets to refresh itself before we read the same.682* @param {boolean=} opt_copyBackgroundContext Flag indicating that if the683* {@code element} has a transparent background, background rules684* from the nearest ancestor element(s) that have background-color685* and/or background-image set should be copied.686* @return {string} String containing all CSS rules present in the original687* document, with modified selectors.688* @see goog.cssom.iframe.style.getBackgroundContext.689*/690goog.cssom.iframe.style.getElementContext = function(691element, opt_forceRuleSetCacheUpdate, opt_copyBackgroundContext) {692var sourceDocument = element.ownerDocument;693if (opt_forceRuleSetCacheUpdate) {694goog.cssom.iframe.style.ruleSetCache_.loadRuleSetsForDocument(695sourceDocument);696}697var ruleSets = goog.cssom.iframe.style.ruleSetCache_.getRuleSetsForDocument(698sourceDocument);699700var elementAncestry = new goog.cssom.iframe.style.NodeAncestry_(element);701var bodySelectorPart = new goog.cssom.iframe.style.CssSelectorPart_('body');702703for (var i = 0; i < ruleSets.length; i++) {704var ruleSet = ruleSets[i];705var selectors = ruleSet.selectors;706// Cache selectors.length since we may be adding rules in the loop.707var ruleCount = selectors.length;708for (var j = 0; j < ruleCount; j++) {709var selector = selectors[j];710// Test whether all or part of this selector would match711// this element or one of its ancestors712var match = selector.matchElementAncestry(elementAncestry);713if (match) {714var ruleIndex = match.selectorPartIndex;715var selectorParts = selector.parts;716var lastSelectorPartIndex = selectorParts.length - 1;717var selectorCopy;718if (match.elementIndex == elementAncestry.nodes.length - 1 ||719ruleIndex < lastSelectorPartIndex) {720// Either the first part(s) of the selector matched this element,721// or the first part(s) of the selector matched a parent element722// and there are more parts of the selector that could target723// children of this element.724// So we inject a new selector, replacing the part that matched this725// element with 'body' so it will continue to match.726var selectorPartsCopy = selectorParts.concat();727selectorPartsCopy.splice(0, ruleIndex + 1, bodySelectorPart);728selectorCopy = new goog.cssom.iframe.style.CssSelector_();729selectorCopy.parts = selectorPartsCopy;730selectors.push(selectorCopy);731} else if (ruleIndex > 0 && ruleIndex == lastSelectorPartIndex) {732// The rule didn't match this element, but the entire rule did733// match an ancestor element. In this case we want to copy734// just the last part of the rule, to give it a chance to be applied735// to additional matching elements inside this element.736// Example DOM structure: body > div.funky > ul > li#editme737// Example CSS selector: .funky ul738// New CSS selector: body ul739selectorCopy = new goog.cssom.iframe.style.CssSelector_();740selectorCopy.parts =741[bodySelectorPart, selectorParts[lastSelectorPartIndex]];742selectors.push(selectorCopy);743}744}745}746}747748// Insert a new ruleset, setting the current inheritable styles of this749// element as the defaults for everything under in the frame.750var defaultPropertiesRuleSet = new goog.cssom.iframe.style.CssRuleSet_();751var computedStyle = goog.cssom.iframe.style.getComputedStyleObject_(element);752753// Copy inheritable styles so they are applied to everything under HTML.754var htmlSelector = new goog.cssom.iframe.style.CssSelector_();755htmlSelector.parts = [new goog.cssom.iframe.style.CssSelectorPart_('html')];756defaultPropertiesRuleSet.selectors = [htmlSelector];757var defaultProperties = {};758for (var i = 0, prop; prop = goog.cssom.iframe.style.inheritedProperties_[i];759i++) {760defaultProperties[prop] = computedStyle[goog.string.toCamelCase(prop)];761}762defaultPropertiesRuleSet.setDeclarationTextFromObject(defaultProperties);763ruleSets.push(defaultPropertiesRuleSet);764765var bodyRuleSet = new goog.cssom.iframe.style.CssRuleSet_();766var bodySelector = new goog.cssom.iframe.style.CssSelector_();767bodySelector.parts = [new goog.cssom.iframe.style.CssSelectorPart_('body')];768// Core set of sane property values for BODY, to prevent copied769// styles from completely breaking the display.770var bodyProperties = {771position: 'relative',772top: '0',773left: '0',774right: 'auto', // Override any existing right value so 'left' works.775display: 'block',776visibility: 'visible'777};778// Text formatting property values, to keep text nodes directly under BODY779// looking right.780for (i = 0; prop = goog.cssom.iframe.style.textProperties_[i]; i++) {781bodyProperties[prop] = computedStyle[goog.string.toCamelCase(prop)];782}783if (opt_copyBackgroundContext &&784goog.cssom.iframe.style.isTransparentValue_(785computedStyle['backgroundColor'])) {786// opt_useAncestorBackgroundRules means that, if the original element787// has a transparent background, background properties rules should be788// added to explicitly make the body have the same background appearance789// as in the original element, even if its positioned somewhere else790// in the DOM.791var bgProperties = goog.cssom.iframe.style.getBackgroundContext(element);792bodyProperties['background-color'] = bgProperties['backgroundColor'];793var elementBgImage = computedStyle['backgroundImage'];794if (!elementBgImage || elementBgImage == 'none') {795bodyProperties['background-image'] = bgProperties['backgroundImage'];796bodyProperties['background-repeat'] = bgProperties['backgroundRepeat'];797bodyProperties['background-position'] =798bgProperties['backgroundPosition'];799}800}801802bodyRuleSet.setDeclarationTextFromObject(bodyProperties, true);803bodyRuleSet.selectors = [bodySelector];804ruleSets.push(bodyRuleSet);805806// Write outputTextParts to doc.807var ruleSetStrings = [];808ruleCount = ruleSets.length;809for (i = 0; i < ruleCount; i++) {810ruleSets[i].writeToArray(ruleSetStrings);811}812return ruleSetStrings.join('');813};814815816/**817* Tests whether a value is equivalent to 'transparent'.818* @param {string} colorValue The value to test.819* @return {boolean} Whether the value is transparent.820* @private821*/822goog.cssom.iframe.style.isTransparentValue_ = function(colorValue) {823return colorValue == 'transparent' || colorValue == 'rgba(0, 0, 0, 0)';824};825826827/**828* Returns an object containing the set of computedStyle/currentStyle829* values for the given element. Note that this should be used with830* caution as it ignores the fact that currentStyle and computedStyle831* are not the same for certain properties.832*833* @param {Element} element The element whose computed style to return.834* @return {Object} Object containing style properties and values.835* @private836*/837goog.cssom.iframe.style.getComputedStyleObject_ = function(element) {838// Return an object containing the element's computedStyle/currentStyle.839// The resulting object can be re-used to read multiple properties, which840// is faster than calling goog.style.getComputedStyle every time.841return element.currentStyle ||842goog.dom.getOwnerDocument(element).defaultView.getComputedStyle(843element, '') ||844{};845};846847848/**849* RegExp that splits a value like "10px" or "-1em" into parts.850* @private851* @type {RegExp}852*/853goog.cssom.iframe.style.valueWithUnitsRegEx_ = /^(-?)([0-9]+)([a-z]*|%)/;854855856/**857* Given an object containing a set of styles, returns a two-element array858* containing the values of background-position-x and background-position-y.859* @param {Object} styleObject Object from which to read style properties.860* @return {Array<string>} The background-position values in the order [x, y].861* @private862*/863goog.cssom.iframe.style.getBackgroundXYValues_ = function(styleObject) {864// Gecko only has backgroundPosition, containing both values.865// IE has only backgroundPositionX/backgroundPositionY.866// WebKit has both.867if (styleObject['backgroundPositionY']) {868return [869styleObject['backgroundPositionX'], styleObject['backgroundPositionY']870];871} else {872return (styleObject['backgroundPosition'] || '0 0').split(' ');873}874};875876877/**878* Generates a set of CSS properties that can be used to make another879* element's background look like the background of a given element.880* This is useful when you want to copy the CSS context of an element,881* but the element's background is transparent. In the original context882* you would see the ancestor's backround color/image showing through,883* but in the new context there might be a something different underneath.884* Note that this assumes the element you're copying context from has a885* fairly standard positioning/layout - it assumes that when the element886* has a transparent background what you're going to see through it is its887* ancestors.888* @param {Element} element The element from which to copy background styles.889* @return {!Object} Object containing background* properties.890*/891goog.cssom.iframe.style.getBackgroundContext = function(element) {892var propertyValues = {'backgroundImage': 'none'};893var ancestor = element;894var currentIframeWindow;895// Walk up the DOM tree to find the ancestor nodes whose backgrounds896// may be visible underneath this element. Background-image and897// background-color don't have to come from the same node, but as soon898// an element with background-color is found there's no need to continue899// because backgrounds farther up the chain won't be visible.900// (This implementation is not sophisticated enough to handle opacity,901// or multple layered partially-transparent background images.)902while ((ancestor = /** @type {!Element} */ (ancestor.parentNode)) &&903ancestor.nodeType == goog.dom.NodeType.ELEMENT) {904var computedStyle =905goog.cssom.iframe.style.getComputedStyleObject_(ancestor);906// Copy background color if a non-transparent value is found.907var backgroundColorValue = computedStyle['backgroundColor'];908if (!goog.cssom.iframe.style.isTransparentValue_(backgroundColorValue)) {909propertyValues['backgroundColor'] = backgroundColorValue;910}911// If a background image value is found, copy background-image,912// background-repeat, and background-position.913if (computedStyle['backgroundImage'] &&914computedStyle['backgroundImage'] != 'none') {915propertyValues['backgroundImage'] = computedStyle['backgroundImage'];916propertyValues['backgroundRepeat'] = computedStyle['backgroundRepeat'];917// Calculate the offset between the original element and the element918// providing the background image, so the background position can be919// adjusted.920var relativePosition;921if (currentIframeWindow) {922relativePosition =923goog.style.getFramedPageOffset(element, currentIframeWindow);924var frameElement = currentIframeWindow.frameElement;925var iframeRelativePosition = goog.style.getRelativePosition(926/** @type {!Element} */ (frameElement), ancestor);927var iframeBorders = goog.style.getBorderBox(frameElement);928relativePosition.x += iframeRelativePosition.x + iframeBorders.left;929relativePosition.y += iframeRelativePosition.y + iframeBorders.top;930} else {931relativePosition = goog.style.getRelativePosition(element, ancestor);932}933var backgroundXYValues =934goog.cssom.iframe.style.getBackgroundXYValues_(computedStyle);935// Parse background-repeat-* values in the form "10px", and adjust them.936for (var i = 0; i < 2; i++) {937var positionValue = backgroundXYValues[i];938var coordinate = i == 0 ? 'X' : 'Y';939var positionProperty = 'backgroundPosition' + coordinate;940// relative position to its ancestor.941var positionValueParts =942goog.cssom.iframe.style.valueWithUnitsRegEx_.exec(positionValue);943if (positionValueParts) {944var value =945parseInt(positionValueParts[1] + positionValueParts[2], 10);946var units = positionValueParts[3];947// This only attempts to handle pixel values for now (plus948// '0anything', which is equivalent to 0px).949// TODO(user) Convert non-pixel values to pixels when possible.950if (value == 0 || units == 'px') {951value -=952(coordinate == 'X' ? relativePosition.x : relativePosition.y);953}954positionValue = value + units;955}956propertyValues[positionProperty] = positionValue;957}958propertyValues['backgroundPosition'] =959propertyValues['backgroundPositionX'] + ' ' +960propertyValues['backgroundPositionY'];961}962if (propertyValues['backgroundColor']) {963break;964}965if (ancestor.tagName == goog.dom.TagName.HTML) {966try {967currentIframeWindow = goog.dom.getWindow(968/** @type {Document} */ (ancestor.parentNode));969// This could theoretically throw a security exception if the parent970// iframe is in a different domain.971ancestor = currentIframeWindow.frameElement;972if (!ancestor) {973// Loop has reached the top level window.974break;975}976} catch (e) {977// We don't have permission to go up to the parent window, stop here.978break;979}980}981}982return propertyValues;983};984985986