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 **/