Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(763)

Side by Side Diff: dart/pkg/shadow_dom/lib/src/platform/ShadowCSS.js

Issue 59073003: Version 0.8.10.4 (Closed) Base URL: http://dart.googlecode.com/svn/trunk/
Patch Set: Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « dart/pkg/shadow_dom/lib/shadow_dom.min.js ('k') | dart/pkg/shadow_dom/tool/build.json » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 /*
2 * Copyright 2012 The Polymer Authors. All rights reserved.
3 * Use of this source code is governed by a BSD-style
4 * license that can be found in the LICENSE file.
5 */
6
7 /*
8 This is a limited shim for ShadowDOM css styling.
9 https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#style s
10
11 The intention here is to support only the styling features which can be
12 relatively simply implemented. The goal is to allow users to avoid the
13 most obvious pitfalls and do so without compromising performance significantly .
14 For ShadowDOM styling that's not covered here, a set of best practices
15 can be provided that should allow users to accomplish more complex styling.
16
17 The following is a list of specific ShadowDOM styling features and a brief
18 discussion of the approach used to shim.
19
20 Shimmed features:
21
22 * @host: ShadowDOM allows styling of the shadowRoot's host element using the
23 @host rule. To shim this feature, the @host styles are reformatted and
24 prefixed with a given scope name and promoted to a document level stylesheet.
25 For example, given a scope name of .foo, a rule like this:
26
27 @host {
28 * {
29 background: red;
30 }
31 }
32
33 becomes:
34
35 .foo {
36 background: red;
37 }
38
39 * encapsultion: Styles defined within ShadowDOM, apply only to
40 dom inside the ShadowDOM. Polymer uses one of two techniques to imlement
41 this feature.
42
43 By default, rules are prefixed with the host element tag name
44 as a descendant selector. This ensures styling does not leak out of the 'top'
45 of the element's ShadowDOM. For example,
46
47 div {
48 font-weight: bold;
49 }
50
51 becomes:
52
53 x-foo div {
54 font-weight: bold;
55 }
56
57 becomes:
58
59
60 Alternatively, if Platform.ShadowCSS.strictStyling is set to true then
61 selectors are scoped by adding an attribute selector suffix to each
62 simple selector that contains the host element tag name. Each element
63 in the element's ShadowDOM template is also given the scope attribute.
64 Thus, these rules match only elements that have the scope attribute.
65 For example, given a scope name of x-foo, a rule like this:
66
67 div {
68 font-weight: bold;
69 }
70
71 becomes:
72
73 div[x-foo] {
74 font-weight: bold;
75 }
76
77 Note that elements that are dynamically added to a scope must have the scope
78 selector added to them manually.
79
80 * ::pseudo: These rules are converted to rules that take advantage of the
81 pseudo attribute. For example, a shadowRoot like this inside an x-foo
82
83 <div pseudo="x-special">Special</div>
84
85 with a rule like this:
86
87 x-foo::x-special { ... }
88
89 becomes:
90
91 x-foo [pseudo=x-special] { ... }
92
93 * ::part(): These rules are converted to rules that take advantage of the
94 part attribute. For example, a shadowRoot like this inside an x-foo
95
96 <div part="special">Special</div>
97
98 with a rule like this:
99
100 x-foo::part(special) { ... }
101
102 becomes:
103
104 x-foo [part=special] { ... }
105
106 Unaddressed ShadowDOM styling features:
107
108 * upper/lower bound encapsulation: Styles which are defined outside a
109 shadowRoot should not cross the ShadowDOM boundary and should not apply
110 inside a shadowRoot.
111
112 This styling behavior is not emulated. Some possible ways to do this that
113 were rejected due to complexity and/or performance concerns include: (1) reset
114 every possible property for every possible selector for a given scope name;
115 (2) re-implement css in javascript.
116
117 As an alternative, users should make sure to use selectors
118 specific to the scope in which they are working.
119
120 * ::distributed: This behavior is not emulated. It's often not necessary
121 to style the contents of a specific insertion point and instead, descendants
122 of the host element can be styled selectively. Users can also create an
123 extra node around an insertion point and style that node's contents
124 via descendent selectors. For example, with a shadowRoot like this:
125
126 <style>
127 content::-webkit-distributed(div) {
128 background: red;
129 }
130 </style>
131 <content></content>
132
133 could become:
134
135 <style>
136 / *@polyfill .content-container div * /
137 content::-webkit-distributed(div) {
138 background: red;
139 }
140 </style>
141 <div class="content-container">
142 <content></content>
143 </div>
144
145 Note the use of @polyfill in the comment above a ShadowDOM specific style
146 declaration. This is a directive to the styling shim to use the selector
147 in comments in lieu of the next selector when running under polyfill.
148 */
149 (function(scope) {
150
151 var ShadowCSS = {
152 strictStyling: false,
153 registry: {},
154 // Shim styles for a given root associated with a name and extendsName
155 // 1. cache root styles by name
156 // 2. optionally tag root nodes with scope name
157 // 3. shim polyfill directives /* @polyfill */ and /* @polyfill-rule */
158 // 4. shim @host and scoping
159 shimStyling: function(root, name, extendsName) {
160 var typeExtension = this.isTypeExtension(extendsName);
161 // use caching to make working with styles nodes easier and to facilitate
162 // lookup of extendee
163 var def = this.registerDefinition(root, name, extendsName);
164 // find styles and apply shimming...
165 if (this.strictStyling) {
166 this.applyScopeToContent(root, name);
167 }
168 // insert @polyfill and @polyfill-rule rules into style elements
169 // scoping process takes care of shimming these
170 this.insertPolyfillDirectives(def.rootStyles);
171 this.insertPolyfillRules(def.rootStyles);
172 var cssText = this.stylesToShimmedCssText(def.scopeStyles, name,
173 typeExtension);
174 // note: we only need to do rootStyles since these are unscoped.
175 cssText += this.extractPolyfillUnscopedRules(def.rootStyles);
176 // provide shimmedStyle for user extensibility
177 def.shimmedStyle = cssTextToStyle(cssText);
178 if (root) {
179 root.shimmedStyle = def.shimmedStyle;
180 }
181 // remove existing style elements
182 for (var i=0, l=def.rootStyles.length, s; (i<l) && (s=def.rootStyles[i]);
183 i++) {
184 s.parentNode.removeChild(s);
185 }
186 // add style to document
187 addCssToDocument(cssText);
188 },
189 registerDefinition: function(root, name, extendsName) {
190 var def = this.registry[name] = {
191 root: root,
192 name: name,
193 extendsName: extendsName
194 }
195 var styles = root ? root.querySelectorAll('style') : [];
196 styles = styles ? Array.prototype.slice.call(styles, 0) : [];
197 def.rootStyles = styles;
198 def.scopeStyles = def.rootStyles;
199 var extendee = this.registry[def.extendsName];
200 if (extendee && (!root || root.querySelector('shadow'))) {
201 def.scopeStyles = extendee.scopeStyles.concat(def.scopeStyles);
202 }
203 return def;
204 },
205 isTypeExtension: function(extendsName) {
206 return extendsName && extendsName.indexOf('-') < 0;
207 },
208 applyScopeToContent: function(root, name) {
209 if (root) {
210 // add the name attribute to each node in root.
211 Array.prototype.forEach.call(root.querySelectorAll('*'),
212 function(node) {
213 node.setAttribute(name, '');
214 });
215 // and template contents too
216 Array.prototype.forEach.call(root.querySelectorAll('template'),
217 function(template) {
218 this.applyScopeToContent(template.content, name);
219 },
220 this);
221 }
222 },
223 /*
224 * Process styles to convert native ShadowDOM rules that will trip
225 * up the css parser; we rely on decorating the stylesheet with comments.
226 *
227 * For example, we convert this rule:
228 *
229 * (comment start) @polyfill :host menu-item (comment end)
230 * shadow::-webkit-distributed(menu-item) {
231 *
232 * to this:
233 *
234 * scopeName menu-item {
235 *
236 **/
237 insertPolyfillDirectives: function(styles) {
238 if (styles) {
239 Array.prototype.forEach.call(styles, function(s) {
240 s.textContent = this.insertPolyfillDirectivesInCssText(s.textContent);
241 }, this);
242 }
243 },
244 insertPolyfillDirectivesInCssText: function(cssText) {
245 return cssText.replace(cssPolyfillCommentRe, function(match, p1) {
246 // remove end comment delimiter and add block start
247 return p1.slice(0, -2) + '{';
248 });
249 },
250 /*
251 * Process styles to add rules which will only apply under the polyfill
252 *
253 * For example, we convert this rule:
254 *
255 * (comment start) @polyfill-rule :host menu-item {
256 * ... } (comment end)
257 *
258 * to this:
259 *
260 * scopeName menu-item {...}
261 *
262 **/
263 insertPolyfillRules: function(styles) {
264 if (styles) {
265 Array.prototype.forEach.call(styles, function(s) {
266 s.textContent = this.insertPolyfillRulesInCssText(s.textContent);
267 }, this);
268 }
269 },
270 insertPolyfillRulesInCssText: function(cssText) {
271 return cssText.replace(cssPolyfillRuleCommentRe, function(match, p1) {
272 // remove end comment delimiter
273 return p1.slice(0, -1);
274 });
275 },
276 /*
277 * Process styles to add rules which will only apply under the polyfill
278 * and do not process via CSSOM. (CSSOM is destructive to rules on rare
279 * occasions, e.g. -webkit-calc on Safari.)
280 * For example, we convert this rule:
281 *
282 * (comment start) @polyfill-unscoped-rule menu-item {
283 * ... } (comment end)
284 *
285 * to this:
286 *
287 * menu-item {...}
288 *
289 **/
290 extractPolyfillUnscopedRules: function(styles) {
291 var cssText = '';
292 if (styles) {
293 Array.prototype.forEach.call(styles, function(s) {
294 cssText += this.extractPolyfillUnscopedRulesFromCssText(
295 s.textContent) + '\n\n';
296 }, this);
297 }
298 return cssText;
299 },
300 extractPolyfillUnscopedRulesFromCssText: function(cssText) {
301 var r = '', matches;
302 while (matches = cssPolyfillUnscopedRuleCommentRe.exec(cssText)) {
303 r += matches[1].slice(0, -1) + '\n\n';
304 }
305 return r;
306 },
307 // apply @host and scope shimming
308 stylesToShimmedCssText: function(styles, name, typeExtension) {
309 return this.shimAtHost(styles, name, typeExtension) +
310 this.shimScoping(styles, name, typeExtension);
311 },
312 // form: @host { .foo { declarations } }
313 // becomes: scopeName.foo { declarations }
314 shimAtHost: function(styles, name, typeExtension) {
315 if (styles) {
316 return this.convertAtHostStyles(styles, name, typeExtension);
317 }
318 },
319 convertAtHostStyles: function(styles, name, typeExtension) {
320 var cssText = stylesToCssText(styles), self = this;
321 cssText = cssText.replace(hostRuleRe, function(m, p1) {
322 return self.scopeHostCss(p1, name, typeExtension);
323 });
324 cssText = rulesToCss(this.findAtHostRules(cssToRules(cssText),
325 new RegExp('^' + name + selectorReSuffix, 'm')));
326 return cssText;
327 },
328 scopeHostCss: function(cssText, name, typeExtension) {
329 var self = this;
330 return cssText.replace(selectorRe, function(m, p1, p2) {
331 return self.scopeHostSelector(p1, name, typeExtension) + ' ' + p2 + '\n\t' ;
332 });
333 },
334 // supports scopig by name and [is=name] syntax
335 scopeHostSelector: function(selector, name, typeExtension) {
336 var r = [], parts = selector.split(','), is = '[is=' + name + ']';
337 parts.forEach(function(p) {
338 p = p.trim();
339 // selector: *|:scope -> name
340 if (p.match(hostElementRe)) {
341 p = p.replace(hostElementRe, typeExtension ? is + '$1$3' :
342 name + '$1$3');
343 // selector: .foo -> name.foo (OR) [bar] -> name[bar]
344 } else if (p.match(hostFixableRe)) {
345 p = typeExtension ? is + p : name + p;
346 }
347 r.push(p);
348 }, this);
349 return r.join(', ');
350 },
351 // consider styles that do not include component name in the selector to be
352 // unscoped and in need of promotion;
353 // for convenience, also consider keyframe rules this way.
354 findAtHostRules: function(cssRules, matcher) {
355 return Array.prototype.filter.call(cssRules,
356 this.isHostRule.bind(this, matcher));
357 },
358 isHostRule: function(matcher, cssRule) {
359 return (cssRule.selectorText && cssRule.selectorText.match(matcher)) ||
360 (cssRule.cssRules && this.findAtHostRules(cssRule.cssRules, matcher).lengt h) ||
361 (cssRule.type == CSSRule.WEBKIT_KEYFRAMES_RULE);
362 },
363 /* Ensure styles are scoped. Pseudo-scoping takes a rule like:
364 *
365 * .foo {... }
366 *
367 * and converts this to
368 *
369 * scopeName .foo { ... }
370 */
371 shimScoping: function(styles, name, typeExtension) {
372 if (styles) {
373 return this.convertScopedStyles(styles, name, typeExtension);
374 }
375 },
376 convertScopedStyles: function(styles, name, typeExtension) {
377 var cssText = stylesToCssText(styles).replace(hostRuleRe, '');
378 cssText = this.insertPolyfillHostInCssText(cssText);
379 cssText = this.convertColonHost(cssText);
380 cssText = this.convertPseudos(cssText);
381 cssText = this.convertParts(cssText);
382 cssText = this.convertCombinators(cssText);
383 var rules = cssToRules(cssText);
384 cssText = this.scopeRules(rules, name, typeExtension);
385 return cssText;
386 },
387 convertPseudos: function(cssText) {
388 return cssText.replace(cssPseudoRe, ' [pseudo=$1]');
389 },
390 convertParts: function(cssText) {
391 return cssText.replace(cssPartRe, ' [part=$1]');
392 },
393 /*
394 * convert a rule like :host(.foo) > .bar { }
395 *
396 * to
397 *
398 * scopeName.foo > .bar, .foo scopeName > .bar { }
399 * TODO(sorvell): file bug since native impl does not do the former yet.
400 * http://jsbin.com/OganOCI/2/edit
401 */
402 convertColonHost: function(cssText) {
403 // p1 = :host, p2 = contents of (), p3 rest of rule
404 return cssText.replace(cssColonHostRe, function(m, p1, p2, p3) {
405 return p2 ? polyfillHostNoCombinator + p2 + p3 + ', '
406 + p2 + ' ' + p1 + p3 :
407 p1 + p3;
408 });
409 },
410 /*
411 * Convert ^ and ^^ combinators by replacing with space.
412 */
413 convertCombinators: function(cssText) {
414 return cssText.replace('^^', ' ').replace('^', ' ');
415 },
416 // change a selector like 'div' to 'name div'
417 scopeRules: function(cssRules, name, typeExtension) {
418 var cssText = '';
419 Array.prototype.forEach.call(cssRules, function(rule) {
420 if (rule.selectorText && (rule.style && rule.style.cssText)) {
421 cssText += this.scopeSelector(rule.selectorText, name, typeExtension,
422 this.strictStyling) + ' {\n\t';
423 cssText += this.propertiesFromRule(rule) + '\n}\n\n';
424 } else if (rule.media) {
425 cssText += '@media ' + rule.media.mediaText + ' {\n';
426 cssText += this.scopeRules(rule.cssRules, name);
427 cssText += '\n}\n\n';
428 } else if (rule.cssText) {
429 cssText += rule.cssText + '\n\n';
430 }
431 }, this);
432 return cssText;
433 },
434 scopeSelector: function(selector, name, typeExtension, strict) {
435 var r = [], parts = selector.split(',');
436 parts.forEach(function(p) {
437 p = p.trim();
438 if (this.selectorNeedsScoping(p, name, typeExtension)) {
439 p = strict ? this.applyStrictSelectorScope(p, name) :
440 this.applySimpleSelectorScope(p, name, typeExtension);
441 }
442 r.push(p);
443 }, this);
444 return r.join(', ');
445 },
446 selectorNeedsScoping: function(selector, name, typeExtension) {
447 var matchScope = typeExtension ? name : '\\[is=' + name + '\\]';
448 var re = new RegExp('^(' + matchScope + ')' + selectorReSuffix, 'm');
449 return !selector.match(re);
450 },
451 // scope via name and [is=name]
452 applySimpleSelectorScope: function(selector, name, typeExtension) {
453 var scoper = typeExtension ? '[is=' + name + ']' : name;
454 if (selector.match(polyfillHostRe)) {
455 selector = selector.replace(polyfillHostNoCombinator, scoper);
456 return selector.replace(polyfillHostRe, scoper + ' ');
457 } else {
458 return scoper + ' ' + selector;
459 }
460 },
461 // return a selector with [name] suffix on each simple selector
462 // e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name]
463 applyStrictSelectorScope: function(selector, name) {
464 var splits = [' ', '>', '+', '~'],
465 scoped = selector,
466 attrName = '[' + name + ']';
467 splits.forEach(function(sep) {
468 var parts = scoped.split(sep);
469 scoped = parts.map(function(p) {
470 // remove :host since it should be unnecessary
471 var t = p.trim().replace(polyfillHostRe, '');
472 if (t && (splits.indexOf(t) < 0) && (t.indexOf(attrName) < 0)) {
473 p = t.replace(/([^:]*)(:*)(.*)/, '$1' + attrName + '$2$3')
474 }
475 return p;
476 }).join(sep);
477 });
478 return scoped;
479 },
480 insertPolyfillHostInCssText: function(selector) {
481 return selector.replace(hostRe, polyfillHost).replace(colonHostRe,
482 polyfillHost);
483 },
484 propertiesFromRule: function(rule) {
485 var properties = rule.style.cssText;
486 // TODO(sorvell): Chrome cssom incorrectly removes quotes from the content
487 // property. (https://code.google.com/p/chromium/issues/detail?id=247231)
488 if (rule.style.content && !rule.style.content.match(/['"]+/)) {
489 properties = 'content: \'' + rule.style.content + '\';\n' +
490 rule.style.cssText.replace(/content:[^;]*;/g, '');
491 }
492 return properties;
493 }
494 };
495
496 var hostRuleRe = /@host[^{]*{(([^}]*?{[^{]*?}[\s\S]*?)+)}/gim,
497 selectorRe = /([^{]*)({[\s\S]*?})/gim,
498 hostElementRe = /(.*)((?:\*)|(?:\:scope))(.*)/,
499 hostFixableRe = /^[.\[:]/,
500 cssCommentRe = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim,
501 cssPolyfillCommentRe = /\/\*\s*@polyfill ([^*]*\*+([^/*][^*]*\*+)*\/)([^{]*? ){/gim,
502 cssPolyfillRuleCommentRe = /\/\*\s@polyfill-rule([^*]*\*+([^/*][^*]*\*+)*)\/ /gim,
503 cssPolyfillUnscopedRuleCommentRe = /\/\*\s@polyfill-unscoped-rule([^*]*\*+([ ^/*][^*]*\*+)*)\//gim,
504 cssPseudoRe = /::(x-[^\s{,(]*)/gim,
505 cssPartRe = /::part\(([^)]*)\)/gim,
506 // note: :host pre-processed to -host.
507 cssColonHostRe = /(-host)(?:\(([^)]*)\))?([^,{]*)/gim,
508 selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$',
509 hostRe = /@host/gim,
510 colonHostRe = /\:host/gim,
511 polyfillHost = '-host',
512 /* host name without combinator */
513 polyfillHostNoCombinator = '-host-no-combinator',
514 polyfillHostRe = /-host/gim;
515
516 function stylesToCssText(styles, preserveComments) {
517 var cssText = '';
518 Array.prototype.forEach.call(styles, function(s) {
519 cssText += s.textContent + '\n\n';
520 });
521 // strip comments for easier processing
522 if (!preserveComments) {
523 cssText = cssText.replace(cssCommentRe, '');
524 }
525 return cssText;
526 }
527
528 function cssTextToStyle(cssText) {
529 var style = document.createElement('style');
530 style.textContent = cssText;
531 return style;
532 }
533
534 function cssToRules(cssText) {
535 var style = cssTextToStyle(cssText);
536 document.head.appendChild(style);
537 var rules = style.sheet.cssRules;
538 style.parentNode.removeChild(style);
539 return rules;
540 }
541
542 function rulesToCss(cssRules) {
543 for (var i=0, css=[]; i < cssRules.length; i++) {
544 css.push(cssRules[i].cssText);
545 }
546 return css.join('\n\n');
547 }
548
549 function addCssToDocument(cssText) {
550 if (cssText) {
551 getSheet().appendChild(document.createTextNode(cssText));
552 }
553 }
554
555 var sheet;
556 function getSheet() {
557 if (!sheet) {
558 sheet = document.createElement("style");
559 sheet.setAttribute('ShadowCSSShim', '');
560 }
561 return sheet;
562 }
563
564 // add polyfill stylesheet to document
565 if (window.ShadowDOMPolyfill) {
566 addCssToDocument('style { display: none !important; }\n');
567 var head = document.querySelector('head');
568 head.insertBefore(getSheet(), head.childNodes[0]);
569 }
570
571 // exports
572 scope.ShadowCSS = ShadowCSS;
573
574 })(window.Platform);
OLDNEW
« no previous file with comments | « dart/pkg/shadow_dom/lib/shadow_dom.min.js ('k') | dart/pkg/shadow_dom/tool/build.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698