imgix-optimizer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,643 @@
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 ImgixBgImage = function () {
29
+ function ImgixBgImage(el) {
30
+ classCallCheck(this, ImgixBgImage);
31
+
32
+ // Length of time to complete fade-in transition.
33
+ this.timeToFade = 500;
34
+ // The primary element (i.e. the one with the background image).
35
+ this.el = $(el);
36
+ // Background image CSS property must be present.
37
+ if (this.el.css('background-image') == 'none') {
38
+ return;
39
+ }
40
+ // Prepare the element and its container for optimization.
41
+ this.initEl();
42
+ // Kick off the optimization process.
43
+ this.initOptimization();
44
+ // Listen for window resize events.
45
+ this.initEventListeners();
46
+ }
47
+
48
+ /**
49
+ * Load an image in memory (not within the DOM) with the same source as the
50
+ * placeholder image. Once that has completed, we know we're safe to begin
51
+ * processing.
52
+ */
53
+
54
+
55
+ createClass(ImgixBgImage, [{
56
+ key: 'initOptimization',
57
+ value: function initOptimization() {
58
+ var _this = this;
59
+
60
+ $('<img>').on('load', function () {
61
+ return _this.renderTmpPlaceholderEl();
62
+ }).attr('src', this.placeholderImgUrl);
63
+ }
64
+
65
+ // ---------------------------------------- | Main Element
66
+
67
+ /**
68
+ * Prepare the main element and its container for optimization.
69
+ */
70
+
71
+ }, {
72
+ key: 'initEl',
73
+ value: function initEl() {
74
+ this.setPlaceholderImgUrl();
75
+ this.setContainerTmpCss();
76
+ this.setElTmpCss();
77
+ }
78
+
79
+ /**
80
+ * Set reference to original image URL, which is expected to be a small
81
+ * placeholder.
82
+ */
83
+
84
+ }, {
85
+ key: 'setPlaceholderImgUrl',
86
+ value: function setPlaceholderImgUrl() {
87
+ this.placeholderImgUrl = this.el.css('background-image').replace('url(', '').replace(')', '').replace(/\"/gi, "").replace(/\'/gi, "").split(', ')[0];
88
+ }
89
+
90
+ /**
91
+ * The parent of our jumbotron container should be relatively positioned
92
+ * (temporarily) so that we can absolutely position the temp image in the
93
+ * correct location.
94
+ */
95
+
96
+ }, {
97
+ key: 'setContainerTmpCss',
98
+ value: function setContainerTmpCss() {
99
+ this.el.parent().css('position', 'relative');
100
+ }
101
+
102
+ /**
103
+ * The main element must have a position set for it to be rendered on top of
104
+ * the temporary full-size image. We assume that if the element is not
105
+ * explicitly positioned absolutely, then it can safely be positioned
106
+ * relatively.
107
+ */
108
+
109
+ }, {
110
+ key: 'setElTmpCss',
111
+ value: function setElTmpCss() {
112
+ if (this.el.css('position') != 'absolute') {
113
+ this.el.css('position', 'relative');
114
+ }
115
+ }
116
+
117
+ // ---------------------------------------- | Placeholder Image (Temp)
118
+
119
+ /**
120
+ * Render a clone of the element with the background image directly behind
121
+ * itself.
122
+ */
123
+
124
+ }, {
125
+ key: 'renderTmpPlaceholderEl',
126
+ value: function renderTmpPlaceholderEl() {
127
+ this.initTmpPlaceholderEl();
128
+ this.setTmpPlaceholderElCss();
129
+ this.addTmpPlaceholderElToDom();
130
+ this.renderFullSizeImg();
131
+ }
132
+
133
+ /**
134
+ * Create a clone of the element with the background image. Remove content
135
+ * from the clone -- often elements with a background image contain content.
136
+ */
137
+
138
+ }, {
139
+ key: 'initTmpPlaceholderEl',
140
+ value: function initTmpPlaceholderEl() {
141
+ this.tmpPlaceholderEl = this.el.clone();
142
+ this.tmpPlaceholderEl.html('');
143
+ }
144
+
145
+ /**
146
+ * Position the clone directly behind the main element
147
+ */
148
+
149
+ }, {
150
+ key: 'setTmpPlaceholderElCss',
151
+ value: function setTmpPlaceholderElCss() {
152
+ this.tmpPlaceholderEl.css({
153
+ position: 'absolute',
154
+ top: this.el.position().top,
155
+ left: this.el.position().left,
156
+ width: this.el.outerWidth(),
157
+ height: this.el.outerHeight(),
158
+ backgroundColor: 'transparent'
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Add temporary element to the DOM, directly before the main element
164
+ * containing the background image.
165
+ */
166
+
167
+ }, {
168
+ key: 'addTmpPlaceholderElToDom',
169
+ value: function addTmpPlaceholderElToDom() {
170
+ this.tmpPlaceholderEl.insertBefore(this.el);
171
+ }
172
+
173
+ // ---------------------------------------- | Full-Size Image (Temp)
174
+
175
+ /**
176
+ * Create another clone, this time of the temporary placeholder image. This
177
+ * new element sits behind the other two and is responsible for loading the
178
+ * full-size image.
179
+ */
180
+
181
+ }, {
182
+ key: 'renderFullSizeImg',
183
+ value: function renderFullSizeImg() {
184
+ this.removeElBgImg();
185
+ this.initTmpFullSizeEl();
186
+ this.setTmpFullSizeElImg();
187
+ this.addTmpFullSizeElToDom();
188
+ this.initTransition();
189
+ }
190
+
191
+ /**
192
+ * Remove the background color and image from the main element. The user won't
193
+ * notice this transition because the temp duplicate image is already set and
194
+ * is sitting behind the primary element.
195
+ *
196
+ * This also stores a reference to the original background color so we can put
197
+ * it back when the transition is complete.
198
+ */
199
+
200
+ }, {
201
+ key: 'removeElBgImg',
202
+ value: function removeElBgImg() {
203
+ this.elBgColor = this.el.css('background-color');
204
+ this.el.css('background-color', 'transparent');
205
+ this.el.css('background-image', '');
206
+ }
207
+
208
+ /**
209
+ * The temporary full-size element is a clone of the temporary placeholder
210
+ * image element.
211
+ */
212
+
213
+ }, {
214
+ key: 'initTmpFullSizeEl',
215
+ value: function initTmpFullSizeEl() {
216
+ this.tmpFullSizeEl = this.tmpPlaceholderEl.clone();
217
+ }
218
+
219
+ /**
220
+ * Sets a reference to the full-size image URL based on the current dimensions
221
+ * of the main element.
222
+ */
223
+
224
+ }, {
225
+ key: 'setFullSizeImgUrl',
226
+ value: function setFullSizeImgUrl() {
227
+ // Work with the placeholdler image URL, which has been pulled from the
228
+ // background-image css property of the main elements.
229
+ var url = this.placeholderImgUrl.split('?');
230
+ // q is an array of querystring parameters as ["k=v", "k=v", ...].
231
+ var q = url[url.length - 1].split('&');
232
+ // Mapping q converts the array to an object of querystring parameters as
233
+ // { k: v, k: v, ... }.
234
+ var args = {};
235
+ q.map(function (x) {
236
+ return args[x.split('=')[0]] = x.split('=')[1];
237
+ });
238
+ // If the image's container is wider than it is tall, we only set width and
239
+ // unset height, and vice versa.
240
+ if (this.el.width() >= this.el.height()) {
241
+ args['w'] = this.el.width();
242
+ delete args['h'];
243
+ } else {
244
+ args['h'] = this.el.height();
245
+ delete args['w'];
246
+ }
247
+ // Redefine q and go the other direction -- take the args object and convert
248
+ // it back to an array of querystring parameters, as ["k=v", "k=v", ...].
249
+ q = [];
250
+ for (var k in args) {
251
+ q.push(k + '=' + args[k]);
252
+ }
253
+ // Store the result and return.
254
+ return this.fullSizeImgUrl = url[0] + '?' + q.join('&');
255
+ }
256
+
257
+ /**
258
+ * Change the URL of this temporary element's background image to be the
259
+ * full-size image.
260
+ */
261
+
262
+ }, {
263
+ key: 'setTmpFullSizeElImg',
264
+ value: function setTmpFullSizeElImg() {
265
+ this.setFullSizeImgUrl();
266
+ this.tmpFullSizeEl.css('background-image', 'url("' + this.fullSizeImgUrl + '")');
267
+ }
268
+
269
+ /**
270
+ * Add the temporary full-size element direct before the temporary placeholder
271
+ * element.
272
+ */
273
+
274
+ }, {
275
+ key: 'addTmpFullSizeElToDom',
276
+ value: function addTmpFullSizeElToDom() {
277
+ this.tmpFullSizeEl.insertBefore(this.tmpPlaceholderEl);
278
+ }
279
+
280
+ // ---------------------------------------- | Transition
281
+
282
+ /**
283
+ * Load full-size image in memory. When it has loaded we can confidentally
284
+ * fade out the placeholder, knowing the full-size image will be in its place.
285
+ */
286
+
287
+ }, {
288
+ key: 'initTransition',
289
+ value: function initTransition() {
290
+ $('<img>').on('load', $.proxy(this.transitionImg, this)).attr('src', this.fullSizeImgUrl);
291
+ }
292
+
293
+ /**
294
+ * Fade out the temporary placeholder, set the background-image on the main
295
+ * element to the full-size URL, then remove the temporary elements behind the
296
+ * main element
297
+ */
298
+
299
+ }, {
300
+ key: 'transitionImg',
301
+ value: function transitionImg() {
302
+ var _this2 = this;
303
+
304
+ this.fadeOutTmpPlaceholderEl();
305
+ setTimeout(function () {
306
+ _this2.updateElImg();
307
+ _this2.replaceElTmpCss();
308
+ _this2.removeTmpEls();
309
+ }, this.timeToFade);
310
+ }
311
+
312
+ /**
313
+ * Fade out the placeholder element. This was the temporary clone of the main
314
+ * element that has a placeholder background image.
315
+ *
316
+ * Rememeber the main element's background image was unset and its color set
317
+ * to transparent. That is why fading out this temporary image will work
318
+ * properly.
319
+ */
320
+
321
+ }, {
322
+ key: 'fadeOutTmpPlaceholderEl',
323
+ value: function fadeOutTmpPlaceholderEl() {
324
+ this.tmpPlaceholderEl.fadeTo(this.timeToFade, 0);
325
+ }
326
+
327
+ /**
328
+ * Reset the image URL (this helps if the size of the element has changed),
329
+ * then set the background image to the new source.
330
+ */
331
+
332
+ }, {
333
+ key: 'updateElImg',
334
+ value: function updateElImg() {
335
+ this.setFullSizeImgUrl();
336
+ this.el.css('background-image', 'url(\'' + this.fullSizeImgUrl + '\')');
337
+ }
338
+
339
+ /**
340
+ * Set the background color back to what it was before the transition.
341
+ */
342
+
343
+ }, {
344
+ key: 'replaceElTmpCss',
345
+ value: function replaceElTmpCss() {
346
+ this.el.css('background-color', this.elBgColor);
347
+ }
348
+
349
+ /**
350
+ * Remove both temporary elements from the DOM.
351
+ */
352
+
353
+ }, {
354
+ key: 'removeTmpEls',
355
+ value: function removeTmpEls() {
356
+ this.tmpPlaceholderEl.remove();
357
+ this.tmpFullSizeEl.remove();
358
+ this.tmpPlaceholderEl = undefined;
359
+ this.tmpFullSizeEl = undefined;
360
+ }
361
+
362
+ // ---------------------------------------- | Event Listeners
363
+
364
+ /**
365
+ * Listener for window resize events and update the image when the event ends.
366
+ */
367
+
368
+ }, {
369
+ key: 'initEventListeners',
370
+ value: function initEventListeners() {
371
+ var _this3 = this;
372
+
373
+ this.initResizeEnd();
374
+ $(window).on('resizeEnd', function (event) {
375
+ return _this3.updateElImg();
376
+ });
377
+ }
378
+
379
+ /**
380
+ * Trigger "resizeEnd" event on the window object after resizing has ceased
381
+ * for at least 0.5 seconds.
382
+ */
383
+
384
+ }, {
385
+ key: 'initResizeEnd',
386
+ value: function initResizeEnd() {
387
+ $(window).resize(function () {
388
+ if (this.resizeTo) {
389
+ clearTimeout(this.resizeTo);
390
+ }
391
+ this.resizeTo = setTimeout(function () {
392
+ $(this).trigger('resizeEnd');
393
+ }, 500);
394
+ });
395
+ }
396
+ }]);
397
+ return ImgixBgImage;
398
+ }();
399
+
400
+ var ImgixImage = function () {
401
+ function ImgixImage(img) {
402
+ classCallCheck(this, ImgixImage);
403
+
404
+ // Length of crossfade transition.
405
+ this.timeToFade = 500;
406
+ // Main (pixellated placeholder) image.
407
+ this.placeholderImg = $(img);
408
+ // Kick off the optimization process.
409
+ this.initOptimization();
410
+ }
411
+
412
+ /**
413
+ * Load an image in memory (not within the DOM) with the same source as the
414
+ * placeholder image. Once that has completed, we know we're safe to begin
415
+ * processing.
416
+ */
417
+
418
+
419
+ createClass(ImgixImage, [{
420
+ key: 'initOptimization',
421
+ value: function initOptimization() {
422
+ $('<img>').on('load', $.proxy(this.renderFullSizeImg, this)).attr('src', this.placeholderImg.attr('src'));
423
+ }
424
+
425
+ // ---------------------------------------- | Full-Size Image
426
+
427
+ /**
428
+ * Render the full-size image behind the placeholder image.
429
+ */
430
+
431
+ }, {
432
+ key: 'renderFullSizeImg',
433
+ value: function renderFullSizeImg() {
434
+ this.initFullSizeImg();
435
+ this.setFullSizeImgTempCss();
436
+ this.setFullSizeImgSrc();
437
+ this.addFullSizeImgToDom();
438
+ this.initTransition();
439
+ }
440
+
441
+ /**
442
+ * The full-size image is a clone of the placeholder image. This enables us to
443
+ * easily replace it without losing any necessary styles or attributes.
444
+ */
445
+
446
+ }, {
447
+ key: 'initFullSizeImg',
448
+ value: function initFullSizeImg() {
449
+ this.fullSizeImg = this.placeholderImg.clone();
450
+ }
451
+
452
+ /**
453
+ * Give the full-size image a temporary set of CSS rules so that it can sit
454
+ * directly behind the placeholder image while loading.
455
+ */
456
+
457
+ }, {
458
+ key: 'setFullSizeImgTempCss',
459
+ value: function setFullSizeImgTempCss() {
460
+ this.fullSizeImg.css({
461
+ position: 'absolute',
462
+ top: this.placeholderImg.position().top,
463
+ left: this.placeholderImg.position().left,
464
+ width: this.placeholderImg.width(),
465
+ height: this.placeholderImg.height()
466
+ });
467
+ }
468
+
469
+ /**
470
+ * Prep the full-size image with the attributes necessary to become its full
471
+ * size. Right now it is still just a replica of the placeholder, sitting
472
+ * right behind the placeholder.
473
+ *
474
+ * We set the src directly even though we're using imgix.js because older
475
+ * browsers don't support the srcset attribute which is what imgix.js relies
476
+ * upon.
477
+ */
478
+
479
+ }, {
480
+ key: 'setFullSizeImgSrc',
481
+ value: function setFullSizeImgSrc() {
482
+ var newSrc = this.placeholderImg.attr('src').replace(/(\?|\&)(w=)(\d+)/i, '$1$2' + this.placeholderImg.width()).replace(/(\?|\&)(h=)(\d+)/i, '$1$2' + this.placeholderImg.height());
483
+ this.fullSizeImg.attr('ix-src', newSrc);
484
+ // TODO: Make this a configurable option or document it as a more semantic temporary class
485
+ this.fullSizeImg.addClass('img-responsive tmp-img-placeholder');
486
+ // TODO: This should respect the option from the Optimizer class for the select
487
+ this.fullSizeImg.removeAttr('data-optimize-img');
488
+ }
489
+
490
+ /**
491
+ * Render the full-size image in the DOM.
492
+ */
493
+
494
+ }, {
495
+ key: 'addFullSizeImgToDom',
496
+ value: function addFullSizeImgToDom() {
497
+ this.fullSizeImg.insertBefore(this.placeholderImg);
498
+ }
499
+
500
+ // ---------------------------------------- | Image Transition
501
+
502
+ /**
503
+ * Once the full-size image is loaded, begin the transition. This is the
504
+ * critical piece of this process. Imgix.js uses the ix-src attribute to build
505
+ * out the srcset attribute. Then, based on the sizes attribute, the browser
506
+ * determines which source to render. Therefore we can't preload in memory
507
+ * because we need imgix to do its thing directly in the DOM.
508
+ */
509
+
510
+ }, {
511
+ key: 'initTransition',
512
+ value: function initTransition() {
513
+ var _this = this;
514
+
515
+ this.fullSizeImg.on('load', function () {
516
+ return _this.transitionImg();
517
+ });
518
+ imgix.init();
519
+ }
520
+
521
+ /**
522
+ * Fade out the placeholder image, effectively showing the image behind it.
523
+ *
524
+ * Once the fade out transition has completed, remove any temporary properties
525
+ * from the full-size image (so it gets back to being a clone of the
526
+ * placeholder, with the full-size src).
527
+ *
528
+ * Finally, remove the placeholder image from the DOM since we don't need it
529
+ * any more.
530
+ */
531
+
532
+ }, {
533
+ key: 'transitionImg',
534
+ value: function transitionImg() {
535
+ var _this2 = this;
536
+
537
+ if (!this.placeholderImg) return true;
538
+ this.fadeOutPlaceholder();
539
+ setTimeout(function () {
540
+ _this2.removeFullSizeImgProperties();
541
+ _this2.removeImg();
542
+ }, this.timeToFade);
543
+ }
544
+
545
+ /**
546
+ * Fade out the placeholder image.
547
+ */
548
+
549
+ }, {
550
+ key: 'fadeOutPlaceholder',
551
+ value: function fadeOutPlaceholder() {
552
+ this.placeholderImg.fadeTo(this.timeToFade, 0);
553
+ }
554
+
555
+ /**
556
+ * Remove temporary styles and class from the full-size image, which
557
+ * effectively means it has replaced the placeholder image.
558
+ */
559
+
560
+ }, {
561
+ key: 'removeFullSizeImgProperties',
562
+ value: function removeFullSizeImgProperties() {
563
+ this.fullSizeImg.removeAttr('style');
564
+ // TODO: Update this with how the class is handled above.
565
+ this.fullSizeImg.removeClass('tmp-img-placeholder');
566
+ }
567
+
568
+ /**
569
+ * Remove the placeholder image from the DOM since we no longer need it.
570
+ */
571
+
572
+ }, {
573
+ key: 'removeImg',
574
+ value: function removeImg() {
575
+ if (!this.placeholderImg) {
576
+ return;
577
+ }
578
+ this.placeholderImg.remove();
579
+ this.placeholderImg = undefined;
580
+ }
581
+ }]);
582
+ return ImgixImage;
583
+ }();
584
+
585
+ var Optimizer = function () {
586
+ function Optimizer() {
587
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
588
+ classCallCheck(this, Optimizer);
589
+
590
+ this.initOptions(options);
591
+ this.optimizeImages();
592
+ this.optimizeBgImages();
593
+ }
594
+
595
+ // ---------------------------------------- | Options
596
+
597
+ createClass(Optimizer, [{
598
+ key: 'initOptions',
599
+ value: function initOptions() {
600
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
601
+
602
+ this.options = options;
603
+ var defaultOptions = {
604
+ parent: 'body'
605
+ };
606
+ for (var key in defaultOptions) {
607
+ if (defaultOptions.hasOwnProperty(key) && !this.options[key]) {
608
+ this.options[key] = defaultOptions[key];
609
+ }
610
+ }
611
+ }
612
+
613
+ // ---------------------------------------- | Inline Images
614
+
615
+ }, {
616
+ key: 'optimizeImages',
617
+ value: function optimizeImages() {
618
+ $(this.options.parent + ' img[data-optimize-img]').each(function (idx, img) {
619
+ new ImgixImage(img);
620
+ });
621
+ }
622
+
623
+ // ---------------------------------------- | Background Images
624
+
625
+ }, {
626
+ key: 'optimizeBgImages',
627
+ value: function optimizeBgImages() {
628
+ $(this.options.parent + ' [data-optimize-bg-img]').each(function (idx, img) {
629
+ new ImgixBgImage(img);
630
+ });
631
+ return true;
632
+ }
633
+ }]);
634
+ return Optimizer;
635
+ }();
636
+
637
+ window['Imgix'] = window['Imgix'] || {};
638
+
639
+ Imgix.ImgixBgImage = ImgixBgImage;
640
+ Imgix.ImgixImage = ImgixImage;
641
+ Imgix.Optimizer = Optimizer;
642
+
643
+ }());