Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-destructive @keyframes rule transformation. #3163

Merged
merged 1 commit into from
Feb 4, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib/css-parse.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
node.type = this.types.MEDIA_RULE;
} else if (s.match(this._rx.keyframesRule)) {
node.type = this.types.KEYFRAMES_RULE;
node.keyframesName =
node.selector.split(this._rx.multipleSpaces).pop();
}
} else {
if (s.indexOf(this.VAR_START) === 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/custom-style.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
e.textContent;
}
if (e.textContent) {
styleUtil.forEachStyleRule(styleUtil.rulesForStyle(e), function(rule) {
styleUtil.forEachRule(styleUtil.rulesForStyle(e), function(rule) {
styleTransformer.documentRule(rule);
});
// Allow all custom-styles defined in this turn to register
Expand Down
6 changes: 3 additions & 3 deletions src/lib/style-extends.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
var styleUtil = Polymer.StyleUtil;

return {

hasExtends: function(cssText) {
return Boolean(cssText.match(this.rx.EXTEND));
},

transform: function(style) {
var rules = styleUtil.rulesForStyle(style);
var self = this;
styleUtil.forEachStyleRule(rules, function(rule) {
styleUtil.forEachRule(rules, function(rule) {
var map = self._mapRule(rule);
if (rule.parent) {
var m;
Expand Down Expand Up @@ -72,7 +72,7 @@
// TODO: this misses `%foo, .bar` as an unetended selector but
// this seems rare and could possibly be unsupported.
source.selector = source.selector.replace(this.rx.STRIP, '');
source.selector = (source.selector && source.selector + ',\n') +
source.selector = (source.selector && source.selector + ',\n') +
target.selector;
if (source.extends) {
source.extends.forEach(function(e) {
Expand Down
99 changes: 96 additions & 3 deletions src/lib/style-properties.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@
// decorates styles with rule info and returns an array of used style
// property names
decorateStyles: function(styles) {
var self = this, props = {};
var self = this, props = {}, keyframes = [];
styleUtil.forRulesInStyles(styles, function(rule) {
self.decorateRule(rule);
self.collectPropertiesInCssText(rule.propertyInfo.cssText, props);
}, function onKeyframesRule(rule) {
keyframes.push(rule);
});
// Cache all found keyframes rules for later reference:
styles._keyframes = keyframes;
// return this list of property names *consumes* in these styles.
var names = [];
for (var i in props) {
Expand Down Expand Up @@ -87,7 +91,10 @@
var parts = cssText.split(';');
for (var i=0, p; i<parts.length; i++) {
p = parts[i];
if (p.match(this.rx.MIXIN_MATCH) || p.match(this.rx.VAR_MATCH)) {
if (p.match(this.rx.MIXIN_MATCH) ||
p.match(this.rx.VAR_MATCH) ||
this.rx.ANIMATION_MATCH.test(p) ||
styleUtil.isKeyframesSelector(rule)) {
customCssText += p + ';\n';
}
}
Expand Down Expand Up @@ -181,6 +188,46 @@
rule.cssText = output;
},

// Apply keyframe transformations to the cssText of a given rule. The
// keyframeTransforms object is a map of keyframe names to transformer
// functions which take in cssText and spit out transformed cssText.
applyKeyframeTransforms: function(rule, keyframeTransforms) {
var input = rule.cssText;
var output = rule.cssText;
if (rule.hasAnimations == null) {
// Cache whether or not the rule has any animations to begin with:
rule.hasAnimations = this.rx.ANIMATION_MATCH.test(input);
}
// If there are no animations referenced, we can skip transforms:
if (rule.hasAnimations) {
var transform;
// If we haven't transformed this rule before, we iterate over all
// transforms:
if (rule.keyframeNamesToTransform == null) {
rule.keyframeNamesToTransform = [];
for (var keyframe in keyframeTransforms) {
transform = keyframeTransforms[keyframe];
output = transform(input);
// If the transform actually changed the CSS text, we cache the
// transform name for future use:
if (input !== output) {
input = output;
rule.keyframeNamesToTransform.push(keyframe);
}
}
} else {
// If we already have a list of keyframe names that apply to this
// rule, we apply only those keyframe name transforms:
for (var i = 0; i < rule.keyframeNamesToTransform.length; ++i) {
transform = keyframeTransforms[rule.keyframeNamesToTransform[i]];
input = transform(input);
}
output = input;
}
}
rule.cssText = output;
},

// Test if the rules in these styles matches the given `element` and if so,
// collect any custom properties into `props`.
propertyDataFromStyles: function(styles, element) {
Expand Down Expand Up @@ -256,15 +303,60 @@
hostSelector;
var hostRx = new RegExp(this.rx.HOST_PREFIX + rxHostSelector +
this.rx.HOST_SUFFIX);
var keyframeTransforms =
this._elementKeyframeTransforms(element, scopeSelector);
return styleTransformer.elementStyles(element, function(rule) {
self.applyProperties(rule, properties);
if (rule.cssText && !nativeShadow) {
if (!nativeShadow &&
!Polymer.StyleUtil.isKeyframesSelector(rule) &&
rule.cssText) {
// NOTE: keyframe transforms only scope munge animation names, so it
// is not necessary to apply them in ShadowDOM.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a reminder to hopefully lessen confusion in the future.

self.applyKeyframeTransforms(rule, keyframeTransforms);
self._scopeSelector(rule, hostRx, hostSelector,
element._scopeCssViaAttr, scopeSelector);
}
});
},

_elementKeyframeTransforms: function(element, scopeSelector) {
var keyframesRules = element._styles._keyframes;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this keyframes specific handling should be factored into a separate function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

var keyframeTransforms = {};
if (!nativeShadow) {
// For non-ShadowDOM, we transform all known keyframes rules in
// advance for the current scope. This allows us to catch keyframes
// rules that appear anywhere in the stylesheet:
for (var i = 0, keyframesRule = keyframesRules[i];
i < keyframesRules.length;
keyframesRule = keyframesRules[++i]) {
this._scopeKeyframes(keyframesRule, scopeSelector);
keyframeTransforms[keyframesRule.keyframesName] =
this._keyframesRuleTransformer(keyframesRule);
}
}
return keyframeTransforms;
},

// Generate a factory for transforming a chunk of CSS text to handle a
// particular scoped keyframes rule.
_keyframesRuleTransformer: function(keyframesRule) {
return function(cssText) {
return cssText.replace(
keyframesRule.keyframesNameRx,
keyframesRule.transformedKeyframesName);
};
},

// Transforms `@keyframes` names to be unique for the current host.
// Example: @keyframes foo-anim -> @keyframes foo-anim-x-foo-0
_scopeKeyframes: function(rule, scopeId) {
rule.keyframesNameRx = new RegExp(rule.keyframesName, 'g');
rule.transformedKeyframesName = rule.keyframesName + '-' + scopeId;
rule.transformedSelector = rule.transformedSelector || rule.selector;
rule.selector = rule.transformedSelector.replace(
rule.keyframesName, rule.transformedKeyframesName);
},

// Strategy: x scope shim a selector e.g. to scope `.x-foo-42` (via classes):
// non-host selector: .a.x-foo -> .x-foo-42 .a.x-foo
// host selector: x-foo.wide -> x-foo.x-foo-42.wide
Expand Down Expand Up @@ -359,6 +451,7 @@
// var(--a, fallback-literal(with-one-nested-parentheses))
VAR_MATCH: /(^|\W+)var\([\s]*([^,)]*)[\s]*,?[\s]*((?:[^,)]*)|(?:[^;]*\([^;)]*\)))[\s]*?\)/gi,
VAR_CAPTURE: /\([\s]*(--[^,\s)]*)(?:,[\s]*(--[^,\s)]*))?(?:\)|,)/gi,
ANIMATION_MATCH: /(animation\s*:)|(animation-name\s*:)/,
IS_VAR: /^--/,
BRACKETED: /\{[^}]*\}/g,
HOST_PREFIX: '(?:^|[^.#[:])',
Expand Down
8 changes: 6 additions & 2 deletions src/lib/style-transformer.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,12 @@
// transforms a css rule to a scoped rule.
_transformRule: function(rule, transformer, scope, hostScope) {
var p$ = rule.selector.split(COMPLEX_SELECTOR_SEP);
for (var i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
p$[i] = transformer.call(this, p, scope, hostScope);
// we want to skip transformation of rules that appear in keyframes,
// because they are keyframe selectors, not element selectors.
if (!styleUtil.isKeyframesSelector(rule)) {
for (var i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
p$[i] = transformer.call(this, p, scope, hostScope);
}
}
// NOTE: save transformedSelector for subsequent matching of elements
// against selectors (e.g. when calculating style properties)
Expand Down
29 changes: 21 additions & 8 deletions src/lib/style-util.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@
rules = this.parser.parse(rules);
}
if (callback) {
this.forEachStyleRule(rules, callback);
this.forEachRule(rules, callback);
}
return this.parser.stringify(rules, preserveProperties);
},

forRulesInStyles: function(styles, callback) {
forRulesInStyles: function(styles, styleRuleCallback, keyframesRuleCallback) {
if (styles) {
for (var i=0, l=styles.length, s; (i<l) && (s=styles[i]); i++) {
this.forEachStyleRule(this.rulesForStyle(s), callback);
this.forEachRule(
this.rulesForStyle(s),
styleRuleCallback,
keyframesRuleCallback);
}
}
},
Expand All @@ -44,21 +47,31 @@
return style.__cssRules;
},

forEachStyleRule: function(node, callback) {
// Tests if a rule is a keyframes selector, which looks almost exactly
// like a normal selector but is not (it has nothing to do with scoping
// for example).
isKeyframesSelector: function(rule) {
return rule.parent &&
rule.parent.type === this.ruleTypes.KEYFRAMES_RULE;
},

forEachRule: function(node, styleRuleCallback, keyframesRuleCallback) {
if (!node) {
return;
}
var skipRules = false;
if (node.type === this.ruleTypes.STYLE_RULE) {
callback(node);
} else if (node.type === this.ruleTypes.KEYFRAMES_RULE ||
node.type === this.ruleTypes.MIXIN_RULE) {
styleRuleCallback(node);
} else if (keyframesRuleCallback &&
node.type === this.ruleTypes.KEYFRAMES_RULE) {
keyframesRuleCallback(node);
} else if (node.type === this.ruleTypes.MIXIN_RULE) {
skipRules = true;
}
var r$ = node.rules;
if (r$ && !skipRules) {
for (var i=0, l=r$.length, r; (i<l) && (r=r$[i]); i++) {
this.forEachStyleRule(r, callback);
this.forEachRule(r, styleRuleCallback, keyframesRuleCallback);
}
}
},
Expand Down
60 changes: 60 additions & 0 deletions test/smoke/keyframes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!doctype html>
<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<script src="../../../webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="../../polymer.html">

<body>
<style is="custom-style">
:root {
--color: blue;
--anim-color: red;
}

.alternative {
--color: green;
--anim-color: blue;
}
</style>

<dom-module id="test-keyframes">
<template><style>
:host {
display: block;
color: var(--color);
animation: foo 3s;
height: 20px;
}

@keyframes foo {
0% {
background: var(--anim-color);
}

100% {
background: yellow;
}
}
</style><content></content></template>
<script>
Polymer({
is: 'test-keyframes'
});

</script>
</dom-module>

<p>Text should be the color blue. Background should animate from the color red to the color yellow, and then become transparent.</p>
<test-keyframes>red</test-keyframes>

<p>Text should be the color green. Background should animate from the color blue to the color yellow, and then become transparent.</p>
<test-keyframes class="alternative">blue</test-keyframes>

</body>
Loading