1 /**
  2  * @fileOverview Make Elements flow like waterfall.
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add("waterfall/base", function (S, Node, Base) {
  6 
  7     var $ = Node.all,
  8         win = S.Env.host,
  9         RESIZE_DURATION = 50;
 10 
 11     /**
 12      * @class
 13      * Make Elements flow like waterfall.
 14      * @name Waterfall
 15      */
 16     function Waterfall() {
 17         Waterfall.superclass.constructor.apply(this, arguments);
 18         this._init();
 19     }
 20 
 21 
 22     function timedChunk(items, process, context, callback) {
 23         var todo = [].concat(S.makeArray(items)),
 24             stopper = {},
 25             timer;
 26         if (todo.length > 0) {
 27             timer = setTimeout(function () {
 28                 var start = +new Date();
 29                 do {
 30                     var item = todo.shift();
 31                     process.call(context, item);
 32                 } while (todo.length > 0 && (+new Date() - start < 50));
 33 
 34                 if (todo.length > 0) {
 35                     timer = setTimeout(arguments.callee, 25);
 36                 } else {
 37                     callback && callback.call(context, items);
 38                 }
 39             }, 25);
 40         } else {
 41             callback && S.later(callback, 0, false, context, [items]);
 42         }
 43 
 44         stopper.stop = function () {
 45             if (timer) {
 46                 clearTimeout(timer);
 47                 todo = [];
 48                 items.each(function (item) {
 49                     item.stop();
 50                 });
 51             }
 52         };
 53 
 54         return stopper;
 55     }
 56 
 57 
 58     Waterfall.ATTRS =
 59     /**
 60      * @lends Waterfall
 61      */
 62     {
 63         /**
 64          * Container which contains waterfall elements.
 65          * @type Node
 66          */
 67         container:{
 68             setter:function (v) {
 69                 return $(v);
 70             }
 71         },
 72 
 73         /**
 74          * Array of height of current waterfall cols.
 75          * @protected
 76          * @type Number[]
 77          */
 78         curColHeights:{
 79             value:[]
 80         },
 81 
 82         /**
 83          * Horizontal alignment of waterfall items with container.
 84          * Enum: 'left','center','right'.
 85          * @type String
 86          * @since 1.3
 87          */
 88         align:{
 89             value:'center'
 90         },
 91 
 92         /**
 93          * Minimum col count of waterfall items.
 94          * Event window resize to 0.
 95          * Default: 1.
 96          * @type Number
 97          */
 98         minColCount:{
 99             value:1
100         },
101 
102         /**
103          * Effect config object when waterfall item is added to container.
104          * Default: { effect:"fadeIn",duration:1 }
105          * @type Object
106          * @example
107          * <code>
108          *      {
109          *          effect:'fadeIn', // or slideUp
110          *          duration:1 // unit is second
111          *      }
112          * </code>
113          */
114         effect:{
115             value:{
116                 effect:"fadeIn",
117                 duration:1
118             }
119         },
120 
121         /**
122          * Column's width.
123          * @type Number
124          */
125         colWidth:{},
126 
127         /**
128          * Waterfall items grouped by col.
129          * @private
130          * @type (Node[])[]
131          * @example
132          * <code>
133          *  [[node11,node12],[node21,node22]]
134          * </code>
135          */
136         colItems:{
137             value:[]
138         },
139 
140         /**
141          * Effect config object when waterfall item is adjusted on window resize.
142          * Default: { easing:"",duration:1 }
143          * @type Object
144          * @example
145          * <code>
146          *      {
147          *          easing:'', // easing type
148          *          duration:1 // unit is second
149          *      }
150          * </code>
151          */
152         adjustEffect:{}
153     };
154 
155     function doResize() {
156         var self = this, containerRegion = self._containerRegion || {};
157         // 宽度没变就没必要调整
158         if (containerRegion && self.get("container").width() === containerRegion.width) {
159             return
160         }
161         self.adjust();
162     }
163 
164     function recalculate() {
165         var self = this,
166             container = self.get("container"),
167             containerWidth = container.width(),
168             curColHeights = self.get("curColHeights");
169         // 当前列数
170         curColHeights.length = Math.max(parseInt(containerWidth / self.get("colWidth")),
171             self.get("minColCount"));
172         // 当前容器宽度
173         self._containerRegion = {
174             width:containerWidth
175         };
176         S.each(curColHeights, function (v, i) {
177             curColHeights[i] = 0;
178         });
179         self.set("colItems", []);
180     }
181 
182     function adjustItemAction(self, add, itemRaw, callback) {
183         var effect = self.get("effect"),
184             item = $(itemRaw),
185             align = self.get("align"),
186             curColHeights = self.get("curColHeights"),
187             container = self.get("container"),
188             curColCount = curColHeights.length,
189             col = 0,
190             containerRegion = self._containerRegion,
191             guard = Number.MAX_VALUE;
192 
193         if (!curColCount) {
194             return undefined;
195         }
196 
197         // 固定左边或右边
198         if (item.hasClass("ks-waterfall-fixed-left")) {
199             col = 0;
200         } else if (item.hasClass("ks-waterfall-fixed-right")) {
201             col = curColCount > 0 ? curColCount - 1 : 0;
202         } else {
203             // 否则找到最短的列
204             for (var i = 0; i < curColCount; i++) {
205                 if (curColHeights[i] < guard) {
206                     guard = curColHeights[i];
207                     col = i;
208                 }
209             }
210         }
211 
212         // 元素保持间隔不变,居中
213         var margin = align === 'left' ? 0 :
214                 Math.max(containerRegion.width - curColCount * self.get("colWidth"), 0),
215             colProp;
216 
217         if (align === 'center') {
218             margin /= 2;
219         }
220 
221         colProp = {
222             // 元素间固定间隔好点
223             left:col * self.get("colWidth") + margin,
224             top:curColHeights[col]
225         };
226 
227         /*
228          不在容器里,就加上
229          */
230         if (add) {
231             // 初始需要动画,那么先把透明度换成 0
232             item.css(colProp);
233             if (effect && effect.effect) {
234                 // has layout to allow to compute height
235                 item.css("visibility", "hidden");
236             }
237             container.append(item);
238             callback && callback();
239         }
240         // 否则调整,需要动画
241         else {
242             var adjustEffect = self.get("adjustEffect");
243             if (adjustEffect) {
244                 item.animate(colProp, adjustEffect.duration,
245                     adjustEffect.easing, callback);
246             } else {
247                 item.css(colProp);
248                 callback && callback();
249             }
250         }
251 
252         // 加入到 dom 树才能取得高度
253         curColHeights[col] += item.outerHeight(true);
254         var colItems = self.get("colItems");
255         colItems[col] = colItems[col] || [];
256         colItems[col].push(item);
257         item.attr("data-waterfall-col", col);
258 
259         return item;
260     }
261 
262     function addItem(itemRaw) {
263         var self = this,
264         // update curColHeights first
265         // because may slideDown to affect height
266             item = adjustItemAction(self, true, itemRaw),
267             effect = self.get("effect");
268         // then animate
269         if (effect && effect.effect) {
270             // 先隐藏才能调用 fadeIn slideDown
271             item.hide();
272             item.css("visibility", "");
273             item[effect.effect](
274                 effect.duration,
275                 0,
276                 effect.easing
277             );
278         }
279     }
280 
281     S.extend(Waterfall, Base,
282         /**
283          * @lends Waterfall
284          */
285         {
286             /**
287              * Whether is adjusting waterfall items.
288              * @returns Boolean
289              */
290             isAdjusting:function () {
291                 return !!this._adjuster;
292             },
293 
294             /**
295              * Whether is adding waterfall item.
296              * @since 1.3
297              * @returns Boolean
298              */
299             isAdding:function () {
300                 return !!this._adder;
301             },
302 
303             _init:function () {
304                 var self = this;
305                 // 一开始就 adjust 一次,可以对已有静态数据处理
306                 doResize.call(self);
307                 self.__onResize = S.buffer(doResize, RESIZE_DURATION, self);
308                 $(win).on("resize", self.__onResize);
309             },
310 
311 
312             /**
313              * Ajust the height of one specified item.
314              * @param {NodeList} item Waterfall item to be adjusted.
315              * @param {Object} cfg Config object.
316              * @param {Function} cfg.callback Callback function after the item is adjusted.
317              * @param {Function} cfg.process Adjust logic function.
318              * If returns a number, it is used as item height after adjust.
319              * else use item.outerHeight(true) as item height after adjust.
320              * @param {Object} cfg.effect Same as {@link Waterfall#adjustEffect}
321              * @param {Number} cfg.effect.duration
322              * @param {String} cfg.effect.easing
323              */
324             adjustItem:function (item, cfg) {
325                 var self = this;
326                 cfg = cfg || {};
327 
328                 if (self.isAdjusting()) {
329                     return;
330                 }
331 
332                 var originalOuterHeight = item.outerHeight(true),
333                     outerHeight;
334 
335                 if (cfg.process) {
336                     outerHeight = cfg.process.call(self);
337                 }
338 
339                 if (outerHeight === undefined) {
340                     outerHeight = item.outerHeight(true);
341                 }
342 
343                 var diff = outerHeight - originalOuterHeight,
344                     curColHeights = self.get("curColHeights"),
345                     col = parseInt(item.attr("data-waterfall-col")),
346                     colItems = self.get("colItems")[col],
347                     items = [],
348                     original = Math.max.apply(Math, curColHeights),
349                     now;
350 
351                 for (var i = 0; i < colItems.length; i++) {
352                     if (colItems[i][0] === item[0]) {
353                         break;
354                     }
355                 }
356 
357                 i++;
358 
359                 while (i < colItems.length) {
360                     items.push(colItems[i]);
361                     i++;
362                 }
363 
364                 curColHeights[col] += diff;
365 
366                 now = Math.max.apply(Math, curColHeights);
367 
368                 if (now != original) {
369                     self.get("container").height(now);
370                 }
371 
372                 var effect = cfg.effect,
373                     num = items.length;
374 
375                 if (!num) {
376                     return cfg.callback && cfg.callback.call(self);
377                 }
378 
379                 function check() {
380                     num--;
381                     if (num <= 0) {
382                         self._adjuster = 0;
383                         cfg.callback && cfg.callback.call(self);
384                     }
385                 }
386 
387                 if (effect === undefined) {
388                     effect = self.get("adjustEffect");
389                 }
390 
391                 return self._adjuster = timedChunk(items, function (item) {
392                     if (effect) {
393                         item.animate({
394                                 top:parseInt(item.css("top")) + diff
395                             },
396                             effect.duration,
397                             effect.easing,
398                             check);
399                     } else {
400                         item.css("top", parseInt(item.css("top")) + diff);
401                         check();
402                     }
403                 });
404             },
405 
406             /**
407              * Remove a waterfall item.
408              * @param {NodeList} item Waterfall item to be removed.
409              * @param {Object} cfg Config object.
410              * @param {Function} cfg.callback Callback function to be called after remove.
411              * @param {Object} cfg.effect Same as {@link Waterfall#adjustEffect}
412              * @param {Number} cfg.effect.duration
413              * @param {String} cfg.effect.easing
414              */
415             removeItem:function (item, cfg) {
416                 cfg = cfg || {};
417                 var self = this,
418                     callback = cfg.callback;
419                 self.adjustItem(item, S.mix(cfg, {
420                     process:function () {
421                         item.remove();
422                         return 0;
423                     },
424                     callback:function () {
425                         var col = parseInt(item.attr("data-waterfall-col")),
426                             colItems = self.get("colItems")[col];
427                         for (var i = 0; i < colItems.length; i++) {
428                             if (colItems[i][0] == item[0]) {
429                                 colItems.splice(i, 1);
430                                 break;
431                             }
432                         }
433                         callback && callback();
434                     }
435                 }));
436             },
437 
438             /**
439              * Readjust existing waterfall item.
440              * @param {Function} [callback] Callback function to be called after adjust.
441              */
442             adjust:function (callback) {
443                 S.log("waterfall:adjust");
444                 var self = this,
445                     items = self.get("container").all(".ks-waterfall");
446                 /* 正在加,直接开始这次调整,剩余的加和正在调整的一起处理 */
447                 /* 正在调整中,取消上次调整,开始这次调整 */
448                 if (self.isAdjusting()) {
449                     self._adjuster.stop();
450                     self._adjuster = 0;
451                 }
452                 /*计算容器宽度等信息*/
453                 recalculate.call(self);
454                 var num = items.length;
455 
456                 function check() {
457                     num--;
458                     if (num <= 0) {
459                         self.get("container").height(Math.max.apply(Math, self.get("curColHeights")));
460                         self._adjuster = 0;
461                         callback && callback.call(self);
462                         self.fire('adjustComplete', {
463                             items:items
464                         });
465                     }
466                 }
467 
468                 if (!num) {
469                     return callback && callback.call(self);
470                 }
471 
472                 return self._adjuster = timedChunk(items, function (item) {
473                     adjustItemAction(self, false, item, check);
474                 });
475             },
476 
477             /**
478              * Add array of waterfall items to current instance.
479              * @param {NodeList[]} items Waterfall items to be added.
480              * @param {Function} [callback] Callback function to be called after waterfall items are added.
481              */
482             addItems:function (items, callback) {
483                 var self = this;
484 
485                 /* 正在调整中,直接这次加,和调整的节点一起处理 */
486                 /* 正在加,直接这次加,一起处理 */
487                 self._adder = timedChunk(items,
488                     addItem,
489                     self,
490                     function () {
491                         self.get("container").height(Math.max.apply(Math,
492                             self.get("curColHeights")));
493                         self._adder = 0;
494                         callback && callback.call(self);
495                         self.fire('addComplete', {
496                             items:items
497                         });
498                     });
499 
500                 return self._adder;
501             },
502 
503             /**
504              * Destroy current instance.
505              */
506             destroy:function () {
507                 $(win).detach("resize", this.__onResize);
508             }
509         });
510 
511 
512     return Waterfall;
513 
514 }, {
515     requires:['node', 'base']
516 });
517 /**
518  * 2012-03-21 yiminghe@gmail.com
519  *  - 增加动画特效
520  *  - 增加删除/调整接口
521  **/