imgix-optimizer 0.0.1

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 ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "imgix-optimizer",
3
+ "version": "0.0.1",
4
+ "devDependencies": {
5
+ "babel-core": "^6.26.3",
6
+ "babel-preset-es2015-rollup": "^3.0.0",
7
+ "rollup": "^0.60.0",
8
+ "rollup-plugin-babel": "^3.0.7",
9
+ "rollup-plugin-commonjs": "^9.1.0",
10
+ "rollup-plugin-delete": "^0.1.2",
11
+ "rollup-plugin-node-resolve": "^3.0.0",
12
+ "rollup-plugin-serve": "^0.4.2",
13
+ "rollup-plugin-uglify": "^4.0.0"
14
+ },
15
+ "scripts": {
16
+ "copy": "cp dist/imgix-optimizer.js vendor/assets/javascripts/",
17
+ "build": "rollup -c && npm run copy",
18
+ "dev": "rollup -c ./rollup.config.dev.js -w",
19
+ "test": "node test/test.js",
20
+ "pretest": "npm run build"
21
+ },
22
+ "files": [
23
+ "dist/*.js"
24
+ ],
25
+ "license": "MIT"
26
+ }
@@ -0,0 +1,6 @@
1
+ import serve from 'rollup-plugin-serve';
2
+ import cfg from './rollup.config';
3
+
4
+ cfg[0].plugins.push(serve('dist'));
5
+
6
+ export default cfg
data/rollup.config.js ADDED
@@ -0,0 +1,50 @@
1
+ import pkg from './package.json';
2
+
3
+ import babel from 'rollup-plugin-babel';
4
+ import commonjs from 'rollup-plugin-commonjs';
5
+ import del from 'rollup-plugin-delete';
6
+ import resolve from 'rollup-plugin-node-resolve';
7
+ import { uglify } from 'rollup-plugin-uglify';
8
+
9
+ export default [{
10
+ input: 'src/main.js',
11
+ output: {
12
+ name: 'ImgixOptimizer',
13
+ file: `dist/${pkg.name}.js`,
14
+ format: 'iife'
15
+ },
16
+ plugins: [
17
+ del({ targets: ['dist/imgix-optimizer*', 'vendor/assets/javascripts/*.js'] }),
18
+ resolve(),
19
+ commonjs(),
20
+ babel({
21
+ exclude: 'node_modules/**',
22
+ babelrc: false,
23
+ presets: [
24
+ 'es2015-rollup'
25
+ ]
26
+ })
27
+ ]
28
+ },
29
+ // --- MINIFIED ---
30
+ {
31
+ input: 'src/main.js',
32
+ output: {
33
+ name: 'ImgixOptimizer',
34
+ file: `dist/${pkg.name}-${pkg.version}.min.js`,
35
+ format: 'iife'
36
+ },
37
+ plugins: [
38
+ resolve(),
39
+ commonjs(),
40
+ babel({
41
+ exclude: 'node_modules/**',
42
+ babelrc: false,
43
+ presets: [
44
+ 'es2015-rollup'
45
+ ]
46
+ }),
47
+ uglify()
48
+ ]
49
+ }
50
+ ];
@@ -0,0 +1,291 @@
1
+ export default class ImgixBgImage {
2
+
3
+ constructor(el) {
4
+ // Length of time to complete fade-in transition.
5
+ this.timeToFade = 500;
6
+ // The primary element (i.e. the one with the background image).
7
+ this.el = $(el);
8
+ // Background image CSS property must be present.
9
+ if (this.el.css('background-image') == 'none') { return }
10
+ // Prepare the element and its container for optimization.
11
+ this.initEl();
12
+ // Kick off the optimization process.
13
+ this.initOptimization();
14
+ // Listen for window resize events.
15
+ this.initEventListeners();
16
+ }
17
+
18
+ /**
19
+ * Load an image in memory (not within the DOM) with the same source as the
20
+ * placeholder image. Once that has completed, we know we're safe to begin
21
+ * processing.
22
+ */
23
+ initOptimization() {
24
+ $('<img>')
25
+ .on('load', () => this.renderTmpPlaceholderEl())
26
+ .attr('src', this.placeholderImgUrl);
27
+ }
28
+
29
+ // ---------------------------------------- | Main Element
30
+
31
+ /**
32
+ * Prepare the main element and its container for optimization.
33
+ */
34
+ initEl() {
35
+ this.setPlaceholderImgUrl();
36
+ this.setContainerTmpCss();
37
+ this.setElTmpCss();
38
+ }
39
+
40
+ /**
41
+ * Set reference to original image URL, which is expected to be a small
42
+ * placeholder.
43
+ */
44
+ setPlaceholderImgUrl() {
45
+ this.placeholderImgUrl = this.el.css('background-image')
46
+ .replace('url(', '')
47
+ .replace(')', '')
48
+ .replace(/\"/gi, "")
49
+ .replace(/\'/gi, "")
50
+ .split(', ')[0];
51
+ }
52
+
53
+ /**
54
+ * The parent of our jumbotron container should be relatively positioned
55
+ * (temporarily) so that we can absolutely position the temp image in the
56
+ * correct location.
57
+ */
58
+ setContainerTmpCss() {
59
+ this.el.parent().css('position', 'relative');
60
+ }
61
+
62
+ /**
63
+ * The main element must have a position set for it to be rendered on top of
64
+ * the temporary full-size image. We assume that if the element is not
65
+ * explicitly positioned absolutely, then it can safely be positioned
66
+ * relatively.
67
+ */
68
+ setElTmpCss() {
69
+ if (this.el.css('position') != 'absolute') {
70
+ this.el.css('position', 'relative');
71
+ }
72
+ }
73
+
74
+ // ---------------------------------------- | Placeholder Image (Temp)
75
+
76
+ /**
77
+ * Render a clone of the element with the background image directly behind
78
+ * itself.
79
+ */
80
+ renderTmpPlaceholderEl() {
81
+ this.initTmpPlaceholderEl();
82
+ this.setTmpPlaceholderElCss();
83
+ this.addTmpPlaceholderElToDom();
84
+ this.renderFullSizeImg();
85
+ }
86
+
87
+ /**
88
+ * Create a clone of the element with the background image. Remove content
89
+ * from the clone -- often elements with a background image contain content.
90
+ */
91
+ initTmpPlaceholderEl() {
92
+ this.tmpPlaceholderEl = this.el.clone();
93
+ this.tmpPlaceholderEl.html('');
94
+ }
95
+
96
+ /**
97
+ * Position the clone directly behind the main element
98
+ */
99
+ setTmpPlaceholderElCss() {
100
+ this.tmpPlaceholderEl.css({
101
+ position: 'absolute',
102
+ top: this.el.position().top,
103
+ left: this.el.position().left,
104
+ width: this.el.outerWidth(),
105
+ height: this.el.outerHeight(),
106
+ backgroundColor: 'transparent'
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Add temporary element to the DOM, directly before the main element
112
+ * containing the background image.
113
+ */
114
+ addTmpPlaceholderElToDom() {
115
+ this.tmpPlaceholderEl.insertBefore(this.el);
116
+ }
117
+
118
+ // ---------------------------------------- | Full-Size Image (Temp)
119
+
120
+ /**
121
+ * Create another clone, this time of the temporary placeholder image. This
122
+ * new element sits behind the other two and is responsible for loading the
123
+ * full-size image.
124
+ */
125
+ renderFullSizeImg() {
126
+ this.removeElBgImg();
127
+ this.initTmpFullSizeEl();
128
+ this.setTmpFullSizeElImg();
129
+ this.addTmpFullSizeElToDom();
130
+ this.initTransition();
131
+ }
132
+
133
+ /**
134
+ * Remove the background color and image from the main element. The user won't
135
+ * notice this transition because the temp duplicate image is already set and
136
+ * is sitting behind the primary element.
137
+ *
138
+ * This also stores a reference to the original background color so we can put
139
+ * it back when the transition is complete.
140
+ */
141
+ removeElBgImg() {
142
+ this.elBgColor = this.el.css('background-color');
143
+ this.el.css('background-color', 'transparent');
144
+ this.el.css('background-image', '');
145
+ }
146
+
147
+ /**
148
+ * The temporary full-size element is a clone of the temporary placeholder
149
+ * image element.
150
+ */
151
+ initTmpFullSizeEl() {
152
+ this.tmpFullSizeEl = this.tmpPlaceholderEl.clone();
153
+ }
154
+
155
+ /**
156
+ * Sets a reference to the full-size image URL based on the current dimensions
157
+ * of the main element.
158
+ */
159
+ setFullSizeImgUrl() {
160
+ // Work with the placeholdler image URL, which has been pulled from the
161
+ // background-image css property of the main elements.
162
+ let url = this.placeholderImgUrl.split('?');
163
+ // q is an array of querystring parameters as ["k=v", "k=v", ...].
164
+ let q = url[url.length - 1].split('&');
165
+ // Mapping q converts the array to an object of querystring parameters as
166
+ // { k: v, k: v, ... }.
167
+ let args = {};
168
+ q.map((x) => args[x.split('=')[0]] = x.split('=')[1]);
169
+ // If the image's container is wider than it is tall, we only set width and
170
+ // unset height, and vice versa.
171
+ if (this.el.width() >= this.el.height()) {
172
+ args['w'] = this.el.width();
173
+ delete args['h'];
174
+ } else {
175
+ args['h'] = this.el.height();
176
+ delete args['w'];
177
+ }
178
+ // Redefine q and go the other direction -- take the args object and convert
179
+ // it back to an array of querystring parameters, as ["k=v", "k=v", ...].
180
+ q = [];
181
+ for (let k in args) { q.push(`${k}=${args[k]}`) }
182
+ // Store the result and return.
183
+ return this.fullSizeImgUrl = `${url[0]}?${q.join('&')}`;
184
+ }
185
+
186
+ /**
187
+ * Change the URL of this temporary element's background image to be the
188
+ * full-size image.
189
+ */
190
+ setTmpFullSizeElImg() {
191
+ this.setFullSizeImgUrl();
192
+ this.tmpFullSizeEl.css('background-image', `url("${this.fullSizeImgUrl}")`);
193
+ }
194
+
195
+ /**
196
+ * Add the temporary full-size element direct before the temporary placeholder
197
+ * element.
198
+ */
199
+ addTmpFullSizeElToDom() {
200
+ this.tmpFullSizeEl.insertBefore(this.tmpPlaceholderEl);
201
+ }
202
+
203
+ // ---------------------------------------- | Transition
204
+
205
+ /**
206
+ * Load full-size image in memory. When it has loaded we can confidentally
207
+ * fade out the placeholder, knowing the full-size image will be in its place.
208
+ */
209
+ initTransition() {
210
+ $('<img>')
211
+ .on('load', $.proxy(this.transitionImg, this))
212
+ .attr('src', this.fullSizeImgUrl);
213
+ }
214
+
215
+ /**
216
+ * Fade out the temporary placeholder, set the background-image on the main
217
+ * element to the full-size URL, then remove the temporary elements behind the
218
+ * main element
219
+ */
220
+ transitionImg() {
221
+ this.fadeOutTmpPlaceholderEl();
222
+ setTimeout(() => {
223
+ this.updateElImg();
224
+ this.replaceElTmpCss();
225
+ this.removeTmpEls();
226
+ }, this.timeToFade);
227
+ }
228
+
229
+ /**
230
+ * Fade out the placeholder element. This was the temporary clone of the main
231
+ * element that has a placeholder background image.
232
+ *
233
+ * Rememeber the main element's background image was unset and its color set
234
+ * to transparent. That is why fading out this temporary image will work
235
+ * properly.
236
+ */
237
+ fadeOutTmpPlaceholderEl() {
238
+ this.tmpPlaceholderEl.fadeTo(this.timeToFade, 0);
239
+ }
240
+
241
+ /**
242
+ * Reset the image URL (this helps if the size of the element has changed),
243
+ * then set the background image to the new source.
244
+ */
245
+ updateElImg() {
246
+ this.setFullSizeImgUrl();
247
+ this.el.css('background-image', `url('${this.fullSizeImgUrl}')`);
248
+ }
249
+
250
+ /**
251
+ * Set the background color back to what it was before the transition.
252
+ */
253
+ replaceElTmpCss() {
254
+ this.el.css('background-color', this.elBgColor);
255
+ }
256
+
257
+ /**
258
+ * Remove both temporary elements from the DOM.
259
+ */
260
+ removeTmpEls() {
261
+ this.tmpPlaceholderEl.remove();
262
+ this.tmpFullSizeEl.remove();
263
+ this.tmpPlaceholderEl = undefined;
264
+ this.tmpFullSizeEl = undefined;
265
+ }
266
+
267
+ // ---------------------------------------- | Event Listeners
268
+
269
+ /**
270
+ * Listener for window resize events and update the image when the event ends.
271
+ */
272
+ initEventListeners() {
273
+ this.initResizeEnd();
274
+ $(window).on('resizeEnd', (event) => this.updateElImg());
275
+ }
276
+
277
+ /**
278
+ * Trigger "resizeEnd" event on the window object after resizing has ceased
279
+ * for at least 0.5 seconds.
280
+ */
281
+ initResizeEnd() {
282
+ $(window).resize(function() {
283
+ if (this.resizeTo) {
284
+ clearTimeout(this.resizeTo)
285
+ }
286
+ this.resizeTo = setTimeout(function() {
287
+ $(this).trigger('resizeEnd');
288
+ }, 500);
289
+ });
290
+ }
291
+ }
@@ -0,0 +1,143 @@
1
+ export default class ImgixImage {
2
+
3
+ constructor(img) {
4
+ // Length of crossfade transition.
5
+ this.timeToFade = 500;
6
+ // Main (pixellated placeholder) image.
7
+ this.placeholderImg = $(img);
8
+ // Kick off the optimization process.
9
+ this.initOptimization();
10
+ }
11
+
12
+ /**
13
+ * Load an image in memory (not within the DOM) with the same source as the
14
+ * placeholder image. Once that has completed, we know we're safe to begin
15
+ * processing.
16
+ */
17
+ initOptimization() {
18
+ $('<img>')
19
+ .on('load', $.proxy(this.renderFullSizeImg, this))
20
+ .attr('src', this.placeholderImg.attr('src'));
21
+ }
22
+
23
+ // ---------------------------------------- | Full-Size Image
24
+
25
+ /**
26
+ * Render the full-size image behind the placeholder image.
27
+ */
28
+ renderFullSizeImg() {
29
+ this.initFullSizeImg();
30
+ this.setFullSizeImgTempCss();
31
+ this.setFullSizeImgSrc();
32
+ this.addFullSizeImgToDom();
33
+ this.initTransition();
34
+ }
35
+
36
+ /**
37
+ * The full-size image is a clone of the placeholder image. This enables us to
38
+ * easily replace it without losing any necessary styles or attributes.
39
+ */
40
+ initFullSizeImg() {
41
+ this.fullSizeImg = this.placeholderImg.clone();
42
+ }
43
+
44
+ /**
45
+ * Give the full-size image a temporary set of CSS rules so that it can sit
46
+ * directly behind the placeholder image while loading.
47
+ */
48
+ setFullSizeImgTempCss() {
49
+ this.fullSizeImg.css({
50
+ position: 'absolute',
51
+ top: this.placeholderImg.position().top,
52
+ left: this.placeholderImg.position().left,
53
+ width: this.placeholderImg.width(),
54
+ height: this.placeholderImg.height()
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Prep the full-size image with the attributes necessary to become its full
60
+ * size. Right now it is still just a replica of the placeholder, sitting
61
+ * right behind the placeholder.
62
+ *
63
+ * We set the src directly even though we're using imgix.js because older
64
+ * browsers don't support the srcset attribute which is what imgix.js relies
65
+ * upon.
66
+ */
67
+ setFullSizeImgSrc() {
68
+ var newSrc = this.placeholderImg.attr('src')
69
+ .replace(/(\?|\&)(w=)(\d+)/i, '$1$2' + this.placeholderImg.width())
70
+ .replace(/(\?|\&)(h=)(\d+)/i, '$1$2' + this.placeholderImg.height());
71
+ this.fullSizeImg.attr('ix-src', newSrc);
72
+ // TODO: Make this a configurable option or document it as a more semantic temporary class
73
+ this.fullSizeImg.addClass('img-responsive tmp-img-placeholder');
74
+ // TODO: This should respect the option from the Optimizer class for the select
75
+ this.fullSizeImg.removeAttr('data-optimize-img');
76
+ }
77
+
78
+ /**
79
+ * Render the full-size image in the DOM.
80
+ */
81
+ addFullSizeImgToDom() {
82
+ this.fullSizeImg.insertBefore(this.placeholderImg);
83
+ }
84
+
85
+ // ---------------------------------------- | Image Transition
86
+
87
+ /**
88
+ * Once the full-size image is loaded, begin the transition. This is the
89
+ * critical piece of this process. Imgix.js uses the ix-src attribute to build
90
+ * out the srcset attribute. Then, based on the sizes attribute, the browser
91
+ * determines which source to render. Therefore we can't preload in memory
92
+ * because we need imgix to do its thing directly in the DOM.
93
+ */
94
+ initTransition() {
95
+ this.fullSizeImg.on('load', () => this.transitionImg());
96
+ imgix.init();
97
+ }
98
+
99
+ /**
100
+ * Fade out the placeholder image, effectively showing the image behind it.
101
+ *
102
+ * Once the fade out transition has completed, remove any temporary properties
103
+ * from the full-size image (so it gets back to being a clone of the
104
+ * placeholder, with the full-size src).
105
+ *
106
+ * Finally, remove the placeholder image from the DOM since we don't need it
107
+ * any more.
108
+ */
109
+ transitionImg() {
110
+ if (!this.placeholderImg) return true;
111
+ this.fadeOutPlaceholder();
112
+ setTimeout(() => {
113
+ this.removeFullSizeImgProperties();
114
+ this.removeImg();
115
+ }, this.timeToFade);
116
+ }
117
+
118
+ /**
119
+ * Fade out the placeholder image.
120
+ */
121
+ fadeOutPlaceholder() {
122
+ this.placeholderImg.fadeTo(this.timeToFade, 0);
123
+ }
124
+
125
+ /**
126
+ * Remove temporary styles and class from the full-size image, which
127
+ * effectively means it has replaced the placeholder image.
128
+ */
129
+ removeFullSizeImgProperties() {
130
+ this.fullSizeImg.removeAttr('style');
131
+ // TODO: Update this with how the class is handled above.
132
+ this.fullSizeImg.removeClass('tmp-img-placeholder');
133
+ }
134
+
135
+ /**
136
+ * Remove the placeholder image from the DOM since we no longer need it.
137
+ */
138
+ removeImg() {
139
+ if(!this.placeholderImg) { return }
140
+ this.placeholderImg.remove();
141
+ this.placeholderImg = undefined;
142
+ }
143
+ }
data/src/main.js ADDED
@@ -0,0 +1,9 @@
1
+ import ImgixBgImage from './imgix_bg_image';
2
+ import ImgixImage from './imgix_image';
3
+ import Optimizer from './optimizer';
4
+
5
+ window['Imgix'] = window['Imgix'] || {};
6
+
7
+ Imgix.ImgixBgImage = ImgixBgImage;
8
+ Imgix.ImgixImage = ImgixImage;
9
+ Imgix.Optimizer = Optimizer;
data/src/optimizer.js ADDED
@@ -0,0 +1,43 @@
1
+ import ImgixImage from './imgix_image';
2
+ import ImgixBgImage from './imgix_bg_image';
3
+
4
+ export default class Optimizer {
5
+
6
+ constructor(options = {}) {
7
+ this.initOptions(options);
8
+ this.optimizeImages();
9
+ this.optimizeBgImages();
10
+ }
11
+
12
+ // ---------------------------------------- | Options
13
+
14
+ initOptions(options = {}) {
15
+ this.options = options;
16
+ const defaultOptions = {
17
+ parent: 'body'
18
+ }
19
+ for (let key in defaultOptions) {
20
+ if (defaultOptions.hasOwnProperty(key) && !this.options[key]) {
21
+ this.options[key] = defaultOptions[key];
22
+ }
23
+ }
24
+ }
25
+
26
+ // ---------------------------------------- | Inline Images
27
+
28
+ optimizeImages() {
29
+ $(`${this.options.parent} img[data-optimize-img]`).each((idx, img) => {
30
+ new ImgixImage(img);
31
+ });
32
+ }
33
+
34
+ // ---------------------------------------- | Background Images
35
+
36
+ optimizeBgImages() {
37
+ $(`${this.options.parent} [data-optimize-bg-img]`).each((idx, img) => {
38
+ new ImgixBgImage(img);
39
+ });
40
+ return true;
41
+ }
42
+
43
+ }
data/test/test.js ADDED
@@ -0,0 +1,53 @@
1
+ const assert = require('assert');
2
+ const howLongTillLunch = require('..');
3
+
4
+ function MockDate () {
5
+ this.date = 0;
6
+ this.hours = 0;
7
+ this.minutes = 0;
8
+ this.seconds = 0;
9
+ this.milliseconds = 0;
10
+ };
11
+
12
+ Object.assign(MockDate.prototype, {
13
+ getDate () { return this.date; },
14
+ setDate (date) { this.date = date; },
15
+ setHours (h) { this.hours = h; },
16
+ setMinutes (m) { this.minutes = m; },
17
+ setSeconds (s) { this.seconds = s; },
18
+ setMilliseconds (ms) { this.milliseconds = ms; },
19
+ valueOf () {
20
+ return (
21
+ this.milliseconds +
22
+ this.seconds * 1e3 +
23
+ this.minutes * 1e3 * 60 +
24
+ this.hours * 1e3 * 60 * 60 +
25
+ this.date * 1e3 * 60 * 60 * 24
26
+ );
27
+ }
28
+ });
29
+
30
+ const now = new MockDate();
31
+ MockDate.now = () => now.valueOf();
32
+
33
+ global.Date = MockDate;
34
+
35
+ function test(hours, minutes, seconds, expected) {
36
+ now.setHours(hours);
37
+ now.setMinutes(minutes);
38
+ now.setSeconds(seconds);
39
+
40
+ assert.equal(howLongTillLunch(...lunchtime), expected);
41
+ console.log(`\u001B[32m✓\u001B[39m ${expected}`);
42
+ }
43
+
44
+ let lunchtime = [ 12, 30 ];
45
+ test(11, 30, 0, '1 hour');
46
+ test(10, 30, 0, '2 hours');
47
+ test(12, 25, 0, '5 minutes');
48
+ test(12, 29, 15, '45 seconds');
49
+ test(13, 30, 0, '23 hours');
50
+
51
+ // some of us like an early lunch
52
+ lunchtime = [ 11, 0 ];
53
+ test(10, 30, 0, '30 minutes');
File without changes