blazy.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. /*!
  2. hey, [be]Lazy.js - v1.6.2 - 2016.05.09
  3. A fast, small and dependency free lazy load script (https://github.com/dinbror/blazy)
  4. (c) Bjoern Klinggaard - @bklinggaard - http://dinbror.dk/blazy
  5. */
  6. ;
  7. (function(root, blazy) {
  8. if (typeof define === 'function' && define.amd) {
  9. // AMD. Register bLazy as an anonymous module
  10. define(blazy);
  11. } else if (typeof exports === 'object') {
  12. // Node. Does not work with strict CommonJS, but
  13. // only CommonJS-like environments that support module.exports,
  14. // like Node.
  15. module.exports = blazy();
  16. } else {
  17. // Browser globals. Register bLazy on window
  18. root.Blazy = blazy();
  19. }
  20. })(this, function() {
  21. 'use strict';
  22. //private vars
  23. var _source, _viewport, _isRetina, _attrSrc = 'src',
  24. _attrSrcset = 'srcset';
  25. // constructor
  26. return function Blazy(options) {
  27. //IE7- fallback for missing querySelectorAll support
  28. if (!document.querySelectorAll) {
  29. var s = document.createStyleSheet();
  30. document.querySelectorAll = function(r, c, i, j, a) {
  31. a = document.all, c = [], r = r.replace(/\[for\b/gi, '[htmlFor').split(',');
  32. for (i = r.length; i--;) {
  33. s.addRule(r[i], 'k:v');
  34. for (j = a.length; j--;) a[j].currentStyle.k && c.push(a[j]);
  35. s.removeRule(0);
  36. }
  37. return c;
  38. };
  39. }
  40. //options and helper vars
  41. var scope = this;
  42. var util = scope._util = {};
  43. util.elements = [];
  44. util.destroyed = true;
  45. scope.options = options || {};
  46. scope.options.error = scope.options.error || false;
  47. scope.options.offset = scope.options.offset || 100;
  48. scope.options.success = scope.options.success || false;
  49. scope.options.selector = scope.options.selector || '.b-lazy';
  50. scope.options.separator = scope.options.separator || '|';
  51. scope.options.container = scope.options.container ? document.querySelectorAll(scope.options.container) : false;
  52. scope.options.errorClass = scope.options.errorClass || 'b-error';
  53. scope.options.breakpoints = scope.options.breakpoints || false; // obsolete
  54. scope.options.loadInvisible = scope.options.loadInvisible || false;
  55. scope.options.successClass = scope.options.successClass || 'b-loaded';
  56. scope.options.validateDelay = scope.options.validateDelay || 25;
  57. scope.options.saveViewportOffsetDelay = scope.options.saveViewportOffsetDelay || 50;
  58. scope.options.srcset = scope.options.srcset || 'data-srcset';
  59. scope.options.src = _source = scope.options.src || 'data-src';
  60. _isRetina = window.devicePixelRatio > 1;
  61. _viewport = {};
  62. _viewport.top = 0 - scope.options.offset;
  63. _viewport.left = 0 - scope.options.offset;
  64. /* public functions
  65. ************************************/
  66. scope.revalidate = function() {
  67. initialize(this);
  68. };
  69. scope.load = function(elements, force) {
  70. var opt = this.options;
  71. if (elements.length === undefined) {
  72. loadElement(elements, force, opt);
  73. } else {
  74. each(elements, function(element) {
  75. loadElement(element, force, opt);
  76. });
  77. }
  78. };
  79. scope.destroy = function() {
  80. var self = this;
  81. var util = self._util;
  82. if (self.options.container) {
  83. each(self.options.container, function(object) {
  84. unbindEvent(object, 'scroll', util.validateT);
  85. });
  86. }
  87. unbindEvent(window, 'scroll', util.validateT);
  88. unbindEvent(window, 'resize', util.validateT);
  89. unbindEvent(window, 'resize', util.saveViewportOffsetT);
  90. // destroy handler for scroller
  91. if (self.scroller) {
  92. self.scroller._xscroll && self.scroller._xscroll.off("scroll scrollend afterrender", util.validateT, self.scroller._xscroll);
  93. }
  94. util.count = 0;
  95. util.elements.length = 0;
  96. util.destroyed = true;
  97. };
  98. //throttle, ensures that we don't call the functions too often
  99. util.validateT = throttle(function() {
  100. validate(scope);
  101. }, scope.options.validateDelay, scope);
  102. util.saveViewportOffsetT = throttle(function() {
  103. saveViewportOffset(scope.options.offset);
  104. }, scope.options.saveViewportOffsetDelay, scope);
  105. saveViewportOffset(scope.options.offset);
  106. //handle multi-served image src (obsolete)
  107. each(scope.options.breakpoints, function(object) {
  108. if (object.width >= window.screen.width) {
  109. _source = object.src;
  110. return false;
  111. }
  112. });
  113. // start lazy load
  114. setTimeout(function() {
  115. initialize(scope);
  116. }); // "dom ready" fix
  117. };
  118. /* Private helper functions
  119. ************************************/
  120. function initialize(self) {
  121. var util = self._util;
  122. // First we create an array of elements to lazy load
  123. util.elements = toArray(self.options.selector);
  124. util.count = util.elements.length;
  125. // Then we bind resize and scroll events if not already binded
  126. if (util.destroyed) {
  127. util.destroyed = false;
  128. if (self.options.container) {
  129. each(self.options.container, function(object) {
  130. bindEvent(object, 'scroll', util.validateT);
  131. });
  132. }
  133. bindEvent(window, 'resize', util.saveViewportOffsetT);
  134. bindEvent(window, 'resize', util.validateT);
  135. bindEvent(window, 'scroll', util.validateT);
  136. // scroll handler for scroller
  137. if (self.options.scroller) {
  138. var scroller = self.options.scroller._xscroll
  139. var eventType = scroller.userConfig.useOriginScroll ? "scroll" : "scrollend";
  140. scroller.on("afterrender", util.validateT, self);
  141. scroller.on(eventType, util.validateT, self);
  142. }
  143. }
  144. // And finally, we start to lazy load.
  145. validate(self);
  146. }
  147. function validate(self) {
  148. var util = self._util;
  149. for (var i = 0; i < util.count; i++) {
  150. var element = util.elements[i];
  151. if (elementInView(element) || hasClass(element, self.options.successClass)) {
  152. self.load(element);
  153. util.elements.splice(i, 1);
  154. util.count--;
  155. i--;
  156. }
  157. }
  158. if (util.count === 0) {
  159. self.destroy();
  160. }
  161. }
  162. function elementInView(ele) {
  163. var rect = ele.getBoundingClientRect();
  164. return (
  165. // Intersection
  166. rect.right >= _viewport.left && rect.bottom >= _viewport.top && rect.left <= _viewport.right && rect.top <= _viewport.bottom
  167. );
  168. }
  169. function loadElement(ele, force, options) {
  170. // if element is visible, not loaded or forced
  171. if (!hasClass(ele, options.successClass) && (force || options.loadInvisible || (ele.offsetWidth > 0 && ele.offsetHeight > 0))) {
  172. var dataSrc = ele.getAttribute(_source) || ele.getAttribute(options.src); // fallback to default 'data-src'
  173. if (dataSrc) {
  174. var dataSrcSplitted = dataSrc.split(options.separator);
  175. var src = dataSrcSplitted[_isRetina && dataSrcSplitted.length > 1 ? 1 : 0];
  176. var isImage = equal(ele, 'img');
  177. // Image or background image
  178. if (isImage || ele.src === undefined) {
  179. var img = new Image();
  180. // using EventListener instead of onerror and onload
  181. // due to bug introduced in chrome v50
  182. // (https://productforums.google.com/forum/#!topic/chrome/p51Lk7vnP2o)
  183. var onErrorHandler = function() {
  184. if (options.error) options.error(ele, "invalid");
  185. addClass(ele, options.errorClass);
  186. unbindEvent(img, 'error', onErrorHandler);
  187. unbindEvent(img, 'load', onLoadHandler);
  188. };
  189. var onLoadHandler = function() {
  190. // Is element an image
  191. if (isImage) {
  192. setSrc(ele, src); //src
  193. handleSource(ele, _attrSrcset, options.srcset); //srcset
  194. //picture element
  195. var parent = ele.parentNode;
  196. if (parent && equal(parent, 'picture')) {
  197. each(parent.getElementsByTagName('source'), function(source) {
  198. handleSource(source, _attrSrcset, options.srcset);
  199. });
  200. }
  201. if (options.scroller) {
  202. options.scroller.reset()
  203. }
  204. // or background-image
  205. } else {
  206. ele.style.backgroundImage = 'url("' + src + '")';
  207. }
  208. itemLoaded(ele, options);
  209. unbindEvent(img, 'load', onLoadHandler);
  210. unbindEvent(img, 'error', onErrorHandler);
  211. };
  212. bindEvent(img, 'error', onErrorHandler);
  213. bindEvent(img, 'load', onLoadHandler);
  214. setSrc(img, src); //preload
  215. } else { // An item with src like iframe, unity, simpelvideo etc
  216. setSrc(ele, src);
  217. itemLoaded(ele, options);
  218. }
  219. } else {
  220. // video with child source
  221. if (equal(ele, 'video')) {
  222. each(ele.getElementsByTagName('source'), function(source) {
  223. handleSource(source, _attrSrc, options.src);
  224. });
  225. ele.load();
  226. itemLoaded(ele, options);
  227. } else {
  228. if (options.error) options.error(ele, "missing");
  229. addClass(ele, options.errorClass);
  230. }
  231. }
  232. }
  233. }
  234. function itemLoaded(ele, options) {
  235. addClass(ele, options.successClass);
  236. if (options.success) options.success(ele);
  237. // cleanup markup, remove data source attributes
  238. ele.removeAttribute(options.src);
  239. each(options.breakpoints, function(object) {
  240. ele.removeAttribute(object.src);
  241. });
  242. }
  243. function setSrc(ele, src) {
  244. ele[_attrSrc] = src;
  245. }
  246. function handleSource(ele, attr, dataAttr) {
  247. var dataSrc = ele.getAttribute(dataAttr);
  248. if (dataSrc) {
  249. ele[attr] = dataSrc;
  250. ele.removeAttribute(dataAttr);
  251. }
  252. }
  253. function equal(ele, str) {
  254. return ele.nodeName.toLowerCase() === str;
  255. }
  256. function hasClass(ele, className) {
  257. return (' ' + ele.className + ' ').indexOf(' ' + className + ' ') !== -1;
  258. }
  259. function addClass(ele, className) {
  260. if (!hasClass(ele, className)) {
  261. ele.className += ' ' + className;
  262. }
  263. }
  264. function toArray(selector) {
  265. var array = [];
  266. var nodelist = document.querySelectorAll(selector);
  267. for (var i = nodelist.length; i--; array.unshift(nodelist[i])) {}
  268. return array;
  269. }
  270. function saveViewportOffset(offset) {
  271. _viewport.bottom = (window.innerHeight || document.documentElement.clientHeight) + offset;
  272. _viewport.right = (window.innerWidth || document.documentElement.clientWidth) + offset;
  273. }
  274. function bindEvent(ele, type, fn) {
  275. if (ele.attachEvent) {
  276. ele.attachEvent && ele.attachEvent('on' + type, fn);
  277. } else {
  278. ele.addEventListener(type, fn, false);
  279. }
  280. }
  281. function unbindEvent(ele, type, fn) {
  282. if (ele.detachEvent) {
  283. ele.detachEvent && ele.detachEvent('on' + type, fn);
  284. } else {
  285. ele.removeEventListener(type, fn, false);
  286. }
  287. }
  288. function each(object, fn) {
  289. if (object && fn) {
  290. var l = object.length;
  291. for (var i = 0; i < l && fn(object[i], i) !== false; i++) {}
  292. }
  293. }
  294. function throttle(fn, minDelay, scope) {
  295. var lastCall = 0;
  296. return function() {
  297. var now = +new Date();
  298. if (now - lastCall < minDelay) {
  299. return;
  300. }
  301. lastCall = now;
  302. fn.apply(scope, arguments);
  303. };
  304. }
  305. });