imgix-optimizer 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1583 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ var classCallCheck = function (instance, Constructor) {
5
+ if (!(instance instanceof Constructor)) {
6
+ throw new TypeError("Cannot call a class as a function");
7
+ }
8
+ };
9
+
10
+ var createClass = function () {
11
+ function defineProperties(target, props) {
12
+ for (var i = 0; i < props.length; i++) {
13
+ var descriptor = props[i];
14
+ descriptor.enumerable = descriptor.enumerable || false;
15
+ descriptor.configurable = true;
16
+ if ("value" in descriptor) descriptor.writable = true;
17
+ Object.defineProperty(target, descriptor.key, descriptor);
18
+ }
19
+ }
20
+
21
+ return function (Constructor, protoProps, staticProps) {
22
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
23
+ if (staticProps) defineProperties(Constructor, staticProps);
24
+ return Constructor;
25
+ };
26
+ }();
27
+
28
+ var Image = function () {
29
+ function Image(img) {
30
+ classCallCheck(this, Image);
31
+
32
+ // Length of crossfade transition.
33
+ this.timeToFade = 500;
34
+ // Data attribute applied before processing.
35
+ this.processingAttr = 'data-imgix-img-processed';
36
+ // The main image (pixelated placeholder).
37
+ this.placeholderImg = $(img);
38
+ // Wait for the image to load prior to kicking off the optimization process.
39
+ if (this.placeholderImg.height() > 0) {
40
+ this.init();
41
+ } else {
42
+ this.placeholderImg.on('load', $.proxy(this.init, this));
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Configure the main placeholder image and kick off the optimization process.
48
+ */
49
+
50
+
51
+ createClass(Image, [{
52
+ key: 'init',
53
+ value: function init() {
54
+ this.initPlaceholder();
55
+ this.initOptimization();
56
+ }
57
+
58
+ /**
59
+ * Load an image in memory (not within the DOM) with the same source as the
60
+ * placeholder image. Once that has completed, we know we're safe to begin
61
+ * listening for the image to intersect the viewport.
62
+ */
63
+
64
+ }, {
65
+ key: 'initOptimization',
66
+ value: function initOptimization() {
67
+ $('<img>').on('load', $.proxy(this.listenForIntersection, this)).attr('src', this.placeholderImg.attr('src'));
68
+ }
69
+
70
+ // ---------------------------------------- | Lazy Loading Control
71
+
72
+ /**
73
+ * When the placeholder image intersects the viewport, begin processing.
74
+ * (IntersectionObserver and Object.assign() are not supported by IE, but the
75
+ * polyfills are loaded by Imgix.Optimizer.)
76
+ */
77
+
78
+ }, {
79
+ key: 'listenForIntersection',
80
+ value: function listenForIntersection() {
81
+ var observer = new IntersectionObserver($.proxy(this.onIntersection, this));
82
+ observer.observe(this.placeholderImg[0]);
83
+ }
84
+
85
+ /**
86
+ * When the placeholder image intersects the viewport, check if it is in the
87
+ * viewport and has not yet been processed. If those conditions are true,
88
+ * begin rendering the full size image and the transition process.
89
+ */
90
+
91
+ }, {
92
+ key: 'onIntersection',
93
+ value: function onIntersection(entries, observer) {
94
+ var img = $(entries[0].target);
95
+ if (!entries[0].isIntersecting || $(img).attr(this.processingAttr)) return;
96
+ img.attr(this.processingAttr, true);
97
+ this.renderFullSizeImg();
98
+ }
99
+
100
+ // ---------------------------------------- | Placeholder Image
101
+
102
+ /**
103
+ * Make necessary CSS adjustments to main placeholder image.
104
+ */
105
+
106
+ }, {
107
+ key: 'initPlaceholder',
108
+ value: function initPlaceholder() {
109
+ this.wrapPlaceholder();
110
+ this.setPlaceholderCss();
111
+ }
112
+
113
+ /**
114
+ * Wrap the placeholder image in a <div>. This enables better control over the
115
+ * wrapping element and provides a more fluid transition process.
116
+ */
117
+
118
+ }, {
119
+ key: 'wrapPlaceholder',
120
+ value: function wrapPlaceholder() {
121
+ this.tmpWrapper = $('<div>').css({
122
+ position: 'relative',
123
+ height: this.placeholderImg[0].getBoundingClientRect().height,
124
+ width: this.placeholderImg[0].getBoundingClientRect().width,
125
+ margin: this.placeholderImg.css('margin')
126
+ });
127
+ this.placeholderImg.wrap(this.tmpWrapper);
128
+ }
129
+
130
+ /**
131
+ * The main image must have a position set for it to remain in front of the
132
+ * full-size image. We assume that if the element is not explicitly positioned
133
+ * absolutely, then it can safely be positioned relatively.
134
+ *
135
+ * And temporarily remove any margin from the image, as the box model gets
136
+ * delegated to the temporary wrapper during the transition period.
137
+ */
138
+
139
+ }, {
140
+ key: 'setPlaceholderCss',
141
+ value: function setPlaceholderCss() {
142
+ if (this.placeholderImg.css('position') != 'absolute') {
143
+ this.placeholderImg.css('position', 'relative');
144
+ }
145
+ this.placeholderImg.css({ margin: 0 });
146
+ }
147
+
148
+ // ---------------------------------------- | Full-Size Image
149
+
150
+ /**
151
+ * Render the full-size image behind the placeholder image.
152
+ */
153
+
154
+ }, {
155
+ key: 'renderFullSizeImg',
156
+ value: function renderFullSizeImg() {
157
+ this.initFullSizeImg();
158
+ this.setFullSizeImgTempCss();
159
+ this.setFullSizeImgSrc();
160
+ this.addFullSizeImgToDom();
161
+ this.initTransition();
162
+ }
163
+
164
+ /**
165
+ * The full-size image is a clone of the placeholder image. This enables us to
166
+ * easily replace it without losing any necessary styles or attributes.
167
+ */
168
+
169
+ }, {
170
+ key: 'initFullSizeImg',
171
+ value: function initFullSizeImg() {
172
+ this.fullSizeImg = this.placeholderImg.clone();
173
+ }
174
+
175
+ /**
176
+ * Give the full-size image a temporary set of CSS rules so that it can sit
177
+ * directly behind the placeholder image while loading.
178
+ */
179
+
180
+ }, {
181
+ key: 'setFullSizeImgTempCss',
182
+ value: function setFullSizeImgTempCss() {
183
+ this.fullSizeImg.css({
184
+ position: 'absolute',
185
+ top: this.placeholderImg.position().top,
186
+ left: this.placeholderImg.position().left,
187
+ width: '100%',
188
+ height: '100%'
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Return the width and height of the placeholder image, including decimals.
194
+ * Uses precise measurements like this helps ensure the element doesn't slide
195
+ * when transitioning to the full size image.
196
+ */
197
+
198
+ }, {
199
+ key: 'getPlaceholderImgRect',
200
+ value: function getPlaceholderImgRect() {
201
+ return {
202
+ width: this.placeholderImg[0].getBoundingClientRect().width,
203
+ height: this.placeholderImg[0].getBoundingClientRect().height
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Prep the full-size image with the attributes necessary to become its full
209
+ * size. Right now it is still just a replica of the placeholder, sitting
210
+ * right behind the placeholder.
211
+ *
212
+ * We set the src directly even though we're using imgix.js because older
213
+ * browsers don't support the srcset attribute which is what imgix.js relies
214
+ * upon.
215
+ */
216
+
217
+ }, {
218
+ key: 'setFullSizeImgSrc',
219
+ value: function setFullSizeImgSrc() {
220
+ var newSrc = this.placeholderImg.attr('src').replace(/(\?|\&)(w=)(\d+)/i, '$1$2' + this.getPlaceholderImgRect().width).replace(/(\?|\&)(h=)(\d+)/i, '$1$2' + this.getPlaceholderImgRect().height);
221
+ // Add a height attribute if it is missing. This is the key to the image not
222
+ // jumping around after transitioning to the full-size image.
223
+ if (newSrc.search(/(\?|\&)(h=)(\d+)/i) < 0) {
224
+ newSrc = newSrc + '&h=' + this.getPlaceholderImgRect().height + '&fit=crop';
225
+ }
226
+ this.fullSizeImg.attr('ix-src', newSrc);
227
+ // TODO: Make this a configurable option or document it as a more semantic temporary class
228
+ this.fullSizeImg.addClass('img-responsive imgix-optimizing');
229
+ // TODO: This should respect the option from the Optimizer class for the select
230
+ this.fullSizeImg.removeAttr('data-optimize-img');
231
+ }
232
+
233
+ /**
234
+ * Render the full-size image in the DOM.
235
+ */
236
+
237
+ }, {
238
+ key: 'addFullSizeImgToDom',
239
+ value: function addFullSizeImgToDom() {
240
+ this.fullSizeImg.insertBefore(this.placeholderImg);
241
+ }
242
+
243
+ // ---------------------------------------- | Image Transition
244
+
245
+ /**
246
+ * Once the full-size image is loaded, begin the transition. This is the
247
+ * critical piece of this process. Imgix.js uses the ix-src attribute to build
248
+ * out the srcset attribute. Then, based on the sizes attribute, the browser
249
+ * determines which source to render. Therefore we can't preload in memory
250
+ * because we need imgix to do its thing directly in the DOM.
251
+ */
252
+
253
+ }, {
254
+ key: 'initTransition',
255
+ value: function initTransition() {
256
+ var _this = this;
257
+
258
+ this.fullSizeImg.on('load', function () {
259
+ return _this.transitionImg();
260
+ });
261
+ imgix.init();
262
+ }
263
+
264
+ /**
265
+ * Fade out the placeholder image, effectively showing the image behind it.
266
+ *
267
+ * Once the fade out transition has completed, remove any temporary properties
268
+ * from the full-size image (so it gets back to being a clone of the
269
+ * placeholder, with the full-size src).
270
+ *
271
+ * Finally, remove the placeholder image from the DOM since we don't need it
272
+ * any more.
273
+ */
274
+
275
+ }, {
276
+ key: 'transitionImg',
277
+ value: function transitionImg() {
278
+ var _this2 = this;
279
+
280
+ if (!this.placeholderImg) return true;
281
+ this.fadeOutPlaceholder();
282
+ setTimeout(function () {
283
+ _this2.removeFullSizeImgProperties();
284
+ _this2.removeImg();
285
+ // this.unwrapImg();
286
+ // 215 x 161.3 // 215 x 161 // 216.66 x 163
287
+ }, this.timeToFade);
288
+ }
289
+
290
+ /**
291
+ * Fade out the placeholder image.
292
+ */
293
+
294
+ }, {
295
+ key: 'fadeOutPlaceholder',
296
+ value: function fadeOutPlaceholder() {
297
+ this.placeholderImg.fadeTo(this.timeToFade, 0);
298
+ }
299
+
300
+ /**
301
+ * Remove temporary styles and class from the full-size image, which
302
+ * effectively means it has replaced the placeholder image.
303
+ */
304
+
305
+ }, {
306
+ key: 'removeFullSizeImgProperties',
307
+ value: function removeFullSizeImgProperties() {
308
+ this.fullSizeImg.removeAttr('style');
309
+ // TODO: Update this with how the class is handled above.
310
+ this.fullSizeImg.removeClass('imgix-optimizing');
311
+ }
312
+
313
+ /**
314
+ * Remove the placeholder image from the DOM since we no longer need it.
315
+ */
316
+
317
+ }, {
318
+ key: 'removeImg',
319
+ value: function removeImg() {
320
+ if (!this.placeholderImg) {
321
+ return;
322
+ }
323
+ this.placeholderImg.remove();
324
+ this.placeholderImg = undefined;
325
+ }
326
+
327
+ /**
328
+ * Remove the temporary wrapper and and give the margin back to the image.
329
+ */
330
+
331
+ }, {
332
+ key: 'unwrapImg',
333
+ value: function unwrapImg() {
334
+ this.fullSizeImg.css('margin', this.tmpWrapper.css('margin')).unwrap();
335
+ }
336
+ }]);
337
+ return Image;
338
+ }();
339
+
340
+ var BackgroundImage = function () {
341
+ function BackgroundImage(el) {
342
+ classCallCheck(this, BackgroundImage);
343
+
344
+ // Length of time to complete fade-in transition.
345
+ this.timeToFade = 500;
346
+ // Data attribute applied before processing.
347
+ this.processingAttr = 'data-imgix-bg-processed';
348
+ // Device pixel ratio assumes 1 if not set.
349
+ this.dpr = window['devicePixelRatio'] || 1;
350
+ // The largest image that has been loaded. This assumes the height of the
351
+ // container will not change.
352
+ this.largestImageWidth = 0;
353
+ // The primary element (i.e. the one with the background image).
354
+ this.el = $(el);
355
+ // Background image CSS property must be present.
356
+ if (this.el.css('background-image') == 'none') {
357
+ return;
358
+ }
359
+ // Prepare the element and its container for optimization.
360
+ this.initEl();
361
+ // Kick off the optimization process.
362
+ this.initOptimization();
363
+ // Listen for window resize events.
364
+ this.initEventListeners();
365
+ }
366
+
367
+ /**
368
+ * Load an image in memory (not within the DOM) with the same source as the
369
+ * placeholder image. Once that has completed, we know we're safe to begin
370
+ * processing.
371
+ */
372
+
373
+
374
+ createClass(BackgroundImage, [{
375
+ key: 'initOptimization',
376
+ value: function initOptimization() {
377
+ $('<img>').on('load', $.proxy(this.listenForIntersection, this)).attr('src', this.placeholderImgUrl);
378
+ }
379
+
380
+ /**
381
+ * When the element intersects the viewport, begin processing.
382
+ * (IntersectionObserver and Object.assign() are not supported by IE, but the
383
+ * polyfills are loaded by Imgix.Optimizer.)
384
+ */
385
+
386
+ }, {
387
+ key: 'listenForIntersection',
388
+ value: function listenForIntersection() {
389
+ var observer = new IntersectionObserver($.proxy(this.onIntersection, this));
390
+ observer.observe(this.el[0]);
391
+ }
392
+
393
+ /**
394
+ * When the element intersects the viewport, check if it is in the viewport
395
+ * and has not yet been processed. If those conditions are true, begin
396
+ * rendering the full size image and the transition process.
397
+ */
398
+
399
+ }, {
400
+ key: 'onIntersection',
401
+ value: function onIntersection(entries, observer) {
402
+ var el = $(entries[0].target);
403
+ if (!entries[0].isIntersecting || $(el).attr(this.processingAttr)) return;
404
+ $(el).attr(this.processingAttr, true);
405
+ this.renderTmpPlaceholderEl();
406
+ }
407
+
408
+ // ---------------------------------------- | Main Element
409
+
410
+ /**
411
+ * Prepare the main element and its container for optimization.
412
+ */
413
+
414
+ }, {
415
+ key: 'initEl',
416
+ value: function initEl() {
417
+ this.setPlaceholderImgUrl();
418
+ this.setContainerTmpCss();
419
+ this.setElTmpCss();
420
+ }
421
+
422
+ /**
423
+ * Set reference to original image URL, which is expected to be a small
424
+ * placeholder.
425
+ */
426
+
427
+ }, {
428
+ key: 'setPlaceholderImgUrl',
429
+ value: function setPlaceholderImgUrl() {
430
+ this.placeholderImgUrl = this.el.css('background-image').replace('url(', '').replace(')', '').replace(/\"/gi, '').replace(/\'/gi, '').split(', ')[0];
431
+ }
432
+
433
+ /**
434
+ * The parent of our jumbotron container should be relatively positioned
435
+ * (temporarily) so that we can absolutely position the temp image in the
436
+ * correct location.
437
+ */
438
+
439
+ }, {
440
+ key: 'setContainerTmpCss',
441
+ value: function setContainerTmpCss() {
442
+ this.parentStyles = {
443
+ display: this.el.parent().css('display'),
444
+ position: this.el.parent().css('position')
445
+ };
446
+ this.el.parent().css({
447
+ display: 'block',
448
+ position: 'relative'
449
+ });
450
+ }
451
+
452
+ /**
453
+ * The main element must have a position set for it to be rendered on top of
454
+ * the temporary full-size image. We assume that if the element is not
455
+ * explicitly positioned absolutely, then it can safely be positioned
456
+ * relatively.
457
+ */
458
+
459
+ }, {
460
+ key: 'setElTmpCss',
461
+ value: function setElTmpCss() {
462
+ if (this.el.css('position') != 'absolute') {
463
+ this.el.css('position', 'relative');
464
+ }
465
+ }
466
+
467
+ // ---------------------------------------- | Placeholder Image (Temp)
468
+
469
+ /**
470
+ * Render a clone of the element with the background image directly behind
471
+ * itself.
472
+ */
473
+
474
+ }, {
475
+ key: 'renderTmpPlaceholderEl',
476
+ value: function renderTmpPlaceholderEl() {
477
+ this.initTmpPlaceholderEl();
478
+ this.setTmpPlaceholderElCss();
479
+ this.addTmpPlaceholderElToDom();
480
+ this.renderFullSizeImg();
481
+ }
482
+
483
+ /**
484
+ * Create a clone of the element with the background image. Remove content
485
+ * from the clone -- often elements with a background image contain content.
486
+ */
487
+
488
+ }, {
489
+ key: 'initTmpPlaceholderEl',
490
+ value: function initTmpPlaceholderEl() {
491
+ this.tmpPlaceholderEl = this.el.clone();
492
+ this.tmpPlaceholderEl.html('');
493
+ }
494
+
495
+ /**
496
+ * Position the clone directly behind the main element
497
+ */
498
+
499
+ }, {
500
+ key: 'setTmpPlaceholderElCss',
501
+ value: function setTmpPlaceholderElCss() {
502
+ this.tmpPlaceholderEl.addClass('imgix-optimizing');
503
+ this.tmpPlaceholderEl.css({
504
+ position: 'absolute',
505
+ top: this.el.position().top,
506
+ left: this.el.position().left,
507
+ width: this.el.outerWidth(),
508
+ height: this.el.outerHeight(),
509
+ backgroundColor: 'transparent'
510
+ });
511
+ }
512
+
513
+ /**
514
+ * Add temporary element to the DOM, directly before the main element
515
+ * containing the background image.
516
+ */
517
+
518
+ }, {
519
+ key: 'addTmpPlaceholderElToDom',
520
+ value: function addTmpPlaceholderElToDom() {
521
+ this.tmpPlaceholderEl.insertBefore(this.el);
522
+ }
523
+
524
+ // ---------------------------------------- | Full-Size Image (Temp)
525
+
526
+ /**
527
+ * Create another clone, this time of the temporary placeholder image. This
528
+ * new element sits behind the other two and is responsible for loading the
529
+ * full-size image.
530
+ */
531
+
532
+ }, {
533
+ key: 'renderFullSizeImg',
534
+ value: function renderFullSizeImg() {
535
+ this.removeElBgImg();
536
+ this.initTmpFullSizeEl();
537
+ this.setTmpFullSizeElImg();
538
+ this.addTmpFullSizeElToDom();
539
+ this.initTransition();
540
+ }
541
+
542
+ /**
543
+ * Remove the background color and image from the main element. The user won't
544
+ * notice this transition because the temp duplicate image is already set and
545
+ * is sitting behind the primary element.
546
+ *
547
+ * This also stores a reference to the original background color so we can put
548
+ * it back when the transition is complete.
549
+ */
550
+
551
+ }, {
552
+ key: 'removeElBgImg',
553
+ value: function removeElBgImg() {
554
+ this.elBgColor = this.el.css('background-color');
555
+ this.el.css('background-color', 'transparent');
556
+ this.el.css('background-image', '');
557
+ }
558
+
559
+ /**
560
+ * The temporary full-size element is a clone of the temporary placeholder
561
+ * image element.
562
+ */
563
+
564
+ }, {
565
+ key: 'initTmpFullSizeEl',
566
+ value: function initTmpFullSizeEl() {
567
+ this.tmpFullSizeEl = this.tmpPlaceholderEl.clone();
568
+ }
569
+
570
+ /**
571
+ * Sets a reference to the full-size image URL based on the current dimensions
572
+ * of the main element.
573
+ */
574
+
575
+ }, {
576
+ key: 'setFullSizeImgUrl',
577
+ value: function setFullSizeImgUrl() {
578
+ // If the full size image URL exists and if the new size is going to be
579
+ // smaller than the largest size loaded, then we stick with the largest size
580
+ // that has been used.
581
+ if (this.fullSizeImgUrl && this.el.outerWidth() * this.dpr <= this.largestImageWidth) return;
582
+ // Assume that the new width will be the largest size used.
583
+ this.largestImageWidth = this.el.outerWidth() * this.dpr;
584
+ // Work with the placeholder image URL, which has been pulled from the
585
+ // background-image css property of the main elements.
586
+ var url = this.placeholderImgUrl.split('?');
587
+ // q is an array of querystring parameters as ["k=v", "k=v", ...].
588
+ var q = url[url.length - 1].split('&');
589
+ // Mapping q converts the array to an object of querystring parameters as
590
+ // { k: v, k: v, ... }.
591
+ var args = {};
592
+ q.map(function (x) {
593
+ return args[x.split('=')[0]] = x.split('=')[1];
594
+ });
595
+ // If the image's container is wider than it is tall, we only set width and
596
+ // unset height, and vice versa.
597
+ if (this.el.outerWidth() >= this.el.outerHeight()) {
598
+ args['w'] = this.largestImageWidth;
599
+ delete args['h'];
600
+ } else {
601
+ args['h'] = this.el.outerHeight() * this.dpr;
602
+ delete args['w'];
603
+ }
604
+ // Redefine q and go the other direction -- take the args object and convert
605
+ // it back to an array of querystring parameters, as ["k=v", "k=v", ...].
606
+ q = [];
607
+ for (var k in args) {
608
+ q.push(k + '=' + args[k]);
609
+ }
610
+ // Store the result and return.
611
+ return this.fullSizeImgUrl = url[0] + '?' + q.join('&');
612
+ }
613
+
614
+ /**
615
+ * Change the URL of this temporary element's background image to be the
616
+ * full-size image.
617
+ */
618
+
619
+ }, {
620
+ key: 'setTmpFullSizeElImg',
621
+ value: function setTmpFullSizeElImg() {
622
+ this.setFullSizeImgUrl();
623
+ this.tmpFullSizeEl.css('background-image', 'url("' + this.fullSizeImgUrl + '")');
624
+ }
625
+
626
+ /**
627
+ * Add the temporary full-size element direct before the temporary placeholder
628
+ * element.
629
+ */
630
+
631
+ }, {
632
+ key: 'addTmpFullSizeElToDom',
633
+ value: function addTmpFullSizeElToDom() {
634
+ this.tmpFullSizeEl.insertBefore(this.tmpPlaceholderEl);
635
+ }
636
+
637
+ // ---------------------------------------- | Transition
638
+
639
+ /**
640
+ * Load full-size image in memory. When it has loaded we can confidentally
641
+ * fade out the placeholder, knowing the full-size image will be in its place.
642
+ */
643
+
644
+ }, {
645
+ key: 'initTransition',
646
+ value: function initTransition() {
647
+ $('<img>').on('load', $.proxy(this.transitionImg, this)).attr('src', this.fullSizeImgUrl);
648
+ }
649
+
650
+ /**
651
+ * Fade out the temporary placeholder, set the background-image on the main
652
+ * element to the full-size URL, then remove the temporary elements behind the
653
+ * main element
654
+ */
655
+
656
+ }, {
657
+ key: 'transitionImg',
658
+ value: function transitionImg() {
659
+ var _this = this;
660
+
661
+ this.fadeOutTmpPlaceholderEl();
662
+ setTimeout(function () {
663
+ _this.updateElImg();
664
+ _this.replaceElTmpCss();
665
+ _this.replaceContainerTmpCss();
666
+ _this.removeTmpEls();
667
+ }, this.timeToFade);
668
+ }
669
+
670
+ /**
671
+ * Fade out the placeholder element. This was the temporary clone of the main
672
+ * element that has a placeholder background image.
673
+ *
674
+ * Rememeber the main element's background image was unset and its color set
675
+ * to transparent. That is why fading out this temporary image will work
676
+ * properly.
677
+ */
678
+
679
+ }, {
680
+ key: 'fadeOutTmpPlaceholderEl',
681
+ value: function fadeOutTmpPlaceholderEl() {
682
+ this.tmpPlaceholderEl.fadeTo(this.timeToFade, 0);
683
+ }
684
+
685
+ /**
686
+ * Reset the image URL (this helps if the size of the element has changed),
687
+ * then set the background image to the new source.
688
+ */
689
+
690
+ }, {
691
+ key: 'updateElImg',
692
+ value: function updateElImg() {
693
+ var _this2 = this;
694
+
695
+ this.setFullSizeImgUrl();
696
+ $('<img>').on('load', function (event) {
697
+ return _this2.el.css('background-image', 'url(\'' + _this2.fullSizeImgUrl + '\')');
698
+ }).attr('src', this.placeholderImgUrl);
699
+ }
700
+
701
+ /**
702
+ * Set the background color back to what it was before the transition.
703
+ */
704
+
705
+ }, {
706
+ key: 'replaceElTmpCss',
707
+ value: function replaceElTmpCss() {
708
+ this.el.css('background-color', this.elBgColor);
709
+ }
710
+
711
+ /**
712
+ * Reset the container's adjusted CSS properties.
713
+ */
714
+
715
+ }, {
716
+ key: 'replaceContainerTmpCss',
717
+ value: function replaceContainerTmpCss() {
718
+ this.el.parent().css({
719
+ display: this.parentStyles.display,
720
+ position: this.parentStyles.position
721
+ });
722
+ }
723
+
724
+ /**
725
+ * Remove both temporary elements from the DOM.
726
+ */
727
+
728
+ }, {
729
+ key: 'removeTmpEls',
730
+ value: function removeTmpEls() {
731
+ this.tmpPlaceholderEl.remove();
732
+ this.tmpFullSizeEl.remove();
733
+ this.tmpPlaceholderEl = undefined;
734
+ this.tmpFullSizeEl = undefined;
735
+ }
736
+
737
+ // ---------------------------------------- | Event Listeners
738
+
739
+ /**
740
+ * Listener for window resize events and update the image when the event ends.
741
+ */
742
+
743
+ }, {
744
+ key: 'initEventListeners',
745
+ value: function initEventListeners() {
746
+ var _this3 = this;
747
+
748
+ this.initResizeEnd();
749
+ $(window).on('resizeEnd', function (event) {
750
+ return _this3.updateElImg();
751
+ });
752
+ }
753
+
754
+ /**
755
+ * Trigger "resizeEnd" event on the window object after resizing has ceased
756
+ * for at least 0.5 seconds.
757
+ */
758
+
759
+ }, {
760
+ key: 'initResizeEnd',
761
+ value: function initResizeEnd() {
762
+ $(window).resize(function () {
763
+ if (this.resizeTo) {
764
+ clearTimeout(this.resizeTo);
765
+ }
766
+ this.resizeTo = setTimeout(function () {
767
+ $(this).trigger('resizeEnd');
768
+ }, 500);
769
+ });
770
+ }
771
+ }]);
772
+ return BackgroundImage;
773
+ }();
774
+
775
+ /**
776
+ * Copyright 2016 Google Inc. All Rights Reserved.
777
+ *
778
+ * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE.
779
+ *
780
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
781
+ *
782
+ */
783
+
784
+ (function(window, document) {
785
+
786
+
787
+ // Exits early if all IntersectionObserver and IntersectionObserverEntry
788
+ // features are natively supported.
789
+ if ('IntersectionObserver' in window &&
790
+ 'IntersectionObserverEntry' in window &&
791
+ 'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
792
+
793
+ // Minimal polyfill for Edge 15's lack of `isIntersecting`
794
+ // See: https://github.com/w3c/IntersectionObserver/issues/211
795
+ if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
796
+ Object.defineProperty(window.IntersectionObserverEntry.prototype,
797
+ 'isIntersecting', {
798
+ get: function () {
799
+ return this.intersectionRatio > 0;
800
+ }
801
+ });
802
+ }
803
+ return;
804
+ }
805
+
806
+
807
+ /**
808
+ * Creates the global IntersectionObserverEntry constructor.
809
+ * https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
810
+ * @param {Object} entry A dictionary of instance properties.
811
+ * @constructor
812
+ */
813
+ function IntersectionObserverEntry(entry) {
814
+ this.time = entry.time;
815
+ this.target = entry.target;
816
+ this.rootBounds = entry.rootBounds;
817
+ this.boundingClientRect = entry.boundingClientRect;
818
+ this.intersectionRect = entry.intersectionRect || getEmptyRect();
819
+ this.isIntersecting = !!entry.intersectionRect;
820
+
821
+ // Calculates the intersection ratio.
822
+ var targetRect = this.boundingClientRect;
823
+ var targetArea = targetRect.width * targetRect.height;
824
+ var intersectionRect = this.intersectionRect;
825
+ var intersectionArea = intersectionRect.width * intersectionRect.height;
826
+
827
+ // Sets intersection ratio.
828
+ if (targetArea) {
829
+ // Round the intersection ratio to avoid floating point math issues:
830
+ // https://github.com/w3c/IntersectionObserver/issues/324
831
+ this.intersectionRatio = Number((intersectionArea / targetArea).toFixed(4));
832
+ } else {
833
+ // If area is zero and is intersecting, sets to 1, otherwise to 0
834
+ this.intersectionRatio = this.isIntersecting ? 1 : 0;
835
+ }
836
+ }
837
+
838
+
839
+ /**
840
+ * Creates the global IntersectionObserver constructor.
841
+ * https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
842
+ * @param {Function} callback The function to be invoked after intersection
843
+ * changes have queued. The function is not invoked if the queue has
844
+ * been emptied by calling the `takeRecords` method.
845
+ * @param {Object=} opt_options Optional configuration options.
846
+ * @constructor
847
+ */
848
+ function IntersectionObserver(callback, opt_options) {
849
+
850
+ var options = opt_options || {};
851
+
852
+ if (typeof callback != 'function') {
853
+ throw new Error('callback must be a function');
854
+ }
855
+
856
+ if (options.root && options.root.nodeType != 1) {
857
+ throw new Error('root must be an Element');
858
+ }
859
+
860
+ // Binds and throttles `this._checkForIntersections`.
861
+ this._checkForIntersections = throttle(
862
+ this._checkForIntersections.bind(this), this.THROTTLE_TIMEOUT);
863
+
864
+ // Private properties.
865
+ this._callback = callback;
866
+ this._observationTargets = [];
867
+ this._queuedEntries = [];
868
+ this._rootMarginValues = this._parseRootMargin(options.rootMargin);
869
+
870
+ // Public properties.
871
+ this.thresholds = this._initThresholds(options.threshold);
872
+ this.root = options.root || null;
873
+ this.rootMargin = this._rootMarginValues.map(function(margin) {
874
+ return margin.value + margin.unit;
875
+ }).join(' ');
876
+ }
877
+
878
+
879
+ /**
880
+ * The minimum interval within which the document will be checked for
881
+ * intersection changes.
882
+ */
883
+ IntersectionObserver.prototype.THROTTLE_TIMEOUT = 100;
884
+
885
+
886
+ /**
887
+ * The frequency in which the polyfill polls for intersection changes.
888
+ * this can be updated on a per instance basis and must be set prior to
889
+ * calling `observe` on the first target.
890
+ */
891
+ IntersectionObserver.prototype.POLL_INTERVAL = null;
892
+
893
+ /**
894
+ * Use a mutation observer on the root element
895
+ * to detect intersection changes.
896
+ */
897
+ IntersectionObserver.prototype.USE_MUTATION_OBSERVER = true;
898
+
899
+
900
+ /**
901
+ * Starts observing a target element for intersection changes based on
902
+ * the thresholds values.
903
+ * @param {Element} target The DOM element to observe.
904
+ */
905
+ IntersectionObserver.prototype.observe = function(target) {
906
+ var isTargetAlreadyObserved = this._observationTargets.some(function(item) {
907
+ return item.element == target;
908
+ });
909
+
910
+ if (isTargetAlreadyObserved) {
911
+ return;
912
+ }
913
+
914
+ if (!(target && target.nodeType == 1)) {
915
+ throw new Error('target must be an Element');
916
+ }
917
+
918
+ this._registerInstance();
919
+ this._observationTargets.push({element: target, entry: null});
920
+ this._monitorIntersections();
921
+ this._checkForIntersections();
922
+ };
923
+
924
+
925
+ /**
926
+ * Stops observing a target element for intersection changes.
927
+ * @param {Element} target The DOM element to observe.
928
+ */
929
+ IntersectionObserver.prototype.unobserve = function(target) {
930
+ this._observationTargets =
931
+ this._observationTargets.filter(function(item) {
932
+
933
+ return item.element != target;
934
+ });
935
+ if (!this._observationTargets.length) {
936
+ this._unmonitorIntersections();
937
+ this._unregisterInstance();
938
+ }
939
+ };
940
+
941
+
942
+ /**
943
+ * Stops observing all target elements for intersection changes.
944
+ */
945
+ IntersectionObserver.prototype.disconnect = function() {
946
+ this._observationTargets = [];
947
+ this._unmonitorIntersections();
948
+ this._unregisterInstance();
949
+ };
950
+
951
+
952
+ /**
953
+ * Returns any queue entries that have not yet been reported to the
954
+ * callback and clears the queue. This can be used in conjunction with the
955
+ * callback to obtain the absolute most up-to-date intersection information.
956
+ * @return {Array} The currently queued entries.
957
+ */
958
+ IntersectionObserver.prototype.takeRecords = function() {
959
+ var records = this._queuedEntries.slice();
960
+ this._queuedEntries = [];
961
+ return records;
962
+ };
963
+
964
+
965
+ /**
966
+ * Accepts the threshold value from the user configuration object and
967
+ * returns a sorted array of unique threshold values. If a value is not
968
+ * between 0 and 1 and error is thrown.
969
+ * @private
970
+ * @param {Array|number=} opt_threshold An optional threshold value or
971
+ * a list of threshold values, defaulting to [0].
972
+ * @return {Array} A sorted list of unique and valid threshold values.
973
+ */
974
+ IntersectionObserver.prototype._initThresholds = function(opt_threshold) {
975
+ var threshold = opt_threshold || [0];
976
+ if (!Array.isArray(threshold)) threshold = [threshold];
977
+
978
+ return threshold.sort().filter(function(t, i, a) {
979
+ if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
980
+ throw new Error('threshold must be a number between 0 and 1 inclusively');
981
+ }
982
+ return t !== a[i - 1];
983
+ });
984
+ };
985
+
986
+
987
+ /**
988
+ * Accepts the rootMargin value from the user configuration object
989
+ * and returns an array of the four margin values as an object containing
990
+ * the value and unit properties. If any of the values are not properly
991
+ * formatted or use a unit other than px or %, and error is thrown.
992
+ * @private
993
+ * @param {string=} opt_rootMargin An optional rootMargin value,
994
+ * defaulting to '0px'.
995
+ * @return {Array<Object>} An array of margin objects with the keys
996
+ * value and unit.
997
+ */
998
+ IntersectionObserver.prototype._parseRootMargin = function(opt_rootMargin) {
999
+ var marginString = opt_rootMargin || '0px';
1000
+ var margins = marginString.split(/\s+/).map(function(margin) {
1001
+ var parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin);
1002
+ if (!parts) {
1003
+ throw new Error('rootMargin must be specified in pixels or percent');
1004
+ }
1005
+ return {value: parseFloat(parts[1]), unit: parts[2]};
1006
+ });
1007
+
1008
+ // Handles shorthand.
1009
+ margins[1] = margins[1] || margins[0];
1010
+ margins[2] = margins[2] || margins[0];
1011
+ margins[3] = margins[3] || margins[1];
1012
+
1013
+ return margins;
1014
+ };
1015
+
1016
+
1017
+ /**
1018
+ * Starts polling for intersection changes if the polling is not already
1019
+ * happening, and if the page's visibility state is visible.
1020
+ * @private
1021
+ */
1022
+ IntersectionObserver.prototype._monitorIntersections = function() {
1023
+ if (!this._monitoringIntersections) {
1024
+ this._monitoringIntersections = true;
1025
+
1026
+ // If a poll interval is set, use polling instead of listening to
1027
+ // resize and scroll events or DOM mutations.
1028
+ if (this.POLL_INTERVAL) {
1029
+ this._monitoringInterval = setInterval(
1030
+ this._checkForIntersections, this.POLL_INTERVAL);
1031
+ }
1032
+ else {
1033
+ addEvent(window, 'resize', this._checkForIntersections, true);
1034
+ addEvent(document, 'scroll', this._checkForIntersections, true);
1035
+
1036
+ if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) {
1037
+ this._domObserver = new MutationObserver(this._checkForIntersections);
1038
+ this._domObserver.observe(document, {
1039
+ attributes: true,
1040
+ childList: true,
1041
+ characterData: true,
1042
+ subtree: true
1043
+ });
1044
+ }
1045
+ }
1046
+ }
1047
+ };
1048
+
1049
+
1050
+ /**
1051
+ * Stops polling for intersection changes.
1052
+ * @private
1053
+ */
1054
+ IntersectionObserver.prototype._unmonitorIntersections = function() {
1055
+ if (this._monitoringIntersections) {
1056
+ this._monitoringIntersections = false;
1057
+
1058
+ clearInterval(this._monitoringInterval);
1059
+ this._monitoringInterval = null;
1060
+
1061
+ removeEvent(window, 'resize', this._checkForIntersections, true);
1062
+ removeEvent(document, 'scroll', this._checkForIntersections, true);
1063
+
1064
+ if (this._domObserver) {
1065
+ this._domObserver.disconnect();
1066
+ this._domObserver = null;
1067
+ }
1068
+ }
1069
+ };
1070
+
1071
+
1072
+ /**
1073
+ * Scans each observation target for intersection changes and adds them
1074
+ * to the internal entries queue. If new entries are found, it
1075
+ * schedules the callback to be invoked.
1076
+ * @private
1077
+ */
1078
+ IntersectionObserver.prototype._checkForIntersections = function() {
1079
+ var rootIsInDom = this._rootIsInDom();
1080
+ var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect();
1081
+
1082
+ this._observationTargets.forEach(function(item) {
1083
+ var target = item.element;
1084
+ var targetRect = getBoundingClientRect(target);
1085
+ var rootContainsTarget = this._rootContainsTarget(target);
1086
+ var oldEntry = item.entry;
1087
+ var intersectionRect = rootIsInDom && rootContainsTarget &&
1088
+ this._computeTargetAndRootIntersection(target, rootRect);
1089
+
1090
+ var newEntry = item.entry = new IntersectionObserverEntry({
1091
+ time: now(),
1092
+ target: target,
1093
+ boundingClientRect: targetRect,
1094
+ rootBounds: rootRect,
1095
+ intersectionRect: intersectionRect
1096
+ });
1097
+
1098
+ if (!oldEntry) {
1099
+ this._queuedEntries.push(newEntry);
1100
+ } else if (rootIsInDom && rootContainsTarget) {
1101
+ // If the new entry intersection ratio has crossed any of the
1102
+ // thresholds, add a new entry.
1103
+ if (this._hasCrossedThreshold(oldEntry, newEntry)) {
1104
+ this._queuedEntries.push(newEntry);
1105
+ }
1106
+ } else {
1107
+ // If the root is not in the DOM or target is not contained within
1108
+ // root but the previous entry for this target had an intersection,
1109
+ // add a new record indicating removal.
1110
+ if (oldEntry && oldEntry.isIntersecting) {
1111
+ this._queuedEntries.push(newEntry);
1112
+ }
1113
+ }
1114
+ }, this);
1115
+
1116
+ if (this._queuedEntries.length) {
1117
+ this._callback(this.takeRecords(), this);
1118
+ }
1119
+ };
1120
+
1121
+
1122
+ /**
1123
+ * Accepts a target and root rect computes the intersection between then
1124
+ * following the algorithm in the spec.
1125
+ * TODO(philipwalton): at this time clip-path is not considered.
1126
+ * https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
1127
+ * @param {Element} target The target DOM element
1128
+ * @param {Object} rootRect The bounding rect of the root after being
1129
+ * expanded by the rootMargin value.
1130
+ * @return {?Object} The final intersection rect object or undefined if no
1131
+ * intersection is found.
1132
+ * @private
1133
+ */
1134
+ IntersectionObserver.prototype._computeTargetAndRootIntersection =
1135
+ function(target, rootRect) {
1136
+
1137
+ // If the element isn't displayed, an intersection can't happen.
1138
+ if (window.getComputedStyle(target).display == 'none') return;
1139
+
1140
+ var targetRect = getBoundingClientRect(target);
1141
+ var intersectionRect = targetRect;
1142
+ var parent = getParentNode(target);
1143
+ var atRoot = false;
1144
+
1145
+ while (!atRoot) {
1146
+ var parentRect = null;
1147
+ var parentComputedStyle = parent.nodeType == 1 ?
1148
+ window.getComputedStyle(parent) : {};
1149
+
1150
+ // If the parent isn't displayed, an intersection can't happen.
1151
+ if (parentComputedStyle.display == 'none') return;
1152
+
1153
+ if (parent == this.root || parent == document) {
1154
+ atRoot = true;
1155
+ parentRect = rootRect;
1156
+ } else {
1157
+ // If the element has a non-visible overflow, and it's not the <body>
1158
+ // or <html> element, update the intersection rect.
1159
+ // Note: <body> and <html> cannot be clipped to a rect that's not also
1160
+ // the document rect, so no need to compute a new intersection.
1161
+ if (parent != document.body &&
1162
+ parent != document.documentElement &&
1163
+ parentComputedStyle.overflow != 'visible') {
1164
+ parentRect = getBoundingClientRect(parent);
1165
+ }
1166
+ }
1167
+
1168
+ // If either of the above conditionals set a new parentRect,
1169
+ // calculate new intersection data.
1170
+ if (parentRect) {
1171
+ intersectionRect = computeRectIntersection(parentRect, intersectionRect);
1172
+
1173
+ if (!intersectionRect) break;
1174
+ }
1175
+ parent = getParentNode(parent);
1176
+ }
1177
+ return intersectionRect;
1178
+ };
1179
+
1180
+
1181
+ /**
1182
+ * Returns the root rect after being expanded by the rootMargin value.
1183
+ * @return {Object} The expanded root rect.
1184
+ * @private
1185
+ */
1186
+ IntersectionObserver.prototype._getRootRect = function() {
1187
+ var rootRect;
1188
+ if (this.root) {
1189
+ rootRect = getBoundingClientRect(this.root);
1190
+ } else {
1191
+ // Use <html>/<body> instead of window since scroll bars affect size.
1192
+ var html = document.documentElement;
1193
+ var body = document.body;
1194
+ rootRect = {
1195
+ top: 0,
1196
+ left: 0,
1197
+ right: html.clientWidth || body.clientWidth,
1198
+ width: html.clientWidth || body.clientWidth,
1199
+ bottom: html.clientHeight || body.clientHeight,
1200
+ height: html.clientHeight || body.clientHeight
1201
+ };
1202
+ }
1203
+ return this._expandRectByRootMargin(rootRect);
1204
+ };
1205
+
1206
+
1207
+ /**
1208
+ * Accepts a rect and expands it by the rootMargin value.
1209
+ * @param {Object} rect The rect object to expand.
1210
+ * @return {Object} The expanded rect.
1211
+ * @private
1212
+ */
1213
+ IntersectionObserver.prototype._expandRectByRootMargin = function(rect) {
1214
+ var margins = this._rootMarginValues.map(function(margin, i) {
1215
+ return margin.unit == 'px' ? margin.value :
1216
+ margin.value * (i % 2 ? rect.width : rect.height) / 100;
1217
+ });
1218
+ var newRect = {
1219
+ top: rect.top - margins[0],
1220
+ right: rect.right + margins[1],
1221
+ bottom: rect.bottom + margins[2],
1222
+ left: rect.left - margins[3]
1223
+ };
1224
+ newRect.width = newRect.right - newRect.left;
1225
+ newRect.height = newRect.bottom - newRect.top;
1226
+
1227
+ return newRect;
1228
+ };
1229
+
1230
+
1231
+ /**
1232
+ * Accepts an old and new entry and returns true if at least one of the
1233
+ * threshold values has been crossed.
1234
+ * @param {?IntersectionObserverEntry} oldEntry The previous entry for a
1235
+ * particular target element or null if no previous entry exists.
1236
+ * @param {IntersectionObserverEntry} newEntry The current entry for a
1237
+ * particular target element.
1238
+ * @return {boolean} Returns true if a any threshold has been crossed.
1239
+ * @private
1240
+ */
1241
+ IntersectionObserver.prototype._hasCrossedThreshold =
1242
+ function(oldEntry, newEntry) {
1243
+
1244
+ // To make comparing easier, an entry that has a ratio of 0
1245
+ // but does not actually intersect is given a value of -1
1246
+ var oldRatio = oldEntry && oldEntry.isIntersecting ?
1247
+ oldEntry.intersectionRatio || 0 : -1;
1248
+ var newRatio = newEntry.isIntersecting ?
1249
+ newEntry.intersectionRatio || 0 : -1;
1250
+
1251
+ // Ignore unchanged ratios
1252
+ if (oldRatio === newRatio) return;
1253
+
1254
+ for (var i = 0; i < this.thresholds.length; i++) {
1255
+ var threshold = this.thresholds[i];
1256
+
1257
+ // Return true if an entry matches a threshold or if the new ratio
1258
+ // and the old ratio are on the opposite sides of a threshold.
1259
+ if (threshold == oldRatio || threshold == newRatio ||
1260
+ threshold < oldRatio !== threshold < newRatio) {
1261
+ return true;
1262
+ }
1263
+ }
1264
+ };
1265
+
1266
+
1267
+ /**
1268
+ * Returns whether or not the root element is an element and is in the DOM.
1269
+ * @return {boolean} True if the root element is an element and is in the DOM.
1270
+ * @private
1271
+ */
1272
+ IntersectionObserver.prototype._rootIsInDom = function() {
1273
+ return !this.root || containsDeep(document, this.root);
1274
+ };
1275
+
1276
+
1277
+ /**
1278
+ * Returns whether or not the target element is a child of root.
1279
+ * @param {Element} target The target element to check.
1280
+ * @return {boolean} True if the target element is a child of root.
1281
+ * @private
1282
+ */
1283
+ IntersectionObserver.prototype._rootContainsTarget = function(target) {
1284
+ return containsDeep(this.root || document, target);
1285
+ };
1286
+
1287
+
1288
+ /**
1289
+ * Adds the instance to the global IntersectionObserver registry if it isn't
1290
+ * already present.
1291
+ * @private
1292
+ */
1293
+ IntersectionObserver.prototype._registerInstance = function() {
1294
+ };
1295
+
1296
+
1297
+ /**
1298
+ * Removes the instance from the global IntersectionObserver registry.
1299
+ * @private
1300
+ */
1301
+ IntersectionObserver.prototype._unregisterInstance = function() {
1302
+ };
1303
+
1304
+
1305
+ /**
1306
+ * Returns the result of the performance.now() method or null in browsers
1307
+ * that don't support the API.
1308
+ * @return {number} The elapsed time since the page was requested.
1309
+ */
1310
+ function now() {
1311
+ return window.performance && performance.now && performance.now();
1312
+ }
1313
+
1314
+
1315
+ /**
1316
+ * Throttles a function and delays its execution, so it's only called at most
1317
+ * once within a given time period.
1318
+ * @param {Function} fn The function to throttle.
1319
+ * @param {number} timeout The amount of time that must pass before the
1320
+ * function can be called again.
1321
+ * @return {Function} The throttled function.
1322
+ */
1323
+ function throttle(fn, timeout) {
1324
+ var timer = null;
1325
+ return function () {
1326
+ if (!timer) {
1327
+ timer = setTimeout(function() {
1328
+ fn();
1329
+ timer = null;
1330
+ }, timeout);
1331
+ }
1332
+ };
1333
+ }
1334
+
1335
+
1336
+ /**
1337
+ * Adds an event handler to a DOM node ensuring cross-browser compatibility.
1338
+ * @param {Node} node The DOM node to add the event handler to.
1339
+ * @param {string} event The event name.
1340
+ * @param {Function} fn The event handler to add.
1341
+ * @param {boolean} opt_useCapture Optionally adds the even to the capture
1342
+ * phase. Note: this only works in modern browsers.
1343
+ */
1344
+ function addEvent(node, event, fn, opt_useCapture) {
1345
+ if (typeof node.addEventListener == 'function') {
1346
+ node.addEventListener(event, fn, opt_useCapture || false);
1347
+ }
1348
+ else if (typeof node.attachEvent == 'function') {
1349
+ node.attachEvent('on' + event, fn);
1350
+ }
1351
+ }
1352
+
1353
+
1354
+ /**
1355
+ * Removes a previously added event handler from a DOM node.
1356
+ * @param {Node} node The DOM node to remove the event handler from.
1357
+ * @param {string} event The event name.
1358
+ * @param {Function} fn The event handler to remove.
1359
+ * @param {boolean} opt_useCapture If the event handler was added with this
1360
+ * flag set to true, it should be set to true here in order to remove it.
1361
+ */
1362
+ function removeEvent(node, event, fn, opt_useCapture) {
1363
+ if (typeof node.removeEventListener == 'function') {
1364
+ node.removeEventListener(event, fn, opt_useCapture || false);
1365
+ }
1366
+ else if (typeof node.detatchEvent == 'function') {
1367
+ node.detatchEvent('on' + event, fn);
1368
+ }
1369
+ }
1370
+
1371
+
1372
+ /**
1373
+ * Returns the intersection between two rect objects.
1374
+ * @param {Object} rect1 The first rect.
1375
+ * @param {Object} rect2 The second rect.
1376
+ * @return {?Object} The intersection rect or undefined if no intersection
1377
+ * is found.
1378
+ */
1379
+ function computeRectIntersection(rect1, rect2) {
1380
+ var top = Math.max(rect1.top, rect2.top);
1381
+ var bottom = Math.min(rect1.bottom, rect2.bottom);
1382
+ var left = Math.max(rect1.left, rect2.left);
1383
+ var right = Math.min(rect1.right, rect2.right);
1384
+ var width = right - left;
1385
+ var height = bottom - top;
1386
+
1387
+ return (width >= 0 && height >= 0) && {
1388
+ top: top,
1389
+ bottom: bottom,
1390
+ left: left,
1391
+ right: right,
1392
+ width: width,
1393
+ height: height
1394
+ };
1395
+ }
1396
+
1397
+
1398
+ /**
1399
+ * Shims the native getBoundingClientRect for compatibility with older IE.
1400
+ * @param {Element} el The element whose bounding rect to get.
1401
+ * @return {Object} The (possibly shimmed) rect of the element.
1402
+ */
1403
+ function getBoundingClientRect(el) {
1404
+ var rect;
1405
+
1406
+ try {
1407
+ rect = el.getBoundingClientRect();
1408
+ } catch (err) {
1409
+ // Ignore Windows 7 IE11 "Unspecified error"
1410
+ // https://github.com/w3c/IntersectionObserver/pull/205
1411
+ }
1412
+
1413
+ if (!rect) return getEmptyRect();
1414
+
1415
+ // Older IE
1416
+ if (!(rect.width && rect.height)) {
1417
+ rect = {
1418
+ top: rect.top,
1419
+ right: rect.right,
1420
+ bottom: rect.bottom,
1421
+ left: rect.left,
1422
+ width: rect.right - rect.left,
1423
+ height: rect.bottom - rect.top
1424
+ };
1425
+ }
1426
+ return rect;
1427
+ }
1428
+
1429
+
1430
+ /**
1431
+ * Returns an empty rect object. An empty rect is returned when an element
1432
+ * is not in the DOM.
1433
+ * @return {Object} The empty rect.
1434
+ */
1435
+ function getEmptyRect() {
1436
+ return {
1437
+ top: 0,
1438
+ bottom: 0,
1439
+ left: 0,
1440
+ right: 0,
1441
+ width: 0,
1442
+ height: 0
1443
+ };
1444
+ }
1445
+
1446
+ /**
1447
+ * Checks to see if a parent element contains a child element (including inside
1448
+ * shadow DOM).
1449
+ * @param {Node} parent The parent element.
1450
+ * @param {Node} child The child element.
1451
+ * @return {boolean} True if the parent node contains the child node.
1452
+ */
1453
+ function containsDeep(parent, child) {
1454
+ var node = child;
1455
+ while (node) {
1456
+ if (node == parent) return true;
1457
+
1458
+ node = getParentNode(node);
1459
+ }
1460
+ return false;
1461
+ }
1462
+
1463
+
1464
+ /**
1465
+ * Gets the parent node of an element or its host element if the parent node
1466
+ * is a shadow root.
1467
+ * @param {Node} node The node whose parent to get.
1468
+ * @return {Node|null} The parent node or null if no parent exists.
1469
+ */
1470
+ function getParentNode(node) {
1471
+ var parent = node.parentNode;
1472
+
1473
+ if (parent && parent.nodeType == 11 && parent.host) {
1474
+ // If the parent is a shadow root, return the host element.
1475
+ return parent.host;
1476
+ }
1477
+ return parent;
1478
+ }
1479
+
1480
+
1481
+ // Exposes the constructors globally.
1482
+ window.IntersectionObserver = IntersectionObserver;
1483
+ window.IntersectionObserverEntry = IntersectionObserverEntry;
1484
+
1485
+ }(window, document));
1486
+
1487
+ if (typeof Object.assign != 'function') {
1488
+ // Must be writable: true, enumerable: false, configurable: true
1489
+ Object.defineProperty(Object, 'assign', {
1490
+ value: function assign(target, varArgs) {
1491
+
1492
+ if (target == null) {
1493
+ // TypeError if undefined or null
1494
+ throw new TypeError('Cannot convert undefined or null to object');
1495
+ }
1496
+
1497
+ var to = Object(target);
1498
+
1499
+ for (var index = 1; index < arguments.length; index++) {
1500
+ var nextSource = arguments[index];
1501
+
1502
+ if (nextSource != null) {
1503
+ // Skip over if undefined or null
1504
+ for (var nextKey in nextSource) {
1505
+ // Avoid bugs when hasOwnProperty is shadowed
1506
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
1507
+ to[nextKey] = nextSource[nextKey];
1508
+ }
1509
+ }
1510
+ }
1511
+ }
1512
+ return to;
1513
+ },
1514
+ writable: true,
1515
+ configurable: true
1516
+ });
1517
+ }
1518
+
1519
+ var Optimizer = function () {
1520
+ function Optimizer() {
1521
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1522
+ classCallCheck(this, Optimizer);
1523
+
1524
+ this.initDependencies();
1525
+ this.initOptions(options);
1526
+ this.optimizeImages();
1527
+ this.optimizeBgImages();
1528
+ }
1529
+
1530
+ // ---------------------------------------- | Dependencies
1531
+
1532
+ createClass(Optimizer, [{
1533
+ key: 'initDependencies',
1534
+ value: function initDependencies() {
1535
+ }
1536
+
1537
+ // ---------------------------------------- | Options
1538
+
1539
+ }, {
1540
+ key: 'initOptions',
1541
+ value: function initOptions() {
1542
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1543
+
1544
+ this.options = options;
1545
+ var defaultOptions = {
1546
+ parent: 'body'
1547
+ };
1548
+ for (var key in defaultOptions) {
1549
+ if (defaultOptions.hasOwnProperty(key) && !this.options[key]) {
1550
+ this.options[key] = defaultOptions[key];
1551
+ }
1552
+ }
1553
+ }
1554
+
1555
+ // ---------------------------------------- | Inline Images
1556
+
1557
+ }, {
1558
+ key: 'optimizeImages',
1559
+ value: function optimizeImages() {
1560
+ $(this.options.parent + ' img[data-optimize-img]').each(function (idx, img) {
1561
+ new Image(img);
1562
+ });
1563
+ }
1564
+
1565
+ // ---------------------------------------- | Background Images
1566
+
1567
+ }, {
1568
+ key: 'optimizeBgImages',
1569
+ value: function optimizeBgImages() {
1570
+ $(this.options.parent + ' [data-optimize-bg-img]').each(function (idx, img) {
1571
+ new BackgroundImage(img);
1572
+ });
1573
+ return true;
1574
+ }
1575
+ }]);
1576
+ return Optimizer;
1577
+ }();
1578
+
1579
+ window['Imgix'] = window['Imgix'] || {};
1580
+
1581
+ Imgix.Optimizer = Optimizer;
1582
+
1583
+ }());