imgix-optimizer 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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