1 /**
  2  * @fileOverview animation framework for KISSY
  3  * @author   yiminghe@gmail.com,lifesinger@gmail.com
  4  */
  5 KISSY.add('anim/base', function (S, DOM, Event, Easing, UA, AM, Fx, Q) {
  6 
  7     var camelCase = DOM._camelCase,
  8         specialVals = ["hide", "show", "toggle"],
  9     // shorthand css properties
 10         SHORT_HANDS = {
 11             // http://www.w3.org/Style/CSS/Tracker/issues/9
 12             // http://snook.ca/archives/html_and_css/background-position-x-y
 13             // backgroundPositionX  backgroundPositionY does not support
 14             background:[
 15                 "backgroundPosition"
 16             ],
 17             border:[
 18                 "borderBottomWidth",
 19                 "borderLeftWidth",
 20                 'borderRightWidth',
 21                 // 'borderSpacing', 组合属性?
 22                 'borderTopWidth'
 23             ],
 24             borderBottom:["borderBottomWidth"],
 25             borderLeft:["borderLeftWidth"],
 26             borderTop:["borderTopWidth"],
 27             borderRight:["borderRightWidth"],
 28             font:[
 29                 'fontSize',
 30                 'fontWeight'
 31             ],
 32             margin:[
 33                 'marginBottom',
 34                 'marginLeft',
 35                 'marginRight',
 36                 'marginTop'
 37             ],
 38             padding:[
 39                 'paddingBottom',
 40                 'paddingLeft',
 41                 'paddingRight',
 42                 'paddingTop'
 43             ]
 44         },
 45         defaultConfig = {
 46             duration:1,
 47             easing:'easeNone'
 48         },
 49         rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i;
 50 
 51     Anim.SHORT_HANDS = SHORT_HANDS;
 52 
 53 
 54     /**
 55      * @class A class for constructing animation instances.
 56      * @param {HTMLElement|window} elem Html dom node or window
 57      * (window can only animate scrollTop/scrollLeft)
 58      * @param {Object} props style map
 59      * @param {Number|Object} [duration] duration(s) or anim config
 60      * @param {String|Function} [duration.easing] easing fn or string
 61      * @param {Function} [duration.complete] callback function when this animation is complete
 62      * @param {Number} [duration.duration] duration(s)
 63      * @param {String|Boolean} [duration.queue] current animation's queue, if false then no queue
 64      * @param {Function|String} [easing] easing fn or string
 65      * @param {Function} [callback] callback function when this animation is complete
 66      * @extends Event.Target
 67      * @name Anim
 68      *
 69      */
 70     function Anim(elem, props, duration, easing, callback) {
 71         var self = this, config;
 72 
 73         // ignore non-exist element
 74         if (!(elem = DOM.get(elem))) {
 75             return;
 76         }
 77 
 78         // factory or constructor
 79         if (!(self instanceof Anim)) {
 80             return new Anim(elem, props, duration, easing, callback);
 81         }
 82 
 83         /**
 84          * the transition properties
 85          */
 86         if (S.isString(props)) {
 87             props = S.unparam(String(props), ";", ":");
 88         } else {
 89             // clone to prevent collision within multiple instance
 90             props = S.clone(props);
 91         }
 92 
 93         /**
 94          * 驼峰属性名
 95          */
 96         for (var prop in props) {
 97             var camelProp = camelCase(S.trim(prop));
 98             if (prop != camelProp) {
 99                 props[camelProp] = props[prop];
100                 delete props[prop];
101             }
102         }
103 
104         /**
105          * animation config
106          */
107         if (S.isPlainObject(duration)) {
108             config = S.clone(duration);
109         } else {
110             config = {
111                 duration:parseFloat(duration) || undefined,
112                 easing:easing,
113                 complete:callback
114             };
115         }
116 
117         config = S.merge(defaultConfig, config);
118         self.config = config;
119         config.duration *= 1000;
120 
121         // domEl deprecated!
122         self.elem = self['domEl'] = elem;
123         self.props = props;
124 
125         // 实例属性
126         self._backupProps = {};
127         self._fxs = {};
128 
129         // register callback
130         self.on("complete", onComplete);
131     }
132 
133 
134     function onComplete(e) {
135         var self = this,
136             _backupProps,
137             config = self.config;
138 
139         // only recover after complete anim
140         if (!S.isEmptyObject(_backupProps = self._backupProps)) {
141             DOM.css(self.elem, _backupProps);
142         }
143 
144         if (config.complete) {
145             config.complete.call(self, e);
146         }
147     }
148 
149     function runInternal() {
150         var self = this,
151             config = self.config,
152             _backupProps = self._backupProps,
153             elem = self.elem,
154             elemStyle,
155             hidden,
156             val,
157             prop,
158             specialEasing = (config['specialEasing'] || {}),
159             fxs = self._fxs,
160             props = self.props;
161 
162         // 进入该函数即代表执行(q[0] 已经是 ...)
163         saveRunning(self);
164 
165         if (self.fire("start") === false) {
166             // no need to invoke complete
167             self.stop(0);
168             return;
169         }
170 
171         if (elem.nodeType == DOM.ELEMENT_NODE) {
172             hidden = (DOM.css(elem, "display") === "none");
173             for (prop in props) {
174                 val = props[prop];
175                 // 直接结束
176                 if (val == "hide" && hidden || val == 'show' && !hidden) {
177                     // need to invoke complete
178                     self.stop(1);
179                     return;
180                 }
181             }
182         }
183 
184         // 放在前面,设置 overflow hidden,否则后面 ie6  取 width/height 初值导致错误
185         // <div style='width:0'><div style='width:100px'></div></div>
186         if (elem.nodeType == DOM.ELEMENT_NODE &&
187             (props.width || props.height)) {
188             // Make sure that nothing sneaks out
189             // Record all 3 overflow attributes because IE does not
190             // change the overflow attribute when overflowX and
191             // overflowY are set to the same value
192             elemStyle = elem.style;
193             S.mix(_backupProps, {
194                 overflow:elemStyle.overflow,
195                 overflow-x:elemStyle.overflowX,
196                 overflow-y:elemStyle.overflowY
197             });
198             elemStyle.overflow = "hidden";
199             // inline element should has layout/inline-block
200             if (DOM.css(elem, "display") === "inline" &&
201                 DOM.css(elem, "float") === "none") {
202                 if (UA['ie']) {
203                     elemStyle.zoom = 1;
204                 } else {
205                     elemStyle.display = "inline-block";
206                 }
207             }
208         }
209 
210         // 分离 easing
211         S.each(props, function (val, prop) {
212             if (!props.hasOwnProperty(prop)) {
213                 return;
214             }
215             var easing;
216             if (S.isArray(val)) {
217                 easing = specialEasing[prop] = val[1];
218                 props[prop] = val[0];
219             } else {
220                 easing = specialEasing[prop] = (specialEasing[prop] || config.easing);
221             }
222             if (S.isString(easing)) {
223                 easing = specialEasing[prop] = Easing[easing];
224             }
225             specialEasing[prop] = easing || Easing['easeNone'];
226         });
227 
228 
229         // 扩展分属性
230         S.each(SHORT_HANDS, function (shortHands, p) {
231             var sh,
232                 origin,
233                 val;
234             if (val = props[p]) {
235                 origin = {};
236                 S.each(shortHands, function (sh) {
237                     // 得到原始分属性之前值
238                     origin[sh] = DOM.css(elem, sh);
239                     specialEasing[sh] = specialEasing[p];
240                 });
241                 DOM.css(elem, p, val);
242                 for (sh in origin) {
243                     // 得到期待的分属性最后值
244                     props[sh] = DOM.css(elem, sh);
245                     // 还原
246                     DOM.css(elem, sh, origin[sh]);
247                 }
248                 // 删除复合属性
249                 delete props[p];
250             }
251         });
252 
253         // 取得单位,并对单个属性构建 Fx 对象
254         for (prop in props) {
255 
256             if (!props.hasOwnProperty(prop)) {
257                 continue;
258             }
259 
260             val = S.trim(props[prop]);
261 
262             var to,
263                 from,
264                 propCfg = {
265                     prop:prop,
266                     anim:self,
267                     easing:specialEasing[prop]
268                 },
269                 fx = Fx.getFx(propCfg);
270 
271             // hide/show/toggle : special treat!
272             if (S.inArray(val, specialVals)) {
273                 // backup original value
274                 _backupProps[prop] = DOM.style(elem, prop);
275                 if (val == "toggle") {
276                     val = hidden ? "show" : "hide";
277                 }
278                 if (val == "hide") {
279                     to = 0;
280                     from = fx.cur();
281                     // 执行完后隐藏
282                     _backupProps.display = 'none';
283                 } else {
284                     from = 0;
285                     to = fx.cur();
286                     // prevent flash of content
287                     DOM.css(elem, prop, from);
288                     DOM.show(elem);
289                 }
290                 val = to;
291             } else {
292                 to = val;
293                 from = fx.cur();
294             }
295 
296             val += "";
297 
298             var unit = "",
299                 parts = val.match(rfxnum);
300 
301             if (parts) {
302                 to = parseFloat(parts[2]);
303                 unit = parts[3];
304 
305                 // 有单位但单位不是 px
306                 if (unit && unit !== "px") {
307                     DOM.css(elem, prop, val);
308                     from = (to / fx.cur()) * from;
309                     DOM.css(elem, prop, from + unit);
310                 }
311 
312                 // 相对
313                 if (parts[1]) {
314                     to = ( (parts[ 1 ] === "-=" ? -1 : 1) * to ) + from;
315                 }
316             }
317 
318             propCfg.from = from;
319             propCfg.to = to;
320             propCfg.unit = unit;
321             fx.load(propCfg);
322             fxs[prop] = fx;
323         }
324 
325         self._startTime = S.now();
326 
327         AM.start(self);
328     }
329 
330 
331     S.augment(Anim, Event.Target,
332         /**
333          * @lends Anim.prototype
334          */
335         {
336 
337             /**
338              * @return {Boolean} whether this animation is running
339              */
340             isRunning:function () {
341                 return isRunning(this);
342             },
343 
344             isPaused:function () {
345                 return isPaused(this);
346             },
347 
348             pause:function () {
349                 var self = this;
350                 if (self.isRunning()) {
351                     self._pauseDiff = S.now() - self._startTime;
352                     AM.stop(self);
353                     removeRunning(self);
354                     savePaused(self);
355                 }
356                 return self;
357             },
358 
359             resume:function () {
360                 var self = this;
361                 if (self.isPaused()) {
362                     self._startTime = S.now() - self._pauseDiff;
363                     removePaused(self);
364                     saveRunning(self);
365                     AM.start(self);
366                 }
367                 return self;
368             },
369 
370             _runInternal:runInternal,
371 
372             /**
373              * start this animation
374              */
375             run:function () {
376                 var self = this,
377                     queueName = self.config.queue;
378 
379                 if (queueName === false) {
380                     runInternal.call(self);
381                 } else {
382                     // 当前动画对象加入队列
383                     Q.queue(self);
384                 }
385 
386                 return self;
387             },
388 
389             _frame:function () {
390 
391                 var self = this,
392                     prop,
393                     config = self.config,
394                     end = 1,
395                     c,
396                     fx,
397                     fxs = self._fxs;
398 
399                 for (prop in fxs) {
400                     if (fxs.hasOwnProperty(prop) &&
401                         // 当前属性没有结束
402                         !((fx = fxs[prop]).finished)) {
403                         // 非短路
404                         if (config.frame) {
405                             c = config.frame(fx);
406                         }
407                         // 结束
408                         if (c == 1 ||
409                             // 不执行自带
410                             c == 0) {
411                             fx.finished = c;
412                             end &= c;
413                         } else {
414                             end &= fx.frame();
415                             // 最后通知下
416                             if (end && config.frame) {
417                                 config.frame(fx);
418                             }
419                         }
420                     }
421                 }
422 
423                 if ((self.fire("step") === false) || end) {
424                     // complete 事件只在动画到达最后一帧时才触发
425                     self.stop(end);
426                 }
427             },
428 
429             /**
430              * stop this animation
431              * @param {Boolean} [finish] whether jump to the last position of this animation
432              */
433             stop:function (finish) {
434                 var self = this,
435                     config = self.config,
436                     queueName = config.queue,
437                     prop,
438                     fx,
439                     fxs = self._fxs;
440 
441                 // already stopped
442                 if (!self.isRunning()) {
443                     // 从自己的队列中移除
444                     if (queueName !== false) {
445                         Q.remove(self);
446                     }
447                     return;
448                 }
449 
450                 if (finish) {
451                     for (prop in fxs) {
452                         if (fxs.hasOwnProperty(prop) &&
453                             // 当前属性没有结束
454                             !((fx = fxs[prop]).finished)) {
455                             // 非短路
456                             if (config.frame) {
457                                 config.frame(fx, 1);
458                             } else {
459                                 fx.frame(1);
460                             }
461                         }
462                     }
463                     self.fire("complete");
464                 }
465 
466                 AM.stop(self);
467 
468                 removeRunning(self);
469 
470                 if (queueName !== false) {
471                     // notify next anim to run in the same queue
472                     Q.dequeue(self);
473                 }
474 
475                 return self;
476             }
477         });
478 
479     var runningKey = S.guid("ks-anim-unqueued-" + S.now() + "-");
480 
481     function saveRunning(anim) {
482         var elem = anim.elem,
483             allRunning = DOM.data(elem, runningKey);
484         if (!allRunning) {
485             DOM.data(elem, runningKey, allRunning = {});
486         }
487         allRunning[S.stamp(anim)] = anim;
488     }
489 
490     function removeRunning(anim) {
491         var elem = anim.elem,
492             allRunning = DOM.data(elem, runningKey);
493         if (allRunning) {
494             delete allRunning[S.stamp(anim)];
495             if (S.isEmptyObject(allRunning)) {
496                 DOM.removeData(elem, runningKey);
497             }
498         }
499     }
500 
501     function isRunning(anim) {
502         var elem = anim.elem,
503             allRunning = DOM.data(elem, runningKey);
504         if (allRunning) {
505             return !!allRunning[S.stamp(anim)];
506         }
507         return 0;
508     }
509 
510 
511     var pausedKey = S.guid("ks-anim-paused-" + S.now() + "-");
512 
513     function savePaused(anim) {
514         var elem = anim.elem,
515             paused = DOM.data(elem, pausedKey);
516         if (!paused) {
517             DOM.data(elem, pausedKey, paused = {});
518         }
519         paused[S.stamp(anim)] = anim;
520     }
521 
522     function removePaused(anim) {
523         var elem = anim.elem,
524             paused = DOM.data(elem, pausedKey);
525         if (paused) {
526             delete paused[S.stamp(anim)];
527             if (S.isEmptyObject(paused)) {
528                 DOM.removeData(elem, pausedKey);
529             }
530         }
531     }
532 
533     function isPaused(anim) {
534         var elem = anim.elem,
535             paused = DOM.data(elem, pausedKey);
536         if (paused) {
537             return !!paused[S.stamp(anim)];
538         }
539         return 0;
540     }
541 
542     /**
543      * stop all the anims currently running
544      * @param {HTMLElement} elem element which anim belongs to
545      * @param {Boolean} end whether jump to last position
546      * @param {Boolean} clearQueue whether clean current queue
547      * @param {String|Boolean} queueName current queue's name to be cleared
548      * @private
549      */
550     Anim.stop = function (elem, end, clearQueue, queueName) {
551         if (
552         // default queue
553             queueName === null ||
554                 // name of specified queue
555                 S.isString(queueName) ||
556                 // anims not belong to any queue
557                 queueName === false
558             ) {
559             return stopQueue.apply(undefined, arguments);
560         }
561         // first stop first anim in queues
562         if (clearQueue) {
563             Q.removeQueues(elem);
564         }
565         var allRunning = DOM.data(elem, runningKey),
566         // can not stop in for/in , stop will modified allRunning too
567             anims = S.merge(allRunning);
568         for (var k in anims) {
569             anims[k].stop(end);
570         }
571     };
572 
573     S.each(["pause", "resume"], function (action) {
574         Anim[action] = function (elem, queueName) {
575             if (
576             // default queue
577                 queueName === null ||
578                     // name of specified queue
579                     S.isString(queueName) ||
580                     // anims not belong to any queue
581                     queueName === false
582                 ) {
583                 return pauseResumeQueue(elem, queueName, action);
584             }
585             pauseResumeQueue(elem, undefined, action);
586         };
587     });
588 
589     function pauseResumeQueue(elem, queueName, action) {
590         var allAnims = DOM.data(elem, action == 'resume' ? pausedKey : runningKey),
591         // can not stop in for/in , stop will modified allRunning too
592             anims = S.merge(allAnims);
593         for (var k in anims) {
594             var anim = anims[k];
595             if (queueName === undefined ||
596                 anim.config.queue == queueName) {
597                 anim[action]();
598             }
599         }
600     }
601 
602     /**
603      *
604      * @param elem element which anim belongs to
605      * @param queueName queue'name if set to false only remove
606      * @param end
607      * @param clearQueue
608      * @private
609      */
610     function stopQueue(elem, end, clearQueue, queueName) {
611         if (clearQueue && queueName !== false) {
612             Q.removeQueue(elem, queueName);
613         }
614         var allRunning = DOM.data(elem, runningKey),
615             anims = S.merge(allRunning);
616         for (var k in anims) {
617             var anim = anims[k];
618             if (anim.config.queue == queueName) {
619                 anim.stop(end);
620             }
621         }
622     }
623 
624     /**
625      * whether elem is running anim
626      * @param {HTMLElement} elem
627      * @private
628      */
629     Anim.isRunning = function (elem) {
630         var allRunning = DOM.data(elem, runningKey);
631         return allRunning && !S.isEmptyObject(allRunning);
632     };
633 
634     /**
635      * whether elem has paused anim
636      * @param {HTMLElement} elem
637      * @private
638      */
639     Anim.isPaused = function (elem) {
640         var paused = DOM.data(elem, pausedKey);
641         return paused && !S.isEmptyObject(paused);
642     };
643 
644     Anim.Q = Q;
645 
646     if (SHORT_HANDS) {
647     }
648     return Anim;
649 }, {
650     requires:["dom", "event", "./easing", "ua", "./manager", "./fx", "./queue"]
651 });
652 
653 /**
654  * 2011-11
655  * - 重构,抛弃 emile,优化性能,只对需要的属性进行动画
656  * - 添加 stop/stopQueue/isRunning,支持队列管理
657  *
658  * 2011-04
659  * - 借鉴 yui3 ,中央定时器,否则 ie6 内存泄露?
660  * - 支持配置 scrollTop/scrollLeft
661  *
662  *
663  * TODO:
664  *  - 效率需要提升,当使用 nativeSupport 时仍做了过多动作
665  *  - opera nativeSupport 存在 bug ,浏览器自身 bug ?
666  *  - 实现 jQuery Effects 的 queue / specialEasing / += / 等特性
667  *
668  * NOTES:
669  *  - 与 emile 相比,增加了 borderStyle, 使得 border: 5px solid #ccc 能从无到有,正确显示
670  *  - api 借鉴了 YUI, jQuery 以及 http://www.w3.org/TR/css3-transitions/
671  *  - 代码实现了借鉴了 Emile.js: http://github.com/madrobby/emile *
672  */
673