Skip to content

Commit b9f2482

Browse files
author
Chris Joel
committed
Non-destructive @keyframes rule transformation.
Previously, the transformer did not disambiguate selectors in `@media` blocks and keyframes in `@keyframes` blocks. Now, the transformer can safely transform `@keyframes` blocks. Before a selector is transformed, if the selector has a parent, it is checked. If the checked parent is a `@keyframes` rule, the selector transformation is skipped. Element-specific `@keyframes` are suffixed with the scoped element name. For example, `@keyframes foo` in an element scoped with `x-el-0` will by transformed to `@keyframes foo-x-el-0`. References to that animation in the element's local styles will be updated as well. Added tests for the new keyframes transformation.
1 parent 4a9ef8e commit b9f2482

11 files changed

Lines changed: 722 additions & 266 deletions

‎src/lib/css-parse.html‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
node.type = this.types.MEDIA_RULE;
7474
} else if (s.match(this._rx.keyframesRule)) {
7575
node.type = this.types.KEYFRAMES_RULE;
76+
node.keyframesName =
77+
node.selector.split(this._rx.multipleSpaces).pop();
7678
}
7779
} else {
7880
if (s.indexOf(this.VAR_START) === 0) {

‎src/lib/custom-style.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
e.textContent;
140140
}
141141
if (e.textContent) {
142-
styleUtil.forEachStyleRule(styleUtil.rulesForStyle(e), function(rule) {
142+
styleUtil.forEachRule(styleUtil.rulesForStyle(e), function(rule) {
143143
styleTransformer.documentRule(rule);
144144
});
145145
// Allow all custom-styles defined in this turn to register

‎src/lib/style-extends.html‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
var styleUtil = Polymer.StyleUtil;
1717

1818
return {
19-
19+
2020
hasExtends: function(cssText) {
2121
return Boolean(cssText.match(this.rx.EXTEND));
2222
},
2323

2424
transform: function(style) {
2525
var rules = styleUtil.rulesForStyle(style);
2626
var self = this;
27-
styleUtil.forEachStyleRule(rules, function(rule) {
27+
styleUtil.forEachRule(rules, function(rule) {
2828
var map = self._mapRule(rule);
2929
if (rule.parent) {
3030
var m;
@@ -72,7 +72,7 @@
7272
// TODO: this misses `%foo, .bar` as an unetended selector but
7373
// this seems rare and could possibly be unsupported.
7474
source.selector = source.selector.replace(this.rx.STRIP, '');
75-
source.selector = (source.selector && source.selector + ',\n') +
75+
source.selector = (source.selector && source.selector + ',\n') +
7676
target.selector;
7777
if (source.extends) {
7878
source.extends.forEach(function(e) {

‎src/lib/style-properties.html‎

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@
2525
// decorates styles with rule info and returns an array of used style
2626
// property names
2727
decorateStyles: function(styles) {
28-
var self = this, props = {};
28+
var self = this, props = {}, keyframes = [];
2929
styleUtil.forRulesInStyles(styles, function(rule) {
3030
self.decorateRule(rule);
3131
self.collectPropertiesInCssText(rule.propertyInfo.cssText, props);
32+
}, function onKeyframesRule(rule) {
33+
keyframes.push(rule);
3234
});
35+
// Cache all found keyframes rules for later reference:
36+
styles._keyframes = keyframes;
3337
// return this list of property names *consumes* in these styles.
3438
var names = [];
3539
for (var i in props) {
@@ -87,7 +91,10 @@
8791
var parts = cssText.split(';');
8892
for (var i=0, p; i<parts.length; i++) {
8993
p = parts[i];
90-
if (p.match(this.rx.MIXIN_MATCH) || p.match(this.rx.VAR_MATCH)) {
94+
if (p.match(this.rx.MIXIN_MATCH) ||
95+
p.match(this.rx.VAR_MATCH) ||
96+
this.rx.ANIMATION_MATCH.test(p) ||
97+
styleUtil.isKeyframesSelector(rule)) {
9198
customCssText += p + ';\n';
9299
}
93100
}
@@ -181,6 +188,46 @@
181188
rule.cssText = output;
182189
},
183190

191+
// Apply keyframe transformations to the cssText of a given rule. The
192+
// keyframeTransforms object is a map of keyframe names to transformer
193+
// functions which take in cssText and spit out transformed cssText.
194+
applyKeyframeTransforms: function(rule, keyframeTransforms) {
195+
var input = rule.cssText;
196+
var output = rule.cssText;
197+
if (rule.hasAnimations == null) {
198+
// Cache whether or not the rule has any animations to begin with:
199+
rule.hasAnimations = this.rx.ANIMATION_MATCH.test(input);
200+
}
201+
// If there are no animations referenced, we can skip transforms:
202+
if (rule.hasAnimations) {
203+
var transform;
204+
// If we haven't transformed this rule before, we iterate over all
205+
// transforms:
206+
if (rule.keyframeNamesToTransform == null) {
207+
rule.keyframeNamesToTransform = [];
208+
for (var keyframe in keyframeTransforms) {
209+
transform = keyframeTransforms[keyframe];
210+
output = transform(input);
211+
// If the transform actually changed the CSS text, we cache the
212+
// transform name for future use:
213+
if (input !== output) {
214+
input = output;
215+
rule.keyframeNamesToTransform.push(keyframe);
216+
}
217+
}
218+
} else {
219+
// If we already have a list of keyframe names that apply to this
220+
// rule, we apply only those keyframe name transforms:
221+
for (var i = 0; i < rule.keyframeNamesToTransform.length; ++i) {
222+
transform = keyframeTransforms[rule.keyframeNamesToTransform[i]];
223+
input = transform(input);
224+
}
225+
output = input;
226+
}
227+
}
228+
rule.cssText = output;
229+
},
230+
184231
// Test if the rules in these styles matches the given `element` and if so,
185232
// collect any custom properties into `props`.
186233
propertyDataFromStyles: function(styles, element) {
@@ -256,15 +303,60 @@
256303
hostSelector;
257304
var hostRx = new RegExp(this.rx.HOST_PREFIX + rxHostSelector +
258305
this.rx.HOST_SUFFIX);
306+
var keyframeTransforms =
307+
this._elementKeyframeTransforms(element, scopeSelector);
259308
return styleTransformer.elementStyles(element, function(rule) {
260309
self.applyProperties(rule, properties);
261-
if (rule.cssText && !nativeShadow) {
310+
if (!nativeShadow &&
311+
!Polymer.StyleUtil.isKeyframesSelector(rule) &&
312+
rule.cssText) {
313+
// NOTE: keyframe transforms only scope munge animation names, so it
314+
// is not necessary to apply them in ShadowDOM.
315+
self.applyKeyframeTransforms(rule, keyframeTransforms);
262316
self._scopeSelector(rule, hostRx, hostSelector,
263317
element._scopeCssViaAttr, scopeSelector);
264318
}
265319
});
266320
},
267321

322+
_elementKeyframeTransforms: function(element, scopeSelector) {
323+
var keyframesRules = element._styles._keyframes;
324+
var keyframeTransforms = {};
325+
if (!nativeShadow) {
326+
// For non-ShadowDOM, we transform all known keyframes rules in
327+
// advance for the current scope. This allows us to catch keyframes
328+
// rules that appear anywhere in the stylesheet:
329+
for (var i = 0, keyframesRule = keyframesRules[i];
330+
i < keyframesRules.length;
331+
keyframesRule = keyframesRules[++i]) {
332+
this._scopeKeyframes(keyframesRule, scopeSelector);
333+
keyframeTransforms[keyframesRule.keyframesName] =
334+
this._keyframesRuleTransformer(keyframesRule);
335+
}
336+
}
337+
return keyframeTransforms;
338+
},
339+
340+
// Generate a factory for transforming a chunk of CSS text to handle a
341+
// particular scoped keyframes rule.
342+
_keyframesRuleTransformer: function(keyframesRule) {
343+
return function(cssText) {
344+
return cssText.replace(
345+
keyframesRule.keyframesNameRx,
346+
keyframesRule.transformedKeyframesName);
347+
};
348+
},
349+
350+
// Transforms `@keyframes` names to be unique for the current host.
351+
// Example: @keyframes foo-anim -> @keyframes foo-anim-x-foo-0
352+
_scopeKeyframes: function(rule, scopeId) {
353+
rule.keyframesNameRx = new RegExp(rule.keyframesName, 'g');
354+
rule.transformedKeyframesName = rule.keyframesName + '-' + scopeId;
355+
rule.transformedSelector = rule.transformedSelector || rule.selector;
356+
rule.selector = rule.transformedSelector.replace(
357+
rule.keyframesName, rule.transformedKeyframesName);
358+
},
359+
268360
// Strategy: x scope shim a selector e.g. to scope `.x-foo-42` (via classes):
269361
// non-host selector: .a.x-foo -> .x-foo-42 .a.x-foo
270362
// host selector: x-foo.wide -> x-foo.x-foo-42.wide
@@ -359,6 +451,7 @@
359451
// var(--a, fallback-literal(with-one-nested-parentheses))
360452
VAR_MATCH: /(^|\W+)var\([\s]*([^,)]*)[\s]*,?[\s]*((?:[^,)]*)|(?:[^;]*\([^;)]*\)))[\s]*?\)/gi,
361453
VAR_CAPTURE: /\([\s]*(--[^,\s)]*)(?:,[\s]*(--[^,\s)]*))?(?:\)|,)/gi,
454+
ANIMATION_MATCH: /(animation\s*:)|(animation-name\s*:)/,
362455
IS_VAR: /^--/,
363456
BRACKETED: /\{[^}]*\}/g,
364457
HOST_PREFIX: '(?:^|[^.#[:])',

‎src/lib/style-transformer.html‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,12 @@
148148
// transforms a css rule to a scoped rule.
149149
_transformRule: function(rule, transformer, scope, hostScope) {
150150
var p$ = rule.selector.split(COMPLEX_SELECTOR_SEP);
151-
for (var i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
152-
p$[i] = transformer.call(this, p, scope, hostScope);
151+
// we want to skip transformation of rules that appear in keyframes,
152+
// because they are keyframe selectors, not element selectors.
153+
if (!styleUtil.isKeyframesSelector(rule)) {
154+
for (var i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
155+
p$[i] = transformer.call(this, p, scope, hostScope);
156+
}
153157
}
154158
// NOTE: save transformedSelector for subsequent matching of elements
155159
// against selectors (e.g. when calculating style properties)

‎src/lib/style-util.html‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@
2424
rules = this.parser.parse(rules);
2525
}
2626
if (callback) {
27-
this.forEachStyleRule(rules, callback);
27+
this.forEachRule(rules, callback);
2828
}
2929
return this.parser.stringify(rules, preserveProperties);
3030
},
3131

32-
forRulesInStyles: function(styles, callback) {
32+
forRulesInStyles: function(styles, styleRuleCallback, keyframesRuleCallback) {
3333
if (styles) {
3434
for (var i=0, l=styles.length, s; (i<l) && (s=styles[i]); i++) {
35-
this.forEachStyleRule(this.rulesForStyle(s), callback);
35+
this.forEachRule(
36+
this.rulesForStyle(s),
37+
styleRuleCallback,
38+
keyframesRuleCallback);
3639
}
3740
}
3841
},
@@ -44,21 +47,31 @@
4447
return style.__cssRules;
4548
},
4649

47-
forEachStyleRule: function(node, callback) {
50+
// Tests if a rule is a keyframes selector, which looks almost exactly
51+
// like a normal selector but is not (it has nothing to do with scoping
52+
// for example).
53+
isKeyframesSelector: function(rule) {
54+
return rule.parent &&
55+
rule.parent.type === this.ruleTypes.KEYFRAMES_RULE;
56+
},
57+
58+
forEachRule: function(node, styleRuleCallback, keyframesRuleCallback) {
4859
if (!node) {
4960
return;
5061
}
5162
var skipRules = false;
5263
if (node.type === this.ruleTypes.STYLE_RULE) {
53-
callback(node);
54-
} else if (node.type === this.ruleTypes.KEYFRAMES_RULE ||
55-
node.type === this.ruleTypes.MIXIN_RULE) {
64+
styleRuleCallback(node);
65+
} else if (keyframesRuleCallback &&
66+
node.type === this.ruleTypes.KEYFRAMES_RULE) {
67+
keyframesRuleCallback(node);
68+
} else if (node.type === this.ruleTypes.MIXIN_RULE) {
5669
skipRules = true;
5770
}
5871
var r$ = node.rules;
5972
if (r$ && !skipRules) {
6073
for (var i=0, l=r$.length, r; (i<l) && (r=r$[i]); i++) {
61-
this.forEachStyleRule(r, callback);
74+
this.forEachRule(r, styleRuleCallback, keyframesRuleCallback);
6275
}
6376
}
6477
},

‎test/smoke/keyframes.html‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!doctype html>
2+
<!--
3+
@license
4+
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
5+
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
6+
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
7+
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
8+
Code distributed by Google as part of the polymer project is also
9+
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
10+
-->
11+
<script src="../../../webcomponentsjs/webcomponents-lite.js"></script>
12+
<link rel="import" href="../../polymer.html">
13+
14+
<body>
15+
<style is="custom-style">
16+
:root {
17+
--color: blue;
18+
--anim-color: red;
19+
}
20+
21+
.alternative {
22+
--color: green;
23+
--anim-color: blue;
24+
}
25+
</style>
26+
27+
<dom-module id="test-keyframes">
28+
<template><style>
29+
:host {
30+
display: block;
31+
color: var(--color);
32+
animation: foo 3s;
33+
height: 20px;
34+
}
35+
36+
@keyframes foo {
37+
0% {
38+
background: var(--anim-color);
39+
}
40+
41+
100% {
42+
background: yellow;
43+
}
44+
}
45+
</style><content></content></template>
46+
<script>
47+
Polymer({
48+
is: 'test-keyframes'
49+
});
50+
51+
</script>
52+
</dom-module>
53+
54+
<p>Text should be the color blue. Background should animate from the color red to the color yellow, and then become transparent.</p>
55+
<test-keyframes>red</test-keyframes>
56+
57+
<p>Text should be the color green. Background should animate from the color blue to the color yellow, and then become transparent.</p>
58+
<test-keyframes class="alternative">blue</test-keyframes>
59+
60+
</body>

0 commit comments

Comments
 (0)