postpone
Version:
A polyfill for postponing the loading of media.
727 lines (691 loc) • 25.2 kB
JavaScript
/**
* Require the given path.
*
* @param {String} path
* @return {Object} exports
* @api public
*/
function require(path, parent, orig) {
var resolved = require.resolve(path);
// lookup failed
if (null == resolved) {
orig = orig || path;
parent = parent || 'root';
var err = new Error('Failed to require "' + orig + '" from "' + parent + '"');
err.path = orig;
err.parent = parent;
err.require = true;
throw err;
}
var module = require.modules[resolved];
// perform real require()
// by invoking the module's
// registered function
if (!module._resolving && !module.exports) {
var mod = {};
mod.exports = {};
mod.client = mod.component = true;
module._resolving = true;
module.call(this, mod.exports, require.relative(resolved), mod);
delete module._resolving;
module.exports = mod.exports;
}
return module.exports;
}
/**
* Registered modules.
*/
require.modules = {};
/**
* Registered aliases.
*/
require.aliases = {};
/**
* Resolve `path`.
*
* Lookup:
*
* - PATH/index.js
* - PATH.js
* - PATH
*
* @param {String} path
* @return {String} path or null
* @api private
*/
require.resolve = function(path) {
if (path.charAt(0) === '/') path = path.slice(1);
var paths = [
path,
path + '.js',
path + '.json',
path + '/index.js',
path + '/index.json'
];
for (var i = 0; i < paths.length; i++) {
var path = paths[i];
if (require.modules.hasOwnProperty(path)) return path;
if (require.aliases.hasOwnProperty(path)) return require.aliases[path];
}
};
/**
* Normalize `path` relative to the current path.
*
* @param {String} curr
* @param {String} path
* @return {String}
* @api private
*/
require.normalize = function(curr, path) {
var segs = [];
if ('.' != path.charAt(0)) return path;
curr = curr.split('/');
path = path.split('/');
for (var i = 0; i < path.length; ++i) {
if ('..' == path[i]) {
curr.pop();
} else if ('.' != path[i] && '' != path[i]) {
segs.push(path[i]);
}
}
return curr.concat(segs).join('/');
};
/**
* Register module at `path` with callback `definition`.
*
* @param {String} path
* @param {Function} definition
* @api private
*/
require.register = function(path, definition) {
require.modules[path] = definition;
};
/**
* Alias a module definition.
*
* @param {String} from
* @param {String} to
* @api private
*/
require.alias = function(from, to) {
if (!require.modules.hasOwnProperty(from)) {
throw new Error('Failed to alias "' + from + '", it does not exist');
}
require.aliases[to] = from;
};
/**
* Return a require function relative to the `parent` path.
*
* @param {String} parent
* @return {Function}
* @api private
*/
require.relative = function(parent) {
var p = require.normalize(parent, '..');
/**
* lastIndexOf helper.
*/
function lastIndexOf(arr, obj) {
var i = arr.length;
while (i--) {
if (arr[i] === obj) return i;
}
return -1;
}
/**
* The relative require() itself.
*/
function localRequire(path) {
var resolved = localRequire.resolve(path);
return require(resolved, parent, path);
}
/**
* Resolve relative to the parent.
*/
localRequire.resolve = function(path) {
var c = path.charAt(0);
if ('/' == c) return path.slice(1);
if ('.' == c) return require.normalize(p, path);
// resolve deps by returning
// the dep in the nearest "deps"
// directory
var segs = parent.split('/');
var i = lastIndexOf(segs, 'deps') + 1;
if (!i) i = 0;
path = segs.slice(0, i + 1).join('/') + '/deps/' + path;
return path;
};
/**
* Check if module is defined at `path`.
*/
localRequire.exists = function(path) {
return require.modules.hasOwnProperty(localRequire.resolve(path));
};
return localRequire;
};
require.register("postpone/index.js", Function("exports, require, module",
"(function ( root, factory ) {\n\
if ( typeof define === \"function\" && define.amd ) {\n\
define( [], factory );\n\
} else if ( typeof exports === \"object\" ) {\n\
module.exports = factory();\n\
} else {\n\
root.Postpone = factory();\n\
}\n\
}(this, function() {\n\
\"use strict\";\n\
\n\
/**\n\
* Creates a new Postpone instance.\n\
* @constructor\n\
* @param {number|string} [threshold] - The distance from an edge at which an\n\
* element should be considered to be at the edge.\n\
*/\n\
var Postpone = function( threshold ) {\n\
if ( !( this instanceof Postpone ) ) return new Postpone( threshold );\n\
\n\
/**\n\
* The init method for Postpone gets the object running. It runs\n\
* postpone.postpone() to attach scroll event handlers and check if any\n\
* elements are already visible. Then, init will start the watch process.\n\
* @returns this\n\
*/\n\
this.init = function( threshold ) {\n\
/**\n\
* @property {string} tags - A list of all the tags for which postpone\n\
* will work;\n\
*/\n\
this.tags = \"audio, embed, iframe, img, image, object, picture, use, video, tref\";\n\
/**\n\
* @property {array} elements - An array of all the postponed elements in the document.\n\
*/\n\
this.elements = [];\n\
/**\n\
* @property {array} visible - An array of all the non-hidden postponed\n\
* elements in the document.\n\
*/\n\
this.elements.visible = [];\n\
/**\n\
* @property {object} scrollElements - A variable to keep track of the\n\
* elements with respoect to which the postponed elements scroll.\n\
*/\n\
this.scrollElements = [];\n\
this.setThreshold( threshold );\n\
\n\
/** Call method to start looking for postponed media. */\n\
return this.start();\n\
};\n\
\n\
return this.init( threshold );\n\
};\n\
\n\
/**\n\
* The main postpone method. This method iterates over all the elements with\n\
* a `postpone` attribute and links them to a scroll event so that they are not\n\
* loaded until they become visible.\n\
* @returns this\n\
*/\n\
Postpone.prototype.postpone = function() {\n\
/**\n\
* Remove any previous event handlers so they can be reattached for new\n\
* postponed elements without duplicating old ones. This must be done\n\
* before updating the scroll elements so the references to the event\n\
* callbacks still exist.\n\
*/\n\
this.unbindEvents();\n\
\n\
/**\n\
* Update the elements and scroll elements to properly load or postpone\n\
* them.\n\
*/\n\
this.getElements();\n\
this.getScrollElements();\n\
\n\
/**\n\
* If any of the postponed elements should be visible to begin with,\n\
* load them.\n\
*/\n\
for ( var id in this.scrollElements ) {\n\
for ( var i = 0, element = {}; i < this.scrollElements[ id ].length; i++ ) {\n\
element = this.scrollElements[ id ][ i ];\n\
if ( this.isInViewport( element, this.scrollElements[ id ].element ) ) {\n\
this.load( element );\n\
}\n\
}\n\
}\n\
\n\
if ( this.elements.length ) {\n\
/** Attach scroll event listeners. */\n\
this.bindEvents();\n\
}\n\
\n\
return this;\n\
};\n\
\n\
/**\n\
* A helper method to unbind the scroll event callbacks.\n\
* @returns this\n\
*/\n\
Postpone.prototype.unbindEvents = function() {\n\
for ( var id in this.scrollElements ) {\n\
this._removeEventListener( id === \"window\" ? window : this.scrollElements[ id ].element, this.scrollElements[ id ].callback );\n\
}\n\
};\n\
\n\
/**\n\
* A helper method to bind the scroll event callbacks.\n\
* @returns this\n\
*/\n\
Postpone.prototype.bindEvents = function() {\n\
for ( var id in this.scrollElements ) {\n\
this.scrollElements[ id ].callback = Function.prototype.bind ? this.scrollHandler.bind( this ) : function( _this ) { return function() { return _this.scrollHandler.apply( _this, arguments ); }; }( this );\n\
this._addEventListener( id === \"window\" ? window : this.scrollElements[ id ].element, this.scrollElements[ id ].callback );\n\
}\n\
\n\
return this;\n\
};\n\
\n\
/**\n\
* A helper method to find all of the elements with a postponed attribute.\n\
* @returns {Boolean} A boolean stating whether new postpone elements have been\n\
* found.\n\
*/\n\
Postpone.prototype.getElements = function() {\n\
var elements = [],\n\
visible = [],\n\
matches = this._slice( document.querySelectorAll( this.tags ) ),\n\
postpone = null,\n\
change = false;\n\
\n\
for ( var i = 0; i < matches.length; i++ ) {\n\
postpone = matches[ i ].getAttribute( \"postpone\" );\n\
if ( typeof postpone === \"string\" && postpone !== \"false\" ) {\n\
elements.push( matches[ i ] );\n\
if ( this.isVisible( matches[ i ] ) ) {\n\
visible.push( matches[ i ] );\n\
/** Check if this element is not already postponed. */\n\
if ( matches[ i ] !== this.elements.visible[ visible.length - 1 ] ) {\n\
change = true;\n\
}\n\
}\n\
}\n\
}\n\
\n\
/** Check if old postponed elements are no longer on the page. */\n\
if ( this.elements.visible.length !== visible.length ) {\n\
change = true;\n\
}\n\
this.elements = elements;\n\
this.elements.visible = visible;\n\
\n\
return change;\n\
};\n\
\n\
/**\n\
* A helper method to find all of the elements with respect to which\n\
* postponed elements scroll. The elements are stored with a unique ID as\n\
* their key.\n\
* @returns {object} A hash with arrays of postponed elements associated with\n\
* IDs of their scroll elements.\n\
*/\n\
Postpone.prototype.getScrollElements = function() {\n\
this.scrollElements = {};\n\
\n\
var id = \"\",\n\
element = {},\n\
scrollElement = {};\n\
\n\
for ( var i = 0; i < this.elements.visible.length; i++ ) {\n\
element = this.elements.visible[ i ];\n\
/**\n\
* Find the element relative to which the postponed element's\n\
* position should be calculated.\n\
*/\n\
if ( element.getAttribute( \"data-scroll-element\" ) ) {\n\
scrollElement = document.querySelector( element.getAttribute( \"data-scroll-element\" ) );\n\
/**\n\
* If the scroll element does not have an ID, generate one and\n\
* assign it as a data attribute.\n\
*/\n\
id = scrollElement.getAttribute( \"data-id\" );\n\
if ( !id ) {\n\
scrollElement.setAttribute( \"data-id\", id = new Date().getTime() );\n\
}\n\
/**\n\
* If the element does not have a scroll element specified then\n\
* assume its position should be calculated relative to the window.\n\
*/\n\
} else {\n\
scrollElement = \"window\";\n\
id = \"window\";\n\
}\n\
/**\n\
* If the array already has this id as a key, then add the current\n\
* element to the array in its value, otherwise create a new key.\n\
*/\n\
if ( this.scrollElements[ id ] ) {\n\
this.scrollElements[ id ].push( element );\n\
} else {\n\
this.scrollElements[ id ] = [ element ];\n\
this.scrollElements[ id ].element = scrollElement;\n\
}\n\
}\n\
return this.scrollElements;\n\
};\n\
\n\
/**\n\
* Method to watch the document for new postponed elements.\n\
* @returns this\n\
*/\n\
Postpone.prototype.watch = function() {\n\
/**\n\
* Refresh the array of postponed elements, this.elements. If the postponed\n\
* elements have changed, then process them.\n\
*/\n\
if ( this.getElements() ) {\n\
this.postpone();\n\
}\n\
/**\n\
* This timeout calls the watch method every 500ms. In other words,\n\
* postpone will look for new postponed elements twice a second.\n\
* @property {number} timeout - The ID for the current timeout.\n\
*/\n\
this.timeout = window.setTimeout( (function( _this ) {\n\
return function() {\n\
return _this.watch();\n\
};\n\
})( this ), 500);\n\
\n\
return this;\n\
};\n\
\n\
/**\n\
* Method to start watching for elements that should postponed.\n\
* @returns this\n\
*/\n\
Postpone.prototype.start = function() {\n\
/** Ensure that watching has stopped before starting to watch. */\n\
if ( this.timeout ) this.stop();\n\
/**\n\
* Call `postpone` to ensure events are bound and items in view are\n\
* loaded.\n\
*/\n\
this.postpone();\n\
/** Start watching. */\n\
this.watch();\n\
\n\
return this;\n\
};\n\
\n\
/**\n\
* Method to stop watching for elements that should postponed and unbind events\n\
* associated with postponed elements.\n\
* @returns this\n\
*/\n\
Postpone.prototype.stop = function() {\n\
if ( this.timeout ) window.clearTimeout( this.timeout );\n\
\n\
/* Unbind the scroll events associated with postponed elements. */\n\
this.unbindEvents();\n\
\n\
return this;\n\
};\n\
\n\
/**\n\
* This method defines the scroll event handler used to test if postponed\n\
* elementes are visible.\n\
* @param {object} e - Event object.\n\
* @returns this\n\
*/\n\
Postpone.prototype.scrollHandler = function( e ) {\n\
var scrollElement = e.srcElement || e.target || window.document,\n\
elements = this.scrollElements[ scrollElement === window.document ? scrollElement = \"window\" : scrollElement.getAttribute( \"data-id\" ) ],\n\
element = {},\n\
scrolledIntoView = false;\n\
\n\
for ( var i = 0; i < elements.length; i++ ) {\n\
element = elements[ i ];\n\
\n\
/**\n\
* If an element is visible then we no longer need to postpone it\n\
* and can download it.\n\
*/\n\
if ( this.isInViewport( element, scrollElement ) ) {\n\
this.load( element );\n\
}\n\
}\n\
\n\
return this;\n\
};\n\
\n\
/**\n\
* A convenience method to easily set the threshold property of postpone.\n\
* @param {number|string} threshold - The distance from an edge at which an\n\
* element should be considered to be at the edge.\n\
* @returns this\n\
*/\n\
Postpone.prototype.setThreshold = function( threshold ) {\n\
threshold = threshold ? threshold : 0;\n\
/**\n\
* @property {object} threshold - A hash containing the value and unit of\n\
* measurement of the desired postpone threshold.\n\
*/\n\
this.threshold = {};\n\
/**\n\
* @property {number} value - The number of units from an edge at\n\
* which an element should be considered to be at the edge. This is\n\
* useful to start loading images or other resources before they scroll\n\
* into view to prevent flash of content.\n\
*/\n\
this.threshold.value = parseInt( threshold, 10 );\n\
/**\n\
* @property {string} unit - The unit of measurement for the threshold\n\
* value. Currently, only `vh` and `px` are supported. By default, the unit\n\
* is `vh`.\n\
*/\n\
this.threshold.unit = ( typeof threshold === \"number\" ) ? \"vh\" : ( threshold.match(/[a-zA-Z]+/)[ 0 ] || \"vh\" );\n\
\n\
return this;\n\
};\n\
/**\n\
* Small helper method to find the total vertical offset of an element.\n\
* @param {object} el - The element we wish to locate.\n\
* @returns {number} The total vertical offset of the element.\n\
*/\n\
Postpone.prototype.offsetTop = function( el ) {\n\
var temp = el,\n\
o = 0;\n\
/** Iterate over all parents of el up to body to find the vertical offset. */\n\
while ( temp && temp.tagName.toLowerCase() !== \"body\" && temp.tagName.toLowerCase() !== \"html\" ) {\n\
o += temp.offsetTop;\n\
temp = temp.offsetParent;\n\
}\n\
\n\
return o;\n\
};\n\
\n\
/**\n\
* Small helper method to determine if an element is visually hidden or not.\n\
* This method check if the element provided, or any of its parents have the\n\
* style `display: none;`.\n\
* @param {object} el - The element we wish to locate.\n\
* @returns {boolean} Returns true if the element is visible and false if it is\n\
* hidden.\n\
*/\n\
Postpone.prototype.isVisible = function( el ) {\n\
var temp = el,\n\
isVisible = true;\n\
/**\n\
* Iterate over all parents of el up to HTML to find if el or a parent is\n\
* hidden.\n\
*/\n\
while ( temp && temp.parentElement && isVisible ) {\n\
isVisible = temp.currentStyle ? temp.currentStyle.display !== \"none\" : document.defaultView.getComputedStyle( temp ).getPropertyValue( \"display\" ) !== \"none\";\n\
temp = temp.parentElement;\n\
}\n\
\n\
return isVisible;\n\
};\n\
\n\
/**\n\
* Helper method to determine if an element is in the browser's viewport.\n\
* @param {object} el - The element we wish to test.\n\
* @param {object} [scrollElement] - The element with respect to which `el` scrolls.\n\
* @returns {boolean} Return true if the `el` is in view and false if it is not.\n\
*/\n\
Postpone.prototype.isInViewport = function( el, scrollElement ) {\n\
/** If no scroll element is specified, then assume the scroll element is the window. */\n\
scrollElement = scrollElement ? scrollElement : \"window\";\n\
\n\
if ( scrollElement === \"window\" ) {\n\
scrollElement = document.documentElement.scrollTop ? document.documentElement : document.body;\n\
}\n\
/** Use clientHeight instead of window.innerHeight for compatability with ie8. */\n\
var viewPortHeight = document.documentElement.clientHeight,\n\
top = this.offsetTop( el ),\n\
scrollHeight = scrollElement.scrollTop + this.offsetTop( scrollElement ),\n\
isHighEnough = false,\n\
isLowEnough = false,\n\
threshold = 0;\n\
\n\
if ( this.threshold.unit === \"vh\" ) {\n\
threshold = viewPortHeight * this.threshold.value / 100;\n\
} else if ( this.threshold.unit === \"px\" ) {\n\
threshold = this.threshold.value;\n\
}\n\
\n\
/** Check if element is above bottom of screen. */\n\
isHighEnough = viewPortHeight + scrollHeight + threshold >= top;\n\
\n\
/** Check if element is below top of screen. */\n\
isLowEnough = ( el.height || 0 ) + top + threshold >= scrollHeight;\n\
\n\
return isHighEnough && isLowEnough;\n\
};\n\
\n\
/**\n\
* This method takes care of loading the media that should no longer be\n\
* postponed.\n\
* @param {object} el - The element that should be loaded.\n\
* @returns {object} The element that was loaded.\n\
*/\n\
Postpone.prototype.load = function( el ) {\n\
var child = {},\n\
i = 0;\n\
el.removeAttribute( \"postpone\" );\n\
\n\
/** If the element has a `data-src` attribute then copy it to `src`. */\n\
if ( ~\"audio, embed, iframe, img, picture, video\".indexOf( el.tagName.toLowerCase() ) && el.getAttribute( \"data-src\" ) ) {\n\
el.setAttribute( \"src\", el.getAttribute( \"data-src\" ) );\n\
}\n\
\n\
if ( ~\"image, tref, use\".indexOf( el.tagName.toLowerCase() ) && el.getAttribute( \"data-xlink:href\" ) ) {\n\
el.setAttribute( \"xlink:href\", el.getAttribute( \"data-xlink:href\" ) );\n\
}\n\
\n\
else if ( ~\"audio, video\".indexOf( el.tagName.toLowerCase() ) && el.children.length ) {\n\
for ( i = 0; i < el.children.length; i++ ) {\n\
child = el.children[ i ];\n\
if ( child.tagName.toLowerCase() === \"source\" && child.getAttribute( \"data-src\" ) ) {\n\
child.setAttribute( \"src\", child.getAttribute( \"data-src\" ) );\n\
}\n\
}\n\
}\n\
\n\
else if ( el.tagName.toLowerCase() === \"picture\" && el.children.length ) {\n\
for ( i = 0; i < el.children.length; i++ ) {\n\
child = el.children[ i ];\n\
if ( child.tagName.toLowerCase() === \"source\" ) {\n\
if ( child.getAttribute( \"data-src\" ) ) {\n\
child.setAttribute( \"src\", child.getAttribute( \"data-src\" ) );\n\
}\n\
if ( child.getAttribute( \"data-srcset\" ) ) {\n\
child.setAttribute( \"srcset\", child.getAttribute( \"data-srcset\" ) );\n\
}\n\
}\n\
}\n\
}\n\
\n\
else if ( el.tagName.toLowerCase() === \"object\" && el.getAttribute( \"data-data\" ) ) {\n\
el.setAttribute( \"data\", el.getAttribute( \"data-data\" ) );\n\
\n\
/**\n\
* This is necessary to make Safari 7 refresh the object's new content.\n\
*/\n\
var activeElement = document.activeElement;\n\
el.focus();\n\
\n\
if ( activeElement ) {\n\
activeElement.focus();\n\
}\n\
}\n\
\n\
return el;\n\
};\n\
\n\
/**\n\
* A helper method to convert array-like objects into arrays.\n\
* @param {object} arr - The object to be converted.\n\
* @returns {array} An array representation of the supplied object.\n\
* @api private\n\
*/\n\
Postpone.prototype._slice = function( object ) {\n\
/** Try to use `slice` to convert the object. */\n\
try {\n\
return Array.prototype.slice.call( object );\n\
/**\n\
* If that doesn't work, manually iterate over the object and convert\n\
* it to an array.\n\
*/\n\
} catch(e) {\n\
var array = [];\n\
for ( var i = 0; i < object.length; i++ ) {\n\
array.push( object[ i ] );\n\
}\n\
return array;\n\
}\n\
};\n\
\n\
/**\n\
* A helper method to abstract event listener creation.\n\
* @param {object} el - The element to which the event should be added.\n\
* @param {function} callback - The callback to be executed when the event\n\
* is fired.\n\
* @returns undefined\n\
* @api private\n\
*/\n\
Postpone.prototype._addEventListener = function( el, callback ) {\n\
/** Try to add the event using `addEventListener`. */\n\
try {\n\
return el.addEventListener( \"scroll\", callback );\n\
/** If that doesn't work, add the event using `attachEvent`. */\n\
} catch(e) {\n\
return el.attachEvent( \"onscroll\", callback );\n\
}\n\
};\n\
\n\
/**\n\
* A helper method to abstract event listener removal.\n\
* @param {object} el - The element from which the event should be removed.\n\
* @param {function} callback - The callback to be executed when the event\n\
* is fired.\n\
* @returns undefined\n\
* @api private\n\
*/\n\
Postpone.prototype._removeEventListener = function( el, callback ) {\n\
/** Try to remove the event using `removeEventListener`. */\n\
try {\n\
return el.removeEventListener( \"scroll\", callback );\n\
/** If that doesn't work, remove the event using `detachEvent`. */\n\
} catch(e) {\n\
return el.detachEvent( \"onscroll\", callback );\n\
}\n\
};\n\
\n\
/** Expose `Postpone`. */\n\
return Postpone;\n\
}));\n\
\n\
//@ sourceURL=postpone/index.js"
));
require.alias("postpone/index.js", "postpone/index.js");