1 /**
  2  * @fileOverview 数据延迟加载组件
  3  */
  4 KISSY.add('datalazyload', function (S, DOM, Event, Base, undefined) {
  5 
  6     var win = S.Env.host,
  7         doc = win.document,
  8         IMG_SRC_DATA = 'data-ks-lazyload',
  9         AREA_DATA_CLS = 'ks-datalazyload',
 10         CUSTOM = '-custom',
 11         MANUAL = 'manual',
 12         DISPLAY = 'display',
 13         DEFAULT = 'default',
 14         NONE = 'none',
 15         SCROLL = 'scroll',
 16         TOUCH_MOVE = "touchmove",
 17         RESIZE = 'resize',
 18         DURATION = 100;
 19 
 20     function isValidContainer(c) {
 21         return c.nodeType != 9;
 22     }
 23 
 24     function getContainer(elem, cs) {
 25         for (var i = 0; i < cs.length; i++) {
 26             if (isValidContainer(cs[i])) {
 27                 if (cs[i].contains(elem)) {
 28                     return cs[i];
 29                 }
 30             }
 31         }
 32         return 0;
 33     }
 34 
 35     /**
 36      * 加载图片 src
 37      * @static
 38      */
 39     function loadImgSrc(img, flag) {
 40         flag = flag || IMG_SRC_DATA;
 41         var dataSrc = img.getAttribute(flag);
 42 
 43         if (dataSrc && img.src != dataSrc) {
 44             img.src = dataSrc;
 45             img.removeAttribute(flag);
 46         }
 47     }
 48 
 49     function removeExisting(newed, exists) {
 50         var ret = [];
 51         for (var i = 0; i < newed.length; i++) {
 52             if (!S.inArray(newed[i], exists)) {
 53                 ret.push(newed[i]);
 54             }
 55         }
 56         return ret;
 57     }
 58 
 59     /**
 60      * 从 textarea 中加载数据
 61      * @static
 62      */
 63     function loadAreaData(area, execScript) {
 64         // 采用隐藏 textarea 但不去除方式,去除会引发 Chrome 下错乱
 65         area.style.display = NONE;
 66         area.className = ''; // clear hook
 67         var content = DOM.create('<div>');
 68         // area 直接是 container 的儿子
 69         area.parentNode.insertBefore(content, area);
 70         DOM.html(content, area.value, execScript);
 71     }
 72 
 73     /**
 74      * filter for lazyload textarea
 75      */
 76     function filterArea(area) {
 77         return DOM.hasClass(area, AREA_DATA_CLS);
 78     }
 79 
 80     /**
 81      * @name DataLazyload
 82      * @class
 83      * LazyLoad elements which are out of current viewPort.
 84      * @extends Base
 85      */
 86     function DataLazyload(containers, config) {
 87         var self = this;
 88 
 89         // factory or constructor
 90         if (!(self instanceof DataLazyload)) {
 91             return new DataLazyload(containers, config);
 92         }
 93 
 94         // 允许仅传递 config 一个参数
 95         if (config === undefined) {
 96             config = containers;
 97             containers = [doc];
 98         }
 99 
100         // containers 是一个 HTMLElement 时
101         if (!S.isArray(containers)) {
102             containers = [DOM.get(containers) || doc];
103         }
104 
105         config = config || {};
106 
107         config.containers = containers;
108 
109         DataLazyload.superclass.constructor.call(self, config);
110 
111         /**
112          * 需要延迟下载的图片
113          * @type Array
114          * @private
115          */
116         //self._images
117 
118         /*
119          * 需要延迟处理的 textarea
120          * @type Array
121          * @private
122          */
123         //self._areaes
124 
125         /**
126          * 和延迟项绑定的回调函数
127          * @type object
128          */
129         self._callbacks = {els:[], fns:[]};
130 
131         self._init();
132     }
133 
134     DataLazyload.ATTRS =
135     /**
136      * @lends DataLazyload#
137      */
138     {
139         mod:{
140             value:MANUAL
141         },
142         /**
143          * Distance outside viewport or specified container to pre load.
144          * Default : pre load one screen height and width.
145          * @type Number|Object
146          * @example
147          * <code>
148          *  diff : 50 // pre load 50px outside viewport or specified container
149          *  // or more detailed :
150          *  {
151          *    left:20, // pre load 50px outside left edge of viewport or specified container
152          *    right:30, // pre load 50px outside right edge of viewport or specified container
153          *    top:50, // pre load 50px outside top edge of viewport or specified container
154          *    bottom:60 // pre load 50px outside bottom edge of viewport or specified container
155          *  }
156          * </code>
157          */
158         diff:{
159             value:DEFAULT
160         },
161         /**
162          * Placeholder img url for lazy loaded _images.
163          * Default : empty
164          * @type String
165          */
166         placeholder:{
167             value:NONE
168         },
169 
170         /**
171          * Whether execute script in lazy loaded textarea.
172          * Default : true
173          * @type Boolean
174          */
175         execScript:{
176             value:true
177         },
178 
179         /**
180          * Containers which will be monitor scroll event to lazy load elements within it.
181          * Default : [ document ]
182          * @type HTMLElement[]
183          */
184         containers:{
185             valueFn:function () {
186                 return [doc];
187             }
188         },
189 
190         /**
191          * Whether destroy this component when all lazy loaded elements are loaded.
192          * Default : true
193          * @type Boolean
194          * @since 1.3
195          */
196         autoDestroy:{
197             value:true
198         }
199     };
200 
201     // 两块区域是否相交
202     function isCross(r1, r2) {
203         var r = {};
204         r.top = Math.max(r1.top, r2.top);
205         r.bottom = Math.min(r1.bottom, r2.bottom);
206         r.left = Math.max(r1.left, r2.left);
207         r.right = Math.min(r1.right, r2.right);
208         return r.bottom >= r.top && r.right >= r.left;
209     }
210 
211     S.extend(DataLazyload,
212         Base,
213         /**
214          * @lends DataLazyload#
215          */
216         {
217 
218             /**
219              * 初始化
220              */
221             _init:function () {
222                 var self = this;
223                 self._filterItems();
224                 self._initLoadEvent();
225             },
226 
227             /**
228              * 获取并初始化需要延迟的 _images 和 _areaes
229              */
230             _filterItems:function () {
231                 var self = this,
232                     containers = self.get("containers"),
233                     n, N, imgs, _areaes, img,
234                     lazyImgs = [], lazyAreas = [];
235 
236                 for (n = 0, N = containers.length; n < N; ++n) {
237                     imgs = removeExisting(DOM.query('img', containers[n]), lazyImgs);
238                     lazyImgs = lazyImgs.concat(S.filter(imgs, self._filterImg, self));
239 
240                     _areaes = removeExisting(DOM.query('textarea', containers[n]), lazyAreas);
241                     lazyAreas = lazyAreas.concat(S.filter(_areaes, filterArea, self));
242                 }
243 
244                 self._images = lazyImgs;
245                 self._areaes = lazyAreas;
246             },
247 
248             /**
249              * filter for lazyload image
250              */
251             _filterImg:function (img) {
252                 var self = this,
253                     dataSrc = img.getAttribute(IMG_SRC_DATA),
254                     placeholder = self.get("placeholder"),
255                     isManualMod = self.get("mod") === MANUAL;
256 
257                 // 手工模式,只处理有 data-src 的图片
258                 if (isManualMod) {
259                     if (dataSrc) {
260                         if (placeholder !== NONE) {
261                             img.src = placeholder;
262                         }
263                         return true;
264                     }
265                 }
266                 // 自动模式,只处理 threshold 外无 data-src 的图片
267                 else {
268                     // 注意:已有 data-src 的项,可能已有其它实例处理过,不用再次处理
269                     if (!dataSrc && !self._checkElemInViewport(img)) {
270                         DOM.attr(img, IMG_SRC_DATA, img.src);
271                         if (placeholder !== NONE) {
272                             img.src = placeholder;
273                         } else {
274                             img.removeAttribute('src');
275                         }
276                         return true;
277                     }
278                 }
279             },
280 
281 
282             /**
283              * 初始化加载事件
284              */
285             _initLoadEvent:function () {
286                 var self = this,
287                     autoDestroy = self.get("autoDestroy"),
288                 // 加载延迟项
289                     loadItems = function () {
290                         self._loadItems();
291                         if (autoDestroy &&
292                             self._getItemsLength() === 0) {
293                             self.destroy();
294                         }
295                     },
296                 // 加载函数
297                     load = S.buffer(loadItems, DURATION, this);
298 
299                 // scroll 和 resize 时,加载图片
300                 Event.on(win, SCROLL, load);
301                 Event.on(win, TOUCH_MOVE, load);
302                 Event.on(win, RESIZE, load);
303 
304                 S.each(self.get("containers"), function (c) {
305                     if (isValidContainer(c)) {
306                         Event.on(c, SCROLL, load);
307                         Event.on(c, TOUCH_MOVE, load);
308                     }
309                 });
310 
311                 self._loadFn = load;
312 
313                 // 需要立即加载一次,以保证第一屏的延迟项可见
314                 if (self._getItemsLength()) {
315                     S.ready(loadItems);
316                 }
317             },
318 
319             /**
320              * 加载延迟项
321              */
322             _loadItems:function () {
323                 var self = this;
324                 self._loadImgs();
325                 self._loadAreas();
326                 self._fireCallbacks();
327             },
328 
329             /**
330              * 加载图片
331              */
332             _loadImgs:function () {
333                 var self = this;
334                 self._images = S.filter(self._images, self._loadImg, self);
335             },
336 
337             /**
338              * 监控滚动,处理图片
339              */
340             _loadImg:function (img) {
341                 var self = this;
342                 if (self._checkElemInViewport(img)) {
343                     loadImgSrc(img);
344                 } else {
345                     return true;
346                 }
347             },
348 
349 
350             /**
351              * 加载 textarea 数据
352              */
353             _loadAreas:function () {
354                 var self = this;
355                 self._areaes = S.filter(self._areaes, self._loadArea, self);
356             },
357 
358             /**
359              * 监控滚动,处理 textarea
360              */
361             _loadArea:function (area) {
362                 var self = this;
363                 if (self._checkElemInViewport(area)) {
364                     loadAreaData(area, self.get("execScript"));
365                 } else {
366                     return true;
367                 }
368             },
369 
370             /**
371              * 触发回调
372              */
373             _fireCallbacks:function () {
374                 var self = this,
375                     callbacks = self._callbacks,
376                     els = callbacks.els,
377                     fns = callbacks.fns,
378                     i, el, fn, remainEls = [], remainFns = [];
379 
380                 for (i = 0; (el = els[i]) && (fn = fns[i++]);) {
381                     if (self._checkElemInViewport(el)) {
382                         fn.call(el);
383                     } else {
384                         remainEls.push(el);
385                         remainFns.push(fn);
386                     }
387 
388                 }
389                 callbacks.els = remainEls;
390                 callbacks.fns = remainFns;
391             },
392 
393             /**
394              * Register callback function.
395              * When el is in viewport, then fn is called.
396              * @param {HTMLElement|String} el Html element to be monitored.
397              * @param {Function} fn Callback function to be called when el is in viewport.
398              */
399             addCallback:function (el, fn) {
400                 var self = this,
401                     callbacks = self._callbacks;
402                 el = DOM.get(el);
403 
404                 if (el && S.isFunction(fn)) {
405                     callbacks.els.push(el);
406                     callbacks.fns.push(fn);
407                 }
408 
409                 // add 立即检测,防止首屏元素问题
410                 self._fireCallbacks();
411             },
412 
413             /**
414              * Remove a callback function. See {@link DataLazyload#addCallback}
415              * @param {HTMLElement|String} el Html element to be monitored.
416              * @param {Function} [fn] Callback function to be called when el is in viewport.
417              *                        If not specified, remove all callbacks associated with el.
418              * @since 1.3
419              */
420             removeCallback:function (el, fn) {
421                 var callbacks = this._callbacks,
422                     els = [],
423                     fns = [],
424                     curFns = callbacks.fns;
425 
426                 el = DOM.get(el);
427 
428                 S.each(callbacks.els, function (curEl, index) {
429                     if (curEl == el) {
430                         if (fn === undefined || fn == curFns[index]) {
431                             return;
432                         }
433                     }
434 
435                     els.push(curEl);
436                     fns.push(curFns[index]);
437                 });
438 
439                 callbacks.fns = fns;
440                 callbacks.els = els;
441             },
442 
443             /**
444              * Add a array of imgs or textareas to be lazy loaded to monitor list.
445              * @param {HTMLElement[]} els Array of imgs or textareas to be lazy loaded
446              * @since 1.3
447              */
448             addElements:function (els) {
449                 if (!S.isArray(els)) {
450                     els = [els];
451                 }
452                 var self = this,
453                     imgs = self._images || [],
454                     areaes = self._areaes || [];
455                 S.each(els, function (el) {
456                     var nodeName = el.nodeName.toLowerCase();
457                     if (nodeName == "img") {
458                         if (!S.inArray(el, imgs)) {
459                             imgs.push(el);
460                         }
461                     } else if (nodeName == "textarea") {
462                         if (!S.inArray(el, areaes)) {
463                             areaes.push(el);
464                         }
465                     }
466                 });
467                 self._images = imgs;
468                 self._areaes = areaes;
469             },
470 
471             /**
472              * Remove a array of element from monitor list. See {@link DataLazyload#addElements}.
473              * @param {HTMLElement[]} els Array of imgs or textareas to be lazy loaded
474              * @since 1.3
475              */
476             removeElements:function (els) {
477                 if (!S.isArray(els)) {
478                     els = [els];
479                 }
480                 var self = this,
481                     imgs = [], areaes = [];
482                 S.each(self._images, function (img) {
483                     if (!S.inArray(img, els)) {
484                         imgs.push(img);
485                     }
486                 });
487                 S.each(self._areaes, function (area) {
488                     if (!S.inArray(area, els)) {
489                         areaes.push(area);
490                     }
491                 });
492                 self._images = imgs;
493                 self._areaes = areaes;
494             },
495 
496             /**
497              * 获取 c 的有效渲染区域(加上预加载差值)
498              * @protected
499              */
500             _getBoundingRect:function (c) {
501                 var vh, vw, left, top;
502 
503                 if (c !== undefined &&
504                     !S.isWindow(c) &&
505                     c.nodeType != 9) {
506                     vh = DOM.outerHeight(c);
507                     vw = DOM.outerWidth(c);
508                     var offset = DOM.offset(c);
509                     left = offset.left;
510                     top = offset.top;
511                 } else {
512                     vh = DOM.viewportHeight();
513                     vw = DOM.viewportWidth();
514                     left = DOM.scrollLeft();
515                     top = DOM.scrollTop();
516                 }
517 
518                 var diff = this.get("diff"),
519                     diffX = diff === DEFAULT ? vw : diff,
520                     diffX0 = 0,
521                     diffX1 = diffX,
522                     diffY = diff === DEFAULT ? vh : diff,
523                 // 兼容,默认只向下预读
524                     diffY0 = 0,
525                     diffY1 = diffY,
526                     right = left + vw,
527                     bottom = top + vh;
528 
529                 if (S.isObject(diff)) {
530                     diffX0 = diff.left || 0;
531                     diffX1 = diff.right || 0;
532                     diffY0 = diff.top || 0;
533                     diffY1 = diff.bottom || 0;
534                 }
535 
536                 left -= diffX0;
537                 right += diffX1;
538                 top -= diffY0;
539                 bottom += diffY1;
540 
541                 return {
542                     left:left,
543                     top:top,
544                     right:right,
545                     bottom:bottom
546                 };
547             },
548 
549             /**
550              * 获取当前延迟项的数量
551              * @protected
552              */
553             _getItemsLength:function () {
554                 var self = this;
555                 return self._images.length + self._areaes.length + self._callbacks.els.length;
556             },
557 
558             /**
559              * 判断 textarea 元素是否一部分在可视区域内(容器内并且在窗口 viewport 内)
560              * @private
561              * @param elem
562              */
563             _checkElemInViewport:function (elem) {
564                 // 注:elem 可能处于 display: none 状态,DOM.offset(elem).top 返回 0
565                 // 这种情况下用 elem.parentNode 的 Y 值来替代
566                 elem = DOM.css(elem, DISPLAY) === NONE ? elem.parentNode : elem;
567 
568                 var self = this,
569                     elemOffset = DOM.offset(elem),
570                     inContainer = true,
571                     container = getContainer(elem, self.get("containers")),
572                     windowRegion = self._getBoundingRect(),
573                     inWin,
574                     containerRegion,
575                     left = elemOffset.left,
576                     top = elemOffset.top,
577                     elemRegion = {
578                         left:left,
579                         top:top,
580                         right:left + DOM.outerWidth(elem),
581                         bottom:top + DOM.outerHeight(elem)
582                     };
583 
584                 if (container) {
585                     containerRegion = self._getBoundingRect(container);
586                     inContainer = isCross(containerRegion, elemRegion);
587                 }
588 
589                 // 确保在容器内出现
590                 // 并且在视窗内也出现
591                 inWin = isCross(windowRegion, elemRegion);
592                 return inContainer && inWin;
593             },
594 
595             /**
596              * Destroy this component.Will fire destroy event.
597              */
598             destroy:function () {
599                 var self = this, load = self._loadFn;
600                 Event.remove(win, SCROLL, load);
601                 Event.remove(win, TOUCH_MOVE, load);
602                 Event.remove(win, RESIZE, load);
603                 S.each(self.get("containers"), function (c) {
604                     if (isValidContainer(c)) {
605                         Event.remove(c, SCROLL, load);
606                         Event.remove(c, TOUCH_MOVE, load);
607                     }
608                 });
609                 self._callbacks.els = [];
610                 self._callbacks.fns = [];
611                 self._images = [];
612                 self._areaes = [];
613                 S.log("datalazyload is destroyed!");
614                 self.fire("destroy");
615             }
616         });
617 
618     /**
619      * Load lazyload textarea and imgs manually.
620      * @name loadCustomLazyData
621      * @function
622      * @memberOf DataLazyload
623      * @param {HTMLElement[]} containers Containers with in which lazy loaded elements are loaded.
624      * @param {String} type Type of lazy loaded element. "img" or "textarea"
625      * @param {String} [flag] flag which will be searched to find lazy loaded elements from containers.
626      * Default "data-ks-lazyload-custom" for img attribute and "ks-lazyload-custom" for textarea css class.
627      */
628     function loadCustomLazyData(containers, type, flag) {
629         var imgs;
630 
631         if (type === 'img-src') {
632             type = 'img';
633         }
634 
635         // 支持数组
636         if (!S.isArray(containers)) {
637             containers = [DOM.get(containers)];
638         }
639 
640         // 遍历处理
641         S.each(containers, function (container) {
642             switch (type) {
643                 case 'img':
644                     if (container.nodeName === 'IMG') { // 本身就是图片
645                         imgs = [container];
646                     } else {
647                         imgs = DOM.query('img', container);
648                     }
649 
650                     S.each(imgs, function (img) {
651                         loadImgSrc(img, flag || (IMG_SRC_DATA + CUSTOM));
652                     });
653                     break;
654 
655                 default:
656                     DOM.query('textarea', container).each(function (area) {
657                         if (DOM.hasClass(area, flag || (AREA_DATA_CLS + CUSTOM))) {
658                             loadAreaData(area, true);
659                         }
660                     });
661             }
662         });
663     }
664 
665 
666     DataLazyload.loadCustomLazyData = loadCustomLazyData;
667 
668     S.DataLazyload = DataLazyload;
669 
670     return DataLazyload;
671 
672 }, { requires:['dom', 'event', 'base'] });
673 
674 /**
675  * NOTES:
676  *
677  * 模式为 auto 时:
678  *  1. 在 Firefox 下非常完美。脚本运行时,还没有任何图片开始下载,能真正做到延迟加载。
679  *  2. 在 IE 下不尽完美。脚本运行时,有部分图片已经与服务器建立链接,这部分 abort 掉,
680  *     再在滚动时延迟加载,反而增加了链接数。
681  *  3. 在 Safari 和 Chrome 下,因为 webkit 内核 bug,导致无法 abort 掉下载。该
682  *     脚本完全无用。
683  *  4. 在 Opera 下,和 Firefox 一致,完美。
684  *  5. 2010-07-12: 发现在 Firefox 下,也有导致部分 Aborted 链接。
685  *
686  * 模式为 manual 时:(要延迟加载的图片,src 属性替换为 data-lazyload-src, 并将 src 的值赋为 placeholder )
687  *  1. 在任何浏览器下都可以完美实现。
688  *  2. 缺点是不渐进增强,无 JS 时,图片不能展示。
689  *
690  * 缺点:
691  *  1. 对于大部分情况下,需要拖动查看内容的页面(比如搜索结果页),快速滚动时加载有损用
692  *     户体验(用户期望所滚即所得),特别是网速不好时。
693  *  2. auto 模式不支持 Webkit 内核浏览器;IE 下,有可能导致 HTTP 链接数的增加。
694  *
695  * 优点:
696  *  1. 可以很好的提高页面初始加载速度。
697  *  2. 第一屏就跳转,延迟加载图片可以减少流量。
698  *
699  * 参考资料:
700  *  1. http://davidwalsh.name/lazyload MooTools 的图片延迟插件
701  *  2. http://vip.qq.com/ 模板输出时,就替换掉图片的 src
702  *  3. http://www.appelsiini.net/projects/lazyload jQuery Lazyload
703  *  4. http://www.dynamixlabs.com/2008/01/17/a-quick-look-add-a-loading-icon-to-your-larger-_images/
704  *  5. http://www.nczonline.net/blog/2009/11/30/empty-image-src-can-destroy-your-site/
705  *
706  * 特别要注意的测试用例:
707  *  1. 初始窗口很小,拉大窗口时,图片加载正常
708  *  2. 页面有滚动位置时,刷新页面,图片加载正常
709  *  3. 手动模式,第一屏有延迟图片时,加载正常
710  *
711  * 2009-12-17 补充:
712  *  1. textarea 延迟加载约定:页面中需要延迟的 dom 节点,放在
713  *       <textarea class='ks-datalazysrc invisible'>dom code</textarea>
714  *     里。可以添加 hidden 等 class, 但建议用 invisible, 并设定 height = '实际高度',这样可以保证
715  *     滚动时,diff 更真实有效。
716  *     注意:textarea 加载后,会替换掉父容器中的所有内容。
717  *  2. 延迟 callback 约定:dataLazyload.addCallback(el, fn) 表示当 el 即将出现时,触发 fn.
718  *  3. 所有操作都是最多触发一次,比如 callback. 来回拖动滚动条时,只有 el 第一次出现时会触发 fn 回调。
719  */
720 
721 /**
722  * xTODO:
723  *   - [取消] 背景图片的延迟加载(对于 css 里的背景图片和 sprite 很难处理)
724  *   - [取消] 加载时的 loading 图(对于未设定大小的图片,很难完美处理[参考资料 4])
725  */
726 
727 /**
728  * UPDATE LOG:
729  *   - 2012-04-27 yiminghe@gmail.com refactor to extend base, add removeCallback/addElements ...
730  *   - 2012-04-27 yiminghe@gmail.com 检查是否在视窗内改做判断区域相交,textaera 可设置高度,宽度
731  *   - 2012-04-25 yiminghe@gmail.com refactor, 监控容器内滚动,包括横轴滚动
732  *   - 2012-04-12 yiminghe@gmail.com monitor touchmove in iphone
733  *   - 2011-12-21 yiminghe@gmail.com 增加 removeElements 与 destroy 接口
734  *   - 2010-07-31 yubo IMG_SRC_DATA 由 data-lazyload-src 更名为 data-ks-lazyload + 支持 touch 设备
735  *   - 2010-07-10 yiminghe@gmail.com 重构,使用正则表达式识别 html 中的脚本,使用 EventTarget 自定义事件机制来处理回调
736  *   - 2010-05-10 yubo ie6 下,在 dom ready 后执行,会导致 placeholder 重复加载,为比避免此问题,默认为 none, 去掉占位图
737  *   - 2010-04-05 yubo 重构,使得对 YUI 的依赖仅限于 YDOM
738  *   - 2009-12-17 yubo 将 imglazyload 升级为 datalazyload, 支持 textarea 方式延迟和特定元素即将出现时的回调函数
739  */
740