Nav apraksta

animit.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. /*
  2. Copyright 2013-2015 ASIAL CORPORATION
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. /**
  14. * Minimal animation library for managing css transition on mobile browsers.
  15. */
  16. 'use strict';
  17. import _Object$keys from 'babel-runtime/core-js/object/keys';
  18. import _setImmediate from 'babel-runtime/core-js/set-immediate';
  19. var TIMEOUT_RATIO = 1.4;
  20. var util = {};
  21. // capitalize string
  22. util.capitalize = function (str) {
  23. return str.charAt(0).toUpperCase() + str.slice(1);
  24. };
  25. /**
  26. * @param {Object} params
  27. * @param {String} params.property
  28. * @param {Float} params.duration
  29. * @param {String} params.timing
  30. */
  31. util.buildTransitionValue = function (params) {
  32. params.property = params.property || 'all';
  33. params.duration = params.duration || 0.4;
  34. params.timing = params.timing || 'linear';
  35. var props = params.property.split(/ +/);
  36. return props.map(function (prop) {
  37. return prop + ' ' + params.duration + 's ' + params.timing;
  38. }).join(', ');
  39. };
  40. /**
  41. * Add an event handler on "transitionend" event.
  42. */
  43. util.onceOnTransitionEnd = function (element, callback) {
  44. if (!element) {
  45. return function () {};
  46. }
  47. var fn = function fn(event) {
  48. if (element == event.target) {
  49. event.stopPropagation();
  50. removeListeners();
  51. callback();
  52. }
  53. };
  54. var removeListeners = function removeListeners() {
  55. util._transitionEndEvents.forEach(function (eventName) {
  56. element.removeEventListener(eventName, fn, false);
  57. });
  58. };
  59. util._transitionEndEvents.forEach(function (eventName) {
  60. element.addEventListener(eventName, fn, false);
  61. });
  62. return removeListeners;
  63. };
  64. util._transitionEndEvents = function () {
  65. if ('ontransitionend' in window) {
  66. return ['transitionend'];
  67. }
  68. if ('onwebkittransitionend' in window) {
  69. return ['webkitTransitionEnd'];
  70. }
  71. if (util.vendorPrefix === 'webkit' || util.vendorPrefix === 'o' || util.vendorPrefix === 'moz' || util.vendorPrefix === 'ms') {
  72. return [util.vendorPrefix + 'TransitionEnd', 'transitionend'];
  73. }
  74. return [];
  75. }();
  76. util._cssPropertyDict = function () {
  77. var styles = window.getComputedStyle(document.documentElement, '');
  78. var dict = {};
  79. var a = 'A'.charCodeAt(0);
  80. var z = 'z'.charCodeAt(0);
  81. var upper = function upper(s) {
  82. return s.substr(1).toUpperCase();
  83. };
  84. for (var i = 0; i < styles.length; i++) {
  85. var key = styles[i].replace(/^[-]+/, '').replace(/[-][a-z]/g, upper).replace(/^moz/, 'Moz');
  86. if (a <= key.charCodeAt(0) && z >= key.charCodeAt(0)) {
  87. if (key !== 'cssText' && key !== 'parentText') {
  88. dict[key] = true;
  89. }
  90. }
  91. }
  92. return dict;
  93. }();
  94. util.hasCssProperty = function (name) {
  95. return name in util._cssPropertyDict;
  96. };
  97. /**
  98. * Vendor prefix for css property.
  99. */
  100. util.vendorPrefix = function () {
  101. var styles = window.getComputedStyle(document.documentElement, ''),
  102. pre = (Array.prototype.slice.call(styles).join('').match(/-(moz|webkit|ms)-/) || styles.OLink === '' && ['', 'o'])[1];
  103. return pre;
  104. }();
  105. util.forceLayoutAtOnce = function (elements, callback) {
  106. this.batchImmediate(function () {
  107. elements.forEach(function (element) {
  108. // force layout
  109. element.offsetHeight;
  110. });
  111. callback();
  112. });
  113. };
  114. util.batchImmediate = function () {
  115. var callbacks = [];
  116. return function (callback) {
  117. if (callbacks.length === 0) {
  118. _setImmediate(function () {
  119. var concreateCallbacks = callbacks.slice(0);
  120. callbacks = [];
  121. concreateCallbacks.forEach(function (callback) {
  122. callback();
  123. });
  124. });
  125. }
  126. callbacks.push(callback);
  127. };
  128. }();
  129. util.batchAnimationFrame = function () {
  130. var callbacks = [];
  131. var raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
  132. setTimeout(callback, 1000 / 60);
  133. };
  134. return function (callback) {
  135. if (callbacks.length === 0) {
  136. raf(function () {
  137. var concreateCallbacks = callbacks.slice(0);
  138. callbacks = [];
  139. concreateCallbacks.forEach(function (callback) {
  140. callback();
  141. });
  142. });
  143. }
  144. callbacks.push(callback);
  145. };
  146. }();
  147. util.transitionPropertyName = function () {
  148. if (util.hasCssProperty('transitionDuration')) {
  149. return 'transition';
  150. }
  151. if (util.hasCssProperty(util.vendorPrefix + 'TransitionDuration')) {
  152. return util.vendorPrefix + 'Transition';
  153. }
  154. throw new Error('Invalid state');
  155. }();
  156. /**
  157. * @param {HTMLElement} element
  158. */
  159. var Animit = function Animit(element, defaults) {
  160. if (!(this instanceof Animit)) {
  161. return new Animit(element, defaults);
  162. }
  163. if (element instanceof HTMLElement) {
  164. this.elements = [element];
  165. } else if (Object.prototype.toString.call(element) === '[object Array]') {
  166. this.elements = element;
  167. } else {
  168. throw new Error('First argument must be an array or an instance of HTMLElement.');
  169. }
  170. this.defaults = defaults;
  171. this.transitionQueue = [];
  172. this.lastStyleAttributeDict = [];
  173. };
  174. Animit.prototype = {
  175. /**
  176. * @property {Array}
  177. */
  178. transitionQueue: undefined,
  179. /**
  180. * @property {Array}
  181. */
  182. elements: undefined,
  183. /**
  184. * @property {Object}
  185. */
  186. defaults: undefined,
  187. /**
  188. * Start animation sequence with passed animations.
  189. *
  190. * @param {Function} callback
  191. */
  192. play: function play(callback) {
  193. if (typeof callback === 'function') {
  194. this.transitionQueue.push(function (done) {
  195. callback();
  196. done();
  197. });
  198. }
  199. this.startAnimation();
  200. return this;
  201. },
  202. /**
  203. * Most of the animations follow this default process.
  204. *
  205. * @param {from} css or options object containing css
  206. * @param {to} css or options object containing css
  207. * @param {delay} delay to wait
  208. */
  209. default: function _default(from, to, delay) {
  210. function step(params, duration, timing) {
  211. if (params.duration !== undefined) {
  212. duration = params.duration;
  213. }
  214. if (params.timing !== undefined) {
  215. timing = params.timing;
  216. }
  217. return {
  218. css: params.css || params,
  219. duration: duration,
  220. timing: timing
  221. };
  222. }
  223. return this.saveStyle().queue(step(from, 0, this.defaults.timing)).wait(delay === undefined ? this.defaults.delay : delay).queue(step(to, this.defaults.duration, this.defaults.timing)).restoreStyle();
  224. },
  225. /**
  226. * Queue transition animations or other function.
  227. *
  228. * e.g. animit(elt).queue({color: 'red'})
  229. * e.g. animit(elt).queue({color: 'red'}, {duration: 0.4})
  230. * e.g. animit(elt).queue({css: {color: 'red'}, duration: 0.2})
  231. *
  232. * @param {Object|Animit.Transition|Function} transition
  233. * @param {Object} [options]
  234. */
  235. queue: function queue(transition, options) {
  236. var queue = this.transitionQueue;
  237. if (transition && options) {
  238. options.css = transition;
  239. transition = new Animit.Transition(options);
  240. }
  241. if (!(transition instanceof Function || transition instanceof Animit.Transition)) {
  242. if (transition.css) {
  243. transition = new Animit.Transition(transition);
  244. } else {
  245. transition = new Animit.Transition({
  246. css: transition
  247. });
  248. }
  249. }
  250. if (transition instanceof Function) {
  251. queue.push(transition);
  252. } else if (transition instanceof Animit.Transition) {
  253. queue.push(transition.build());
  254. } else {
  255. throw new Error('Invalid arguments');
  256. }
  257. return this;
  258. },
  259. /**
  260. * Queue transition animations.
  261. *
  262. * @param {Float} seconds
  263. */
  264. wait: function wait(seconds) {
  265. if (seconds > 0) {
  266. this.transitionQueue.push(function (done) {
  267. setTimeout(done, 1000 * seconds);
  268. });
  269. }
  270. return this;
  271. },
  272. saveStyle: function saveStyle() {
  273. this.transitionQueue.push(function (done) {
  274. this.elements.forEach(function (element, index) {
  275. var css = this.lastStyleAttributeDict[index] = {};
  276. for (var i = 0; i < element.style.length; i++) {
  277. css[element.style[i]] = element.style[element.style[i]];
  278. }
  279. }.bind(this));
  280. done();
  281. }.bind(this));
  282. return this;
  283. },
  284. /**
  285. * Restore element's style.
  286. *
  287. * @param {Object} [options]
  288. * @param {Float} [options.duration]
  289. * @param {String} [options.timing]
  290. * @param {String} [options.transition]
  291. */
  292. restoreStyle: function restoreStyle(options) {
  293. options = options || {};
  294. var self = this;
  295. if (options.transition && !options.duration) {
  296. throw new Error('"options.duration" is required when "options.transition" is enabled.');
  297. }
  298. var transitionName = util.transitionPropertyName;
  299. if (options.transition || options.duration && options.duration > 0) {
  300. var transitionValue = options.transition || 'all ' + options.duration + 's ' + (options.timing || 'linear');
  301. this.transitionQueue.push(function (done) {
  302. var elements = this.elements;
  303. var timeoutId;
  304. var clearTransition = function clearTransition() {
  305. elements.forEach(function (element) {
  306. element.style[transitionName] = '';
  307. });
  308. };
  309. // add "transitionend" event handler
  310. var removeListeners = util.onceOnTransitionEnd(elements[0], function () {
  311. clearTimeout(timeoutId);
  312. clearTransition();
  313. done();
  314. });
  315. // for fail safe.
  316. timeoutId = setTimeout(function () {
  317. removeListeners();
  318. clearTransition();
  319. done();
  320. }, options.duration * 1000 * TIMEOUT_RATIO);
  321. // transition and style settings
  322. elements.forEach(function (element, index) {
  323. var css = self.lastStyleAttributeDict[index];
  324. if (!css) {
  325. throw new Error('restoreStyle(): The style is not saved. Invoke saveStyle() before.');
  326. }
  327. self.lastStyleAttributeDict[index] = undefined;
  328. var name;
  329. for (var i = 0, len = element.style.length; i < len; i++) {
  330. name = element.style[i];
  331. if (css[name] === undefined) {
  332. css[name] = '';
  333. }
  334. }
  335. element.style[transitionName] = transitionValue;
  336. _Object$keys(css).forEach(function (key) {
  337. if (key !== transitionName) {
  338. element.style[key] = css[key];
  339. }
  340. });
  341. element.style[transitionName] = transitionValue;
  342. });
  343. });
  344. } else {
  345. this.transitionQueue.push(function (done) {
  346. reset();
  347. done();
  348. });
  349. }
  350. return this;
  351. function reset() {
  352. // Clear transition animation settings.
  353. self.elements.forEach(function (element, index) {
  354. element.style[transitionName] = 'none';
  355. var css = self.lastStyleAttributeDict[index];
  356. if (!css) {
  357. throw new Error('restoreStyle(): The style is not saved. Invoke saveStyle() before.');
  358. }
  359. self.lastStyleAttributeDict[index] = undefined;
  360. for (var i = 0, name = ''; i < element.style.length; i++) {
  361. name = element.style[i];
  362. if (typeof css[element.style[i]] === 'undefined') {
  363. css[element.style[i]] = '';
  364. }
  365. }
  366. _Object$keys(css).forEach(function (key) {
  367. element.style[key] = css[key];
  368. });
  369. });
  370. }
  371. },
  372. /**
  373. * Start animation sequence.
  374. */
  375. startAnimation: function startAnimation() {
  376. this._dequeueTransition();
  377. return this;
  378. },
  379. _dequeueTransition: function _dequeueTransition() {
  380. var transition = this.transitionQueue.shift();
  381. if (this._currentTransition) {
  382. throw new Error('Current transition exists.');
  383. }
  384. this._currentTransition = transition;
  385. var self = this;
  386. var called = false;
  387. var done = function done() {
  388. if (!called) {
  389. called = true;
  390. self._currentTransition = undefined;
  391. self._dequeueTransition();
  392. } else {
  393. throw new Error('Invalid state: This callback is called twice.');
  394. }
  395. };
  396. if (transition) {
  397. transition.call(this, done);
  398. }
  399. }
  400. };
  401. /**
  402. * @param {Animit} arguments
  403. */
  404. Animit.runAll = function () /* arguments... */{
  405. for (var i = 0; i < arguments.length; i++) {
  406. arguments[i].play();
  407. }
  408. };
  409. /**
  410. * @param {Object} options
  411. * @param {Float} [options.duration]
  412. * @param {String} [options.property]
  413. * @param {String} [options.timing]
  414. */
  415. Animit.Transition = function (options) {
  416. this.options = options || {};
  417. this.options.duration = this.options.duration || 0;
  418. this.options.timing = this.options.timing || 'linear';
  419. this.options.css = this.options.css || {};
  420. this.options.property = this.options.property || 'all';
  421. };
  422. Animit.Transition.prototype = {
  423. /**
  424. * @param {HTMLElement} element
  425. * @return {Function}
  426. */
  427. build: function build() {
  428. if (_Object$keys(this.options.css).length === 0) {
  429. throw new Error('options.css is required.');
  430. }
  431. var css = createActualCssProps(this.options.css);
  432. if (this.options.duration > 0) {
  433. var transitionValue = util.buildTransitionValue(this.options);
  434. var self = this;
  435. return function (callback) {
  436. var elements = this.elements;
  437. var timeout = self.options.duration * 1000 * TIMEOUT_RATIO;
  438. var timeoutId;
  439. var removeListeners = util.onceOnTransitionEnd(elements[0], function () {
  440. clearTimeout(timeoutId);
  441. callback();
  442. });
  443. timeoutId = setTimeout(function () {
  444. removeListeners();
  445. callback();
  446. }, timeout);
  447. elements.forEach(function (element) {
  448. element.style[util.transitionPropertyName] = transitionValue;
  449. _Object$keys(css).forEach(function (name) {
  450. element.style[name] = css[name];
  451. });
  452. });
  453. };
  454. }
  455. if (this.options.duration <= 0) {
  456. return function (callback) {
  457. var elements = this.elements;
  458. elements.forEach(function (element) {
  459. element.style[util.transitionPropertyName] = '';
  460. _Object$keys(css).forEach(function (name) {
  461. element.style[name] = css[name];
  462. });
  463. });
  464. if (elements.length > 0) {
  465. util.forceLayoutAtOnce(elements, function () {
  466. util.batchAnimationFrame(callback);
  467. });
  468. } else {
  469. util.batchAnimationFrame(callback);
  470. }
  471. };
  472. }
  473. function createActualCssProps(css) {
  474. var result = {};
  475. _Object$keys(css).forEach(function (name) {
  476. var value = css[name];
  477. if (util.hasCssProperty(name)) {
  478. result[name] = value;
  479. return;
  480. }
  481. var prefixed = util.vendorPrefix + util.capitalize(name);
  482. if (util.hasCssProperty(prefixed)) {
  483. result[prefixed] = value;
  484. } else {
  485. result[prefixed] = value;
  486. result[name] = value;
  487. }
  488. });
  489. return result;
  490. }
  491. }
  492. };
  493. export default Animit;