├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE.txt ├── README.md ├── demo ├── app.js ├── index.html └── webpack.config.dev.js ├── karma.conf.js ├── lib └── bundle.js ├── package.json ├── src ├── Animation.js ├── CSSProperty.js ├── Transition.js ├── easings.js ├── index.js ├── index.spec.js ├── utils.js └── utils.spec.js ├── test └── test.html ├── webpack.config.dev.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "optional": ["runtime"], 4 | "env": { 5 | "production": { 6 | "optional": [] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf # doesn't work yet 7 | charset = utf-8 8 | trim_trailing_whitespace = true # doesn't work yet 9 | insert_final_newline = true # doesn't work yet 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | node_modules/* 3 | webpack.* 4 | karma.* 5 | demo/webpack.* 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "ecmaFeatures": { 8 | "classes": true, 9 | "jsx": true, 10 | "modules": true 11 | }, 12 | "plugins": [ 13 | "react" 14 | ], 15 | "rules": { 16 | "quotes": [2, "single"], 17 | "eol-last": [0], 18 | "no-mixed-requires": [0], 19 | "no-underscore-dangle": [0], 20 | "space-after-keywords": [1, "always"], 21 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 22 | "react/jsx-uses-react": 2, 23 | "react/jsx-no-undef": 2, 24 | "react/self-closing-comp": 1, 25 | "react/wrap-multilines": 1, 26 | "react/jsx-quotes": [1, "single", "avoid-escape"], 27 | "react/jsx-uses-vars": 2, 28 | "react/react-in-jsx-scope": 2, 29 | "react/no-did-mount-set-state": 2, 30 | "react/no-did-update-set-state": 2, 31 | "react/jsx-curly-spacing": [2, "always"] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### SublimeText ### 2 | *.sublime-workspace 3 | 4 | ### OSX ### 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear on external disk 14 | .Spotlight-V100 15 | .Trashes 16 | 17 | ### Windows ### 18 | # Windows image file caches 19 | Thumbs.db 20 | ehthumbs.db 21 | 22 | # Folder config file 23 | Desktop.ini 24 | 25 | # Recycle Bin used on file shares 26 | $RECYCLE.BIN/ 27 | 28 | # App specific 29 | 30 | coverage 31 | node_modules 32 | bower_components 33 | .tmp 34 | npm-debug.log 35 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Cruft 2 | *.sublime-workspace 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | Icon 7 | ._* 8 | .Spotlight-V100 9 | .Trashes 10 | Thumbs.db 11 | ehthumbs.db 12 | Desktop.ini 13 | $RECYCLE.BIN/ 14 | .tmp 15 | npm-debug.log 16 | 17 | # Code / build 18 | coverage 19 | node_modules 20 | bower_components 21 | demo 22 | test 23 | karma* 24 | webpack* 25 | .eslint* 26 | .editor* 27 | .travis* 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Formidable Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Move it 2 | 3 | ![](https://cloud.githubusercontent.com/assets/333073/9832219/6e0ff88e-5974-11e5-94c4-1efcac24f657.jpg) 4 | 5 | ### Description 6 | Javascript animation and transition utility. Uses CSS under the hood. Useful for React performant animations. 7 | 8 | ### Features 9 | - Supports animations and transitions with the same API 10 | - Animations/transitions definitions are in plain javascript. This means you can create functions that create animations. 11 | - Cleans up everything on completion 12 | - Automatically prefixed 13 | - Supports only setting `from` or `to` 14 | 15 | ### Installation 16 | ``` 17 | npm i --save moveit 18 | ``` 19 | 20 | ### Demo 21 | `npm start` then visit http://127.0.0.1:3000 22 | 23 | ### Usage 24 | ```js 25 | import { transition, animation } from 'moveit'; 26 | 27 | transition(node, definition, override?, callback?); 28 | ``` 29 | `definition` is an object with `keyframes` and standard CSS properties for 30 | animation / transition. `keyframes` takes percentages (or `from` and `to`) as 31 | keys and CSS maps as values. 32 | 33 | Possible properties : 34 | - `delay` 35 | - `duration` 36 | - `ease` 37 | 38 | Animation only : 39 | - `iterationCount` 40 | - `direction` 41 | - `fillMode` 42 | 43 | ### Easing 44 | You can pass a string corresponding to one of these [easings](https://github.com/jide/moveit/blob/master/src/easings.js), or use a custom function using CSS `cubic-bezier()` syntax. 45 | 46 | ### Example 47 | ```js 48 | import { transition } from 'moveit'; 49 | 50 | const definition = { 51 | keyframes: { 52 | from: { 53 | opacity: '0' 54 | }, 55 | to: { 56 | opacity: '1' 57 | } 58 | }, 59 | ease: 'ease-in', 60 | duration: '1s' 61 | }; 62 | 63 | transition(node, definition); 64 | ``` 65 | 66 | ### You can use transition or animation 67 | API is the same, but with animation you can add intermediate keyframes. 68 | ```js 69 | import { animation } from 'moveit'; 70 | 71 | const definition = { 72 | keyframes: { 73 | from: { 74 | opacity: '0' 75 | }, 76 | '50%': { 77 | opacity: '.3' 78 | } 79 | to: { 80 | opacity: '1' 81 | } 82 | }, 83 | ease: 'ease-in', 84 | duration: '1s' 85 | }; 86 | 87 | animation(node, definition); 88 | ``` 89 | 90 | ### Callback on end 91 | ```js 92 | transition(node, definition, () => console.log('done !')); 93 | ``` 94 | 95 | ### Override definition 96 | ```js 97 | const override = { 98 | ease: 'ease-out' 99 | }; 100 | 101 | transition(node, definition, override); 102 | ``` 103 | 104 | ### With React 105 | ```js 106 | // You could import this from an animations.js file, pass it through props... 107 | const openMenu = props => { 108 | return { 109 | keyframes: { 110 | to: { 111 | transform: `translate3d(${props.left}px, 0, 0)` 112 | } 113 | }, 114 | ease: 'ease-out-cubic', 115 | duration: '1s' 116 | }; 117 | }; 118 | 119 | class Menu extends Component { 120 | handleClick() { 121 | transition(React.findDOMNode(this.refs.animated), openMenu({ left: this.props.menuWidth - window.innerWidth })); 122 | } 123 | 124 | render() { 125 | return ( 126 |
127 | 128 |
129 | ); 130 | } 131 | } 132 | ``` 133 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | import { transition } from '../src'; 2 | 3 | let node = document.createElement('div'); 4 | node.style.background = 'red'; 5 | node.style.width = '30px'; 6 | node.style.height = '30px'; 7 | 8 | document.body.appendChild(node); 9 | 10 | let definition = { 11 | keyframes: { 12 | from: { 13 | opacity: 0, 14 | transform: 'scale(0)' 15 | }, 16 | to: { 17 | opacity: 1, 18 | transform: 'scale(1)' 19 | } 20 | }, 21 | ease: 'ease-in', 22 | duration: '5s' 23 | }; 24 | 25 | transition(node, definition, () => console.log('done')); 26 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | Demo 10 | 11 | 12 | 17 | 18 | 19 | 22 |
23 |
24 | 25 | 26 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /demo/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /*globals __dirname:false */ 2 | "use strict"; 3 | 4 | var webpack = require("webpack"); 5 | 6 | module.exports = { 7 | devServer: { 8 | contentBase: __dirname, 9 | noInfo: false 10 | }, 11 | output: { 12 | path: __dirname, 13 | filename: "main.js" 14 | }, 15 | cache: true, 16 | devtool: "source-map", 17 | entry: { 18 | app: ["./demo/app.js"] 19 | }, 20 | stats: { 21 | colors: true, 22 | reasons: true 23 | }, 24 | resolve: { 25 | extensions: ["", ".js"] 26 | }, 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.js$/, 31 | exclude: [/node_modules/], 32 | loaders: ["babel-loader?optional[]=runtime&stage=0"] 33 | }, 34 | { 35 | test: /\.json$/, 36 | loaders: ['json'] 37 | } 38 | ] 39 | }, 40 | plugins: [ 41 | new webpack.NoErrorsPlugin() 42 | ] 43 | }; 44 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | browsers: ['Chrome'], 6 | files: [ 7 | 'src/**/*.spec.*' 8 | ], 9 | frameworks: ['jasmine'], 10 | reporters: ["spec"], 11 | preprocessors: { 12 | 'src/**/*.spec.*': ['webpack'] 13 | }, 14 | webpack: { 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.js$/, 19 | exclude: [/node_modules/], 20 | loader: "babel-loader?optional[]=runtime&stage=0" 21 | } 22 | ] 23 | }, 24 | node: { 25 | fs: 'empty' 26 | }, 27 | watch: true, 28 | plugins: [new webpack.SourceMapDevToolPlugin("[file].map")] 29 | }, 30 | webpackServer: { 31 | noInfo: true 32 | } 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /lib/bundle.js: -------------------------------------------------------------------------------- 1 | !function(t,e){for(var n in e)t[n]=e[n]}(exports,function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return t[r].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t,e){if("undefined"==typeof e||null===e)return null;for(var n=arguments.length,r=Array(n>2?n-2:0),i=2;n>i;i++)r[i-2]=arguments[i];var o=s.getArguments.apply(void 0,[t].concat(r)),u=a(o,2),f=u[0],l=u[1],v="transition"===t?new c["default"](e,f):new d["default"](e,f);return v.start(l),v}function i(t){for(var e=arguments.length,n=Array(e>1?e-1:0),i=1;e>i;i++)n[i-1]=arguments[i];return r.apply(void 0,["transition",t].concat(n))}function o(t){for(var e=arguments.length,n=Array(e>1?e-1:0),i=1;e>i;i++)n[i-1]=arguments[i];return r.apply(void 0,["animation",t].concat(n))}var a=n(31)["default"],u=n(7)["default"];Object.defineProperty(e,"__esModule",{value:!0}),e.transition=i,e.animation=o;var s=n(6),f=n(26),c=u(f),l=n(24),d=u(l)},function(t,e){var n=t.exports={version:"1.2.1"};"number"==typeof __e&&(__e=n)},function(t,e,n){var r=n(44)("wks"),i=n(9).Symbol;t.exports=function(t){return r[t]||(r[t]=i&&i[t]||(i||n(50))("Symbol."+t))}},function(t,e){t.exports={}},function(t,e,n){var r=n(5),i=n(19);t.exports=n(46)?function(t,e,n){return r.setDesc(t,e,i(1,n))}:function(t,e,n){return t[e]=n,t}},function(t,e){var n=Object;t.exports={create:n.create,getProto:n.getPrototypeOf,isEnum:{}.propertyIsEnumerable,getDesc:n.getOwnPropertyDescriptor,setDesc:n.defineProperty,setDescs:n.defineProperties,getKeys:n.keys,getNames:n.getOwnPropertyNames,getSymbols:n.getOwnPropertySymbols,each:[].forEach}},function(t,e,n){"use strict";function r(t,e){var n=[];for(var r in e){var i=e[r],o=[];for(var a in i){var u=a.replace(/([A-Z])/g,"-$1").toLowerCase(),s="number"==typeof i[a]?i[a]+"px":i[a];o.push(u+":"+s+";")}n.push(r+"{"+o.join("")+"}")}var f=n.join(""),c=b["default"].dash("animation").replace("animation","")+"keyframes";return"@"+c+" "+t+" {"+f+"}"}function i(t,e){var n=N.sheet.insertRule(r(t,e),N.sheet.cssRules.length);return N.sheet.rules[n]}function o(t){for(var e=0;e2?i-2:0),a=2;i>a;a++)o[a-2]=arguments[a];1===o.length?"function"==typeof o[0]?n=o[0]:r=o[0]:(r=o[0],n=o[1]),e=d(t,e),r=d(t,r);for(var u in r)e[u]=r[u];return[e,n]}var y=n(10)["default"],h=n(7)["default"];Object.defineProperty(e,"__esModule",{value:!0}),e.getAnimationText=r,e.insertAnimation=i,e.getKeyframesRuleIndex=o,e.applyStyle=a,e.removeTransition=u,e.removeAnimation=s,e.removeRule=f,e.addListener=c,e.removeListener=l,e.getNormalizedDefinition=d,e.getTransition=v,e.getArguments=p;var m=n(23),b=h(m),x=n(27),g=h(x),_=n(25),N=document.createElement("style");N.type="text/css",document.head.appendChild(N)},function(t,e){"use strict";e["default"]=function(t){return t&&t.__esModule?t:{"default":t}},e.__esModule=!0},function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,e){var n="undefined",r=t.exports=typeof window!=n&&window.Math==Math?window:typeof self!=n&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=r)},function(t,e,n){t.exports={"default":n(32),__esModule:!0}},function(t,e){"use strict";e["default"]=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},e.__esModule=!0},function(t,e,n){"use strict";var r=n(29)["default"];e["default"]=function(){function t(t,e){for(var n=0;n=e.length?{value:void 0,done:!0}:(t=r(e,n),this._i+=t.length,{value:t,done:!1})})},function(t,e,n){n(55);var r=n(3);r.NodeList=r.HTMLCollection=r.Array},function(t,e){t.exports=require("vendor-prefix")},function(t,e,n){"use strict";var r=n(12)["default"],i=n(11)["default"],o=n(7)["default"];Object.defineProperty(e,"__esModule",{value:!0});var a=n(23),u=o(a),s=n(6),f=0,c=function(){function t(e,n){i(this,t),this.type="animation",this.DOMNode=e,this.definition=n}return r(t,[{key:"start",value:function(t){var e=this;f++;var n="animator_"+f,r=s.insertAnimation(n,this.definition.keyframes),i=function c(n){n&&n.target!==e.DOMNode||(s.removeListener(e.type,e.DOMNode,c),s.removeAnimation(e.DOMNode),s.removeRule(r),"function"==typeof t&&t())},o={};o[u["default"].dash("animation-name")]=n;for(var a in this.definition)-1!==a.indexOf("animation-")&&(o[a]=this.definition[a]);return s.applyStyle(this.DOMNode,o),"0ms"===this.definition.duration?void i():void s.addListener(this.type,this.DOMNode,i)}}]),t}();e["default"]=c,t.exports=e["default"]},function(t,e,n){"use strict";function r(t,e){return t+e.charAt(0).toUpperCase()+e.substring(1)}var i=n(30)["default"];Object.defineProperty(e,"__esModule",{value:!0});var o,a;void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend?e.TRANSITIONEND_EVENT=o=["webkitTransitionEnd","transitionend"]:e.TRANSITIONEND_EVENT=o="transitionend",void 0===window.onanimationend&&void 0!==window.onwebkitanimationend?e.ANIMATIONEND_EVENT=a=["webkitAnimationEnd","animationend"]:e.ANIMATIONEND_EVENT=a="animationend";var u={animationIterationCount:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,stopOpacity:!0,strokeDashoffset:!0,strokeOpacity:!0,strokeWidth:!0},s=["Webkit","ms","Moz","O"];i(u).forEach(function(t){s.forEach(function(e){u[r(e,t)]=u[t]})}),e.isUnitlessNumber=u,e.TRANSITIONEND_EVENT=o,e.ANIMATIONEND_EVENT=a},function(t,e,n){"use strict";var r=n(12)["default"],i=n(11)["default"];Object.defineProperty(e,"__esModule",{value:!0});var o=n(6),a=function(){function t(e,n){i(this,t),this.type="transition",this.DOMNode=e,this.definition=n,this.transition=o.getTransition(n)}return r(t,[{key:"start",value:function(t){var e=this;this.transition.keyframes.from&&(o.applyStyle(this.DOMNode,this.transition.keyframes.from),this.DOMNode.offsetWidth+1);var n=function a(n){n&&n.target!==e.DOMNode||(o.removeListener(e.type,e.DOMNode,a),o.removeTransition(e.DOMNode),"function"==typeof t&&t())},r={};for(var i in this.transition)-1!==i.indexOf("transition-")&&(r[i]=this.transition[i]);for(var i in this.transition.keyframes.to)r[i]=this.transition.keyframes.to[i];return o.applyStyle(this.DOMNode,r),"0ms"===this.definition["transition-duration"]?void n():void o.addListener(this.type,this.DOMNode,n)}}]),t}();e["default"]=a,t.exports=e["default"]},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n={linear:"linear",ease:"ease","ease-in":"ease-in","ease-out":"ease-out","ease-in-out":"ease-in-out","ease-in-quad":"cubic-bezier(0.550, 0.085, 0.680, 0.530)","ease-in-cubic":"cubic-bezier(0.550, 0.055, 0.675, 0.190)","ease-in-quart":"cubic-bezier(0.895, 0.030, 0.685, 0.220)","ease-in-quint":"cubic-bezier(0.755, 0.050, 0.855, 0.060)","ease-in-sine":"cubic-bezier(0.470, 0.000, 0.745, 0.715)","ease-in-expo":"cubic-bezier(0.950, 0.050, 0.795, 0.035)","ease-in-circ":"cubic-bezier(0.600, 0.040, 0.980, 0.335)","ease-in-back":"cubic-bezier(0.600, -0.280, 0.735, 0.045)","ease-out-quad":"cubic-bezier(0.250, 0.460, 0.450, 0.940)","ease-out-cubic":"cubic-bezier(0.215, 0.610, 0.355, 1.000)","ease-out-quart":"cubic-bezier(0.165, 0.840, 0.440, 1.000)","ease-out-quint":"cubic-bezier(0.230, 1.000, 0.320, 1.000)","ease-out-sine":"cubic-bezier(0.390, 0.575, 0.565, 1.000)","ease-out-expo":"cubic-bezier(0.190, 1.000, 0.220, 1.000)","ease-out-circ":"cubic-bezier(0.075, 0.820, 0.165, 1.000)","ease-out-back":"cubic-bezier(0.175, 0.885, 0.320, 1.275)","ease-in-out-quad":"cubic-bezier(0.455, 0.030, 0.515, 0.955)","ease-in-out-cubic":"cubic-bezier(0.645, 0.045, 0.355, 1.000)","ease-in-out-quart":"cubic-bezier(0.770, 0.000, 0.175, 1.000)","ease-in-out-quint":"cubic-bezier(0.860, 0.000, 0.070, 1.000)","ease-in-out-sine":"cubic-bezier(0.445, 0.050, 0.550, 0.950)","ease-in-out-expo":"cubic-bezier(1.000, 0.000, 0.000, 1.000)","ease-in-out-circ":"cubic-bezier(0.785, 0.135, 0.150, 0.860)","ease-in-out-back":"cubic-bezier(0.680, -0.550, 0.265, 1.550)"};e["default"]=n,t.exports=e["default"]},function(t,e,n){t.exports={"default":n(33),__esModule:!0}},function(t,e,n){t.exports={"default":n(34),__esModule:!0}},function(t,e,n){t.exports={"default":n(35),__esModule:!0}},function(t,e,n){"use strict";var r=n(10)["default"],i=n(28)["default"];e["default"]=function(){function t(t,e){var n=[],i=!0,o=!1,a=void 0;try{for(var u,s=r(t);!(i=(u=s.next()).done)&&(n.push(u.value),!e||n.length!==e);i=!0);}catch(f){o=!0,a=f}finally{try{!i&&s["return"]&&s["return"]()}finally{if(o)throw a}}return n}return function(e,n){if(Array.isArray(e))return e;if(i(Object(e)))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),e.__esModule=!0},function(t,e,n){n(22),n(21),t.exports=n(53)},function(t,e,n){n(22),n(21),t.exports=n(54)},function(t,e,n){var r=n(5);t.exports=function(t,e,n){return r.setDesc(t,e,n)}},function(t,e,n){n(56),t.exports=n(1).Object.keys},function(t,e,n){var r=n(38);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},function(t,e,n){var r=n(14);t.exports=0 in Object("z")?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},function(t,e){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,e,n){"use strict";var r=n(5),i={};n(4)(i,n(2)("iterator"),function(){return this}),t.exports=function(t,e,o){t.prototype=r.create(i,{next:n(19)(1,o)}),n(20)(t,e+" Iterator")}},function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},function(t,e){t.exports=!0},function(t,e,n){t.exports=function(t,e){var r=n(15),i=(n(1).Object||{})[t]||Object[t],o={};o[t]=e(i),r(r.S+r.F*n(16)(function(){i(1)}),"Object",o)}},function(t,e,n){t.exports=n(4)},function(t,e,n){var r=n(9),i="__core-js_shared__",o=r[i]||(r[i]={});t.exports=function(t){return o[t]||(o[t]={})}},function(t,e,n){var r=n(47),i=n(8);t.exports=function(t){return function(e,n){var o,a,u=String(i(e)),s=r(n),f=u.length;return 0>s||s>=f?t?"":void 0:(o=u.charCodeAt(s),55296>o||o>56319||s+1===f||(a=u.charCodeAt(s+1))<56320||a>57343?t?u.charAt(s):o:t?u.slice(s,s+2):(o-55296<<10)+(a-56320)+65536)}}},function(t,e,n){t.exports=!n(16)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},function(t,e,n){var r=n(37),i=n(8);t.exports=function(t){return r(i(t))}},function(t,e,n){var r=n(8);t.exports=function(t){return Object(r(t))}},function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},function(t,e){t.exports=function(){}},function(t,e,n){var r=n(13),i=n(2)("iterator"),o=n(3);t.exports=n(1).getIteratorMethod=function(t){return void 0!=t?t[i]||t["@@iterator"]||o[r(t)]:void 0}},function(t,e,n){var r=n(36),i=n(52);t.exports=n(1).getIterator=function(t){var e=i(t);if("function"!=typeof e)throw TypeError(t+" is not iterable!");return r(e.call(t))}},function(t,e,n){var r=n(13),i=n(2)("iterator"),o=n(3);t.exports=n(1).isIterable=function(t){var e=Object(t);return i in e||"@@iterator"in e||o.hasOwnProperty(r(e))}},function(t,e,n){"use strict";var r=n(51),i=n(40),o=n(3),a=n(48);n(18)(Array,"Array",function(t,e){this._t=a(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,i(1)):"keys"==e?i(0,n):"values"==e?i(0,t[n]):i(0,[n,t[n]])},"values"),o.Arguments=o.Array,r("keys"),r("values"),r("entries")},function(t,e,n){var r=n(49);n(42)("keys",function(t){return function(e){return t(r(e))}})}])); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moveit", 3 | "version": "0.0.12", 4 | "description": "Move it", 5 | "main": "lib/bundle.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jide/moveit.git" 9 | }, 10 | "author": "Julien De Luca", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/jide/moveit/issues" 14 | }, 15 | "homepage": "https://github.com/jide/moveit", 16 | "scripts": { 17 | "clean": "rimraf lib", 18 | "build": "npm run clean && ./node_modules/webpack/bin/webpack.js -p", 19 | "server-dev": "webpack-dev-server --port 3000 --config demo/webpack.config.dev.js --colors --content-base demo", 20 | "start": "npm run server-dev", 21 | "test": "node node_modules/karma/bin/karma start karma.conf.js", 22 | "test:watch": "./node_modules/karma/bin/karma start karma.conf.js --no-single-run" 23 | }, 24 | "devDependencies": { 25 | "babel": "5.8.23", 26 | "babel-eslint": "4.0.5", 27 | "babel-loader": "5.3.2", 28 | "babel-runtime": "5.8.20", 29 | "eslint": "^1.8.0", 30 | "eslint-config-defaults": "7.0.1", 31 | "eslint-ecma-features": "1.0.0", 32 | "eslint-plugin-filenames": "0.1.2", 33 | "eslint-plugin-react": "2.7.0", 34 | "jasmine": "^2.3.2", 35 | "karma": ">=0.13.2 < 1", 36 | "karma-chrome-launcher": "~0.1.5", 37 | "karma-jasmine": "^0.3.6", 38 | "karma-spec-reporter": "~0.0.16", 39 | "karma-webpack": "1.7.0", 40 | "rimraf": "^2.4.3", 41 | "webpack": "1.12.2", 42 | "webpack-dev-server": "^1.10.0" 43 | }, 44 | "dependencies": { 45 | "vendor-prefix": "^0.1.0" 46 | }, 47 | "keywords": [ 48 | "animation", 49 | "transition", 50 | "css", 51 | "react" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/Animation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import prefix from 'vendor-prefix'; 4 | import { applyStyle, insertAnimation, removeRule, removeAnimation, addListener, removeListener } from './utils'; 5 | 6 | var uniqueID = 0; 7 | 8 | export default class Animation { 9 | 10 | constructor(DOMNode, definition) { 11 | this.type = 'animation'; 12 | this.DOMNode = DOMNode; 13 | this.definition = definition; 14 | } 15 | 16 | start(onEnd) { 17 | uniqueID++; 18 | 19 | let animationName = `animator_${uniqueID}`; 20 | let cssRule = insertAnimation(animationName, this.definition.keyframes); 21 | 22 | let callback = (event) => { 23 | if (!event || event.target === this.DOMNode) { 24 | removeListener(this.type, this.DOMNode, callback); 25 | removeAnimation(this.DOMNode); 26 | removeRule(cssRule); 27 | 28 | if (typeof onEnd === 'function') { 29 | onEnd(); 30 | } 31 | } 32 | }; 33 | 34 | let style = {}; 35 | 36 | style[prefix.dash('animation-name')] = animationName; 37 | 38 | for (let property in this.definition) { 39 | if (property.indexOf('animation-') !== -1) { 40 | style[property] = this.definition[property]; 41 | } 42 | } 43 | 44 | applyStyle(this.DOMNode, style); 45 | 46 | if (this.definition.duration === '0ms') { 47 | callback(); 48 | return; 49 | } 50 | else { 51 | addListener(this.type, this.DOMNode, callback); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/CSSProperty.js: -------------------------------------------------------------------------------- 1 | // Borrowed from https://github.com/angular/angular.js/blob/0fc58516f4e92a46f6d445421c1f04ff9729c549/src/ngAnimate/animateCss.js 2 | var TRANSITIONEND_EVENT; 3 | var ANIMATIONEND_EVENT; 4 | 5 | if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) { 6 | TRANSITIONEND_EVENT = ['webkitTransitionEnd', 'transitionend']; 7 | } 8 | else { 9 | TRANSITIONEND_EVENT = 'transitionend'; 10 | } 11 | 12 | if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { 13 | ANIMATIONEND_EVENT = ['webkitAnimationEnd', 'animationend']; 14 | } 15 | else { 16 | ANIMATIONEND_EVENT = 'animationend'; 17 | } 18 | 19 | // Borrowed from React. 20 | var isUnitlessNumber = { 21 | animationIterationCount: true, 22 | boxFlex: true, 23 | boxFlexGroup: true, 24 | boxOrdinalGroup: true, 25 | columnCount: true, 26 | flex: true, 27 | flexGrow: true, 28 | flexPositive: true, 29 | flexShrink: true, 30 | flexNegative: true, 31 | flexOrder: true, 32 | fontWeight: true, 33 | lineClamp: true, 34 | lineHeight: true, 35 | opacity: true, 36 | order: true, 37 | orphans: true, 38 | tabSize: true, 39 | widows: true, 40 | zIndex: true, 41 | zoom: true, 42 | 43 | // SVG-related properties 44 | fillOpacity: true, 45 | stopOpacity: true, 46 | strokeDashoffset: true, 47 | strokeOpacity: true, 48 | strokeWidth: true, 49 | }; 50 | 51 | function prefixKey(prefix, key) { 52 | return prefix + key.charAt(0).toUpperCase() + key.substring(1); 53 | } 54 | 55 | var prefixes = ['Webkit', 'ms', 'Moz', 'O']; 56 | 57 | Object.keys(isUnitlessNumber).forEach(function(prop) { 58 | prefixes.forEach(function(prefix) { 59 | isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]; 60 | }); 61 | }); 62 | 63 | export { isUnitlessNumber, TRANSITIONEND_EVENT, ANIMATIONEND_EVENT }; 64 | -------------------------------------------------------------------------------- /src/Transition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { getTransition, applyStyle, removeTransition, addListener, removeListener } from './utils'; 4 | 5 | export default class Transition { 6 | 7 | constructor(DOMNode, definition) { 8 | this.type = 'transition'; 9 | this.DOMNode = DOMNode; 10 | this.definition = definition; 11 | this.transition = getTransition(definition); 12 | } 13 | 14 | start(onEnd) { 15 | if (this.transition.keyframes.from) { 16 | applyStyle(this.DOMNode, this.transition.keyframes.from); 17 | 18 | // Trigger a repaint. 19 | let width = this.DOMNode.offsetWidth + 1; // jshint ignore:line 20 | } 21 | 22 | let callback = (event) => { 23 | if (!event || event.target === this.DOMNode) { 24 | removeListener(this.type, this.DOMNode, callback); 25 | removeTransition(this.DOMNode); 26 | 27 | if (typeof onEnd === 'function') { 28 | onEnd(); 29 | } 30 | } 31 | }; 32 | 33 | let style = {}; 34 | 35 | for (let property in this.transition) { 36 | if (property.indexOf('transition-') !== -1) { 37 | style[property] = this.transition[property]; 38 | } 39 | } 40 | 41 | for (let property in this.transition.keyframes.to) { 42 | style[property] = this.transition.keyframes.to[property]; 43 | } 44 | 45 | applyStyle(this.DOMNode, style); 46 | 47 | if (this.definition['transition-duration'] === '0ms') { 48 | callback(); 49 | return; 50 | } 51 | else { 52 | addListener(this.type, this.DOMNode, callback); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/easings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const easings = { 4 | linear: 'linear', 5 | ease: 'ease', 6 | 'ease-in': 'ease-in', 7 | 'ease-out': 'ease-out', 8 | 'ease-in-out': 'ease-in-out', 9 | 'ease-in-quad': 'cubic-bezier(0.550, 0.085, 0.680, 0.530)', 10 | 'ease-in-cubic': 'cubic-bezier(0.550, 0.055, 0.675, 0.190)', 11 | 'ease-in-quart': 'cubic-bezier(0.895, 0.030, 0.685, 0.220)', 12 | 'ease-in-quint': 'cubic-bezier(0.755, 0.050, 0.855, 0.060)', 13 | 'ease-in-sine': 'cubic-bezier(0.470, 0.000, 0.745, 0.715)', 14 | 'ease-in-expo': 'cubic-bezier(0.950, 0.050, 0.795, 0.035)', 15 | 'ease-in-circ': 'cubic-bezier(0.600, 0.040, 0.980, 0.335)', 16 | 'ease-in-back': 'cubic-bezier(0.600, -0.280, 0.735, 0.045)', 17 | 'ease-out-quad': 'cubic-bezier(0.250, 0.460, 0.450, 0.940)', 18 | 'ease-out-cubic': 'cubic-bezier(0.215, 0.610, 0.355, 1.000)', 19 | 'ease-out-quart': 'cubic-bezier(0.165, 0.840, 0.440, 1.000)', 20 | 'ease-out-quint': 'cubic-bezier(0.230, 1.000, 0.320, 1.000)', 21 | 'ease-out-sine': 'cubic-bezier(0.390, 0.575, 0.565, 1.000)', 22 | 'ease-out-expo': 'cubic-bezier(0.190, 1.000, 0.220, 1.000)', 23 | 'ease-out-circ': 'cubic-bezier(0.075, 0.820, 0.165, 1.000)', 24 | 'ease-out-back': 'cubic-bezier(0.175, 0.885, 0.320, 1.275)', 25 | 'ease-in-out-quad': 'cubic-bezier(0.455, 0.030, 0.515, 0.955)', 26 | 'ease-in-out-cubic': 'cubic-bezier(0.645, 0.045, 0.355, 1.000)', 27 | 'ease-in-out-quart': 'cubic-bezier(0.770, 0.000, 0.175, 1.000)', 28 | 'ease-in-out-quint': 'cubic-bezier(0.860, 0.000, 0.070, 1.000)', 29 | 'ease-in-out-sine': 'cubic-bezier(0.445, 0.050, 0.550, 0.950)', 30 | 'ease-in-out-expo': 'cubic-bezier(1.000, 0.000, 0.000, 1.000)', 31 | 'ease-in-out-circ': 'cubic-bezier(0.785, 0.135, 0.150, 0.860)', 32 | 'ease-in-out-back': 'cubic-bezier(0.680, -0.550, 0.265, 1.550)' 33 | }; 34 | 35 | export default easings; 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { getArguments } from './utils'; 4 | import Transition from './Transition'; 5 | import Animation from './Animation'; 6 | 7 | function init(type, DOMNode, ...rest) { 8 | if (typeof DOMNode === 'undefined' || DOMNode === null) { 9 | return null; 10 | } 11 | 12 | let [definition, onEnd] = getArguments(type, ...rest); 13 | 14 | let instance = type === 'transition' ? new Transition(DOMNode, definition) : new Animation(DOMNode, definition); 15 | 16 | instance.start(onEnd); 17 | 18 | return instance; 19 | } 20 | 21 | export function transition(DOMNode, ...rest) { 22 | return init('transition', DOMNode, ...rest); 23 | } 24 | 25 | export function animation(DOMNode, ...rest) { 26 | return init('animation', DOMNode, ...rest); 27 | } 28 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | /* global expect */ 2 | 3 | 'use strict'; 4 | 5 | import { transition, animation } from './index'; 6 | import Transition from './Transition'; 7 | import Animation from './Animation'; 8 | 9 | describe('animata', () => { 10 | describe('return an instance when called with shortcuts', () => { 11 | it('should return a Transition instance when called with transition', done => { 12 | let node = document.createElement('div'); 13 | node.style.background = 'red'; 14 | node.style.width = '30px'; 15 | node.style.height = '30px'; 16 | node.style.position = 'fixed'; 17 | node.style.bottom = 0; 18 | node.style.right = 0; 19 | 20 | document.body.appendChild(node); 21 | 22 | let definition = { 23 | keyframes: { 24 | from: { 25 | opacity: '0' 26 | }, 27 | to: { 28 | opacity: '1' 29 | } 30 | }, 31 | ease: 'ease-in', 32 | duration: '1s' 33 | }; 34 | 35 | let callback = function() { 36 | callbackSpy(); 37 | expect(callbackSpy).toHaveBeenCalled(); 38 | done(); 39 | } 40 | 41 | let callbackSpy = jasmine.createSpy('callback'); 42 | 43 | let instance = transition(node, definition, () => callback()); 44 | 45 | expect(instance instanceof Transition).toBeTruthy(); 46 | }); 47 | 48 | it('should return an Animation instance when called with animation', done => { 49 | let node = document.createElement('div'); 50 | node.style.background = 'blue'; 51 | node.style.width = '30px'; 52 | node.style.height = '30px'; 53 | node.style.position = 'fixed'; 54 | node.style.bottom = 0; 55 | node.style.right = '30px'; 56 | 57 | document.body.appendChild(node); 58 | 59 | let definition = { 60 | keyframes: { 61 | from: { 62 | opacity: '0' 63 | }, 64 | to: { 65 | opacity: '1' 66 | } 67 | }, 68 | ease: 'ease-in', 69 | duration: '1s' 70 | }; 71 | 72 | let callback = function() { 73 | callbackSpy(); 74 | expect(callbackSpy).toHaveBeenCalled(); 75 | done(); 76 | } 77 | 78 | let callbackSpy = jasmine.createSpy('callback'); 79 | 80 | let instance = animation(node, definition, () => callback()); 81 | 82 | expect(instance instanceof Animation).toBeTruthy(); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import prefix from 'vendor-prefix'; 4 | import easings from './easings.js'; 5 | 6 | import { isUnitlessNumber, TRANSITIONEND_EVENT, ANIMATIONEND_EVENT } from './CSSProperty'; 7 | 8 | var DOMStyle = document.createElement('style'); 9 | DOMStyle.type = 'text/css'; 10 | document.head.appendChild(DOMStyle); 11 | 12 | export function getAnimationText(name, keyframes) { 13 | let list = []; 14 | 15 | for (let step in keyframes) { 16 | let rules = keyframes[step]; 17 | 18 | let style = []; 19 | for (let property in rules) { 20 | let CSSProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase(); 21 | let value = typeof rules[property] === 'number' ? rules[property] + 'px' : rules[property]; 22 | style.push(`${CSSProperty}:${value};`); 23 | } 24 | 25 | list.push(`${step}{${style.join('')}}`); 26 | } 27 | 28 | let cssRuleText = list.join(''); 29 | let keyframesRule = prefix.dash('animation').replace('animation', '') + 'keyframes'; 30 | 31 | return `@${keyframesRule} ${name} {${cssRuleText}}`; 32 | } 33 | 34 | export function insertAnimation(animationName, animation) { 35 | let cssRuleIndex = DOMStyle.sheet.insertRule(getAnimationText(animationName, animation), DOMStyle.sheet.cssRules.length); 36 | return DOMStyle.sheet.rules[cssRuleIndex]; 37 | } 38 | 39 | export function getKeyframesRuleIndex(rule) { 40 | for (var i = 0; i < DOMStyle.sheet.cssRules.length; ++i) { 41 | if (DOMStyle.sheet.cssRules[i] === rule) { 42 | return i; 43 | } 44 | } 45 | 46 | return null; 47 | } 48 | 49 | export function applyStyle(DOMNode, style) { 50 | for (let property in style) { 51 | DOMNode.style[property] = style[property]; 52 | } 53 | } 54 | 55 | export function removeTransition(DOMNode) { 56 | let properties = ['transition-delay', 'transition-duration', 'transition-property', 'transition-timing-function']; 57 | 58 | for (let property of properties) { 59 | DOMNode.style.removeProperty(prefix.dash(property)); 60 | } 61 | } 62 | 63 | export function removeAnimation(DOMNode) { 64 | let properties = ['animation-name', 'animation-duration', 'animation-timing-function', 'animation-delay', 'animation-iteration-count', 'animation-direction', 'animation-fill-mode']; 65 | 66 | for (let property of properties) { 67 | DOMNode.style.removeProperty(prefix.dash(property)); 68 | } 69 | } 70 | 71 | export function removeRule(cssRule) { 72 | DOMStyle.sheet.deleteRule(getKeyframesRuleIndex(cssRule)); 73 | } 74 | 75 | export function addListener(eventName, DOMNode, callback) { 76 | if (eventName === 'transition') { 77 | eventName = TRANSITIONEND_EVENT; 78 | } 79 | else { 80 | eventName = ANIMATIONEND_EVENT; 81 | } 82 | 83 | if (Array.isArray(eventName)) { 84 | eventName.forEach(eventName => DOMNode.addEventListener(eventName, callback, false)); 85 | } 86 | else { 87 | DOMNode.addEventListener(eventName, callback, false); 88 | } 89 | } 90 | 91 | export function removeListener(eventName, DOMNode, callback) { 92 | if (eventName === 'transition') { 93 | eventName = TRANSITIONEND_EVENT; 94 | } 95 | else { 96 | eventName = ANIMATIONEND_EVENT; 97 | } 98 | 99 | if (Array.isArray(eventName)) { 100 | eventName.forEach(eventName => DOMNode.removeEventListener(eventName, callback)); 101 | } 102 | else { 103 | DOMNode.removeEventListener(eventName, callback); 104 | } 105 | } 106 | 107 | export function getNormalizedDefinition(type, definition) { 108 | let newDefinition = {}; 109 | 110 | for (let property in definition) { 111 | if (property === 'keyframes') { 112 | newDefinition.keyframes = {}; 113 | 114 | for (let i in definition.keyframes) { 115 | let step = i === '0%' || i === 0 || i === '0' ? 'from' : i; 116 | step = step === '100%' ? 'to' : step; 117 | 118 | newDefinition.keyframes[step] = {}; 119 | 120 | for (let ruleProperty in definition.keyframes[i]) { 121 | let value = definition.keyframes[i][ruleProperty]; 122 | newDefinition.keyframes[step][prefix.dash(ruleProperty)] = typeof value === 'number' && !isUnitlessNumber[ruleProperty] ? value + 'px' : value; 123 | } 124 | } 125 | } 126 | else { 127 | property = property.replace(/([A-Z])/g, '-$1').toLowerCase(); 128 | let value = definition[property]; 129 | 130 | if (property === 'ease' || property === 'timing-function') { 131 | property = 'timing-function'; 132 | value = easings[value]; 133 | } 134 | else if (property === 'duration' && typeof value === 'number') { 135 | value = `${value}ms`; 136 | } 137 | 138 | newDefinition[prefix.dash(type + '-' + property)] = value; 139 | } 140 | } 141 | 142 | return newDefinition; 143 | } 144 | 145 | export function getTransition(definition) { 146 | let transition = { keyframes: { from: {}, to: {} } }; 147 | let transitionableProps = []; 148 | 149 | for (let step in definition.keyframes) { 150 | for (let CSSProperty in definition.keyframes[step]) { 151 | transition.keyframes[step][CSSProperty] = definition.keyframes[step][CSSProperty]; 152 | 153 | if (transitionableProps.indexOf(CSSProperty) === -1) { 154 | transitionableProps.push(CSSProperty); 155 | } 156 | } 157 | } 158 | 159 | // Each transition property should have a group per CSS property that is 160 | // transitionable. E.g: transition-duration: 1s, 1s; 161 | for (let property in definition) { 162 | if (property.indexOf('transition-') !== -1) { 163 | let properties = []; 164 | 165 | for (let i = 0; i < transitionableProps.length; i++) { 166 | properties[i] = definition[property]; 167 | } 168 | 169 | transition[property] = properties.join(','); 170 | } 171 | } 172 | 173 | transition[prefix.dash('transition-property')] = transitionableProps.join(','); 174 | 175 | return transition; 176 | } 177 | 178 | export function getArguments(type, definition, ...rest) { 179 | let onEnd; 180 | let overrides = {}; 181 | 182 | // Build args. 183 | if (rest.length === 1) { 184 | if (typeof rest[0] === 'function') { 185 | onEnd = rest[0]; 186 | } 187 | else { 188 | overrides = rest[0]; 189 | } 190 | } 191 | else { 192 | overrides = rest[0]; 193 | onEnd = rest[1]; 194 | } 195 | 196 | definition = getNormalizedDefinition(type, definition); 197 | overrides = getNormalizedDefinition(type, overrides); 198 | 199 | for (let i in overrides) { 200 | definition[i] = overrides[i]; 201 | } 202 | 203 | return [definition, onEnd]; 204 | } 205 | -------------------------------------------------------------------------------- /src/utils.spec.js: -------------------------------------------------------------------------------- 1 | /* global expect */ 2 | 3 | 'use strict'; 4 | 5 | import { getAnimationText, applyStyle, insertAnimation, removeTransition, getNormalizedDefinition, getArguments, getTransition } from './utils'; 6 | import easings from './easings.js'; 7 | 8 | describe('utils', () => { 9 | 10 | describe('getAnimationText', () => { 11 | it('should return the correct keyframes string', () => { 12 | let keyframes = { 13 | from: { 14 | background: 'blue' 15 | }, 16 | '50%': { 17 | background: 'yellow' 18 | }, 19 | to: { 20 | background: 'green' 21 | } 22 | }; 23 | 24 | let text = /@(.*)keyframes test {from{background:blue;}50%{background:yellow;}to{background:green;}}/; 25 | 26 | expect(getAnimationText('test', keyframes)).toMatch(text); 27 | }); 28 | }); 29 | 30 | describe('insertAnimation', () => { 31 | it('should insert an animation rule', () => { 32 | let keyframes = { 33 | from: { 34 | width: '30px' 35 | }, 36 | to: { 37 | width: '200px' 38 | } 39 | }; 40 | 41 | let rule = insertAnimation('test', keyframes); 42 | 43 | let rules = document.styleSheets[document.styleSheets.length - 1].rules; 44 | let toRule = rules[rules.length - 1]; 45 | 46 | expect(rule).toEqual(toRule); 47 | }); 48 | }); 49 | 50 | describe('applyStyle', () => { 51 | it('should apply style to node without style', () => { 52 | let node = document.createElement('div'); 53 | let style = { 54 | background: 'blue', 55 | color: 'red', 56 | width: '100px' 57 | }; 58 | 59 | applyStyle(node, style); 60 | 61 | for (let property in style) { 62 | expect(node.style[property]).toEqual(style[property]); 63 | } 64 | }); 65 | 66 | it('should apply style to node with style', () => { 67 | let node = document.createElement('div'); 68 | node.style.height = '20px'; 69 | node.style.background = 'yellow'; 70 | 71 | let style = { 72 | background: 'blue', 73 | color: 'red', 74 | width: '100px' 75 | }; 76 | 77 | applyStyle(node, style); 78 | 79 | for (let property in style) { 80 | expect(node.style[property]).toEqual(style[property]); 81 | } 82 | 83 | expect(node.style.height).toEqual('20px'); 84 | }); 85 | }); 86 | 87 | describe('removeTransition', () => { 88 | it('should remove transition styles from node', () => { 89 | let node = document.createElement('div'); 90 | 91 | let style = { 92 | background: 'blue', 93 | color: 'red', 94 | 'transition-duration': '1s', 95 | 'transition-property': 'opacity', 96 | 'transition-timing-function': 'linear' 97 | }; 98 | 99 | applyStyle(node, style); 100 | 101 | removeTransition(node, style); 102 | 103 | expect(node.style.background).toEqual('blue'); 104 | expect(node.style.color).toEqual('red'); 105 | expect(node.style['transition-duration']).toEqual(''); 106 | expect(node.style['transition-property']).toEqual(''); 107 | expect(node.style['transition-timing-function']).toEqual(''); 108 | }); 109 | }); 110 | 111 | describe('getNormalizedDefinition', () => { 112 | it('should return normalized definition', () => { 113 | let easing = easings['ease-in-sine']; 114 | 115 | let definition = { 116 | keyframes: { 117 | 0: { 118 | opacity: '0', 119 | width: 10 120 | }, 121 | '100%': { 122 | opacity: '1', 123 | width: 100 124 | } 125 | }, 126 | ease: 'ease-in-sine', 127 | duration: '1s' 128 | }; 129 | 130 | let normalizedDefinition = { 131 | keyframes: { 132 | from: { 133 | opacity: '0', 134 | width: '10px' 135 | }, 136 | to: { 137 | opacity: '1', 138 | width: '100px' 139 | } 140 | }, 141 | 'transition-timing-function': easing, 142 | 'transition-duration': '1s' 143 | }; 144 | 145 | expect(getNormalizedDefinition('transition', definition)).toEqual(normalizedDefinition); 146 | }); 147 | }); 148 | 149 | describe('getTransition', () => { 150 | it('should return a correct transition object', () => { 151 | let definition = { 152 | keyframes: { 153 | from: { 154 | opacity: '0', 155 | height: '100px' 156 | }, 157 | to: { 158 | opacity: '1', 159 | width: '100px', 160 | height: '100px' 161 | } 162 | }, 163 | 'transition-timing-function': 'ease-in', 164 | 'transition-duration': '1s' 165 | }; 166 | 167 | let transition = { 168 | keyframes: { 169 | from: { 170 | opacity: '0', 171 | height: '100px' 172 | }, 173 | to: { 174 | opacity: '1', 175 | width: '100px', 176 | height: '100px' 177 | } 178 | }, 179 | 'transition-timing-function': 'ease-in,ease-in,ease-in', 180 | 'transition-duration': '1s,1s,1s', 181 | 'transition-property': 'opacity,height,width' 182 | }; 183 | 184 | expect(getTransition(definition)).toEqual(transition); 185 | }); 186 | }); 187 | 188 | describe('getArguments', () => { 189 | it('should return correct arguments', () => { 190 | let definition = { 191 | keyframes: { 192 | '0%': { 193 | opacity: '0', 194 | height: 100 195 | }, 196 | to: { 197 | opacity: '1', 198 | width: 100, 199 | height: 100 200 | } 201 | }, 202 | ease: 'ease-in', 203 | duration: '1s' 204 | }; 205 | 206 | let overrides = { 207 | keyframes: { 208 | from: { 209 | opacity: '1', 210 | width: 30 211 | }, 212 | to: { 213 | height: 200 214 | } 215 | }, 216 | duration: '10s', 217 | ease: 'ease-out' 218 | }; 219 | 220 | let onEnd = () => console.log('done'); 221 | 222 | let toDefinition = { 223 | keyframes: { 224 | from: { 225 | opacity: '1', 226 | width: '30px' 227 | }, 228 | to: { 229 | height: '200px' 230 | } 231 | }, 232 | 'transition-timing-function': 'ease-out', 233 | 'transition-duration': '10s' 234 | }; 235 | 236 | let args = [toDefinition, onEnd]; 237 | 238 | expect(getArguments('transition', definition, overrides, onEnd)).toEqual(args); 239 | }); 240 | }); 241 | 242 | }); 243 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Frontend Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var webpack = require("webpack"); 4 | var config = require("./webpack.config"); 5 | 6 | // **WARNING**: Mutates base configuration. 7 | // We do this because lodash isn't available in `production` mode. 8 | config.plugins = [ 9 | new webpack.SourceMapDevToolPlugin("[file].map") 10 | ]; 11 | 12 | // Export mutated base. 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var webpack = require("webpack"); 4 | var path = require("path"); 5 | 6 | module.exports = { 7 | cache: true, 8 | entry: path.join(__dirname, "src/index.js"), 9 | output: { 10 | path: path.join(__dirname, "lib"), 11 | filename: "bundle.js", 12 | libraryTarget: "commonjs" 13 | }, 14 | resolve: { 15 | extensions: ["", ".js", "jsx"] 16 | }, 17 | externals: [ 18 | "vendor-prefix" 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.jsx?$/, 24 | exclude: [/node_modules/], 25 | loader: "babel-loader?stage=0" 26 | } 27 | ] 28 | }, 29 | plugins: [ 30 | new webpack.optimize.DedupePlugin(), 31 | new webpack.optimize.UglifyJsPlugin({ 32 | compress: { 33 | warnings: false 34 | } 35 | }), 36 | new webpack.DefinePlugin({ 37 | // Signal production, so that webpack removes non-production code that 38 | // is in condtionals like: `if (process.env.NODE_ENV === "production")` 39 | "process.env.NODE_ENV": JSON.stringify("production") 40 | }) 41 | ] 42 | }; 43 | --------------------------------------------------------------------------------