OLD | NEW |
| (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); | |
OLD | NEW |