prodio
Version:
Simplified project management
213 lines (181 loc) • 7.68 kB
JavaScript
/**
* Responsive image loading
* @module Ink.UI.ImageQuery_1
* @version 1
*/
Ink.createModule('Ink.UI.ImageQuery', '1', ['Ink.UI.Common_1','Ink.Dom.Event_1','Ink.Dom.Element_1','Ink.Util.Array_1'], function(Common, Event, Element, InkArray ) {
'use strict';
/**
* @class Ink.UI.ImageQuery
* @constructor
* @version 1
*
* @param {String|DOMElement} selector Selector or element
* @param {Object} [options] Options object
* @param {String|Function} [options.src] String or Callback function (that returns a string) with the path to be used to get the images.
* @param {String|Function} [options.retina] String or Callback function (that returns a string) with the path to be used to get RETINA specific images.
* @param {Array} [options.queries] Array of queries
* @param {String} [options.queries.label] Label of the query. Ex. 'small'
* @param {Number} [options.queries.width] Min-width to use this query
* @param {Function} [options.onLoad] Date format string
*
* @example
* <div class="imageQueryExample all-100 content-center clearfix vspace">
* <img src="/assets/imgs/imagequery/small/image.jpg" />
* </div>
* <script type="text/javascript">
* Ink.requireModules( ['Ink.Dom.Selector_1', 'Ink.UI.ImageQuery_1'], function( Selector, ImageQuery ){
* var imageQueryElement = Ink.s('.imageQueryExample img');
* var imageQueryObj = new ImageQuery('.imageQueryExample img',{
* src: '/assets/imgs/imagequery/{:label}/{:file}',
* queries: [
* {
* label: 'small',
* width: 480
* },
* {
* label: 'medium',
* width: 640
* },
* {
* label: 'large',
* width: 1024
* }
* ]
* });
* } );
* </script>
*/
var ImageQuery = function(selector, options){
/**
* Get elements, create more ImageQueries if selector finds more than one
*
* [improvement] This is a useful pattern. More UI modules could use it.
*/
this._element = Common.elsOrSelector(selector, 'Ink.UI.ImageQuery', /*required=*/true);
// In case we have several elements
for (var i = 1 /* start from second element*/; i < this._element.length; i++) {
new ImageQuery(this._element[i], options);
}
this._element = this._element[0];
/**
* Default options, overriden by data-attributes if any.
* The parameters are:
* @xparam {array} queries Array of objects that determine the label/name and its min-width to be applied.
* @xparam {boolean} allowFirstLoad Boolean flag to allow the loading of the first element.
*/
this._options = Ink.extendObj({
queries:[],
onLoad: null
}, options || {}, Element.data(this._element));
/**
* Determining the original basename (with the querystring) of the file.
*/
var pos;
if( (pos=this._element.src.lastIndexOf('?')) !== -1 ){
var search = this._element.src.substr(pos);
this._filename = this._element.src.replace(search,'').split('/').pop()+search;
} else {
this._filename = this._element.src.split('/').pop();
}
this._init();
};
ImageQuery.prototype = {
/**
* Init function called by the constructor
*
* @method _init
* @private
*/
_init: function(){
// Sort queries by width, in descendant order.
this._options.queries = InkArray.sortMulti(this._options.queries, 'width').reverse();
if( typeof this._options.onLoad === 'function' ){
Event.observe(this._element, 'onload', Ink.bindEvent(this._onLoad, this));
}
Event.observe(window, 'resize', Event.throttle(Ink.bindMethod(this, '_onResize'), 400));
// Imediate call to apply the right images based on the current viewport
this._onResize();
Common.registerInstance(this, this._element);
},
/**
* Handles the resize event (as specified in the _init function)
*
* @method _onResize
* @private
*/
_onResize: function(){
if( !this._options.queries.length ){
return;
}
var current = this._findCurrentQuery();
/**
* Choosing the right src. The rule is:
*
* "If there is specifically defined in the query object, use that. Otherwise uses the global src."
*
* The above rule applies to a retina src.
*/
var src = current.src || this._options.src;
if ( window.devicePixelRatio > 1 && ('retina' in this._options ) ) {
src = current.retina || this._options.retina;
}
/**
* Injects the file variable for usage in the 'templating system' below
*/
current.file = this._filename;
/**
* Since we allow the src to be a callback, let's run it and get the results.
* For the inside, we're passing the element (img) being processed and the object of the selected query.
*/
if( typeof src === 'function' ){
src = src.apply(this,[this._element,current]);
if( typeof src !== 'string' ){
throw '[ImageQuery] :: "src" callback does not return a string';
}
}
/**
* Replace the values of the existing properties on the query object (except src and retina) in the
* defined src and/or retina.
*/
src = src.replace(/{:(.*?)}/g, function(_, prop) {
return current[prop];
});
this._element.src = src;
// Removes the injected file property
delete current.file;
},
/**
* Queries are in a descendant order. We want to find the query with the highest width that fits the viewport, therefore the first one.
*/
_findCurrentQuery: function () {
/**
* Gets viewport width
*/
var viewportWidth = window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
var queries = this._options.queries;
var last = queries.length - 1;
for( var query=0; query < last; query+=1 ){
if (queries[query].width <= viewportWidth){
return queries[query];
}
}
return queries[last];
},
/**
* Handles the element loading (img onload) event
*
* @method _onLoad
* @private
*/
_onLoad: function(){
/**
* Since we allow a callback for this let's run it.
*/
this._options.onLoad.call(this);
}
};
return ImageQuery;
});