/** * @ignore * 数据延迟加载组件 */ KISSY.add('datalazyload', function (S, DOM, Event, Base, undefined) { var win = S.Env.host, doc = win.document, IMG_SRC_DATA = 'data-ks-lazyload', AREA_DATA_CLS = 'ks-datalazyload', CUSTOM = '-custom', DEFAULT = 'default', NONE = 'none', SCROLL = 'scroll', TOUCH_MOVE = "touchmove", RESIZE = 'resize', DURATION = 100; // 加载图片 src var loadImgSrc = function (img, flag) { flag = flag || IMG_SRC_DATA; var dataSrc = img.getAttribute(flag); if (dataSrc && img.src != dataSrc) { img.src = dataSrc; img.removeAttribute(flag); } }; // 从 textarea 中加载数据 function loadAreaData(textarea, execScript) { // 采用隐藏 textarea 但不去除方式,去除会引发 Chrome 下错乱 textarea.style.display = NONE; textarea.className = ''; // clear hook var content = DOM.create('<div>'); // textarea 直接是 container 的儿子 textarea.parentNode.insertBefore(content, textarea); DOM.html(content, textarea.value, execScript); } function getCallbackKey(el, fn) { var id, fid; if (!(id = el.id)) { id = el.id = S.guid('ks-lazyload'); } if (!(fid = fn.ksLazyloadId)) { fid = fn.ksLazyloadId = S.guid('ks-lazyload'); } return id + fid; } function cacheWidth(el) { if (el._ks_lazy_width) { return el._ks_lazy_width; } return el._ks_lazy_width = DOM.outerWidth(el); } function cacheHeight(el) { if (el._ks_lazy_height) { return el._ks_lazy_height; } return el._ks_lazy_height = DOM.outerHeight(el); } /** * whether part of elem can be seen by user. * note: it will not handle display none. * @ignore */ function elementInViewport(elem, windowRegion, containerRegion) { // it's better to removeElements, // but if user want to append it later? // use addElements instead // if (!inDocument(elem)) { // return false; // } // display none or inside display none if (!elem.offsetWidth) { return false; } var elemOffset = DOM.offset(elem), inContainer = true, inWin, left = elemOffset.left, top = elemOffset.top, elemRegion = { left: left, top: top, right: left + cacheWidth(elem), bottom: top + cacheHeight(elem) }; inWin = isCross(windowRegion, elemRegion); if (inWin && containerRegion) { inContainer = isCross(containerRegion, elemRegion); } // 确保在容器内出现 // 并且在视窗内也出现 return inContainer && inWin; } /** * LazyLoad elements which are out of current viewPort. * @class KISSY.DataLazyload * @extends KISSY.Base */ function DataLazyload(container, config) { var self = this; // factory or constructor if (!(self instanceof DataLazyload)) { return new DataLazyload(container, config); } var newConfig = container; if (!S.isPlainObject(newConfig)) { newConfig = config || {}; if (container) { newConfig.container = container; } } DataLazyload.superclass.constructor.call(self, newConfig); // 需要延迟下载的图片 // self._images // 需要延迟处理的 textarea // self._textareas // 和延迟项绑定的回调函数 self._callbacks = {}; self._containerIsNotDocument = self.get('container').nodeType != 9; self['_filterItems'](); self['_initLoadEvent'](); } DataLazyload.ATTRS = { /** * Distance outside viewport or specified container to pre load. * default: pre load one screen height and width. * @cfg {Number|Object} diff * * for example: * * diff : 50 // pre load 50px outside viewport or specified container * // or more detailed : * diff: { * left:20, // pre load 50px outside left edge of viewport or specified container * right:30, // pre load 50px outside right edge of viewport or specified container * top:50, // pre load 50px outside top edge of viewport or specified container * bottom:60 // pre load 50px outside bottom edge of viewport or specified container * } */ /** * @ignore */ diff: { value: DEFAULT }, // TODO: add containerDiff for container is not document /** * Placeholder img url for lazy loaded _images if image 's src is empty. * must be not empty! * * Defaults to: http://a.tbcdn.cn/kissy/1.0.0/build/imglazyload/spaceball.gif * @cfg {String} placeholder */ /** * @ignore */ placeholder: { value: 'http://a.tbcdn.cn/kissy/1.0.0/build/imglazyload/spaceball.gif' }, /** * Whether execute script in lazy loaded textarea. * default: true * @cfg {Boolean} execScript */ /** * @ignore */ execScript: { value: true }, /** * Container which will be monitor scroll event to lazy load elements within it. * default: document * @cfg {HTMLElement} container */ /** * @ignore */ container: { setter: function (el) { el = el || doc; if (S.isWindow(el)) { el = el.document; } else { el = DOM.get(el); if (DOM.nodeName(el) == 'body') { el = el.ownerDocument; } } return el; }, valueFn: function () { return doc; } }, /** * Whether destroy this component when all lazy loaded elements are loaded. * default: true * @cfg {Boolean} autoDestroy */ /** * @ignore */ autoDestroy: { value: true } }; // 两块区域是否相交 function isCross(r1, r2) { var r = {}; r.top = Math.max(r1.top, r2.top); r.bottom = Math.min(r1.bottom, r2.bottom); r.left = Math.max(r1.left, r2.left); r.right = Math.min(r1.right, r2.right); return r.bottom >= r.top && r.right >= r.left; } S.extend(DataLazyload, Base, { /** * get _images and _textareas which will lazyload. * @private */ '_filterItems': function () { var self = this, userConfig = self.userConfig, container = self.get("container"), _images = [], _textareas = [], containers = [container]; // 兼容 1.2 传入数组,进入兼容模式,不检测 container 区域 if (S.isArray(userConfig.container)) { self._backCompact = 1; containers = userConfig.container; } S.each(containers, function (container) { _images = _images.concat(S.filter(DOM.query('img', container), self['_filterImg'], self)); _textareas = _textareas.concat(DOM.query('textarea.' + AREA_DATA_CLS, container)); }); self._images = _images; self._textareas = _textareas; }, /** * filter for lazyload image * @private */ '_filterImg': function (img) { var self = this, placeholder = self.get("placeholder"); if (img.getAttribute(IMG_SRC_DATA)) { if (!img.src) { img.src = placeholder; } return true; } return undefined; }, /** * attach scroll/resize event * @private */ '_initLoadEvent': function () { var self = this, img = new Image(), placeholder = self.get("placeholder"), autoDestroy = self.get("autoDestroy"), // 加载延迟项 loadItems = function () { self['_loadItems'](); if (autoDestroy && self['_isLoadAllLazyElements']()) { self.destroy(); } }; // 加载函数 self._loadFn = S.buffer(loadItems, DURATION, self); self.resume(); img.src = placeholder; function firstLoad() { // 需要立即加载一次,以保证第一屏的延迟项可见 if (!self['_isLoadAllLazyElements']()) { S.ready(loadItems); } } if (img.complete) { firstLoad() } else { img.onload = firstLoad; } }, /** * force datalazyload to recheck constraints and load lazyload * */ refresh: function () { this._loadFn(); }, /** * lazyload all items * @private */ '_loadItems': function () { var self = this, containerRegion, container = self.get('container'), windowRegion; // container is display none if (self._containerIsNotDocument && !container.offsetWidth) { return; } windowRegion = self['_getBoundingRect'](); // 兼容,不检测 container if (!self._backCompact && self._containerIsNotDocument) { containerRegion = self['_getBoundingRect'](self.get('container')); } self['_loadImgs'](windowRegion, containerRegion); self['_loadTextAreas'](windowRegion, containerRegion); self['_fireCallbacks'](windowRegion, containerRegion); }, /** * lazyload images * @private */ '_loadImgs': function (windowRegion, containerRegion) { var self = this; self._images = S.filter(self._images, function (img) { if (elementInViewport(img, windowRegion, containerRegion)) { return loadImgSrc(img); } else { return true; } }, self); }, /** * lazyload textareas * @private */ '_loadTextAreas': function (windowRegion, containerRegion) { var self = this, execScript = self.get('execScript'); self._textareas = S.filter(self._textareas, function (textarea) { if (elementInViewport(textarea, windowRegion, containerRegion)) { return loadAreaData(textarea, execScript); } else { return true; } }, self); }, /** * fire callbacks * @private */ '_fireCallbacks': function (windowRegion, containerRegion) { var self = this, callbacks = self._callbacks; // may call addCallback/removeCallback S.each(callbacks, function (callback, key) { var el = callback.el, remove = false, fn = callback.fn; if (elementInViewport(el, windowRegion, containerRegion)) { remove = fn.call(el); } if (remove !== false) { delete callbacks[key]; } }); }, /** * Register callback function. When el is in viewport, then fn is called. * @param {HTMLElement|String} el html element to be monitored. * @param {function(this: HTMLElement): boolean} fn * Callback function to be called when el is in viewport. * return false to indicate el is actually not in viewport( for example display none element ). */ 'addCallback': function (el, fn) { var self = this, callbacks = self._callbacks; el = DOM.get(el); callbacks[getCallbackKey(el, fn)] = { el: DOM.get(el), fn: fn }; // add 立即检测,防止首屏元素问题 self._loadFn(); }, /** * Remove a callback function. See {@link KISSY.DataLazyload#addCallback} * @param {HTMLElement|String} el html element to be monitored. * @param {Function} [fn] Callback function to be called when el is in viewport. * If not specified, remove all callbacks associated with el. */ 'removeCallback': function (el, fn) { var callbacks = this._callbacks; el = DOM.get(el); delete callbacks[getCallbackKey(el, fn)]; }, /** * get to be lazy loaded elements * @return {Object} eg: {images:,textareas:} */ 'getElements': function () { return { images: this._images, textareas: this._textareas }; }, /** * Add a array of imgs or textareas to be lazy loaded to monitor list. * @param {HTMLElement[]|String} els Array of imgs or textareas to be lazy loaded or selector */ 'addElements': function (els) { if (typeof els == 'string') { els = DOM.query(els); } else if (!S.isArray(els)) { els = [els]; } var self = this, imgs = self._images || [], textareas = self._textareas || []; S.each(els, function (el) { var nodeName = el.nodeName.toLowerCase(); if (nodeName == "img") { if (!S.inArray(el, imgs)) { imgs.push(el); } } else if (nodeName == "textarea") { if (!S.inArray(el, textareas)) { textareas.push(el); } } }); self._images = imgs; self._textareas = textareas; }, /** * Remove a array of element from monitor list. See {@link KISSY.DataLazyload#addElements}. * @param {HTMLElement[]|String} els Array of imgs or textareas to be lazy loaded */ 'removeElements': function (els) { if (typeof els == 'string') { els = DOM.query(els); } else if (!S.isArray(els)) { els = [els]; } var self = this, imgs = [], textareas = []; S.each(self._images, function (img) { if (!S.inArray(img, els)) { imgs.push(img); } }); S.each(self._textareas, function (textarea) { if (!S.inArray(textarea, els)) { textareas.push(textarea); } }); self._images = imgs; self._textareas = textareas; }, /** * get c's bounding textarea. * @param {window|HTMLElement} [c] * @private */ '_getBoundingRect': function (c) { var vh, vw, left, top; if (c !== undefined) { vh = DOM.outerHeight(c); vw = DOM.outerWidth(c); var offset = DOM.offset(c); left = offset.left; top = offset.top; } else { vh = DOM.viewportHeight(); vw = DOM.viewportWidth(); left = DOM.scrollLeft(); top = DOM.scrollTop(); } var diff = this.get("diff"), diffX = diff === DEFAULT ? vw : diff, diffX0 = 0, diffX1 = diffX, diffY = diff === DEFAULT ? vh : diff, // 兼容,默认只向下预读 diffY0 = 0, diffY1 = diffY, right = left + vw, bottom = top + vh; if (S.isObject(diff)) { diffX0 = diff.left || 0; diffX1 = diff.right || 0; diffY0 = diff.top || 0; diffY1 = diff.bottom || 0; } left -= diffX0; right += diffX1; top -= diffY0; bottom += diffY1; return { left: left, top: top, right: right, bottom: bottom }; }, /** * get num of items waiting to lazyload * @private */ '_isLoadAllLazyElements': function () { var self = this; return (self._images.length + self._textareas.length + (S.isEmptyObject(self._callbacks) ? 0 : 1)) == 0; }, /** * pause lazyload */ pause: function () { var self = this, load = self._loadFn; if (self._destroyed) { return; } Event.remove(win, SCROLL, load); Event.remove(win, TOUCH_MOVE, load); Event.remove(win, RESIZE, load); load.stop(); if (self._containerIsNotDocument) { var c = self.get('container'); Event.remove(c, SCROLL, load); Event.remove(c, TOUCH_MOVE, load); } }, /** * resume lazyload */ resume: function () { var self = this, load = self._loadFn; if (self._destroyed) { return; } // scroll 和 resize 时,加载图片 Event.on(win, SCROLL, load); Event.on(win, TOUCH_MOVE, load); Event.on(win, RESIZE, load); if (self._containerIsNotDocument) { var c = self.get('container'); Event.on(c, SCROLL, load); Event.on(c, TOUCH_MOVE, load); } }, /** * Destroy this component.Will fire destroy event. */ destroy: function () { var self = this; self.pause(); self._callbacks = {}; self._images = []; self._textareas = []; S.log("datalazyload is destroyed!"); self.fire("destroy"); self._destroyed = 1; } }); /** * Load lazyload textarea and imgs manually. * @ignore * @method * @param {HTMLElement[]} containers Containers with in which lazy loaded elements are loaded. * @param {String} type Type of lazy loaded element. "img" or "textarea" * @param {String} [flag] flag which will be searched to find lazy loaded elements from containers. * Default "data-ks-lazyload-custom" for img attribute and "ks-lazyload-custom" for textarea css class. */ function loadCustomLazyData(containers, type, flag) { if (type === 'img-src') { type = 'img'; } // 支持数组 if (!S.isArray(containers)) { containers = [DOM.get(containers)]; } var imgFlag = flag || (IMG_SRC_DATA + CUSTOM), areaFlag = flag || (AREA_DATA_CLS + CUSTOM); S.each(containers, function (container) { // 遍历处理 if (type == 'img') { DOM.query('img', container).each(function (img) { loadImgSrc(img, imgFlag); }); } else { DOM.query('textarea.' + areaFlag, container).each(function (textarea) { loadAreaData(textarea, true); }); } }); } DataLazyload.loadCustomLazyData = loadCustomLazyData; S.DataLazyload = DataLazyload; return DataLazyload; }, { requires: ['dom', 'event', 'base'] }); /** * @ignore * * NOTES: * * 模式为 auto 时: * 1. 在 Firefox 下非常完美。脚本运行时,还没有任何图片开始下载,能真正做到延迟加载。 * 2. 在 IE 下不尽完美。脚本运行时,有部分图片已经与服务器建立链接,这部分 abort 掉, * 再在滚动时延迟加载,反而增加了链接数。 * 3. 在 Safari 和 Chrome 下,因为 webkit 内核 bug,导致无法 abort 掉下载。该 * 脚本完全无用。 * 4. 在 Opera 下,和 Firefox 一致,完美。 * 5. 2010-07-12: 发现在 Firefox 下,也有导致部分 Aborted 链接。 * * 模式为 manual 时:(要延迟加载的图片,src 属性替换为 data-lazyload-src, 并将 src 的值赋为 placeholder ) * 1. 在任何浏览器下都可以完美实现。 * 2. 缺点是不渐进增强,无 JS 时,图片不能展示。 * * 缺点: * 1. 对于大部分情况下,需要拖动查看内容的页面(比如搜索结果页),快速滚动时加载有损用 * 户体验(用户期望所滚即所得),特别是网速不好时。 * 2. auto 模式不支持 Webkit 内核浏览器;IE 下,有可能导致 HTTP 链接数的增加。 * * 优点: * 1. 可以很好的提高页面初始加载速度。 * 2. 第一屏就跳转,延迟加载图片可以减少流量。 * * 参考资料: * 1. http://davidwalsh.name/lazyload MooTools 的图片延迟插件 * 2. http://vip.qq.com/ 模板输出时,就替换掉图片的 src * 3. http://www.appelsiini.net/projects/lazyload jQuery Lazyload * 4. http://www.dynamixlabs.com/2008/01/17/a-quick-look-add-a-loading-icon-to-your-larger-_images/ * 5. http://www.nczonline.net/blog/2009/11/30/empty-image-src-can-destroy-your-site/ * * 特别要注意的测试用例: * 1. 初始窗口很小,拉大窗口时,图片加载正常 * 2. 页面有滚动位置时,刷新页面,图片加载正常 * 3. 手动模式,第一屏有延迟图片时,加载正常 * * 2009-12-17 补充: * 1. textarea 延迟加载约定:页面中需要延迟的 dom 节点,放在 * <textarea class='ks-datalazysrc invisible'>dom code</textarea> * 里。可以添加 hidden 等 class, 但建议用 invisible, 并设定 height = '实际高度',这样可以保证 * 滚动时,diff 更真实有效。 * 注意:textarea 加载后,会替换掉父容器中的所有内容。 * 2. 延迟 callback 约定:dataLazyload.addCallback(el, fn) 表示当 el 即将出现时,触发 fn. * 3. 所有操作都是最多触发一次,比如 callback. 来回拖动滚动条时,只有 el 第一次出现时会触发 fn 回调。 * * xTODO: * - [取消] 背景图片的延迟加载(对于 css 里的背景图片和 sprite 很难处理) * - [取消] 加载时的 loading 图(对于未设定大小的图片,很难完美处理[参考资料 4]) * * UPDATE LOG: * - 2012-01-07 yiminghe@gmail.com optimize for performance * - 2012-04-27 yiminghe@gmail.com refactor to extend base, add removeCallback/addElements ... * - 2012-04-27 yiminghe@gmail.com 检查是否在视窗内改做判断区域相交,textarea 可设置高度,宽度 * - 2012-04-25 yiminghe@gmail.com refactor, 监控容器内滚动,包括横轴滚动 * - 2012-04-12 yiminghe@gmail.com monitor touchmove in ios * - 2011-12-21 yiminghe@gmail.com 增加 removeElements 与 destroy 接口 * - 2010-07-31 yubo IMG_SRC_DATA 由 data-lazyload-src 更名为 data-ks-lazyload + 支持 touch 设备 * - 2010-07-10 yiminghe@gmail.com 重构,使用正则表达式识别 html 中的脚本,使用 EventTarget 自定义事件机制来处理回调 * - 2010-05-10 yubo ie6 下,在 dom ready 后执行,会导致 placeholder 重复加载,为比避免此问题,默认为 none, 去掉占位图 * - 2010-04-05 yubo 重构,使得对 YUI 的依赖仅限于 YDOM * - 2009-12-17 yubo 将 imglazyload 升级为 datalazyload, 支持 textarea 方式延迟和特定元素即将出现时的回调函数 */