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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +2 -2
- data/dist/crds.html +57 -0
- data/dist/imgix-optimizer-0.0.6.min.js +1 -0
- data/dist/imgix-optimizer.js +1063 -191
- data/dist/index.html +209 -85
- data/package-lock.json +328 -332
- data/package.json +2 -1
- data/src/{imgix_bg_image.js → background_image.js} +55 -16
- data/src/{imgix_image.js → image.js} +80 -10
- data/src/main.js +0 -4
- data/src/optimizer.js +15 -7
- data/src/polyfills/object_assign.js +32 -0
- metadata +7 -6
- data/dist/imgix-optimizer-0.0.5.min.js +0 -1
- data/vendor/assets/javascripts/imgix-optimizer.js +0 -695
data/package.json
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
{
|
2
2
|
"name": "imgix-optimizer",
|
3
|
-
"version": "0.0.
|
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
|
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') {
|
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', (
|
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
|
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
|
-
//
|
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(
|
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.
|
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) {
|
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
|
-
|
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',
|
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
|
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
|
-
*
|
18
|
+
* listening for the image to intersect the viewport.
|
18
19
|
*/
|
19
20
|
initOptimization() {
|
20
21
|
$('<img>')
|
21
|
-
.on('load', $.proxy(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:
|
76
|
-
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
|
-
|
91
|
-
.
|
92
|
-
.replace(/(\?|\&)(
|
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) {
|
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
|
2
|
-
import
|
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
|
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
|
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.
|
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:
|
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/
|
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/
|
54
|
-
- src/
|
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
|