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