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

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

Issue 22951003: Build shadow_dom package in dart/pkg (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Use ShadowDOM test file Created 7 years, 4 months 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
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 Unaddressed ShadowDOM styling features:
94
95 * upper/lower bound encapsulation: Styles which are defined outside a
96 shadowRoot should not cross the ShadowDOM boundary and should not apply
97 inside a shadowRoot.
98
99 This styling behavior is not emulated. Some possible ways to do this that
100 were rejected due to complexity and/or performance concerns include: (1) reset
101 every possible property for every possible selector for a given scope name;
102 (2) re-implement css in javascript.
103
104 As an alternative, users should make sure to use selectors
105 specific to the scope in which they are working.
106
107 * ::distributed: This behavior is not emulated. It's often not necessary
108 to style the contents of a specific insertion point and instead, descendants
109 of the host element can be styled selectively. Users can also create an
110 extra node around an insertion point and style that node's contents
111 via descendent selectors. For example, with a shadowRoot like this:
112
113 <style>
114 content::-webkit-distributed(div) {
115 background: red;
116 }
117 </style>
118 <content></content>
119
120 could become:
121
122 <style>
123 / *@polyfill .content-container div * /
124 content::-webkit-distributed(div) {
125 background: red;
126 }
127 </style>
128 <div class="content-container">
129 <content></content>
130 </div>
131
132 Note the use of @polyfill in the comment above a ShadowDOM specific style
133 declaration. This is a directive to the styling shim to use the selector
134 in comments in lieu of the next selector when running under polyfill.
135 */
136 (function(scope) {
137
138 var ShadowCSS = {
139 strictStyling: false,
140 registry: {},
141 // Shim styles for a given root associated with a name and extendsName
142 // 1. cache root styles by name
143 // 2. optionally tag root nodes with scope name
144 // 3. shim polyfill directives /* @polyfill */
145 // 4. shim @host and scoping
146 shimStyling: function(root, name, extendsName) {
147 if (root) {
148 // use caching to make working with styles nodes easier and to facilitate
149 // lookup of extendee
150 var def = this.registerDefinition(root, name, extendsName);
151 // find styles and apply shimming...
152 if (this.strictStyling) {
153 this.applyScopeToContent(root, name);
154 }
155 this.shimPolyfillDirectives(def.rootStyles, name);
156 this.applyShimming(def.scopeStyles, name);
157 }
158 },
159 // Shim styles to be placed inside a shadowRoot.
160 // 1. shim polyfill directives /* @polyfill */
161 // 2. shim @host and scoping
162 shimShadowDOMStyling: function(styles, name) {
163 this.shimPolyfillDirectives(styles, name);
164 this.applyShimming(styles, name);
165 },
166 registerDefinition: function(root, name, extendsName) {
167 var def = this.registry[name] = {
168 root: root,
169 name: name,
170 extendsName: extendsName
171 }
172 var styles = root.querySelectorAll('style');
173 styles = styles ? Array.prototype.slice.call(styles, 0) : [];
174 def.rootStyles = styles;
175 def.scopeStyles = def.rootStyles;
176 var extendee = this.registry[def.extendsName];
177 if (extendee) {
178 def.scopeStyles = def.scopeStyles.concat(extendee.scopeStyles);
179 }
180 return def;
181 },
182 applyScopeToContent: function(root, name) {
183 if (root) {
184 // add the name attribute to each node in root.
185 Array.prototype.forEach.call(root.querySelectorAll('*'),
186 function(node) {
187 node.setAttribute(name, '');
188 });
189 // and template contents too
190 Array.prototype.forEach.call(root.querySelectorAll('template'),
191 function(template) {
192 this.applyScopeToContent(template.content, name);
193 },
194 this);
195 }
196 },
197 /*
198 * Process styles to convert native ShadowDOM rules that will trip
199 * up the css parser; we rely on decorating the stylesheet with comments.
200 *
201 * For example, we convert this rule:
202 *
203 * (comment start) @polyfill @host g-menu-item (comment end)
204 * shadow::-webkit-distributed(g-menu-item) {
205 *
206 * to this:
207 *
208 * scopeName g-menu-item {
209 *
210 **/
211 shimPolyfillDirectives: function(styles, name) {
212 if (styles) {
213 Array.prototype.forEach.call(styles, function(s) {
214 s.textContent = this.convertPolyfillDirectives(s.textContent, name);
215 }, this);
216 }
217 },
218 convertPolyfillDirectives: function(cssText, name) {
219 var r = '', l = 0, matches, selector;
220 while (matches = cssPolyfillCommentRe.exec(cssText)) {
221 r += cssText.substring(l, matches.index);
222 // remove end comment delimiter (*/)
223 selector = matches[1].slice(0, -2).replace(hostRe, name);
224 r += this.scopeSelector(selector, name) + '{';
225 l = cssPolyfillCommentRe.lastIndex;
226 }
227 r += cssText.substring(l, cssText.length);
228 return r;
229 },
230 // apply @host and scope shimming
231 applyShimming: function(styles, name) {
232 var cssText = this.shimAtHost(styles, name);
233 cssText += this.shimScoping(styles, name);
234 addCssToDocument(cssText);
235 },
236 // form: @host { .foo { declarations } }
237 // becomes: scopeName.foo { declarations }
238 shimAtHost: function(styles, name) {
239 if (styles) {
240 return this.convertAtHostStyles(styles, name);
241 }
242 },
243 convertAtHostStyles: function(styles, name) {
244 var cssText = stylesToCssText(styles);
245 var r = '', l=0, matches;
246 while (matches = hostRuleRe.exec(cssText)) {
247 r += cssText.substring(l, matches.index);
248 r += this.scopeHostCss(matches[1], name);
249 l = hostRuleRe.lastIndex;
250 }
251 r += cssText.substring(l, cssText.length);
252 var re = new RegExp('^' + name + selectorReSuffix, 'm');
253 var cssText = rulesToCss(this.findAtHostRules(cssToRules(r),
254 re));
255 return cssText;
256 },
257 scopeHostCss: function(cssText, name) {
258 var r = '', matches;
259 while (matches = selectorRe.exec(cssText)) {
260 r += this.scopeHostSelector(matches[1], name) +' ' + matches[2] + '\n\t';
261 }
262 return r;
263 },
264 // supports scopig by name and [is=name] syntax
265 scopeHostSelector: function(selector, name) {
266 var r = [], parts = selector.split(','), is = '[is=' + name + ']';
267 parts.forEach(function(p) {
268 p = p.trim();
269 // selector: *|:scope -> name
270 if (p.match(hostElementRe)) {
271 p = p.replace(hostElementRe, name + '$1$3, ' + is + '$1$3');
272 // selector: .foo -> name.foo, [bar] -> name[bar]
273 } else if (p.match(hostFixableRe)) {
274 p = name + p + ', ' + is + p;
275 }
276 r.push(p);
277 }, this);
278 return r.join(', ');
279 },
280 // consider styles that do not include component name in the selector to be
281 // unscoped and in need of promotion;
282 // for convenience, also consider keyframe rules this way.
283 findAtHostRules: function(cssRules, matcher) {
284 return Array.prototype.filter.call(cssRules,
285 this.isHostRule.bind(this, matcher));
286 },
287 isHostRule: function(matcher, cssRule) {
288 return (cssRule.selectorText && cssRule.selectorText.match(matcher)) ||
289 (cssRule.cssRules && this.findAtHostRules(cssRule.cssRules, matcher).lengt h) ||
290 (cssRule.type == CSSRule.WEBKIT_KEYFRAMES_RULE);
291 },
292 /* Ensure styles are scoped. Pseudo-scoping takes a rule like:
293 *
294 * .foo {... }
295 *
296 * and converts this to
297 *
298 * scopeName .foo { ... }
299 */
300 shimScoping: function(styles, name) {
301 if (styles) {
302 return this.convertScopedStyles(styles, name);
303 }
304 },
305 convertScopedStyles: function(styles, name) {
306 Array.prototype.forEach.call(styles, function(s) {
307 if (s.parentNode) {
308 s.parentNode.removeChild(s);
309 }
310 });
311 var cssText = stylesToCssText(styles).replace(hostRuleRe, '');
312 cssText = this.convertPseudos(cssText);
313 var rules = cssToRules(cssText);
314 cssText = this.scopeRules(rules, name);
315 return cssText;
316 },
317 convertPseudos: function(cssText) {
318 return cssText.replace(cssPseudoRe, ' [pseudo=$1]');
319 },
320 // change a selector like 'div' to 'name div'
321 scopeRules: function(cssRules, name) {
322 var cssText = '';
323 Array.prototype.forEach.call(cssRules, function(rule) {
324 if (rule.selectorText && (rule.style && rule.style.cssText)) {
325 cssText += this.scopeSelector(rule.selectorText, name,
326 this.strictStyling) + ' {\n\t';
327 cssText += this.propertiesFromRule(rule) + '\n}\n\n';
328 } else if (rule.media) {
329 cssText += '@media ' + rule.media.mediaText + ' {\n';
330 cssText += this.scopeRules(rule.cssRules, name);
331 cssText += '\n}\n\n';
332 } else if (rule.cssText) {
333 cssText += rule.cssText + '\n\n';
334 }
335 }, this);
336 return cssText;
337 },
338 scopeSelector: function(selector, name, strict) {
339 var r = [], parts = selector.split(',');
340 parts.forEach(function(p) {
341 p = p.trim();
342 if (this.selectorNeedsScoping(p, name)) {
343 p = strict ? this.applyStrictSelectorScope(p, name) :
344 this.applySimpleSelectorScope(p, name);
345 }
346 r.push(p);
347 }, this);
348 return r.join(', ');
349 },
350 selectorNeedsScoping: function(selector, name) {
351 var matchScope = '(' + name + '|\\[is=' + name + '\\])';
352 var re = new RegExp('^' + matchScope + selectorReSuffix, 'm');
353 return !selector.match(re);
354 },
355 // scope via name and [is=name]
356 applySimpleSelectorScope: function(selector, name) {
357 return name + ' ' + selector + ', ' + '[is=' + name + '] ' + selector;
358 },
359 // return a selector with [name] suffix on each simple selector
360 // e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name]
361 applyStrictSelectorScope: function(selector, name) {
362 var splits = [' ', '>', '+', '~'],
363 scoped = selector,
364 attrName = '[' + name + ']';
365 splits.forEach(function(sep) {
366 var parts = scoped.split(sep);
367 scoped = parts.map(function(p) {
368 var t = p.trim();
369 if (t && (splits.indexOf(t) < 0) && (t.indexOf(attrName) < 0)) {
370 p = t.replace(/([^:]*)(:*)(.*)/, '$1' + attrName + '$2$3')
371 }
372 return p;
373 }).join(sep);
374 });
375 return scoped;
376 },
377 propertiesFromRule: function(rule) {
378 var properties = rule.style.cssText;
379 // TODO(sorvell): Chrome cssom incorrectly removes quotes from the content
380 // property. (https://code.google.com/p/chromium/issues/detail?id=247231)
381 if (rule.style.content && !rule.style.content.match(/['"]+/)) {
382 properties = 'content: \'' + rule.style.content + '\';\n' +
383 rule.style.cssText.replace(/content:[^;]*;/g, '');
384 }
385 return properties;
386 }
387 };
388
389 var hostRuleRe = /@host[^{]*{(([^}]*?{[^{]*?}[\s\S]*?)+)}/gim,
390 selectorRe = /([^{]*)({[\s\S]*?})/gim,
391 hostElementRe = /(.*)((?:\*)|(?:\:scope))(.*)/,
392 hostFixableRe = /^[.\[:]/,
393 cssCommentRe = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim,
394 cssPolyfillCommentRe = /\/\*\s*@polyfill ([^*]*\*+([^/*][^*]*\*+)*\/)([^{]*? ){/gim,
395 cssPseudoRe = /::(x-[^\s{,(]*)/gim,
396 selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$',
397 hostRe = /@host/gim;
398
399 function stylesToCssText(styles, preserveComments) {
400 var cssText = '';
401 Array.prototype.forEach.call(styles, function(s) {
402 cssText += s.textContent + '\n\n';
403 });
404 // strip comments for easier processing
405 if (!preserveComments) {
406 cssText = cssText.replace(cssCommentRe, '');
407 }
408 return cssText;
409 }
410
411 function cssToRules(cssText) {
412 var style = document.createElement('style');
413 style.textContent = cssText;
414 document.head.appendChild(style);
415 var rules = style.sheet.cssRules;
416 style.parentNode.removeChild(style);
417 return rules;
418 }
419
420 function rulesToCss(cssRules) {
421 for (var i=0, css=[]; i < cssRules.length; i++) {
422 css.push(cssRules[i].cssText);
423 }
424 return css.join('\n\n');
425 }
426
427 function addCssToDocument(cssText) {
428 if (cssText) {
429 getSheet().appendChild(document.createTextNode(cssText));
430 }
431 }
432
433 var sheet;
434 function getSheet() {
435 if (!sheet) {
436 sheet = document.createElement("style");
437 sheet.setAttribute('ShadowCSSShim', '');
438 }
439 return sheet;
440 }
441
442 // add polyfill stylesheet to document
443 if (window.ShadowDOMPolyfill) {
444 addCssToDocument('style { display: none !important; }\n');
445 var head = document.querySelector('head');
446 head.insertBefore(getSheet(), head.childNodes[0]);
447 }
448
449 // exports
450 scope.ShadowCSS = ShadowCSS;
451
452 })(window.Platform);
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698