angular-lazy-image
Version:
Loading responsive image when container (which is preventing reflow) is in view.
315 lines (236 loc) • 10.9 kB
JavaScript
/* global angular */
angular.module('afkl.lazyImage')
.service('afklSrcSetService', ['$window', function($window) {
'use strict';
/**
* For other applications wanting the srccset/best image approach it is possible to use this module only
* Loosely based on https://raw.github.com/borismus/srcset-polyfill/master/js/srcset-info.js
*/
var INT_REGEXP = /^[0-9]+$/;
// SRCSET IMG OBJECT
function ImageInfo(options) {
this.src = options.src;
this.w = options.w || Infinity;
this.h = options.h || Infinity;
this.x = options.x || 1;
}
/**
* Parse srcset rules
* @param {string} descString Containing all srcset rules
* @return {object} Srcset rules
*/
var _parseDescriptors = function (descString) {
var descriptors = descString.split(/\s/);
var out = {};
for (var i = 0, l = descriptors.length; i < l; i++) {
var desc = descriptors[i];
if (desc.length > 0) {
var lastChar = desc.slice(-1);
var value = desc.substring(0, desc.length - 1);
var intVal = parseInt(value, 10);
var floatVal = parseFloat(value);
if (value.match(INT_REGEXP) && lastChar === 'w') {
out[lastChar] = intVal;
} else if (value.match(INT_REGEXP) && lastChar === 'h') {
out[lastChar] = intVal;
} else if (!isNaN(floatVal) && lastChar === 'x') {
out[lastChar] = floatVal;
}
}
}
return out;
};
/**
* Returns best candidate under given circumstances
* @param {object} images Candidate image
* @param {function} criteriaFn Rule
* @return {object} Returns best candidate under given criteria
*/
var _getBestCandidateIf = function (images, criteriaFn) {
var bestCandidate = images[0];
for (var i = 0, l = images.length; i < l; i++) {
var candidate = images[i];
if (criteriaFn(candidate, bestCandidate)) {
bestCandidate = candidate;
}
}
return bestCandidate;
};
/**
* Remove candidate under given circumstances
* @param {object} images Candidate image
* @param {function} criteriaFn Rule
* @return {object} Removes images from global image collection (candidates)
*/
var _removeCandidatesIf = function (images, criteriaFn) {
for (var i = images.length - 1; i >= 0; i--) {
var candidate = images[i];
if (criteriaFn(candidate)) {
images.splice(i, 1); // remove it
}
}
return images;
};
/**
* Direct implementation of "processing the image candidates":
* http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content-1.html#processing-the-image-candidates
*
* @param {array} imageCandidates (required)
* @param {object} view (optional)
* @returns {ImageInfo} The best image of the possible candidates.
*/
var getBestImage = function (imageCandidates, view) {
if (!imageCandidates) { return; }
if (!view) {
view = {
'w' : $window.innerWidth || document.documentElement.clientWidth,
'h' : $window.innerHeight || document.documentElement.clientHeight,
'x' : $window.devicePixelRatio || 1
};
}
var images = imageCandidates.slice(0);
/* LARGEST */
// Width
var largestWidth = _getBestCandidateIf(images, function (a, b) { return a.w > b.w; });
// Less than client width.
_removeCandidatesIf(images, (function () { return function (a) { return a.w < view.w; }; })(this));
// If none are left, keep the one with largest width.
if (images.length === 0) { images = [largestWidth]; }
// Height
var largestHeight = _getBestCandidateIf(images, function (a, b) { return a.h > b.h; });
// Less than client height.
_removeCandidatesIf(images, (function () { return function (a) { return a.h < view.h; }; })(this));
// If none are left, keep one with largest height.
if (images.length === 0) { images = [largestHeight]; }
// Pixel density.
var largestPxDensity = _getBestCandidateIf(images, function (a, b) { return a.x > b.x; });
// Remove all candidates with pxdensity less than client pxdensity.
_removeCandidatesIf(images, (function () { return function (a) { return a.x < view.x; }; })(this));
// If none are left, keep one with largest pixel density.
if (images.length === 0) { images = [largestPxDensity]; }
/* SMALLEST */
// Width
var smallestWidth = _getBestCandidateIf(images, function (a, b) { return a.w < b.w; });
// Remove all candidates with width greater than it.
_removeCandidatesIf(images, function (a) { return a.w > smallestWidth.w; });
// Height
var smallestHeight = _getBestCandidateIf(images, function (a, b) { return a.h < b.h; });
// Remove all candidates with height greater than it.
_removeCandidatesIf(images, function (a) { return a.h > smallestHeight.h; });
// Pixel density
var smallestPxDensity = _getBestCandidateIf(images, function (a, b) { return a.x < b.x; });
// Remove all candidates with pixel density less than smallest px density.
_removeCandidatesIf(images, function (a) { return a.x > smallestPxDensity.x; });
return images[0];
};
// options {src: null/string, srcset: string}
// options.src normal url or null
// options.srcset 997-s.jpg 480w, 997-m.jpg 768w, 997-xl.jpg 1x
var getSrcset = function (options) {
var imageCandidates = [];
var srcValue = options.src;
var srcsetValue = options.srcset;
if (!srcsetValue) { return; }
/* PUSH CANDIDATE [{src: _, x: _, w: _, h:_}, ...] */
var _addCandidate = function (img) {
for (var j = 0, ln = imageCandidates.length; j < ln; j++) {
var existingCandidate = imageCandidates[j];
// DUPLICATE
if (existingCandidate.x === img.x &&
existingCandidate.w === img.w &&
existingCandidate.h === img.h) { return; }
}
imageCandidates.push(img);
};
var _parse = function () {
var input = srcsetValue,
position = 0,
rawCandidates = [],
url,
descriptors;
while (input !== '') {
while (input.charAt(0) === ' ') {
input = input.slice(1);
}
position = input.indexOf(' ');
if (position !== -1) {
url = input.slice(0, position);
// if (url === '') { break; }
input = input.slice(position + 1);
position = input.indexOf(',');
if (position === -1) {
descriptors = input;
input = '';
} else {
descriptors = input.slice(0, position);
input = input.slice(position + 1);
}
rawCandidates.push({
url: url,
descriptors: descriptors
});
} else {
rawCandidates.push({
url: input,
descriptors: ''
});
input = '';
}
}
// FROM RAW CANDIDATES PUSH IMAGES TO COMPLETE SET
for (var i = 0, l = rawCandidates.length; i < l; i++) {
var candidate = rawCandidates[i],
desc = _parseDescriptors(candidate.descriptors);
_addCandidate(new ImageInfo({
src: candidate.url,
x: desc.x,
w: desc.w,
h: desc.h
}));
}
if (srcValue) {
_addCandidate(new ImageInfo({src: srcValue}));
}
};
_parse();
// Return best available image for current view based on our list of candidates
var bestImage = getBestImage(imageCandidates);
/**
* Object returning best match at moment, and total collection of candidates (so 'image' API can be used by consumer)
* @type {Object}
*/
var object = {
'best': bestImage, // IMAGE INFORMATION WHICH FITS BEST WHEN API IS REQUESTED
'candidates': imageCandidates // ALL IMAGE CANDIDATES BY GIVEN SRCSET ATTRIBUTES
};
// empty collection
imageCandidates = null;
// pass best match and candidates
return object;
};
// throttle function to be used in directive
function throttle(callback, delay) {
var last, deferTimer;
return function() {
var now = +new Date();
if (last && now < last + delay) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
callback();
}, delay + last - now);
} else {
last = now;
callback();
}
};
}
/**
* PUBLIC API
*/
return {
get: getSrcset, // RETURNS BEST IMAGE AND IMAGE CANDIDATES
image: getBestImage, // RETURNS BEST IMAGE WITH GIVEN CANDIDATES
throttle: throttle // RETURNS A THROTTLER FUNCTION
};
}]);