UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

411 lines (345 loc) 12.7 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2011 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Martin Wittemann (martinwittemann) ************************************************************************ */ /** * This class is responsible for applying CSS3 animations to plain DOM elements. * * The implementation is mostly a cross-browser wrapper for applying the * animations, including transforms. If the browser does not support * CSS animations, but you have set a keep frame, the keep frame will be applied * immediately, thus making the animations optional. * * The API aligns closely to the spec wherever possible. * * http://www.w3.org/TR/css3-animations/ * * {@link qx.bom.element.Animation} is the class, which takes care of the * feature detection for CSS animations and decides which implementation * (CSS or JavaScript) should be used. Most likely, this implementation should * be the one to use. */ qx.Bootstrap.define("qx.bom.element.AnimationCss", { statics : { // initialization __sheet : null, __rulePrefix : "Anni", __id : 0, /** Static map of rules */ __rules : {}, /** The used keys for transforms. */ __transitionKeys : { "scale": true, "rotate" : true, "skew" : true, "translate" : true }, /** Map of cross browser CSS keys. */ __cssAnimationKeys : qx.core.Environment.get("css.animation"), /** * This is the main function to start the animation in reverse mode. * For further details, take a look at the documentation of the wrapper * {@link qx.bom.element.Animation}. * @param el {Element} The element to animate. * @param desc {Map} Animation description. * @param duration {Integer?} The duration of the animation which will * override the duration given in the description. * @return {qx.bom.element.AnimationHandle} The handle. */ animateReverse : function(el, desc, duration) { return this._animate(el, desc, duration, true); }, /** * This is the main function to start the animation. For further details, * take a look at the documentation of the wrapper * {@link qx.bom.element.Animation}. * @param el {Element} The element to animate. * @param desc {Map} Animation description. * @param duration {Integer?} The duration of the animation which will * override the duration given in the description. * @return {qx.bom.element.AnimationHandle} The handle. */ animate : function(el, desc, duration) { return this._animate(el, desc, duration, false); }, /** * Internal method to start an animation either reverse or not. * {@link qx.bom.element.Animation}. * @param el {Element} The element to animate. * @param desc {Map} Animation description. * @param duration {Integer?} The duration of the animation which will * override the duration given in the description. * @param reverse {Boolean} <code>true</code>, if the animation should be * reversed. * @return {qx.bom.element.AnimationHandle} The handle. */ _animate : function(el, desc, duration, reverse) { this.__normalizeDesc(desc); // debug validation if (qx.core.Environment.get("qx.debug")) { this.__validateDesc(desc); } // reverse the keep property if the animation is reverse as well var keep = desc.keep; if (keep != null && (reverse || (desc.alternate && desc.repeat % 2 == 0))) { keep = 100 - keep; } if (!this.__sheet) { this.__sheet = qx.bom.Stylesheet.createElement(); } var keyFrames = desc.keyFrames; if (duration == undefined) { duration = desc.duration; } // if animations are supported if (this.__cssAnimationKeys != null) { var name = this.__addKeyFrames(keyFrames, reverse); var style = name + " " + duration + "ms " + desc.timing + " " + (desc.delay ? desc.delay + "ms " : "") + desc.repeat + " " + (desc.alternate ? "alternate" : ""); qx.bom.Event.addNativeListener(el, this.__cssAnimationKeys["start-event"], this.__onAnimationStart); qx.bom.Event.addNativeListener(el, this.__cssAnimationKeys["iteration-event"], this.__onAnimationIteration); qx.bom.Event.addNativeListener(el, this.__cssAnimationKeys["end-event"], this.__onAnimationEnd); if (qx.core.Environment.get("qx.debug")) { if (qx.bom.element.Style.get(el, "display") == "none") { qx.log.Logger.warn(el, "Some browsers will not animate elements with display==none"); } } el.style[qx.lang.String.camelCase(this.__cssAnimationKeys["name"])] = style; // use the fill mode property if available and suitable if (keep && keep == 100 && this.__cssAnimationKeys["fill-mode"]) { el.style[this.__cssAnimationKeys["fill-mode"]] = "forwards"; } } var animation = new qx.bom.element.AnimationHandle(); animation.desc = desc; animation.el = el; animation.keep = keep; el.$$animation = animation; // additional transform keys if (desc.origin != null) { qx.bom.element.Transform.setOrigin(el, desc.origin); } // fallback for browsers not supporting animations if (this.__cssAnimationKeys == null) { window.setTimeout(function() { qx.bom.element.AnimationCss.__onAnimationEnd({target: el}); }, 0); } return animation; }, /** * Handler for the animation start. * @param e {Event} The native event from the browser. */ __onAnimationStart : function(e) { if (e.target.$$animation) { e.target.$$animation.emit("start", e.target); } }, /** * Handler for the animation iteration. * @param e {Event} The native event from the browser. */ __onAnimationIteration : function(e) { // It could happen that an animation end event is fired before an // animation iteration appears [BUG #6928] if (e.target != null && e.target.$$animation != null) { e.target.$$animation.emit("iteration", e.target); } }, /** * Handler for the animation end. * @param e {Event} The native event from the browser. */ __onAnimationEnd : function(e) { var el = e.target; var animation = el.$$animation; // ignore events when already cleaned up if (!animation) { return; } var desc = animation.desc; if (qx.bom.element.AnimationCss.__cssAnimationKeys != null) { // reset the styling var key = qx.lang.String.camelCase( qx.bom.element.AnimationCss.__cssAnimationKeys["name"] ); el.style[key] = ""; qx.bom.Event.removeNativeListener( el, qx.bom.element.AnimationCss.__cssAnimationKeys["name"], qx.bom.element.AnimationCss.__onAnimationEnd ); } if (desc.origin != null) { qx.bom.element.Transform.setOrigin(el, ""); } qx.bom.element.AnimationCss.__keepFrame(el, desc.keyFrames[animation.keep]); el.$$animation = null; animation.el = null; animation.ended = true; animation.emit("end", el); }, /** * Helper method which takes an element and a key frame description and * applies the properties defined in the given frame to the element. This * method is used to keep the state of the animation. * @param el {Element} The element to apply the frame to. * @param endFrame {Map} The description of the end frame, which is basically * a map containing CSS properties and values including transforms. */ __keepFrame : function(el, endFrame) { // keep the element at this animation step var transforms; for (var style in endFrame) { if (style in qx.bom.element.AnimationCss.__transitionKeys) { if (!transforms) { transforms = {}; } transforms[style] = endFrame[style]; } else { el.style[qx.lang.String.camelCase(style)] = endFrame[style]; } } // transform keeping if (transforms) { qx.bom.element.Transform.transform(el, transforms); } }, /** * Preprocessing of the description to make sure every necessary key is * set to its default. * @param desc {Map} The description of the animation. */ __normalizeDesc : function(desc) { if (!desc.hasOwnProperty("alternate")) { desc.alternate = false; } if (!desc.hasOwnProperty("keep")) { desc.keep = null; } if (!desc.hasOwnProperty("repeat")) { desc.repeat = 1; } if (!desc.hasOwnProperty("timing")) { desc.timing = "linear"; } if (!desc.hasOwnProperty("origin")) { desc.origin = null; } }, /** * Debugging helper to validate the description. * @signature function(desc) * @param desc {Map} The description of the animation. */ __validateDesc : qx.core.Environment.select("qx.debug", { "true" : function(desc) { var possibleKeys = [ "origin", "duration", "keep", "keyFrames", "delay", "repeat", "timing", "alternate" ]; // check for unknown keys for (var name in desc) { if (!(possibleKeys.indexOf(name) != -1)) { qx.Bootstrap.warn("Unknown key '" + name + "' in the animation description."); } }; if (desc.keyFrames == null) { qx.Bootstrap.warn("No 'keyFrames' given > 0"); } else { // check the key frames for (var pos in desc.keyFrames) { if (pos < 0 || pos > 100) { qx.Bootstrap.warn("Keyframe position needs to be between 0 and 100"); } } } }, "default" : null }), /** * Helper to add the given frames to an internal CSS stylesheet. It parses * the description and adds the key frames to the sheet. * @param frames {Map} A map of key frames that describe the animation. * @param reverse {Boolean} <code>true</code>, if the key frames should * be added in reverse order. * @return {String} The generated name of the keyframes rule. */ __addKeyFrames : function(frames, reverse) { var rule = ""; // for each key frame for (var position in frames) { rule += (reverse ? -(position - 100) : position) + "% {"; var frame = frames[position]; var transforms; // each style for (var style in frame) { if (style in this.__transitionKeys) { if (!transforms) { transforms = {}; } transforms[style] = frame[style]; } else { var propName = qx.bom.Style.getPropertyName(style); var prefixed = (propName !== null) ? qx.bom.Style.getCssName(propName) : ""; rule += (prefixed || style) + ":" + frame[style] + ";"; } } // transform handling if (transforms) { rule += qx.bom.element.Transform.getCss(transforms); } rule += "} "; } // cached shorthand if (this.__rules[rule]) { return this.__rules[rule]; } var name = this.__rulePrefix + this.__id++; var selector = this.__cssAnimationKeys["keyframes"] + " " + name; qx.bom.Stylesheet.addRule(this.__sheet, selector, rule); this.__rules[rule] = name; return name; }, /** * Internal helper to reset the cache. */ __clearCache: function() { this.__id = 0; if (this.__sheet) { this.__sheet.ownerNode.remove(); this.__sheet = null; this.__rules = {}; } } }, defer : function(statics) { // iOS 8 seems to stumble over the old sheet object on tab // changes or leaving the browser [BUG #8986] if (qx.core.Environment.get("os.name") === "ios" && parseInt(qx.core.Environment.get("os.version")) >= 8 ) { document.addEventListener("visibilitychange", function() { if (!document.hidden) { statics.__clearCache(); } }, false); } } });