diff --git a/src/apply-preserving-inline-style.js b/src/apply-preserving-inline-style.js index 795be364..4002a2f5 100644 --- a/src/apply-preserving-inline-style.js +++ b/src/apply-preserving-inline-style.js @@ -14,6 +14,28 @@ (function(scope, testing) { + var SVG_TRANSFORM_PROP = '_webAnimationsUpdateSvgTransformAttr'; + + /** + * IE/Edge do not support `transform` styles for SVG elements. Instead, + * `transform` attribute can be animated with some restrictions. + * See https://connect.microsoft.com/IE/feedback/details/811744/ie11-bug-with-implementation-of-css-transforms-in-svg, + * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/1173754/, + * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/101242/, etc. + * The same problem is exhibited by pre-Chrome Android browsers (ICS). + * Unfortunately, there's no easy way to feature-detect it. + */ + function updateSvgTransformAttr(window, element) { + if (!element.namespaceURI || element.namespaceURI.indexOf('/svg') == -1) { + return false; + } + if (!(SVG_TRANSFORM_PROP in window)) { + window[SVG_TRANSFORM_PROP] = + /Trident|MSIE|IEMobile|Edge|Android 4/i.test(window.navigator.userAgent); + } + return window[SVG_TRANSFORM_PROP]; + } + var styleAttributes = { cssText: 1, length: 1, @@ -44,6 +66,7 @@ WEB_ANIMATIONS_TESTING && console.assert(!(element.style instanceof AnimatedCSSStyleDeclaration), 'Element must not already have an animated style attached.'); + this._element = element; // Stores the inline style of the element on its behalf while the // polyfill uses the element's inline style to simulate web animations. // This is needed to fake regular inline style CSSOM access on the element. @@ -51,6 +74,8 @@ this._style = element.style; this._length = 0; this._isAnimatedProperty = {}; + this._updateSvgTransformAttr = updateSvgTransformAttr(window, element); + this._savedTransformAttr = null; // Copy the inline style contents over to the surrogate. for (var i = 0; i < this._style.length; i++) { @@ -110,9 +135,30 @@ _set: function(property, value) { this._style[property] = value; this._isAnimatedProperty[property] = true; + if (this._updateSvgTransformAttr && + scope.unprefixedPropertyName(property) == 'transform') { + // On IE/Edge, also set SVG element's `transform` attribute to 2d + // matrix of the transform. The `transform` style does not work, but + // `transform` attribute can be used instead. + // Notice, if the platform indeed supports SVG/CSS transforms the CSS + // declaration is supposed to override the attribute. + if (this._savedTransformAttr == null) { + this._savedTransformAttr = this._element.getAttribute('transform'); + } + this._element.setAttribute('transform', scope.transformToSvgMatrix(value)); + } }, _clear: function(property) { this._style[property] = this._surrogateStyle[property]; + if (this._updateSvgTransformAttr && + scope.unprefixedPropertyName(property) == 'transform') { + if (this._savedTransformAttr) { + this._element.setAttribute('transform', this._savedTransformAttr); + } else { + this._element.removeAttribute('transform'); + } + this._savedTransformAttr = null; + } delete this._isAnimatedProperty[property]; }, }; @@ -185,7 +231,9 @@ } }; - if (WEB_ANIMATIONS_TESTING) + if (WEB_ANIMATIONS_TESTING) { testing.ensureStyleIsPatched = ensureStyleIsPatched; + testing.updateSvgTransformAttr = updateSvgTransformAttr; + } })(webAnimations1, webAnimationsTesting); diff --git a/src/matrix-decomposition.js b/src/matrix-decomposition.js index 4172513d..aa9cfe76 100644 --- a/src/matrix-decomposition.js +++ b/src/matrix-decomposition.js @@ -435,5 +435,6 @@ scope.dot = dot; scope.makeMatrixDecomposition = makeMatrixDecomposition; + scope.transformListToMatrix = convertToMatrix; })(webAnimations1, webAnimationsTesting); diff --git a/src/property-names.js b/src/property-names.js index c52d990c..f106c066 100644 --- a/src/property-names.js +++ b/src/property-names.js @@ -14,13 +14,15 @@ (function(scope, testing) { - var aliased = {}; + var prefixed = {}; + var unprefixed = {}; function alias(name, aliases) { aliases.concat([name]).forEach(function(candidate) { if (candidate in document.documentElement.style) { - aliased[name] = candidate; + prefixed[name] = candidate; } + unprefixed[candidate] = name; }); } alias('transform', ['webkitTransform', 'msTransform']); @@ -29,7 +31,10 @@ alias('perspectiveOrigin', ['webkitPerspectiveOrigin']); scope.propertyName = function(property) { - return aliased[property] || property; + return prefixed[property] || property; + }; + scope.unprefixedPropertyName = function(property) { + return unprefixed[property] || property; }; })(webAnimations1, webAnimationsTesting); diff --git a/src/transform-handler.js b/src/transform-handler.js index c4482961..c6b4d934 100644 --- a/src/transform-handler.js +++ b/src/transform-handler.js @@ -256,6 +256,19 @@ scope.addPropertiesHandler(parseTransform, mergeTransforms, ['transform']); + scope.transformToSvgMatrix = function(string) { + // matrix( ) + var mat = scope.transformListToMatrix(parseTransform(string)); + return 'matrix(' + + numberToLongString(mat[0]) + ' ' + // + numberToLongString(mat[1]) + ' ' + // + numberToLongString(mat[4]) + ' ' + // + numberToLongString(mat[5]) + ' ' + // + numberToLongString(mat[12]) + ' ' + // + numberToLongString(mat[13]) + // + ')'; + }; + if (WEB_ANIMATIONS_TESTING) testing.parseTransform = parseTransform; diff --git a/test/js/apply-preserving-inline-style.js b/test/js/apply-preserving-inline-style.js index fe080e6b..a8490d20 100644 --- a/test/js/apply-preserving-inline-style.js +++ b/test/js/apply-preserving-inline-style.js @@ -4,9 +4,15 @@ suite('apply-preserving-inline-style', function() { ensureStyleIsPatched(this.element); this.style = this.element.style; document.documentElement.appendChild(this.element); + this.svgContainer = document.createElementNS( + 'http://www.w3.org/2000/svg', 'svg'); + document.documentElement.appendChild(this.svgContainer); + delete window._webAnimationsUpdateSvgTransformAttr; }); teardown(function() { document.documentElement.removeChild(this.element); + document.documentElement.removeChild(this.svgContainer); + delete window._webAnimationsUpdateSvgTransformAttr; }); test('Style is patched', function() { @@ -69,4 +75,119 @@ suite('apply-preserving-inline-style', function() { this.style.cssText = 'top: 0px'; assert.equal(this.style.length, 1); }); + test('Detect SVG transform compatibility', function() { + var element = document.createElement('div'); + var svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + function check(userAgent, shouldUpdateSvgTransformAttr) { + var win = {navigator: {userAgent: userAgent}}; + // Non-SVG element is never updated. + assert.equal(updateSvgTransformAttr(win, element), false); + // SVG element may be updated as tested. + assert.equal(updateSvgTransformAttr(win, svgElement), + shouldUpdateSvgTransformAttr); + } + // Unknown data: assume that transforms supported. + check('', false); + // Chrome: transforms supported. + check('Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E)' + + ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.20' + + ' Mobile Safari/537.36', + false); + // Safari: transforms supported. + check('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) ' + + 'AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 ' + + 'Safari/7046A194A', + false); + // Firefox: transforms supported. + check('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) ' + + 'Gecko/20100101 Firefox/40.1', + false); + // IE: transforms are NOT supported. + check('Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 7.0;' + + ' InfoPath.3; .NET CLR 3.1.40767; Trident/6.0; en-IN)', + true); + // Edge: transforms are NOT supported. + check('Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36' + + ' (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36' + + ' Edge/12.10136', + true); + // ICS Android: transforms are NOT supported. + check('Mozilla/5.0 (Linux; U; Android 4.0.4; en-gb; MZ604 Build/I.7.1-45)' + + ' AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30', + true); + }); + test('Set and clear transform', function() { + // This is not an SVG element, so CSS transform support is not consulted. + window._webAnimationsUpdateSvgTransformAttr = true; + // Set. + this.element.style._set('transform', 'translate(10px, 10px) scale(2)'); + assert.equal(getComputedStyle(this.element).transform, + 'matrix(2, 0, 0, 2, 10, 10)'); + assert.equal(this.element.hasAttribute('transform'), false); + // Clear. + this.element.style._clear('transform'); + assert.equal(getComputedStyle(this.element).transform, 'none'); + assert.equal(this.element.hasAttribute('transform'), false); + }); + test('Set and clear supported transform on SVG element', function() { + window._webAnimationsUpdateSvgTransformAttr = false; + var svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + ensureStyleIsPatched(svgElement); + this.svgContainer.appendChild(svgElement); + // Set. + svgElement.style._set('transform', 'translate(10px, 10px) scale(2)'); + assert.equal(getComputedStyle(svgElement).transform, + 'matrix(2, 0, 0, 2, 10, 10)'); + assert.equal(svgElement.hasAttribute('transform'), false); + // Clear. + svgElement.style._clear('transform'); + assert.equal(getComputedStyle(svgElement).transform, 'none'); + assert.equal(svgElement.hasAttribute('transform'), false); + }); + test('Set and clear transform CSS property not supported on SVG element', function() { + window._webAnimationsUpdateSvgTransformAttr = true; + var svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + ensureStyleIsPatched(svgElement); + this.svgContainer.appendChild(svgElement); + // Set. + svgElement.style._set('transform', 'translate(10px, 10px) scale(2)'); + assert.equal(getComputedStyle(svgElement).transform, + 'matrix(2, 0, 0, 2, 10, 10)'); + assert.equal(svgElement.getAttribute('transform'), + 'matrix(2 0 0 2 10 10)'); + // Clear. + svgElement.style._clear('transform'); + assert.equal(getComputedStyle(svgElement).transform, 'none'); + assert.equal(svgElement.getAttribute('transform'), null); + }); + test('Set and clear prefixed transform CSS property not supported on SVG element', function() { + window._webAnimationsUpdateSvgTransformAttr = true; + var svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + ensureStyleIsPatched(svgElement); + this.svgContainer.appendChild(svgElement); + // Set. + svgElement.style._set('msTransform', 'translate(10px, 10px) scale(2)'); + assert.equal(svgElement.getAttribute('transform'), + 'matrix(2 0 0 2 10 10)'); + // Clear. + svgElement.style._clear('msTransform'); + assert.equal(svgElement.getAttribute('transform'), null); + }); + test('Restore transform CSS property not supported on SVG element', function() { + window._webAnimationsUpdateSvgTransformAttr = true; + var svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + svgElement.setAttribute('transform', 'matrix(2 0 0 2 0 0)'); + ensureStyleIsPatched(svgElement); + this.svgContainer.appendChild(svgElement); + // Set. + svgElement.style._set('transform', 'translate(10px, 10px) scale(2)'); + assert.equal(getComputedStyle(svgElement).transform, + 'matrix(2, 0, 0, 2, 10, 10)'); + assert.equal(svgElement.getAttribute('transform'), + 'matrix(2 0 0 2 10 10)'); + // Clear. + svgElement.style._clear('transform'); + assert.equal(getComputedStyle(svgElement).transform, 'none'); + assert.equal(svgElement.getAttribute('transform'), 'matrix(2 0 0 2 0 0)'); + }); });