imgix-optimizer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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