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