1024 lines
34 KiB
JavaScript
1024 lines
34 KiB
JavaScript
// 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.cssom');
|
|
goog.require('goog.dom');
|
|
goog.require('goog.dom.NodeType');
|
|
goog.require('goog.dom.TagName');
|
|
goog.require('goog.dom.classes');
|
|
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) {
|
|
/**
|
|
* Array of CssSelectorPart objects representing the parts of this selector
|
|
* Example: for the selector 'body h1' the parts would be [body, h1].
|
|
* @type {Array.<goog.cssom.iframe.style.CssSelectorPart_>}
|
|
* @private
|
|
*/
|
|
this.parts_ = [];
|
|
|
|
/**
|
|
* 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.classes.get(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 declarationParts = [];
|
|
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;
|
|
};
|