Project

General

Profile

1
/**
2
 * Dense - Device pixel ratio aware images
3
 *
4
 * @link    http://dense.rah.pw
5
 * @license MIT
6
 */
7

    
8
/*
9
 * Copyright (C) 2013 Jukka Svahn
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a
12
 * copy of this software and associated documentation files (the "Software"),
13
 * to deal in the Software without restriction, including without limitation
14
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15
 * and/or sell copies of the Software, and to permit persons to whom the
16
 * Software is furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies or substantial portions of the Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
26
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
 */
28

    
29
/**
30
 * @name jQuery
31
 * @class
32
 */
33

    
34
/**
35
 * @name fn
36
 * @class
37
 * @memberOf jQuery
38
 */
39

    
40
(function (factory)
41
{
42
    'use strict';
43

    
44
    if (typeof define === 'function' && define.amd)
45
    {
46
        define(['jquery'], factory);
47
    }
48
    else
49
    {
50
        factory(window.jQuery || window.Zepto);
51
    }
52
}(function ($)
53
{
54
    'use strict';
55

    
56
    /**
57
     * An array of checked image URLs.
58
     */
59

    
60
    var pathStack = [],
61

    
62
        /**
63
         * Methods.
64
         */
65

    
66
        methods = {},
67

    
68
        /**
69
         * Regular expression to check whether the URL has a protocol.
70
         *
71
         * Is used to check whether the image URL is external.
72
         */
73

    
74
        regexHasProtocol = /^([a-z]:)?\/\//i,
75

    
76
        /**
77
         * Regular expression that split extensions from the file.
78
         *
79
         * Is used to inject the DPR suffix to the name.
80
         */
81

    
82
        regexSuffix = /\.\w+$/,
83

    
84
        /**
85
         * Device pixel ratio.
86
         */
87

    
88
        devicePixelRatio;
89

    
90
    /**
91
     * Init is the default method responsible for rendering
92
     * a pixel-ratio-aware images.
93
     *
94
     * This method is used to select the images that
95
     * should display retina-size images on high pixel ratio
96
     * devices. Dense defaults to the init method if no
97
     * method is specified.
98
     *
99
     * When attached to an image, the correct image variation is
100
     * selected based on the device's pixel ratio. If the image element
101
     * defines <code>data-{ratio}x</code> attributes (e.g. data-1x, data-2x, data-3x),
102
     * the most appropriate of those is selected.
103
     *
104
     * If no data-ratio attributes are defined, the retina image is
105
     * constructed from the <code>src</code> attribute.
106
     * The searched high pixel ratio images follows
107
     * a <code>{imageName}_{ratio}x.{ext}</code> naming convention.
108
     * For an image found in /path/to/images/image.jpg, the 2x retina
109
     * image would be looked from /path/to/images/image_2x.jpg.
110
     *
111
     * When image is constructed from the src, the image existance is
112
     * verified using HTTP HEAD request, if <code>ping</code> option is
113
     * <code>true</code>. The check makes sure no HTTP error code is returned,
114
     * and that the received content-type is of an image. Vector image formats,
115
     * like svg, are skipped based on the file extension.
116
     *
117
     * This method can also be used to load image in semi-lazy fashion,
118
     * and avoid larger extra HTTP requests due to retina replacements.
119
     * The data-1x attribute can be used to supstitute the src, making
120
     * sure the browser doesn't try to download the normal image variation
121
     * before the JavaScript driven behaviour kicks in.
122
     *
123
     * Some classes are added to the selected elements while Dense is processing
124
     * the document. These classes include <code>dense-image</code>, <code>dense-loading</code>
125
     * and <code>dense-ready</code>. These classes can be used to style the images,
126
     * or hide them while they are being loaded.
127
     *
128
     * @param {Object} [options={}] Options
129
     * @param {Boolean} [options.ping=null] Check image existence. If the default <code>NULL</code> checks local images, <code>FALSE</code> disables checking and <code>TRUE</code> checks even external images cross-domain
130
     * @param {String} [options.dimensions=preserve] What to do with the image's <code>width</code> and <code>height</code> attributes. Either <code>update</code>, <code>remove</code> or <code>preserve</code>
131
     * @param {String} [options.glue=_] String that glues the retina "nx" suffix to the image. This option can be used to change the naming convention between the two commonly used practices, <code>image@2x.jpg</code> and <code>image_2x.jpg</code>
132
     * @param {Array} [options.skipExtensions=['svg']] Skipped image file extensions. There might be situations where you might want to exclude vector image formats
133
     * @return {Object} this
134
     * @method init
135
     * @memberof jQuery.fn.dense
136
     * @fires jQuery.fn.dense#denseRetinaReady.dense
137
     * @example
138
     * $('img').dense({
139
     *  ping: false,
140
     *  dimension: 'update'
141
     * });
142
     */
143

    
144
    methods.init = function (options)
145
    {
146
        options = $.extend({
147
            ping: null,
148
            dimensions: 'preserve',
149
            glue: '_',
150
            skipExtensions: ['svg']
151
        }, options);
152

    
153
        this.each(function ()
154
        {
155
            var $this = $(this);
156

    
157
            if (!$this.is('img') || $this.hasClass('dense-image'))
158
            {
159
                return;
160
            }
161

    
162
            $this.addClass('dense-image dense-loading');
163

    
164
            var image = methods.getImageAttribute.call(this),
165
                originalImage = $this.attr('src'),
166
                ping = false,
167
                updateImage;
168

    
169
            if (!image)
170
            {
171
                if (!originalImage || devicePixelRatio === 1 || $.inArray(originalImage.split('.').pop().split(/[\?\#]/).shift(), options.skipExtensions) !== -1)
172
                {
173
                    $this.removeClass('dense-image dense-loading');
174
                    return;
175
                }
176

    
177
                image = originalImage.replace(regexSuffix, function (extension)
178
                {
179
                    var pixelRatio = $this.attr('data-dense-cap') ? $this.attr('data-dense-cap') : devicePixelRatio;
180
                    return options.glue + pixelRatio + 'x' + extension;
181
                });
182

    
183
                ping = options.ping !== false && $.inArray(image, pathStack) === -1 && (options.ping === true || !regexHasProtocol.test(image) || image.indexOf('//'+document.domain) === 0 || image.indexOf(document.location.protocol+'//'+document.domain) === 0);
184
            }
185

    
186
            updateImage = function ()
187
            {
188
                var readyImage = function ()
189
                {
190
                    $this.removeClass('dense-loading').addClass('dense-ready').trigger('denseRetinaReady.dense');
191
                };
192

    
193
                $this.attr('src', image);
194

    
195
                if (options.dimensions === 'update')
196
                {
197
                    $this.dense('updateDimensions').one('denseDimensionChanged', readyImage);
198
                }
199
                else
200
                {
201
                    if (options.dimensions === 'remove')
202
                    {
203
                        $this.removeAttr('width height');
204
                    }
205

    
206
                    readyImage();
207
                }
208
            };
209

    
210
            if (ping)
211
            {
212
                $.ajax({
213
                    url  : image,
214
                    type : 'HEAD'
215
                })
216
                    .done(function (data, textStatus, jqXHR)
217
                    {
218
                        var type = jqXHR.getResponseHeader('Content-type');
219

    
220
                        if (!type || type.indexOf('image/') === 0)
221
                        {
222
                            pathStack.push(image);
223
                            updateImage();
224
                        }
225
                    });
226
            }
227
            else
228
            {
229
                updateImage();
230
            }
231
        });
232

    
233
        return this;
234
    };
235

    
236
    /**
237
     * Sets an image's width and height attributes to its native values.
238
     *
239
     * Updates an img element's dimensions to the source image's
240
     * real values. This method is asynchronous, so you can not directly
241
     * return its values. Instead, use the 'dense-dimensions-updated'
242
     * event to detect when the action is done.
243
     *
244
     * @return {Object} this
245
     * @method updateDimensions
246
     * @memberof jQuery.fn.dense
247
     * @fires jQuery.fn.dense#denseDimensionChanged.dense
248
     * @example
249
     * var image = $('img').dense('updateDimensions');
250
     */
251

    
252
    methods.updateDimensions = function ()
253
    {
254
        return this.each(function ()
255
        {
256
            var img, $this = $(this), src = $this.attr('src');
257

    
258
            if (src)
259
            {
260
                img = new Image();
261
                img.src = src;
262

    
263
                $(img).on('load.dense', function ()
264
                {
265
                    $this.attr({
266
                        width: img.width,
267
                        height: img.height
268
                    }).trigger('denseDimensionChanged.dense');
269
                });
270
            }
271
        });
272
    };
273

    
274
    /**
275
     * Gets device pixel ratio rounded up to the closest integer.
276
     *
277
     * @return {Integer} The pixel ratio
278
     * @method devicePixelRatio
279
     * @memberof jQuery.fn.dense
280
     * @example
281
     * var ratio = $(window).dense('devicePixelRatio');
282
     * alert(ratio);
283
     */
284

    
285
    methods.devicePixelRatio = function ()
286
    {
287
        var pixelRatio = 1;
288

    
289
        if ($.type(window.devicePixelRatio) !== 'undefined')
290
        {
291
            pixelRatio = window.devicePixelRatio;
292
        }
293
        else if ($.type(window.matchMedia) !== 'undefined')
294
        {
295
            $.each([1.3, 2, 3, 4, 5, 6], function (key, ratio)
296
            {
297
                var mediaQuery = [
298
                    '(-webkit-min-device-pixel-ratio: '+ratio+')',
299
                    '(min-resolution: '+Math.floor(ratio*96)+'dpi)',
300
                    '(min-resolution: '+ratio+'dppx)'
301
                ].join(',');
302

    
303
                if (!window.matchMedia(mediaQuery).matches)
304
                {
305
                    return false;
306
                }
307

    
308
                pixelRatio = ratio;
309
            });
310
        }
311

    
312
        return Math.ceil(pixelRatio);
313
    };
314

    
315
    /**
316
     * Gets an appropriate URL for the pixel ratio from the data attribute list.
317
     *
318
     * Selects the most appropriate <code>data-{ratio}x</code> attribute from
319
     * the given element's attributes. If the devices pixel ratio is greater
320
     * than the largest specified image, the largest one of the available is used.
321
     *
322
     * @return {String|Boolean} The attribute value
323
     * @method getImageAttribute
324
     * @memberof jQuery.fn.dense
325
     * @example
326
     * var image = $('<div data-1x="image.jpg" data-2x="image_2x.jpg" />').dense('getImageAttribute');
327
     * $('body').css('background-image', 'url(' + image + ')');
328
     */
329

    
330
    methods.getImageAttribute = function ()
331
    {
332
        var $this = $(this).eq(0), image = false, url;
333

    
334
        for (var i = 1; i <= devicePixelRatio; i++)
335
        {
336
            url = $this.attr('data-' + i + 'x');
337

    
338
            if (url)
339
            {
340
                image = url;
341
            }
342
        }
343

    
344
        return image;
345
    };
346

    
347
    devicePixelRatio = methods.devicePixelRatio();
348

    
349
    /**
350
     * Dense offers few methods and options that can be used to both customize the
351
     * plugin's functionality and return resulting values. All interaction is done through
352
     * the <code>$.fn.dense()</code> method, that accepts a called method and its options
353
     * object as its arguments. Both arguments are optional, and either one can be omitted.
354
     *
355
     * @param {String} [method=init] The called method
356
     * @param {Object} [options={}] Options passed to the method
357
     * @class dense
358
     * @memberof jQuery.fn
359
     */
360

    
361
    $.fn.dense = function (method, options)
362
    {
363
        if ($.type(method) !== 'string' || $.type(methods[method]) !== 'function')
364
        {
365
            options = method;
366
            method = 'init';
367
        }
368

    
369
        return methods[method].call(this, options);
370
    };
371

    
372
    /**
373
     * Initialize automatically when document is ready.
374
     *
375
     * Dense is initialized automatically if the body element
376
     * has a <code>dense-retina</code> class.
377
     */
378

    
379
    $(function ()
380
    {
381
        $('body.dense-retina img').dense();
382
    });
383

    
384
    /**
385
     * This event is invoked when a retina image has finished loading.
386
     *
387
     * @event jQuery.fn.dense#denseRetinaReady.dense
388
     * @type {Object}
389
     */
390

    
391
    /**
392
     * This event is invoked when an image's dimension values
393
     * have been updated by the <code>updateDimensions</code>
394
     * method.
395
     *
396
     * @event jQuery.fn.dense#denseDimensionChanged.dense
397
     * @type {Object}
398
     */
399
}));
(1-1/6)