-
Notifications
You must be signed in to change notification settings - Fork 0
/
bsp-lazyimage.js
308 lines (235 loc) · 11.2 KB
/
bsp-lazyimage.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
/*
* Utility for lazy loading images
*
* This utility takes either a regular tag with a data-lazy and converts it to an image with src, or takes a picture element with
* data-lazy and converts it to regular srcset attributes. It's compatible with (but does not need) picturefill, as it tries to
* call it if it exists.
*
* Usage:
* 1) require the utility: var lazyLoader = require('lazy-loader');
* 2) init the utility with the context element, as well as any options: self.lazyLoaderInstance.init(document);
* 3) Call whatever public functions you need
*
* addItems: Takes an array of items and adds them to the lazy loading queue. You can change options here, as you can
* reset the offset or throttleInterval if necessary
*
* createCheckListeners: Listens to scroll and mutation events to see any of the images in the queue should be lazy loaded.
* If they are visible and in the viewport, it loads them
*
* checkItems: goes through the queue and checks to see if any images should be lazy loaded.
* If they are visible and in the viewport, it loads them
*
* renderImage: Takes an image that was setup for lazy loading and laods it. This is used if you want to skip the checking of items and handle
* your own checking
*
* Examples of supported HTML syntax:
*
* 1) <span data-lazy="asdf.jpg" alt="asdf" class="asdf"></span>
*
* 2) <img data-lazy="asdf.jpg" alt="asdf" class="asdf"></img>
*
* 3) <div>
* <span data-lazy="asdf.jpg" alt="asdf" class="asdf"></span>
* <span data-lazy="asdf.jpg" alt="asdf" class="asdf"></span>
* </div>
*
* 4) <picture class="asdf">
* <source data-lazy="/asdf.jpg" media="(max-width: 767px)">
* <img data-lazy="asdf.jpg" alt="Responsive Image">
* </picture>
*/
import $ from 'jquery';
import bsp_utils from 'bsp-utils';
var bsp_lazyimage = {
settings: {
'checkVisibility' : false,
'context' : document,
'loadedClass' : 'lazy-loaded',
'nativeImageLoading': true,
'offset' : 250,
'preloaderIconClass': 'bsp-spinner',
'throttleInterval' : 250
},
// this is just used right now to override any of the default settings.
// wanted to leave this here in case we want to add additional items into init
init: function(element, options) {
var self = this;
self.settings.context = element;
$.extend(self.settings, options);
self.modernBrowser = window.MutationObserver || window.WebKitMutationObserver;
},
// Public function to add items to array we check against. We allow additional options here so you
// can override the defaults, or initial items, per new set of items
addItems: function(items, options) {
var self = this;
$.extend(self.settings, options);
self.items.push.apply(self.items,items);
self.checkItems();
},
// Creates a scroll, resize, and mutation event listener and checks against the current array of items
createCheckListeners: function() {
var self = this;
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
self.items = [];
// our own private scroll and resize events, debounced so that we do not trigger scroll eleventy times per scroll
$(window).on('scroll.lazyLoader resize.lazyLoader', bsp_utils.throttle(self.settings.throttleInterval,function() {
// if we have any items to check,go ahead and check
if(self.items.length) {
self.checkItems();
}
}));
// allows all the self/this to work
function checkItemClosure() {
if(self.items.length) {
self.checkItems();
}
}
// we check the mutation observer if it's around. What this allows us to do is only lazy load items that are visible at first
// and as things become visible in the DOM, we go ahead and lazy load them
if (self.modernBrowser) {
new MutationObserver(bsp_utils.throttle(self.settings.throttleInterval, checkItemClosure)).observe(self.settings.context, {
'childList' : true,
'subtree' : true,
'attributes' : true
});
// But if mutation observer is not available, brute-force it with an interval (IE8/9/10)
} else {
setInterval(checkItemClosure, self.settings.throttleInterval);
}
},
// Does the check to see if an item should be rendered. We check the visibility as well as it's location in the viewport
checkItems: function() {
var self = this;
// go through the array of items and check if they are in the viewport
// if we do need to render them, do that, and then remove them out of the array so
// we do not check them again
for (var i=0; i < self.items.length;) {
var $item = $(self.items[i]);
var visible = true;
// by default, we just check the viewport. If we want the optional visibility check, dooo it
if (self.settings.checkVisibility) {
visible = $item.is(":visible");
}
// if you are visible, do stuff. If you are, just move on to the next item
// here we also check if you're a not modern browser. If you are old, we skip the visible check
if (visible || !self.modernBrowser) {
// if you are in the viewport, then render the image, and remove yourself from the array, since you're loaded
// if you aren't in the viewport, move on and check the next item
if(self._isInViewportOrAbove($item)) {
self.renderImage($item);
self.items.splice(i,1);
}
else {
i++;
}
} else{
i++;
}
}
},
// Public function that renders the image and sets a loading class. This is used by other functions in this utility
// or it can be called directly if you'd like to roll your own event handlers
renderImage: function(el) {
var self = this;
var $el = $(el);
var $lazyImage;
var $preloader = $el.find('.' + self.settings.preloaderIconClass);
// for picturefill, we have to change all the data-lazy to srcssets
// for normal images, we replace the src with data-lazy
if($el.prop('tagName') === 'PICTURE') {
// find every data-srcset, make a new srcset with that data
$el.find('[data-lazy]').each(function () {
$(this).attr('srcset', $(this).attr('data-lazy'));
});
$el.addClass(self.settings.loadedClass);
// if we are using picture fill reevaluate so it will pick up the image
if (typeof(window.picturefill) === 'function') {
window.picturefill({ 'reevaluate': true });
}
$preloader.remove();
} else {
// find the source
if($el.data('lazy')) {
$lazyImage = $el;
} else {
$lazyImage = $el.find('[data-lazy]');
}
// if we have an image to load, do it
if($lazyImage.length) {
$lazyImage.each(function() {
var $this = $(this);
// support for requestAnimationFrame. We let the site determine polyfills, and we support either
if(window.requestAnimationFrame) {
window.requestAnimationFrame(function() {
self._loadImage($el, $this);
});
} else {
self._loadImage($el, $this);
}
});
}
}
},
// Private function to load the image and replace the actual image
_loadImage: function($parent, $image) {
var self = this;
// we grab the image path, had some issues pulling this later, so we're grabbing and caching now, it's always here now
var imagePath = $image.attr('data-lazy');
// cache the preloader if exists
var preloader = $parent.find('.' + self.settings.preloaderIconClass);
function replaceImage() {
preloader.remove();
// make sure we have an image path and something weird did not happen. Had an edge case where slick carousel would
// intercept the data-lazy tags and do its own lazy loading, but the time did animation frame and got here, the image
// was already lazy loaded for us and the data-lazy was gone, so the image path was undefined
if(imagePath) {
// replace image
$image.replaceWith($('<img/>', {
'alt' : $image.attr('alt'),
'class' : ($image.attr('class') || '') + ' ' + self.settings.loadedClass,
'src' : imagePath,
'title' : $image.attr('title')
}));
}
}
// we have the option to allow for native image loading, or the arguably better UX option to let the images just pop in when they are ready
if(self.settings.nativeImageLoading) {
replaceImage();
} else {
// this is the temp image that we'll check for smoother loading
var $tempImage = $('<img>').attr('src', imagePath);
// now that we are loaded
$tempImage.one('load', function() {
replaceImage();
}).each(function() {
// gets around issues with caches images not triggering load sometimes
if(this.complete) {
$(this).load();
}
});
}
},
// Simple check to see if the item is in the viewport or above. We do this on purpose so that if we enter
// the page halfway through, we will load all the images above us, to make sure the page won't jump if we scroll up
_isInViewportOrAbove: function($item) {
var self = this;
var elementPosition = $item.offset();
var elementWidth = $item.width();
var offset = self.settings.offset;
self.scrollTop = $(window).scrollTop();
self.scrollLeft = $(window).scrollLeft();
self.windowHeight = $(window).height();
self.windowWidth = $(window).width();
// window height, how much we are scrolled and our offset. We want to make sure the element is above this
var verticalPosition = self.windowHeight + self.scrollTop + offset;
// window width, how much we are scrolled and the offset. We want to make sure the element is greater than 0 and to the left of this
var horizontalPosition = self.windowWidth + self.scrollLeft + offset;
// make sure we are in or above the vertical viewport and insize the horizontal one
if (((elementPosition.left + elementWidth) > 0) && ((verticalPosition > elementPosition.top) && (horizontalPosition > elementPosition.left))) {
return true;
} else {
return false;
}
}
};
export default bsp_lazyimage;