├── .babelrc ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── BANNER ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json ├── src ├── animate.js ├── chain.js ├── event.js ├── index.js ├── utils.js └── vq.js ├── test ├── .eslintrc.yml ├── event.js ├── index.js └── velocity-spy.js ├── testem.yml ├── webpack.config.dev.js ├── webpack.config.js └── webpack.config.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-3"], 3 | "plugins": ["babel-plugin-espower"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: eslint:recommended 3 | env: 4 | es6: true 5 | commonjs: true 6 | browser: true 7 | jquery: true 8 | globals: 9 | Velocity: true 10 | parserOptions: 11 | ecmaVersion: 6 12 | sourceType: module 13 | rules: 14 | dot-notation: 2 15 | eqeqeq: [2, smart] 16 | no-eval: 2 17 | no-use-before-define: [2, nofunc] 18 | radix: [1, as-needed] 19 | no-loop-func: 1 20 | no-multi-spaces: 1 21 | no-warning-comments: 1 22 | no-unused-vars: 1 23 | no-console: 1 24 | camelcase: [1, {properties: never}] 25 | indent: [1, 2] 26 | quotes: [1, single] 27 | semi: [2, always] 28 | strict: [2, global] 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | /dist/ 4 | /.tmp/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.babelrc 2 | /.eslintrc 3 | /testem.yml 4 | 5 | /gulpfile.js 6 | /webpack.config.js 7 | /webpack.config.dev.js 8 | /webpack.config.test.js 9 | 10 | /src 11 | /.tmp 12 | /test 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_script: 5 | - "export DISPLAY=:99.0" 6 | - "sh -e /etc/init.d/xvfb start" 7 | - sleep 3 # give xvfb some time to start 8 | script: 9 | - npm run lint 10 | - npm run test:ci 11 | -------------------------------------------------------------------------------- /BANNER: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${name} v${version} 3 | * ${homepage} 4 | * 5 | * Copyright (c) 2015-2016 ${author} 6 | * Released under the MIT license 7 | * ${homepage}/blob/master/LICENSE 8 | */ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 katashin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vq 2 | 3 | [![npm version](https://badge.fury.io/js/vq.svg)](https://badge.fury.io/js/vq) 4 | [![Build Status](https://travis-ci.org/ktsn/vq.svg?branch=master)](https://travis-ci.org/ktsn/vq) 5 | 6 | A light-weight and functional animation helper for Velocity.js 7 | 8 | ## Dependencies 9 | vq requires [Velocity.js](http://julian.com/research/velocity/). You have to load Velocity.js before using vq. 10 | 11 | ## Installation 12 | 13 | ```sh 14 | $ npm install vq 15 | ``` 16 | 17 | or download from [release page](https://github.com/ktsn/vq/releases). 18 | 19 | ## Demo 20 | - [Sequencial Animation](http://codepen.io/ktsn/pen/KVZeRP) 21 | - [Event Handling](http://codepen.io/ktsn/pen/qZzzqX) 22 | 23 | ## Example 24 | 25 | ```js 26 | /* Animation behaviors 27 | ----------------------------------------*/ 28 | var fadeIn = { 29 | p: { opacity: [1, 0] }, 30 | o: { duration: 500, easing: 'easeOutQuad' } 31 | }; 32 | 33 | var fadeOut = { 34 | p: { opacity: [0, 1] }, 35 | o: { duration: 500, easing: 'easeInQuad' } 36 | }; 37 | 38 | /* Animation elements 39 | ----------------------------------------*/ 40 | var $foo = $('.foo'); 41 | var $bar = $('.bar'); 42 | 43 | /* Animation sequence 44 | ----------------------------------------*/ 45 | var seq = vq.sequence([ 46 | vq($foo, fadeIn), 47 | vq($bar, fadeIn).delay(1000).stagger(30), 48 | vq($bar, fadeOut), 49 | vq($foo, fadeOut), 50 | function() { console.log('complete!') } 51 | ]); 52 | 53 | seq(); 54 | ``` 55 | 56 | ## API 57 | vq provides global function `vq(el, props, opts)`, `vq.sequence(funcs)` and `vq.parallel(funcs)`. 58 | 59 | ### vq(el, props, opts) 60 | This function returns a function that executes animation by given element, property and options. 61 | 62 | ```js 63 | var el = document.getElementById('element'); 64 | var func = vq(el, { width: 400 }, { duration: 600 }); // generate a function 65 | func(); // <-- The animation is executed on this time 66 | ``` 67 | 68 | You can combine the `props` and `opts` to [Velocity-like single object](http://julian.com/research/velocity/#arguments). 69 | 70 | ```js 71 | var fadeIn = { 72 | p: { 73 | opacity: [1, 0] 74 | }, 75 | o: { 76 | duration: 500 77 | } 78 | }; 79 | 80 | var func = vq(el, fadeIn); 81 | func(); 82 | ``` 83 | 84 | You can pass a Velocity's progress callback to the 2nd argument or the value of `p`. 85 | 86 | ```js 87 | var swing = { 88 | p: function(el, t, r, t0, tween) { 89 | var offset = 100 * Math.sin(2 * tween * Math.PI); 90 | el[0].style.transform = 'translate3d(' + offset + 'px, 0, 0)'; 91 | }, 92 | o: { 93 | duration: 1000, 94 | easing: 'easeOutCubic' 95 | } 96 | } 97 | 98 | var func = vq(el, swing); 99 | func(); 100 | ``` 101 | 102 | The function receives 1st argument as completion callback. You can handle the animation completion from the callback; 103 | 104 | ```js 105 | var func = vq(el, props, opts); 106 | func(function() { 107 | console.log('animation is completed'); 108 | }); 109 | ``` 110 | 111 | ### vq.sequence(funcs) 112 | This function receives the array of functions and returns a function to execute them sequentially. If the given function returns Promise object or has callback function as 1st argument, vq.sequence waits until the asynchronous processes are finished. 113 | 114 | ```js 115 | var seq = vq.sequence([ 116 | function(done) { 117 | setTimeout(function() { 118 | console.log('1') 119 | done(); 120 | }, 1000); 121 | }, 122 | function() { 123 | return new Promise(function(resolve, reject) { 124 | setTimeout(function() { 125 | console.log('2'); 126 | resolve(); 127 | }, 500); 128 | }); 129 | }, 130 | function() { console.log('3'); } 131 | ]); 132 | 133 | seq(); 134 | // The output order is 1 -> 2 -> 3 135 | ``` 136 | 137 | The function is useful with using `vq(el, props, opts)`. 138 | 139 | ```js 140 | var animSeq = vq.sequence([ 141 | vq(el1, animation1), 142 | vq(el2, animation2), 143 | vq(el3, animation3), 144 | function() { 145 | console.log('Three animations are completed'); 146 | } 147 | ]); 148 | ``` 149 | 150 | ### vq.parallel(funcs) 151 | This function is same as `vq.sequence` except to execute in parallel. 152 | 153 | ```js 154 | var para = vq.parallel([ 155 | function(done) { 156 | setTimeout(function() { 157 | console.log('1') 158 | done(); 159 | }, 1000); 160 | }, 161 | function() { 162 | return new Promise(function(resolve, reject) { 163 | setTimeout(function() { 164 | console.log('2'); 165 | resolve(); 166 | }, 500); 167 | }); 168 | }, 169 | function() { console.log('3'); } 170 | ]); 171 | 172 | para(); 173 | // The output order may be 3 -> 2 -> 1 174 | ``` 175 | 176 | ### vq.stop(els) 177 | This function creates *animation stopper* for given elements. 178 | If you execute returned function, all animated elements is stopped. 179 | 180 | ```js 181 | var stopFn = vq.stop([el1, el2, el3]); 182 | 183 | var seq = vq.sequence([ 184 | vq(el1, animation1), 185 | vq(el2, animation2), 186 | vq(el3, animation3) 187 | ])(); 188 | 189 | setTimeout(stopFn, 3000); // stop `seq` animations after 3000msec 190 | ``` 191 | 192 | ### Event helpers 193 | 194 | #### DOM Event helpers 195 | vq can handle DOM events by event helpers and execute some animation after receiving the events. 196 | vq has following event helpers. 197 | 198 | - click 199 | - dblclick 200 | - mousedown 201 | - mouseup 202 | - mousemove 203 | - mouseenter 204 | - mouseleave 205 | - focus 206 | - blur 207 | - change 208 | - input 209 | - scroll 210 | - load 211 | 212 | The helper functions expect a DOM element for the argument and returns a new function. 213 | The returned function expect an animation function that you will get from `vq`, `vq.sequence` or `vq.parallel` functions. 214 | 215 | ```js 216 | var click = vq.click(window); 217 | 218 | var fade = vq.sequence([ 219 | click(vq(el, fadeIn)), 220 | click(vq(el, fadeOut)), 221 | function(done) { fade(done); } 222 | ]); 223 | 224 | fade(); 225 | ``` 226 | 227 | You can create a new event helper by using `vq.element` function. 228 | `vq.element` receives an element, a DOM event name and an optional filter function. 229 | If you specify the filter function, your event helper does not execute an animation function until the filter function returns `true`. 230 | 231 | ```js 232 | var helloWorld = vq.element(input, 'input', function(event) { 233 | return event.target.value === 'Hello World'; 234 | }); 235 | 236 | var seq = vq.sequence([ 237 | helloWorld(vq(input, fadeOut)), 238 | // some other animations 239 | ]); 240 | 241 | seq(); 242 | ``` 243 | 244 | #### Delay Helper 245 | 246 | `vq.delay(msec)` returns the function that delays given callback for `msec`. 247 | This function can use for not only animation function but also normal functions. 248 | 249 | ```js 250 | var delay1s = vq.delay(1000); 251 | 252 | var delayedFn = delay1s(function() { 253 | console.log('This message is delayed 1sec'); 254 | }); 255 | 256 | delayedFn(); 257 | ``` 258 | 259 | ### vq chainable helper methods 260 | The function returned by `vq(el, props, opts)` has some chainable helper methods. The helpers can modify animation options and behaviors. 261 | 262 | ```js 263 | vq.sequence([ 264 | vq(el1, animation1), 265 | vq(el2, animation2).delay(100).loop(3), // Add 100ms delay and three times loop 266 | vq(el3, animation3) 267 | ]); 268 | ``` 269 | 270 | #### .delay(msec) 271 | Set the delay of animation start to `msec`. 272 | 273 | #### .duration(msec) 274 | Set the duration of animation to `msec`. 275 | 276 | #### .easing(name) 277 | Set the easing of animation to `name`. `name` should be easing function name that allowed on Velocity.js. 278 | 279 | #### .progress(func, tween) 280 | Set the progress function to `func`. 281 | 282 | `tween` is optional argument and it is set to `props.tween`. `[1, 0]` is set if `tween` is omitted and `props.tween` is not set previously. 283 | 284 | See [Velocity.js documentation](http://julian.com/research/velocity/#progress) to learn tween property. 285 | 286 | #### .display(value) 287 | Set the display option to `value`. 288 | 289 | #### .visibility(value) 290 | Set the visibility option to `value`. 291 | 292 | #### .loop(count) 293 | Set the loop option to `count`. 294 | 295 | #### .stagger(msec) 296 | Set the delay between each element animation to `msec`. 297 | 298 | ## Contribution 299 | Contribution is welcome! Feel free to open an issue or a pull request. 300 | 301 | ## License 302 | MIT 303 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const gulp = require('gulp'); 3 | const gutil = require('gulp-util'); 4 | const rename = require('gulp-rename'); 5 | const eslint = require('gulp-eslint'); 6 | const uglify = require('gulp-uglify'); 7 | const header = require('gulp-header'); 8 | const webpack = require('webpack'); 9 | const Testem = require('testem'); 10 | const yaml = require('js-yaml'); 11 | const fs = require('fs'); 12 | const del = require('del'); 13 | const run = require('run-sequence'); 14 | 15 | gulp.task('clean', () => { 16 | return del(['.tmp', 'dist']); 17 | }); 18 | 19 | gulp.task('eslint', () => { 20 | return gulp.src('src/**/*.js') 21 | .pipe(eslint()) 22 | .pipe(eslint.format()) 23 | .pipe(eslint.failAfterError()); 24 | }); 25 | 26 | gulp.task('webpack', (done) => { 27 | webpack(require('./webpack.config'), (err, stats) => { 28 | if (err) throw new gutil.PluginError('webpack', err); 29 | gutil.log('[webpack]', stats.toString()); 30 | done(); 31 | }); 32 | }); 33 | 34 | gulp.task('webpack:dev', () => { 35 | const compiler = webpack(require('./webpack.config.dev')); 36 | 37 | compiler.watch(200, (err, stats) => { 38 | if (err) throw new gutil.PluginError('webpack', err); 39 | gutil.log('[webpack]', stats.toString()); 40 | }); 41 | }); 42 | 43 | gulp.task('webpack:test', () => { 44 | const compiler = webpack(require('./webpack.config.test')); 45 | 46 | compiler.watch(200, (err) => { 47 | if (err) throw new gutil.PluginError('webpack', err); 48 | }); 49 | }); 50 | 51 | gulp.task('testem', () => { 52 | const testem = new Testem(); 53 | testem.startDev(yaml.safeLoad(fs.readFileSync(__dirname + '/testem.yml'))); 54 | }); 55 | 56 | gulp.task('uglify', () => { 57 | return gulp.src(['dist/**/*.js', '!**/*.min.js']) 58 | .pipe(uglify()) 59 | .pipe(rename({ 60 | suffix: '.min' 61 | })) 62 | .pipe(gulp.dest('dist')); 63 | }); 64 | 65 | gulp.task('header', () => { 66 | return gulp.src(['dist/**/*.js']) 67 | .pipe(header(fs.readFileSync('./BANNER', 'utf-8'), require('./package.json'))) 68 | .pipe(gulp.dest('dist')); 69 | }); 70 | 71 | gulp.task('build', ['clean', 'eslint'], (done) => { 72 | run('webpack', 'uglify', 'header', done); 73 | }); 74 | 75 | gulp.task('test', ['webpack:test', 'testem']); 76 | gulp.task('default', ['webpack:dev']); 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vq", 3 | "version": "1.3.1", 4 | "description": "A light-weight and functional animation helper for Velocity.js", 5 | "keywords": [ 6 | "animation", 7 | "animate", 8 | "browser", 9 | "flow", 10 | "control", 11 | "velocity", 12 | "velocity.js" 13 | ], 14 | "author": "katashin", 15 | "license": "MIT", 16 | "main": "dist/vq.js", 17 | "homepage": "https://github.com/ktsn/vq", 18 | "bugs": "https://github.com/ktsn/vq/issues", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/ktsn/vq.git" 22 | }, 23 | "scripts": { 24 | "prepublish": "npm run build", 25 | "build": "gulp build", 26 | "lint": "gulp eslint", 27 | "test": "gulp test", 28 | "test:ci": "webpack --config webpack.config.test.js && testem ci --launch Firefox" 29 | }, 30 | "devDependencies": { 31 | "babel-core": "^6.3.13", 32 | "babel-loader": "^6.2.0", 33 | "babel-plugin-espower": "^2.1.0", 34 | "babel-preset-es2015": "^6.3.13", 35 | "babel-preset-stage-3": "^6.3.13", 36 | "del": "^2.1.0", 37 | "es6-promise": "^3.1.2", 38 | "glob": "^7.0.3", 39 | "gulp": "^3.9.0", 40 | "gulp-eslint": "^2.0.0", 41 | "gulp-header": "^1.7.1", 42 | "gulp-rename": "^1.2.2", 43 | "gulp-uglify": "^1.5.1", 44 | "gulp-util": "^3.0.7", 45 | "js-yaml": "^3.5.2", 46 | "json-loader": "^0.5.4", 47 | "power-assert": "^1.2.0", 48 | "run-sequence": "^1.1.5", 49 | "testem": "^1.7.1", 50 | "webpack": "^1.12.9" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/animate.js: -------------------------------------------------------------------------------- 1 | let animate; 2 | if (typeof Velocity === 'function') { 3 | animate = Velocity; 4 | } else { 5 | animate = $.Velocity; 6 | } 7 | 8 | function stop(el) { 9 | animate(el, 'stop'); 10 | } 11 | 12 | export { animate, stop }; 13 | -------------------------------------------------------------------------------- /src/chain.js: -------------------------------------------------------------------------------- 1 | import {animate} from './animate'; 2 | import {assign} from './utils'; 3 | 4 | const helpers = { 5 | delay(msec) { 6 | this._opts.delay = msec; 7 | return this; 8 | }, 9 | 10 | duration(msec) { 11 | this._opts.duration = msec; 12 | return this; 13 | }, 14 | 15 | easing(name) { 16 | this._opts.easing = name; 17 | return this; 18 | }, 19 | 20 | progress(fn, tween = null) { 21 | this._props.tween = tween || this._props.tween || [1, 0]; 22 | this._opts.progress = fn; 23 | return this; 24 | }, 25 | 26 | display(value) { 27 | this._opts.display = value; 28 | return this; 29 | }, 30 | 31 | visibility(value) { 32 | this._opts.visibility = value; 33 | return this; 34 | }, 35 | 36 | loop(count) { 37 | this._opts.loop = count; 38 | return this; 39 | }, 40 | 41 | stagger(msec) { 42 | this._opts.stagger = msec; 43 | return this; 44 | } 45 | }; 46 | 47 | function run(el, props, opts, done) { 48 | // Always prevent Velocity enqueuing animations 49 | opts.queue = false; 50 | 51 | if (typeof opts.stagger === 'number') { 52 | staggerImpl(el, props, opts, done); 53 | } else { 54 | opts.complete = done; 55 | animate(el, props, opts); 56 | } 57 | } 58 | 59 | function staggerImpl(els, props, opts, done) { 60 | const interval = opts.stagger; 61 | let i = 0; 62 | const len = els.length; 63 | 64 | const animateWrapper = function animateWrapper() { 65 | // Set complete callback to last animation 66 | if (i === len - 1) { 67 | opts.complete = done; 68 | } 69 | 70 | const el = els[i]; 71 | animate(el, props, opts); 72 | 73 | ++i; 74 | if (i < len) { 75 | setTimeout(animateWrapper, interval); 76 | } 77 | }; 78 | 79 | animateWrapper(); 80 | } 81 | 82 | export default function chain(el, props, opts) { 83 | const fn = function fn(done) { 84 | run(fn._el, fn._props, fn._opts, done); 85 | }; 86 | fn._el = el; 87 | fn._props = props; 88 | fn._opts = opts; 89 | 90 | assign(fn, helpers); 91 | 92 | return fn; 93 | } 94 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | import {unify, on, off, noop} from './utils'; 2 | 3 | /** 4 | * Helper function to create event helpers 5 | * The function should not be exposed 6 | */ 7 | function create(f) { 8 | return behavior => done => { 9 | if (typeof done !== 'function') done = noop; 10 | f(unify(behavior), done); 11 | }; 12 | } 13 | 14 | export function element(el, name, filter = noFilter) { 15 | return create((behavior, done) => { 16 | function go(event) { 17 | if (!filter(event)) return; 18 | 19 | off(el, name, go); 20 | behavior(done); 21 | } 22 | 23 | on(el, name, go); 24 | }); 25 | } 26 | 27 | export function delay(msec) { 28 | return create((behavior, done) => { 29 | setTimeout(() => behavior(done), msec); 30 | }); 31 | } 32 | 33 | function noFilter() { return true; } 34 | 35 | /** 36 | * Convenient helpers for DOM events 37 | */ 38 | 39 | export function click(el) { 40 | return element(el, 'click'); 41 | } 42 | 43 | export function dblclick(el) { 44 | return element(el, 'dblclick'); 45 | } 46 | 47 | export function mousedown(el) { 48 | return element(el, 'mousedown'); 49 | } 50 | 51 | export function mouseup(el) { 52 | return element(el, 'mouseup'); 53 | } 54 | 55 | export function mousemove(el) { 56 | return element(el, 'mousemove'); 57 | } 58 | 59 | export function mouseenter(el) { 60 | return element(el, 'mouseenter'); 61 | } 62 | 63 | export function mouseleave(el) { 64 | return element(el, 'mouseleave'); 65 | } 66 | 67 | export function focus(el) { 68 | return element(el, 'focus'); 69 | } 70 | 71 | export function blur(el) { 72 | return element(el, 'blur'); 73 | } 74 | 75 | export function change(el) { 76 | return element(el, 'change'); 77 | } 78 | 79 | export function input(el) { 80 | return element(el, 'input'); 81 | } 82 | 83 | export function scroll(el) { 84 | return element(el, 'scroll'); 85 | } 86 | 87 | export function load(el) { 88 | return element(el, 'load'); 89 | } 90 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {assign} from './utils'; 2 | import {vq, stop, sequence, parallel} from './vq'; 3 | import * as event from './event'; 4 | 5 | vq.sequence = sequence; 6 | vq.parallel = parallel; 7 | vq.stop = stop; 8 | 9 | assign(vq, event); 10 | 11 | module.exports = vq; 12 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unify the given function to callback contination style 3 | */ 4 | export function unify(fn) { 5 | return function(done) { 6 | if (typeof fn !== 'function') return done(); 7 | 8 | if (fn.length > 0) { 9 | // Ensure there is a callback function as 1st argument 10 | return fn(done); 11 | } 12 | 13 | const res = fn(); 14 | 15 | // Wait until the function is terminated if the returned value is thenable 16 | if (res && typeof res.then === 'function') { 17 | return res.then(done); 18 | } 19 | 20 | // Just `done` if no asynchronous continuation method is given 21 | return done(); 22 | }; 23 | } 24 | 25 | export function clone(obj, deep = false) { 26 | const result = {}; 27 | 28 | Object.keys(obj).forEach(function(key) { 29 | if (deep && obj[key] && typeof obj[key] === 'object') { 30 | result[key] = clone(obj[key]); 31 | } else { 32 | result[key] = obj[key]; 33 | } 34 | }); 35 | 36 | return result; 37 | } 38 | 39 | export function assign(target, ...sources) { 40 | sources.forEach(function(source) { 41 | Object.keys(source).forEach(function(key) { 42 | target[key] = source[key]; 43 | }); 44 | }); 45 | 46 | return target; 47 | } 48 | 49 | export function on(el, name, f) { 50 | el.addEventListener(name, f); 51 | } 52 | 53 | export function off(el, name, f) { 54 | el.removeEventListener(name, f); 55 | } 56 | 57 | export function noop() {} 58 | -------------------------------------------------------------------------------- /src/vq.js: -------------------------------------------------------------------------------- 1 | import chain from './chain'; 2 | import {unify, clone, noop} from './utils'; 3 | import {stop as _stop} from './animate'; 4 | 5 | export function vq(el, props, opts = null) { 6 | if (!el || !props) throw new Error('Must have two or three args'); 7 | 8 | if (!opts) { 9 | if (!('p' in props && 'o' in props)) { 10 | throw new Error('2nd arg must have `p` and `o` property when only two args is given'); 11 | } 12 | 13 | opts = props.o; 14 | props = props.p; 15 | } 16 | 17 | // use `props` as progress callback if it is a function 18 | if (typeof props === 'function') { 19 | opts.progress = props; 20 | props = { 21 | tween: [1, 0] 22 | }; 23 | } 24 | 25 | // Avoid changing original props and opts 26 | // vq may mutate these values internally 27 | props = clone(props); 28 | opts = clone(opts); 29 | 30 | return chain(el, props, opts); 31 | } 32 | 33 | export function sequence(seq) { 34 | return function(done) { 35 | // Do not use ES default parameters because the babel eliminates actual arguments. 36 | // Then we cannot detect whether the callback is set or not. 37 | if (typeof done !== 'function') done = noop; 38 | 39 | sequenceImpl(seq, done); 40 | }; 41 | } 42 | 43 | export function parallel(fns) { 44 | let waiting = fns.length; 45 | 46 | return function(done) { 47 | // Do not use ES default parameters because the babel eliminates actual arguments. 48 | // Then we cannot detect whether the callback is set or not. 49 | if (typeof done !== 'function') done = noop; 50 | 51 | const listener = function listener() { 52 | --waiting; 53 | if (waiting === 0) done(); 54 | }; 55 | 56 | fns.map(unify).forEach(fn => fn(listener)); 57 | }; 58 | } 59 | 60 | export function stop(els) { 61 | return function(done) { 62 | if (typeof done !== 'function') done = noop; 63 | 64 | els.forEach(_stop); 65 | done(); 66 | }; 67 | } 68 | 69 | function sequenceImpl(seq, done) { 70 | if (seq.length === 0) return done(); 71 | 72 | const head = unify(seq[0]); 73 | const tail = seq.slice(1); 74 | 75 | return head(() => sequenceImpl(tail, done)); 76 | } 77 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | mocha: true 4 | -------------------------------------------------------------------------------- /test/event.js: -------------------------------------------------------------------------------- 1 | import assert from 'power-assert'; 2 | import {Promise} from 'es6-promise'; 3 | 4 | import {element, delay} from '../src/event'; 5 | 6 | describe('Event helpers:', () => { 7 | describe('element', () => { 8 | let input; 9 | 10 | beforeEach(() => { 11 | input = document.createElement('input'); 12 | }); 13 | 14 | it('executes callback if the event is emitted', (done) => { 15 | const f = element(input, 'input'); 16 | f(() => done())(); 17 | 18 | emit(input, 'input'); 19 | }); 20 | 21 | it('executes callback only one time', () => { 22 | const f = element(input, 'input'); 23 | let count = 0; 24 | f(() => count++)(); 25 | 26 | emit(input, 'input'); 27 | emit(input, 'input'); 28 | emit(input, 'input'); 29 | 30 | assert(count === 1); 31 | }); 32 | 33 | it('filters events with filter function', () => { 34 | const f = element(input, 'input', (event) => event.target.value === 'filter'); 35 | let count = 0; 36 | f(() => count++)(); 37 | 38 | emit(input, 'input'); 39 | assert(count === 0); 40 | 41 | input.value = 'filter'; 42 | emit(input, 'input'); 43 | assert(count === 1); 44 | }); 45 | 46 | it('detects the finish of listener function by callback or promise', (done) => { 47 | const f = element(input, 'input'); 48 | 49 | let normal = false; 50 | // normal callback 51 | f(() => {})(() => normal = true); 52 | emit(input, 'input'); 53 | assert(normal); 54 | 55 | // callack with completion callback 56 | let cb = false; 57 | f((done) => { 58 | setTimeout(done, 50); 59 | })(() => cb = true); 60 | emit(input, 'input'); 61 | assert(!cb); 62 | setTimeout(() => assert(cb), 50); 63 | 64 | // callback with promise 65 | let p = false; 66 | f(() => { 67 | return new Promise(resolve => { 68 | setTimeout(resolve, 100); 69 | }); 70 | })(() => p = true); 71 | emit(input, 'input'); 72 | assert(!p); 73 | setTimeout(() => assert(p), 100); 74 | 75 | setTimeout(done, 150); 76 | }); 77 | }); 78 | 79 | describe('delay', () => { 80 | 81 | it('delays the given function', (done) => { 82 | const ms = 50; 83 | let flag = false; 84 | 85 | delay(ms)(() => flag = true)(); 86 | assert(flag === false); 87 | setTimeout(() => { 88 | assert(flag === true); 89 | done(); 90 | }, ms); 91 | }); 92 | }); 93 | }); 94 | 95 | function emit(el, name) { 96 | const event = new Event(name); 97 | el.dispatchEvent(event); 98 | } 99 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import spy from './velocity-spy'; 2 | import assert from 'power-assert'; 3 | import {Promise} from 'es6-promise'; 4 | import vq from '../src/index'; 5 | 6 | describe('Index:', () => { 7 | let el; 8 | 9 | beforeEach(() => { 10 | el = document.createElement('div'); 11 | }); 12 | 13 | describe('vq function', () => { 14 | const props = { width: 300, opacity: [1, 0] }; 15 | const opts = { duration: 200, easing: 'easeOutQuad' }; 16 | const prog = function() {}; 17 | 18 | it('returns function object', () => { 19 | assert(typeof vq(el, {}, {}) === 'function'); 20 | }); 21 | 22 | it('through parameters to the Velocity function', () => { 23 | const result = spy(); 24 | 25 | vq(el, props, opts)(); 26 | 27 | const args = result[0].args; 28 | assert(args[0] === el); 29 | Object.keys(props).forEach((key) => assert(args[1][key] === props[key])); 30 | Object.keys(opts).forEach((key) => assert(args[2][key] === opts[key])); 31 | }); 32 | 33 | it('accepts single object format', () => { 34 | const result = spy(); 35 | 36 | vq(el, { p: props, o: opts })(); 37 | 38 | const args = result[0].args; 39 | assert(args[0] === el); 40 | Object.keys(props).forEach((key) => assert(args[1][key] === props[key])); 41 | Object.keys(opts).forEach((key) => assert(args[2][key] === opts[key])); 42 | }); 43 | 44 | it('accepts progress function', () => { 45 | const result = spy(); 46 | 47 | vq(el, prog, opts)(); 48 | 49 | // should set tween value to [1, 0] 50 | assert(result[0].args[1].tween[0] === 1); 51 | assert(result[0].args[1].tween[1] === 0); 52 | 53 | assert(result[0].args[2].progress === prog); 54 | }); 55 | 56 | it('accepts progress function as single object format', () => { 57 | const result = spy(); 58 | 59 | vq(el, { p: prog, o: opts })(); 60 | 61 | // should set tween value to [1, 0] 62 | assert(result[0].args[1].tween[0] === 1); 63 | assert(result[0].args[1].tween[1] === 0); 64 | 65 | assert(result[0].args[2].progress === prog); 66 | }); 67 | 68 | it('throws an error if the arg length is less than two', () => { 69 | const message = /Must have two or three args/; 70 | 71 | assert.throws(() => { 72 | vq(); 73 | }, message); 74 | 75 | assert.throws(() => { 76 | vq(el); 77 | }, message); 78 | }); 79 | 80 | it('throws an error if it receives two args and the 2nd arg is not single object format', () => { 81 | assert.throws(() => { 82 | vq(el, props); 83 | }, /2nd arg must have `p` and `o` property when only two args is given/); 84 | }); 85 | 86 | it('receives callback function to notify completion', () => { 87 | const fn = function() {}; 88 | const result = spy(); 89 | 90 | vq(el, props, opts)(fn); 91 | 92 | assert(result[0].args[2].complete === fn); 93 | }); 94 | }); 95 | 96 | describe('vq.sequence function', () => { 97 | 98 | it('returns thunk function', () => { 99 | assert(typeof vq.sequence([]) === 'function'); 100 | }); 101 | 102 | it('should call the callback after the all functions are finished', (done) => { 103 | let res = 0; 104 | 105 | vq.sequence([ 106 | () => res += 1, 107 | () => res += 2, 108 | () => res += 3, 109 | () => assert(res === 6) 110 | ])(done); 111 | }); 112 | 113 | it('executes the given functions sequentially', (done) => { 114 | const res = []; 115 | 116 | vq.sequence([ 117 | () => res.push(1), 118 | () => res.push(2), 119 | () => res.push(3), 120 | () => { 121 | assert.deepEqual(res, [1, 2, 3]); 122 | done(); 123 | } 124 | ])(); 125 | }); 126 | 127 | it('handles asyncronous functions by callbacks', (done) => { 128 | const res = []; 129 | 130 | vq.sequence([ 131 | (done) => { 132 | setTimeout(() => { 133 | res.push(1); 134 | done(); 135 | }, 1); 136 | }, 137 | () => res.push(2), 138 | () => { 139 | assert.deepEqual(res, [1, 2]); 140 | done(); 141 | } 142 | ])(); 143 | }); 144 | 145 | it('handles asyncronous functions by promises', (done) => { 146 | const res = []; 147 | 148 | vq.sequence([ 149 | () => new Promise((resolve) => { 150 | setTimeout(() => { 151 | res.push(1); 152 | resolve(); 153 | }, 1); 154 | }), 155 | () => res.push(2), 156 | () => { 157 | assert.deepEqual(res, [1, 2]); 158 | done(); 159 | } 160 | ])(); 161 | }); 162 | 163 | it('ignores non-function objects', () => { 164 | const res = []; 165 | 166 | vq.sequence([ 167 | () => res.push(1), 168 | null, 169 | () => res.push(2), 170 | 'this string will be ignored', 171 | () => res.push(3), 172 | 12345, 173 | () => res.push(4), 174 | undefined, 175 | () => res.push(5) 176 | ])(); 177 | 178 | assert.deepEqual(res, [1, 2, 3, 4, 5]); 179 | }); 180 | 181 | it('ignores non-function objects even if edge case', () => { 182 | let res = false; 183 | 184 | vq.sequence([ 185 | null, 186 | () => res = true, 187 | undefined 188 | ])(); 189 | 190 | assert(res); 191 | }); 192 | 193 | it('does nothing if no callback is given', () => { 194 | assert.doesNotThrow(() => { 195 | vq.sequence([ 196 | () => {} 197 | ])(); 198 | }); 199 | }); 200 | }); 201 | 202 | describe('vq.parallel function', () => { 203 | it('returns thunk function', () => { 204 | assert(typeof vq.parallel([]) === 'function'); 205 | }); 206 | 207 | it('calls given functions in parallel', () => { 208 | /* eslint no-unused-vars: 0 */ 209 | let sum = 0; 210 | 211 | vq.parallel([ 212 | (done) => { sum += 1; }, 213 | () => new Promise(() => { sum += 2; }), 214 | () => sum += 3 215 | ])(); 216 | 217 | assert(sum === 6); 218 | }); 219 | 220 | it('should call the callback after all functions are finished', (done) => { 221 | let sum = 0; 222 | 223 | vq.parallel([ 224 | (done) => { 225 | setTimeout(() => { 226 | sum += 2; 227 | assert(sum === 3); 228 | done(); 229 | }, 5); 230 | }, 231 | 232 | () => new Promise((resolve) => { 233 | setTimeout(() => { 234 | sum += 3; 235 | assert(sum === 6); 236 | resolve(); 237 | }, 10); 238 | }), 239 | 240 | () => { 241 | sum += 1; 242 | assert(sum === 1); 243 | } 244 | ])(done); 245 | }); 246 | 247 | it('does nothing if no callback is given', () => { 248 | assert.doesNotThrow(() => { 249 | vq.parallel([ 250 | () => {} 251 | ])(); 252 | }); 253 | }); 254 | }); 255 | 256 | describe('vq.stop function', () => { 257 | it('returns thunk function', () => { 258 | const el = document.createElement('div'); 259 | assert(typeof vq.stop([el]) === 'function'); 260 | }); 261 | 262 | it('emits stop request for all elements', () => { 263 | const result = spy(); 264 | 265 | const els = [ 266 | document.createElement('div'), 267 | document.createElement('p'), 268 | document.createElement('span') 269 | ]; 270 | 271 | vq.stop(els)(); 272 | 273 | result.forEach(({ args }, i) => { 274 | assert.deepEqual(args, [els[i], 'stop']); 275 | }); 276 | }); 277 | 278 | it('call callback function after stopping elements', (done) => { 279 | vq.stop([document.createElement('div')])(done); 280 | }); 281 | }); 282 | }); 283 | -------------------------------------------------------------------------------- /test/velocity-spy.js: -------------------------------------------------------------------------------- 1 | window.Velocity = function(...args) { 2 | velocityStub.__stub.push({ args }); 3 | }; 4 | 5 | function velocityStub() { 6 | velocityStub.__stub = []; 7 | 8 | return velocityStub.__stub; 9 | } 10 | 11 | export default velocityStub; 12 | -------------------------------------------------------------------------------- /testem.yml: -------------------------------------------------------------------------------- 1 | --- 2 | framework: mocha 3 | src_files: 4 | - .tmp/test.js 5 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const conf = require('./webpack.config'); 3 | 4 | conf.watch = true; 5 | conf.debug = true; 6 | conf.devtool = 'source-map'; 7 | 8 | module.exports = conf; 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | context: path.resolve(__dirname, 'src'), 6 | entry: './index.js', 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'vq.js', 10 | library: 'vq', 11 | libraryTarget: 'umd' 12 | }, 13 | resolve: { 14 | modulesDirectories: ['node_modules'], 15 | extensions: ['', '.js'] 16 | }, 17 | module: { 18 | loaders: [ 19 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ } 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /webpack.config.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const path = require('path'); 3 | const glob = require('glob'); 4 | 5 | module.exports = { 6 | context: path.resolve(__dirname), 7 | entry: glob.sync('./test/**/*.js'), 8 | output: { 9 | path: path.resolve(__dirname, '.tmp'), 10 | filename: 'test.js' 11 | }, 12 | resolve: { 13 | modulesDirectories: ['node_modules'], 14 | extensions: ['', '.js', '.json'] 15 | }, 16 | debug: true, 17 | devtool: 'source-map', 18 | module: { 19 | loaders: [ 20 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, 21 | { test: /\.json$/, loader: 'json-loader' } 22 | ] 23 | } 24 | }; 25 | --------------------------------------------------------------------------------