forked from sjwilliams/laziestloader
-
Notifications
You must be signed in to change notification settings - Fork 0
/
slothy-loader.js
484 lines (406 loc) · 14 KB
/
slothy-loader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
/**
* @preserve SlothyLoader - v2.0.0 - 2019-03-06
* @preserve LaziestLoader - v0.7.3 - 2016-01-03
* An lazy loader based on Josh Williams’s
* LaziestLoader, but without jQuery dependency
* v2.0, now with IntersectionObserver
* See: http://sjwilliams.github.io/laziestloader/
* Copyright (c) 2016 Josh Williams; Licensed MIT
*/
var slothyLoader = function(options, callback) {
var slothyloaderEv = new CustomEvent('slothyloader');
var $elements = null,
$loaded = [], // elements with the correct source set
retina = window.devicePixelRatio > 1,
retina3x = window.devicePixelRatio > 2,
didScroll = false,
observers = [];
var defaultOptions = {
selector: '.dvz-lazy',
threshold: 0,
sizePattern: /{{SIZE}}/ig,
getSource: getSource,
event: 'scroll', // DEPRECATED
scrollThrottle: 250, // DEPRECATED // time in ms to throttle scroll. Increase for better performance.
sizeOffsetPercent: 0, // Subtract this % of width from the containing element, and fit images as though the element were that size. 0 = fit to element size; 40 = size - 40%; -30 = size + 30%
setSourceMode: true // plugin sets source attribute of the element. Set to false if you would like to, instead, use the callback to completely manage the element on trigger.
}
const intersectionObserverOpts = { root: null, threshold: [0, 0.25, 0.5, 0.75], rootMargin: (typeof options !== 'undefined' && typeof options.threshold !== 'undefined' ? options.threshold : defaultOptions.threshold) + 'px' };
if (typeof options === 'undefined') options = {}
for (var x in defaultOptions) {
if (typeof defaultOptions[x] !== 'undefined' && typeof options[x] == 'undefined') {
options[x] = defaultOptions[x]
}
}
$elements = document.querySelectorAll(options["selector"])
var useNativeScroll = (typeof options.event === 'string') && (options.event.indexOf('scroll') === 0); // Only concerns legacy way
function supportsIntersectionObserver() {
if (typeof window !== 'undefined') {
return 'IntersectionObserver' in window && 'IntersectionObserverEntry' in window
}
return false;
}
/**
* Generate source path of image to load. Take into account
* type of data supplied and whether or not a retina
* image is available.
*
* Basic option: data attributes specifing a single image to load,
* regardless of viewport.
* Eg:
*
* <img data-src="yourimage.jpg">
* <img data-src="yourimage.jpg" data-src-retina="yourretinaimage.jpg">
*
* Range of sizes: specify a string path with a {{size}} that
* will be replaced by an integer from a list of available sizes.
* Eg:
*
* <img data-pattern="path/toyourimage-{{size}}.jpg" data-widths="[320, 640, 970]">
* <img data-pattern="path/toyourimage-{{size}}.jpg" data-pattern-retina="path/toyourimage-{{size}}@2x.jpg" data-widths="[320, 640, 970]">
* <img data-pattern="path/toyourimage/{{size}}/slug.jpg" data-pattern-retina="path/toyourimage/{{size}}/slug@2x.jpg" data-widths="[320, 640, 970]">
*
* Range of sizes, with slugs: specify a string path with a {{size}} that
* will be replaced by a slug representing an image size.
* Eg:
*
* <img data-pattern="path/toyourimage-{{size}}.jpg" data-widths="[{width: 320, slug: 'small'},{width:900, slug: 'large'}]">
*
* @param {Element} $el
* @return {String}
*/
function getSource($el) {
var source, slug;
var data = $el.dataset;
if (data.pattern && data.widths && Array.isArray(JSON.parse(data.widths))) {
source = retina3x ? data.patternRetina3x : retina ? data.patternRetina : data.pattern;
source = source || data.patternRetina || data.pattern;
var parsedWidths = JSON.parse(data.widths)
// width or slug version?
if (typeof parsedWidths[0] === 'object') {
slug = (function() {
var widths = parsedWidths.map(function(val) {
return val.size;
});
var bestFitWidth = bestFit($el.offsetWidth, widths);
// match best width back to its corresponding slug
for (var i = parsedWidths.length - 1; i >= 0; i--) {
if (parsedWidths[i].size === bestFitWidth) {
return parsedWidths[i].slug;
}
}
})();
source = source.replace(options.sizePattern, slug);
} else {
if (data.patternRetina || data.patternRetina3x) {
source = source.replace(options.sizePattern, bestFit($el.offsetWidth, parsedWidths))
} else {
var bestFitWidth = bestFit($el.offsetWidth, parsedWidths)
if (retina3x) bestFitWidth *= 3
else if (retina) bestFitWidth *= 2
source = source.replace(options.sizePattern, bestFitWidth)
}
}
} else {
source = retina3x ? data.srcRetina3x : retina ? data.srcRetina : data.src;
source = source || data.src;
}
return source;
}
/**
* Reflect loaded state in class names
* and fire event.
*
* @param {Element} $el
*/
function onLoad($el) {
addClass($el, 'll-loaded')
removeClass($el, 'll-notloaded');
if (typeof callback === 'function') {
callback.call($el);
}
}
/**
* Handler that sets up and loads image
*
* @param {Element} $el
*/
function slHandler($el) {
var source;
// set height?
if ($el.dataset.ratio) {
setHeight.call($el);
}
// set content. default: set element source
if (options.setSourceMode) {
source = options.getSource($el);
if (source && $el.getAttribute('src') !== source) {
$el.setAttribute('src', source);
}
}
// applied immediately to reflect that media has started but,
// perhaps, hasn't finished downloading.
addClass($el, 'll-loadstarted');
// Determine when to fire `loaded` event. Wait until
// media is truly loaded if possible, otherwise immediately.
if (options.setSourceMode && ($el.nodeName === 'IMG' || $el.nodeName === 'VIDEO' || $el.nodeName === 'AUDIO') ) {
if ($el.nodeName === 'IMG') {
$el.onload = function() {
onLoad($el);
};
} else {
$el.onloadstart = function() {
onLoad($el);
};
}
} else {
onLoad($el);
}
}
/**
* Handler for Intersection Observer
*
* @param {IntersectionObserverEntry[]} $entries
*/
function slHandleIntersection(entries) {
var entry = entries[0];
var $el = entry.target;
if (entry.intersectionRatio > 0 || entry.intersectionRatio <= 0 && (entry.boundingClientRect.top < 0 || entry.boundingClientRect.y < 0)) {
slHandler($el);
}
}
/**
* Event listener
*
* @param {Event} $e
*/
function slLegacyEvListener(e) { // Used on scroll
var $el = this;
slHandler($el);
}
/**
* Attach observer on each matching element
*/
function bindObservers() {
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
var observer = new IntersectionObserver(slHandleIntersection, intersectionObserverOpts);
observer.observe(el);
observers.push(observer);
}
}
/**
* Attach event handler that sets correct
* media source for the elements' width, or
* allows callback to manipulate element
* exclusively.
*/
function bindLegacyLoader() {
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
el.addEventListener('slothyloader', slLegacyEvListener)
}
}
/**
* Remove event handler from elements
*/
function unbindLegacyLoader() {
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
el.removeEventListener('slothyloader', slLegacyEvListener);
}
}
/**
* Find the best sized image, opting for larger over smaller
*
* @param {Number} targetWidth element width
* @param {Array} widths array of numbers
* @return {Number}
*/
var bestFit = slothyLoader.bestFit = function(targetWidth, widths) {
var selectedWidth = widths[widths.length - 1],
i = widths.length,
offset = targetWidth * (options.sizeOffsetPercent / 100);
// sort smallest to largest
widths.sort(function(a, b) {
return a - b;
});
while (i--) {
if ((targetWidth - offset) <= widths[i]) {
selectedWidth = widths[i];
}
}
return selectedWidth;
};
/**
* Cycle through elements that haven't had their
* source set and, if they're in the viewport within
* the threshold, load their media
*/
function slothyloader() {
var docEl = document.documentElement;
var wHeight = window.innerHeight || docEl.clientHeight;
var wWidth = window.innerWidth || docEl.clientWidth;
var threshold = options.threshold;
var $inview = [];
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
if (includesElement($loaded,el)) continue // If already loaded, ignore
var rect = el.getBoundingClientRect();
if (
rect.bottom + threshold > 0 &&
rect.right + threshold > 0 &&
rect.left < wWidth + threshold &&
rect.top < wHeight + threshold
) $inview.push(el);
}
for (var i = 0; i < $inview.length; ++i) {
var inviewEl = $inview[i];
inviewEl.dispatchEvent(slothyloaderEv)
$loaded.push(inviewEl)
}
}
/**
* Given a lazy element, check if it should have
* its height set based on a data-ratio multiplier.
*/
function setHeight() {
var $el = this,
data = $el.dataset;
data.ratio = data.ratio || data.heightMultiplier; // backwards compatible for old data-height-multiplier code.
if (data.ratio && !isNaN(+data.ratio)) {
var newHeight = Math.round($el.offsetWidth * +data.ratio)
if (newHeight > 0) $el.style.height = newHeight + "px" // avoid 0 offsetWidth when element is display none (different from jQuery)
}
}
// add inital state classes, and check if
// element dimensions need to be set.
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
addClass(el, 'll-init ll-notloaded')
setHeight.call(el);
}
// initial binding
if (supportsIntersectionObserver()) {
bindObservers();
} else {
bindLegacyLoader();
// Watch either native scroll events, throttled by
// options.scrollThrottle, or a custom event that
// implements its own throttling.
if (useNativeScroll) {
window.addEventListener("scroll", function(){
didScroll = true;
});
setInterval(function() {
if (didScroll) {
didScroll = false;
slothyloader();
}
}, options.scrollThrottle);
} else {
if (typeof options.event === 'function') {
options.event(slothyloader);
} else {
window.addEventListener(options.event, function(){
slothyloader();
});
}
}
}
// Helpful native HTML functions for class manipulation to replace jQuery equivalents
function hasClass(el, className) {
var classList = className.split(" ")
for (var i=0; i<classList.length; i++) {
var className = classList[i]
if (typeof el.classList !== 'undefined')
return el.classList.contains(className)
else
return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'))
}
}
function addClass(el, className) {
var classList = className.split(" ")
for (var i=0; i<classList.length; i++) {
var className = classList[i]
if (typeof el.classList !== 'undefined') {
el.classList.add(className)
} else {
if (!hasClass(el, className)) el.className += " " + className
}
}
}
function removeClass(el, className) {
var classList = className.split(" ")
for (var i=0; i<classList.length; i++) {
var className = classList[i]
if (typeof el.classList !== 'undefined')
el.classList.remove(className)
else if (hasClass(el, className)) {
var reg = new RegExp('(\\s|^)' + className + '(\\s|$)')
el.className=el.className.replace(reg, ' ')
}
}
}
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
function includesElement(nodeList, searchElement, fromIndex) {
// 1. Let O be ? ToObject(this value).
if (nodeList == null) {
throw new TypeError('"nodeList" is null or not defined');
}
var o = Object(nodeList);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
// c. Increase k by 1.
// NOTE: === provides the correct "SameValueZero" comparison needed here.
if (o[k] === searchElement) {
return true;
}
k++;
}
// 8. Return false
return false;
}
// reset state on resize
window.addEventListener('resize', function() {
$loaded = [];
unbindLegacyLoader();
bindLegacyLoader();
slothyloader();
});
// initial check for lazy images
window.addEventListener('DOMContentLoaded', function() {
slothyloader();
});
return this;
};
// CustomEvent is to provide support for Event in Internet Explorer
(function () {
if ( typeof window.CustomEvent === "function" ) return false;
function CustomEvent ( event, params ) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
})();
if (typeof module !== 'undefined') module.exports = slothyLoader