imgix-optimizer 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "imgix-optimizer",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "devDependencies": {
5
5
  "babel-core": "^6.26.3",
6
6
  "babel-preset-es2015-rollup": "^3.0.0",
7
+ "intersection-observer": "^0.5.1",
7
8
  "rollup": "^0.60.0",
8
9
  "rollup-plugin-babel": "^3.0.7",
9
10
  "rollup-plugin-commonjs": "^9.1.0",
@@ -1,14 +1,20 @@
1
- export default class ImgixBgImage {
2
-
1
+ export default class BackgroundImage {
3
2
  constructor(el) {
4
3
  // Length of time to complete fade-in transition.
5
4
  this.timeToFade = 500;
5
+ // Data attribute applied before processing.
6
+ this.processingAttr = 'data-imgix-bg-processed';
6
7
  // Device pixel ratio assumes 1 if not set.
7
8
  this.dpr = window['devicePixelRatio'] || 1;
9
+ // The largest image that has been loaded. This assumes the height of the
10
+ // container will not change.
11
+ this.largestImageWidth = 0;
8
12
  // The primary element (i.e. the one with the background image).
9
13
  this.el = $(el);
10
14
  // Background image CSS property must be present.
11
- if (this.el.css('background-image') == 'none') { return }
15
+ if (this.el.css('background-image') == 'none') {
16
+ return;
17
+ }
12
18
  // Prepare the element and its container for optimization.
13
19
  this.initEl();
14
20
  // Kick off the optimization process.
@@ -24,10 +30,32 @@ export default class ImgixBgImage {
24
30
  */
25
31
  initOptimization() {
26
32
  $('<img>')
27
- .on('load', () => this.renderTmpPlaceholderEl())
33
+ .on('load', $.proxy(this.listenForIntersection, this))
28
34
  .attr('src', this.placeholderImgUrl);
29
35
  }
30
36
 
37
+ /**
38
+ * When the element intersects the viewport, begin processing.
39
+ * (IntersectionObserver and Object.assign() are not supported by IE, but the
40
+ * polyfills are loaded by Imgix.Optimizer.)
41
+ */
42
+ listenForIntersection() {
43
+ const observer = new IntersectionObserver($.proxy(this.onIntersection, this));
44
+ observer.observe(this.el[0]);
45
+ }
46
+
47
+ /**
48
+ * When the element intersects the viewport, check if it is in the viewport
49
+ * and has not yet been processed. If those conditions are true, begin
50
+ * rendering the full size image and the transition process.
51
+ */
52
+ onIntersection(entries, observer) {
53
+ let el = $(entries[0].target);
54
+ if (!entries[0].isIntersecting || $(el).attr(this.processingAttr)) return;
55
+ $(el).attr(this.processingAttr, true);
56
+ this.renderTmpPlaceholderEl();
57
+ }
58
+
31
59
  // ---------------------------------------- | Main Element
32
60
 
33
61
  /**
@@ -44,11 +72,12 @@ export default class ImgixBgImage {
44
72
  * placeholder.
45
73
  */
46
74
  setPlaceholderImgUrl() {
47
- this.placeholderImgUrl = this.el.css('background-image')
75
+ this.placeholderImgUrl = this.el
76
+ .css('background-image')
48
77
  .replace('url(', '')
49
78
  .replace(')', '')
50
- .replace(/\"/gi, "")
51
- .replace(/\'/gi, "")
79
+ .replace(/\"/gi, '')
80
+ .replace(/\'/gi, '')
52
81
  .split(', ')[0];
53
82
  }
54
83
 
@@ -61,7 +90,7 @@ export default class ImgixBgImage {
61
90
  this.parentStyles = {
62
91
  display: this.el.parent().css('display'),
63
92
  position: this.el.parent().css('position')
64
- }
93
+ };
65
94
  this.el.parent().css({
66
95
  display: 'block',
67
96
  position: 'relative'
@@ -167,7 +196,13 @@ export default class ImgixBgImage {
167
196
  * of the main element.
168
197
  */
169
198
  setFullSizeImgUrl() {
170
- // Work with the placeholdler image URL, which has been pulled from the
199
+ // If the full size image URL exists and if the new size is going to be
200
+ // smaller than the largest size loaded, then we stick with the largest size
201
+ // that has been used.
202
+ if (this.fullSizeImgUrl && this.el.outerWidth() * this.dpr <= this.largestImageWidth) return;
203
+ // Assume that the new width will be the largest size used.
204
+ this.largestImageWidth = this.el.outerWidth() * this.dpr;
205
+ // Work with the placeholder image URL, which has been pulled from the
171
206
  // background-image css property of the main elements.
172
207
  let url = this.placeholderImgUrl.split('?');
173
208
  // q is an array of querystring parameters as ["k=v", "k=v", ...].
@@ -175,11 +210,11 @@ export default class ImgixBgImage {
175
210
  // Mapping q converts the array to an object of querystring parameters as
176
211
  // { k: v, k: v, ... }.
177
212
  let args = {};
178
- q.map((x) => args[x.split('=')[0]] = x.split('=')[1]);
213
+ q.map(x => (args[x.split('=')[0]] = x.split('=')[1]));
179
214
  // If the image's container is wider than it is tall, we only set width and
180
215
  // unset height, and vice versa.
181
216
  if (this.el.outerWidth() >= this.el.outerHeight()) {
182
- args['w'] = this.el.outerWidth() * this.dpr;
217
+ args['w'] = this.largestImageWidth;
183
218
  delete args['h'];
184
219
  } else {
185
220
  args['h'] = this.el.outerHeight() * this.dpr;
@@ -188,9 +223,11 @@ export default class ImgixBgImage {
188
223
  // Redefine q and go the other direction -- take the args object and convert
189
224
  // it back to an array of querystring parameters, as ["k=v", "k=v", ...].
190
225
  q = [];
191
- for (let k in args) { q.push(`${k}=${args[k]}`) }
226
+ for (let k in args) {
227
+ q.push(`${k}=${args[k]}`);
228
+ }
192
229
  // Store the result and return.
193
- return this.fullSizeImgUrl = `${url[0]}?${q.join('&')}`;
230
+ return (this.fullSizeImgUrl = `${url[0]}?${q.join('&')}`);
194
231
  }
195
232
 
196
233
  /**
@@ -255,7 +292,9 @@ export default class ImgixBgImage {
255
292
  */
256
293
  updateElImg() {
257
294
  this.setFullSizeImgUrl();
258
- this.el.css('background-image', `url('${this.fullSizeImgUrl}')`);
295
+ $('<img>')
296
+ .on('load', event => this.el.css('background-image', `url('${this.fullSizeImgUrl}')`))
297
+ .attr('src', this.placeholderImgUrl);
259
298
  }
260
299
 
261
300
  /**
@@ -292,7 +331,7 @@ export default class ImgixBgImage {
292
331
  */
293
332
  initEventListeners() {
294
333
  this.initResizeEnd();
295
- $(window).on('resizeEnd', (event) => this.updateElImg());
334
+ $(window).on('resizeEnd', event => this.updateElImg());
296
335
  }
297
336
 
298
337
  /**
@@ -302,7 +341,7 @@ export default class ImgixBgImage {
302
341
  initResizeEnd() {
303
342
  $(window).resize(function() {
304
343
  if (this.resizeTo) {
305
- clearTimeout(this.resizeTo)
344
+ clearTimeout(this.resizeTo);
306
345
  }
307
346
  this.resizeTo = setTimeout(function() {
308
347
  $(this).trigger('resizeEnd');
@@ -1,8 +1,9 @@
1
- export default class ImgixImage {
2
-
1
+ export default class Image {
3
2
  constructor(img) {
4
3
  // Length of crossfade transition.
5
4
  this.timeToFade = 500;
5
+ // Data attribute applied before processing.
6
+ this.processingAttr = 'data-imgix-img-processed';
6
7
  // The main image (pixelated placeholder).
7
8
  this.placeholderImg = $(img);
8
9
  // Configure the main placeholder image.
@@ -14,14 +15,36 @@ export default class ImgixImage {
14
15
  /**
15
16
  * Load an image in memory (not within the DOM) with the same source as the
16
17
  * placeholder image. Once that has completed, we know we're safe to begin
17
- * processing.
18
+ * listening for the image to intersect the viewport.
18
19
  */
19
20
  initOptimization() {
20
21
  $('<img>')
21
- .on('load', $.proxy(this.renderFullSizeImg, this))
22
+ .on('load', $.proxy(this.listenForIntersection, this))
22
23
  .attr('src', this.placeholderImg.attr('src'));
23
24
  }
24
25
 
26
+ /**
27
+ * When the placeholder image intersects the viewport, begin processing.
28
+ * (IntersectionObserver and Object.assign() are not supported by IE, but the
29
+ * polyfills are loaded by Imgix.Optimizer.)
30
+ */
31
+ listenForIntersection() {
32
+ const observer = new IntersectionObserver($.proxy(this.onIntersection, this));
33
+ observer.observe(this.placeholderImg[0]);
34
+ }
35
+
36
+ /**
37
+ * When the placeholder image intersects the viewport, check if it is in the
38
+ * viewport and has not yet been processed. If those conditions are true,
39
+ * begin rendering the full size image and the transition process.
40
+ */
41
+ onIntersection(entries, observer) {
42
+ let img = $(entries[0].target);
43
+ if (!entries[0].isIntersecting || $(img).attr(this.processingAttr)) return;
44
+ img.attr(this.processingAttr, true);
45
+ this.renderFullSizeImg();
46
+ }
47
+
25
48
  // ---------------------------------------- | Placeholder Image
26
49
 
27
50
  /**
@@ -29,6 +52,7 @@ export default class ImgixImage {
29
52
  */
30
53
  initPlaceholder() {
31
54
  this.setPlaceholderCss();
55
+ this.setPlaceholderParentTmpCss();
32
56
  }
33
57
 
34
58
  /**
@@ -42,6 +66,21 @@ export default class ImgixImage {
42
66
  }
43
67
  }
44
68
 
69
+ /**
70
+ * The parent of the image container should be relatively positioned
71
+ * (temporarily) so temp image can be absolutely positioned.
72
+ */
73
+ setPlaceholderParentTmpCss() {
74
+ this.parentStyles = {
75
+ display: this.placeholderImg.parent().css('display'),
76
+ position: this.placeholderImg.parent().css('position')
77
+ };
78
+ this.placeholderImg.parent().css({
79
+ display: 'block',
80
+ position: 'relative'
81
+ });
82
+ }
83
+
45
84
  // ---------------------------------------- | Full-Size Image
46
85
 
47
86
  /**
@@ -72,11 +111,23 @@ export default class ImgixImage {
72
111
  position: 'absolute',
73
112
  top: this.placeholderImg.position().top,
74
113
  left: this.placeholderImg.position().left,
75
- width: this.placeholderImg.width(),
76
- height: this.placeholderImg.height()
114
+ width: '100%',
115
+ height: '100%'
77
116
  });
78
117
  }
79
118
 
119
+ /**
120
+ * Return the width and height of the placeholder image, including decimals.
121
+ * Uses precise measurements like this helps ensure the element doesn't slide
122
+ * when transitioning to the full size image.
123
+ */
124
+ getPlaceholderImgRect() {
125
+ return {
126
+ width: this.placeholderImg[0].getBoundingClientRect().width,
127
+ height: this.placeholderImg[0].getBoundingClientRect().height
128
+ };
129
+ }
130
+
80
131
  /**
81
132
  * Prep the full-size image with the attributes necessary to become its full
82
133
  * size. Right now it is still just a replica of the placeholder, sitting
@@ -87,9 +138,15 @@ export default class ImgixImage {
87
138
  * upon.
88
139
  */
89
140
  setFullSizeImgSrc() {
90
- var newSrc = this.placeholderImg.attr('src')
91
- .replace(/(\?|\&)(w=)(\d+)/i, '$1$2' + this.placeholderImg.width())
92
- .replace(/(\?|\&)(h=)(\d+)/i, '$1$2' + this.placeholderImg.height());
141
+ let newSrc = this.placeholderImg
142
+ .attr('src')
143
+ .replace(/(\?|\&)(w=)(\d+)/i, '$1$2' + this.getPlaceholderImgRect().width)
144
+ .replace(/(\?|\&)(h=)(\d+)/i, '$1$2' + this.getPlaceholderImgRect().height);
145
+ // Add a height attribute if it is missing. This is the key to the image not
146
+ // jumping around after transitioning to the full-size image.
147
+ if (newSrc.search(/(\?|\&)(h=)(\d+)/i) < 0) {
148
+ newSrc = `${newSrc}&h=${this.getPlaceholderImgRect().height}&fit=crop`;
149
+ }
93
150
  this.fullSizeImg.attr('ix-src', newSrc);
94
151
  // TODO: Make this a configurable option or document it as a more semantic temporary class
95
152
  this.fullSizeImg.addClass('img-responsive imgix-optimizing');
@@ -133,6 +190,7 @@ export default class ImgixImage {
133
190
  this.fadeOutPlaceholder();
134
191
  setTimeout(() => {
135
192
  this.removeFullSizeImgProperties();
193
+ this.replacePlaceholderParentTmpCss();
136
194
  this.removeImg();
137
195
  }, this.timeToFade);
138
196
  }
@@ -154,11 +212,23 @@ export default class ImgixImage {
154
212
  this.fullSizeImg.removeClass('imgix-optimizing');
155
213
  }
156
214
 
215
+ /**
216
+ * Reset the container's adjusted CSS properties.
217
+ */
218
+ replacePlaceholderParentTmpCss() {
219
+ this.placeholderImg.parent().css({
220
+ display: this.parentStyles.display,
221
+ position: this.parentStyles.position
222
+ });
223
+ }
224
+
157
225
  /**
158
226
  * Remove the placeholder image from the DOM since we no longer need it.
159
227
  */
160
228
  removeImg() {
161
- if(!this.placeholderImg) { return }
229
+ if (!this.placeholderImg) {
230
+ return;
231
+ }
162
232
  this.placeholderImg.remove();
163
233
  this.placeholderImg = undefined;
164
234
  }
data/src/main.js CHANGED
@@ -1,9 +1,5 @@
1
- import ImgixBgImage from './imgix_bg_image';
2
- import ImgixImage from './imgix_image';
3
1
  import Optimizer from './optimizer';
4
2
 
5
3
  window['Imgix'] = window['Imgix'] || {};
6
4
 
7
- Imgix.ImgixBgImage = ImgixBgImage;
8
- Imgix.ImgixImage = ImgixImage;
9
5
  Imgix.Optimizer = Optimizer;
data/src/optimizer.js CHANGED
@@ -1,21 +1,30 @@
1
- import ImgixImage from './imgix_image';
2
- import ImgixBgImage from './imgix_bg_image';
1
+ import Image from './image';
2
+ import BackgroundImage from './background_image';
3
+ import * as IntObs from 'intersection-observer';
4
+ import * as ObjectAssign from './polyfills/object_assign';
3
5
 
4
6
  export default class Optimizer {
5
-
6
7
  constructor(options = {}) {
8
+ this.initDependencies();
7
9
  this.initOptions(options);
8
10
  this.optimizeImages();
9
11
  this.optimizeBgImages();
10
12
  }
11
13
 
14
+ // ---------------------------------------- | Dependencies
15
+
16
+ initDependencies() {
17
+ IntObs;
18
+ ObjectAssign;
19
+ }
20
+
12
21
  // ---------------------------------------- | Options
13
22
 
14
23
  initOptions(options = {}) {
15
24
  this.options = options;
16
25
  const defaultOptions = {
17
26
  parent: 'body'
18
- }
27
+ };
19
28
  for (let key in defaultOptions) {
20
29
  if (defaultOptions.hasOwnProperty(key) && !this.options[key]) {
21
30
  this.options[key] = defaultOptions[key];
@@ -27,7 +36,7 @@ export default class Optimizer {
27
36
 
28
37
  optimizeImages() {
29
38
  $(`${this.options.parent} img[data-optimize-img]`).each((idx, img) => {
30
- new ImgixImage(img);
39
+ new Image(img);
31
40
  });
32
41
  }
33
42
 
@@ -35,9 +44,8 @@ export default class Optimizer {
35
44
 
36
45
  optimizeBgImages() {
37
46
  $(`${this.options.parent} [data-optimize-bg-img]`).each((idx, img) => {
38
- new ImgixBgImage(img);
47
+ new BackgroundImage(img);
39
48
  });
40
49
  return true;
41
50
  }
42
-
43
51
  }
@@ -0,0 +1,32 @@
1
+ if (typeof Object.assign != 'function') {
2
+ // Must be writable: true, enumerable: false, configurable: true
3
+ Object.defineProperty(Object, 'assign', {
4
+ value: function assign(target, varArgs) {
5
+ // .length of function is 2
6
+ 'use strict';
7
+ if (target == null) {
8
+ // TypeError if undefined or null
9
+ throw new TypeError('Cannot convert undefined or null to object');
10
+ }
11
+
12
+ var to = Object(target);
13
+
14
+ for (var index = 1; index < arguments.length; index++) {
15
+ var nextSource = arguments[index];
16
+
17
+ if (nextSource != null) {
18
+ // Skip over if undefined or null
19
+ for (var nextKey in nextSource) {
20
+ // Avoid bugs when hasOwnProperty is shadowed
21
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
22
+ to[nextKey] = nextSource[nextKey];
23
+ }
24
+ }
25
+ }
26
+ }
27
+ return to;
28
+ },
29
+ writable: true,
30
+ configurable: true
31
+ });
32
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imgix-optimizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean C Davis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-29 00:00:00.000000000 Z
11
+ date: 2019-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -37,7 +37,8 @@ files:
37
37
  - LICENSE
38
38
  - README.md
39
39
  - Rakefile
40
- - dist/imgix-optimizer-0.0.5.min.js
40
+ - dist/crds.html
41
+ - dist/imgix-optimizer-0.0.6.min.js
41
42
  - dist/imgix-optimizer.js
42
43
  - dist/index.html
43
44
  - dist/main.css
@@ -50,13 +51,13 @@ files:
50
51
  - package.json
51
52
  - rollup.config.dev.js
52
53
  - rollup.config.js
53
- - src/imgix_bg_image.js
54
- - src/imgix_image.js
54
+ - src/background_image.js
55
+ - src/image.js
55
56
  - src/main.js
56
57
  - src/optimizer.js
58
+ - src/polyfills/object_assign.js
57
59
  - test/test.js
58
60
  - vendor/assets/javascripts/.keep
59
- - vendor/assets/javascripts/imgix-optimizer.js
60
61
  homepage: https://www.ample.co/
61
62
  licenses:
62
63
  - MIT