API Docs for: 0.3.0
Show:

File: modules/Preload/Preload.js

/**
 * @module Preload
 * @namespace Preload
 */


var TW = TW || {};
define(['./XHRLoader', '../Utils/Polyfills'], function(XHRLoader) {

	TW.Preload = TW.Preload || {};


	/**
	 * The preload type for image files, usually png, gif, or jpg/jpeg
	 * @property IMAGE
	 * @type String
	 * @default image
	 * @static
	 */
	TW.Preload.IMAGE = "image";

	/* The preload type for SVG files.
	 * @property SVG
	 * @type String
	 * @default svg
	 * @static
	 */
	TW.Preload.SVG = "svg";

	/**
	 * The preload type for sound files, usually mp3, ogg, or wav.
	 * @property SOUND
	 * @type String
	 * @default sound
	 * @static
	 */
	TW.Preload.SOUND = "sound";

	/**
	 * The preload type for json files, usually with the "json" file extension.
	 * @property JSON
	 * @type String
	 * @default json
	 * @static
	 */
	TW.Preload.JSON = "json";

	/**
	 * The preload type for css files.
	 * @property CSS
	 * @type String
	 * @default css
	 * @static
	 */
	TW.Preload.CSS = "css";

	/**
	 * The preload type for xml files.
	 * @property XML
	 * @type String
	 * @default xml
	 * @static
	 */
	TW.Preload.XML = "xml";

	/**
	 * The preload type for text files, which is also the default file type if the type can not be determined.
	 * @property TEXT
	 * @type String
	 * @default text
	 * @static
	 */
	TW.Preload.TEXT = "text";

	/**
	 * Time in millseconds to assume a load has failed.
	 * @property TIMEOUT_TIME
	 * @type Number
	 * @default 8000
	 * @static
	 */
	TW.Preload.TIMEOUT_TIME = 8000;


	/**
	 * Preload class is object utility for preload different file format.
	 *
	 * @class Preload
	 * @constructor
	 */
	function Preload() {

		/**
		 * The next preload queue to process when this one is complete.
		 * @property next
		 * @type Preload
		 * @default null
		 */
		this.next = null;

		//Protected properties
		this.typeHandlers = null;
		this.extensionHandlers = null;

		this._maxConnections = 1;

		this._numItems = 0;
		this._numItemsLoaded = 0;
		this._targetProgress = 0;
		this._paused = false;
		this._currentLoads = [];
		this._loadQueue = [];
		this._loadedItemsById = {};
		this._loadedItemsBySrc = {};
		this.typeHandlers = {};
		this.extensionHandlers = {};
		this._loadStartWasDispatched = false;

		/**
		 * Determine if this loader has completed already.
		 * @property loaded
		 * @type Boolean
		 * @default false
		 */
		this.loaded = false;

		/**
		 * The current load progress (percentage) for this item.
		 * @property progress
		 * @type Number
		 * @default 0
		 */
		this.progress = 0;

		//Callbacks
		/**
		 * The callback to fire when progress changes.
		 * @event onProgress
		 */
		this.onProgress = null;

		/**
		 * The callback to fire when a load starts.
		 * @event onLoadStart
		 */
		this.onLoadStart = null;

		/**
		 * The callback to fire when a file completes.
		 * @event onFileLoad
		 */
		this.onFileLoad = null;

		/**
		 * The callback to fire when a file progress changes.
		 * @event onFileProgress
		 */
		this.onFileProgress = null;

		/**
		 * The callback to fire when all loading is complete.
		 * @event onComplete
		 */
		this.onComplete = null;

		/**
		 * The callback to fire when the loader encounters an error. If the error was encountered
		 * by a file, the event will contain the required file data, but the target will be the loader.
		 * @event onError
		 */
		this.onError = null;
	}

	//Callback proxies
	Preload.prototype._sendLoadStart = function() {
		if (this.onLoadStart) {
			this.onLoadStart({target: this});
		}
	};

	Preload.prototype._sendProgress = function(value) {
		var event;
		if (value instanceof Number) {
			this.progress = value;
			event = {loaded: this.progress, total: 1};
		} else {
			event = value;
			this.progress = value.loaded / value.total;
			if (isNaN(this.progress) || this.progress === Infinity) {
				this.progress = 0;
			}
		}
		event.target = this;
		if (this.onProgress) {
			this.onProgress(event);
		}
	};

	Preload.prototype._sendFileProgress = function(event) {
		if (this.onFileProgress) {
			event.target = this;
			this.onFileProgress(event);
		}
	};

	Preload.prototype._sendComplete = function() {
		if (this.onComplete) {
			this.onComplete({target: this});
		}
	};

	Preload.prototype._sendFileComplete = function(event) {
		if (this.onFileLoad) {
			event.target = this;
			this.onFileLoad(event);
		}
	};

	Preload.prototype._sendError = function(event) {
		if (this.onError) {
			if (event === null) {
				event = {};
			}
			event.target = this;
			this.onError(event);
		}
	};

	/**
	 * Set the maximum number of concurrent connections.
	 *
	 * @method setMaxConnections
	 * @param {Number} value The number of concurrent loads to allow.
	 *  By default, only a single connection is open at any time.
	 * Note that browsers and servers may have a built-in maximum number of open connections
	 */
	Preload.prototype.setMaxConnections = function(value) {
		this._maxConnections = value;
		if (!this._paused) {
			this._loadNext();
		}
	};

	/**
	 * Load a single file. Note that calling loadFile appends to the current queue, so it can be used multiple times
	 * to add files. Use `loadManifest()` to add multiple files at onces.
	 * To clear the queue first use the `close()` method.
	 *
	 * @method loadFile
	 * @param {Object | String} file The file object or path to load. A file can be either
	 *
	 *  - a path to a resource (string). Note that this kind of load item will be
	 *     converted to an object (next item) in the background.
	 *  - OR an object that contains:
	 *      - src: The source of the file that is being loaded. This property is <b>required</b>.
	 *         The source can either be a string (recommended), or an HTML tag.
	 *      - type: The type of file that will be loaded (image, sound, json, etc).
	 *         Preload does auto-detection of types using the extension. Supported types are defined on Preload,
	 *         such as Preload.IMAGE. It is recommended that a type is specified when
	 *         a non-standard file URI (such as a php script) us used.
	 *      - id: A string identifier which can be used to reference the loaded object.
	 *      - data: An arbitrary data object, which is included with the loaded object
	 *
	 *
	 * @param {Boolean} [loadNow=true] Kick off an immediate load (true) or wait for a load call (false).
	 *  If the queue is paused, and this value is true, the queue will resume.
	 */
	Preload.prototype.loadFile = function(file, loadNow) {
		if (file === null) {
			this._sendError({text: "File is null."});
			return;
		}
		this._addItem(file);

		if (loadNow !== false) {
			this.setPaused(false);
		}
	};

	/**
	 * Load a manifest. This is a shortcut method to load a group of files.
	 * To load a single file, use the loadFile method.
	 * Note that calling loadManifest appends to the current queue, so it can be used multiple times to add files.
	 * To clear the queue first, use the <b>close()</b> method.
	 *
	 * @method loadManifest
	 * @param {Array} manifest The list of files to load. Each file can be either
	 *
	 *  - a path to a resource (string). Note that this kind of load item will be
	 *    converted to an object (next item) in the background.
	 *  - OR an object that contains:
	 *    - *src*: The source of the file that is being loaded. This property is **required**.
	 *      The source can either be a string (recommended), or an HTML tag.
	 *    - *type*: The type of file that will be loaded (image, sound, json, etc).
	 *      Preload does auto-detection of types using the extension. Supported types are defined on Preload,
	 *      such as Preload.IMAGE.
	 *      It is recommended that a type is specified when a non-standard file URI (such as a php script) us used.
	 *    - *id*: A string identifier which can be used to reference the loaded object.
	 *    - *data*: An arbitrary data object, which is included with the loaded object
	 *
	 * @param {Boolean} loadNow Kick off an immediate load (true) or wait for a load call (false).
	 *  The default value is true. If the queue is paused, and this value
	 *  is true, the queue will resume.
	 */
	Preload.prototype.loadManifest = function(manifest, loadNow) {
		var data;

		if (manifest instanceof Array) {
			if (manifest.length === 0) {
				this._sendError({text: "Manifest is empty."});
				return;
			}
			data = manifest;
		} else {
			if (manifest === null) {
				this._sendError({text: "Manifest is null."});
				return;
			}
			data = [manifest];
		}

		for (var i = 0, l = data.length; i < l; i++) {
			this._addItem(data[i], false);
		}

		if (loadNow !== false) {
			this._loadNext();
		}
	};

	/**
	 * Begin loading the queued items.
	 * @method load
	 */
	Preload.prototype.load = function() {
		this.setPaused(false);
	};

	/**
	 * Lookup a loaded item using either the "id" or "src" that was specified when loading it.
	 * @method getResult
	 *
	 * @param {String} value The "id" or "src" of the loaded item.
	 * @return {Object} A result object containing the contents of the object that was initially requested using
	 *  loadFile or loadManifest, including:
	 *
	 *   - `src`: The source of the file that was requested.
	 *   - type: The type of file that was loaded. If it was not specified,
	 *     this is auto-detected by Preload using the file extension.
	 *   - `id`: The id of the loaded object.
	 *     If it was not specified, the ID will be the same as the "src" property.
	 *   - data: Any arbitrary data that was specified, otherwise it will be undefined.
	 *   - `result`: The loaded object. Preload provides usable tag elements when possible:
	 *      - An HTMLImageElement tag (`<image/>`) for images
	 *      - An HTMLAudioElement tag (`<audio/>`) for audio
	 *      - A script tag for JavaScript (`<script></script>`)
	 *      - A style tag for CSS (`<style></style>`)
	 *      - Raw text for JSON or any other kind of loaded item
	 *
	 * This object is also returned via the "onFileLoad" callback, although a "target" will be included,
	 * which is a reference to the Preload instance.
	 */
	Preload.prototype.getResult = function(value) {
		return this._loadedItemsById[value] || this._loadedItemsBySrc[value];
	};

	/**
	 * Pause or resume the current load. The active item will not cancel, but the next
	 * items in the queue will not be processed.
	 *
	 * @method setPaused
	 * @param {Boolean} value Whether the queue should be paused or not.
	 */
	Preload.prototype.setPaused = function(value) {
		this._paused = value;
		if (!this._paused) {
			this._loadNext();
		}
	};

	/**
	 * Close the active queue. Closing a queue completely empties the queue,
	 * and prevents any remaining items from starting to
	 * download. Note that currently there any active loads will remain open, and events may be processed.
	 *
	 * To stop and restart a queue, use the `setPaused(true|false)` method instead.
	 *
	 * @method close
	 */
	Preload.prototype.close = function() {
		while (this._currentLoads.length) {
			this._currentLoads.pop().cancel();
		}
		this._currentLoads = [];
		this._scriptOrder = [];
		this._loadedScripts = [];
	};


	//Protected Methods
	Preload.prototype._addItem = function(item) {
		var loadItem = this._createLoadItem(item);
		if (loadItem !== null) {
			this._loadQueue.push(loadItem);

			this._numItems++;
			this._updateProgress();
		}
	};

	Preload.prototype._loadNext = function() {
		var loadItem;

		if (this._paused) {
			return;
		}

		if (!this._loadStartWasDispatched) {
			this._sendLoadStart();
			this._loadStartWasDispatched = true;
		}

		if (this._numItems === this._numItemsLoaded) {
			this.loaded = true;
			this._sendComplete();
			if (this.next && this.next.load) {
				//LM: Do we need to apply here?
				this.next.load.apply(this.next);
			}
		}

		while (this._loadQueue.length && this._currentLoads.length < this._maxConnections) {
			loadItem = this._loadQueue.shift();
			this._loadItem(loadItem);
		}
	};

	Preload.prototype._loadItem = function(item) {
		item.onProgress = this._handleProgress.bind(this);
		item.onComplete = this._handleFileComplete.bind(this);
		item.onError = this._handleFileError.bind(this);

		this._currentLoads.push(item);

		item.load();
	};

	Preload.prototype._handleFileError = function(event) {
		var loader = event.target;

		var resultData = this._createResultData(loader.getItem());
		this._numItemsLoaded++;
		this._updateProgress();

		this._sendError(resultData);

		if (!this.stopOnError) {
			this._removeLoadItem(loader);
			this._loadNext();
		}
	};

	Preload.prototype._createResultData = function(item) {
		var resultData = {id: item.id, result: null, data: item.data, type: item.type, src: item.src};
		this._loadedItemsById[item.id] = resultData;
		this._loadedItemsBySrc[item.src] = resultData;
		return resultData;
	};

	Preload.prototype._handleFileComplete = function(event) {
		var loader = event.target;
		var item = loader.getItem();
		var resultData = this._createResultData(item);

		this._removeLoadItem(loader);

		if (loader instanceof XHRLoader) {
			resultData.result = this._createResult(item, loader.getResult());
		}

		switch (item.type) {
			case TW.Preload.IMAGE: //LM: Consider moving this to XHRLoader
				if (loader instanceof XHRLoader) {
					var _this = this; // Use closure workaround to maintain reference to item/result
					resultData.result.onload = function() {
						_this._handleFileTagComplete(item, resultData);
					};
					return;
				}
				break;
			case TW.Preload.SVG:
			case TW.Preload.SOUND:
				break;
			default:
				break;
		}

		this._handleFileTagComplete(item, resultData);
	};

	Preload.prototype._handleFileTagComplete = function(item, resultData) {
		this._numItemsLoaded++;

		if (item.completeHandler) {
			item.completeHandler(resultData);
		}

		this._updateProgress();
		this._sendFileComplete(resultData);

		this._loadNext();
	};

	Preload.prototype._removeLoadItem = function(loader) {
		var l = this._currentLoads.length;
		for (var i = 0; i < l; i++) {
			if (this._currentLoads[i] === loader) {
				this._currentLoads.splice(i, 1);
				return;
			}
		}
	};

	Preload.prototype._createResult = function(item, data) {
		var tag = null;
		var resultData;
		switch (item.type) {
			case TW.Preload.IMAGE:
				tag = this._createImage();
				break;
			case TW.Preload.SOUND:
				tag = item.tag || this._createAudio();
				break;
			case TW.Preload.CSS:
				tag = this._createLink();
				break;
			case TW.Preload.SVG:
				tag = this._createSVG();
				tag.appendChild(this._createXML(data, "image/svg+xml"));
				break;
			case TW.Preload.XML:
				resultData = this._createXML(data, "text/xml");
				break;
			case TW.Preload.JSON:
			case TW.Preload.TEXT:
				resultData = data;
		}

		//LM: Might not need to do this with Audio.
		if (tag) {
			if (item.type === this.CSS) {
				tag.href = item.src;
			} else if (item.type !== this.SVG) {
				tag.src = item.src;
			}
			return tag;
		} else {
			return resultData;
		}
	};

	Preload.prototype._createXML = function(data, type) {
		var resultData;
		var parser;

		if (window.DOMParser) {
			/*global DOMParser */
			parser = new DOMParser();
			resultData = parser.parseFromString(data, type);
		} else { // Internet Explorer
			/*global ActiveXObject */
			parser = new ActiveXObject("Microsoft.XMLDOM");
			parser.async = false;
			parser.loadXML(data);
			resultData = parser;
		}

		return resultData;
	};

	// This is item progress!
	Preload.prototype._handleProgress = function(event) {
		var loader = event.target;
		var resultData = this._createResultData(loader.getItem());
		resultData.progress = loader.progress;
		this._sendFileProgress(resultData);
		this._updateProgress();
	};

	Preload.prototype._updateProgress = function() {
		var loaded = this._numItemsLoaded / this._numItems; // Fully Loaded Progress
		var remaining = this._numItems - this._numItemsLoaded;
		if (remaining > 0) {
			var chunk = 0;
			for (var i = 0, l = this._currentLoads.length; i < l; i++) {
				chunk += this._currentLoads[i].progress;
			}
			loaded += (chunk / remaining) * (remaining / this._numItems);
		}
		this._sendProgress({loaded: loaded, total: 1});
	};

	Preload.prototype._createLoadItem = function(loadItem) {
		var item = {};

		// Create/modify a load item
		switch (typeof(loadItem)) {
			case "string":
				item.src = loadItem;
				break;
			case "object":
				/*global HTMLAudioElement */
				if (loadItem instanceof HTMLAudioElement) {
					item.tag = loadItem;
					item.src = item.tag.src;
					item.type = TW.Preload.SOUND;
				} else {
					item = loadItem;
				}
				break;
			default:
				break;
		}

		// Get source extension
		item.ext = this._getNameAfter(item.src, ".");
		if (!item.type) {
			item.type = this.getType(item.ext);
		}
		//If there's no id, set one now.
		if (item.id === null || item.id === "") {
			//item.id = this._getNameAfter(item.src, "/");
			item.id = item.src; //[SB] Using the full src is more robust, and more useful from a user perspective.
		}

		// Give plugins a chance to modify the loadItem
		var customHandler = this.typeHandlers[item.type] || this.extensionHandlers[item.ext];
		if (customHandler) {
			var result = customHandler(item.src, item.type, item.id, item.data);
			//Plugin will handle the load, so just ignore it.
			if (result === false) {
				return null;

				// Load as normal
			} /* else if (result === true) {
			 // Do Nothing
			 // Result is a loader class
			 } */ else {
				if (result.src !== null) {
					item.src = result.src;
				}
				if (result.id !== null) {
					item.id = result.id;
				}
				if (result.tag !== null && result.tag.load instanceof Function) { //Item has what we need load
					item.tag = result.tag;
				}
			}

			// Update the extension in case the type changed
			item.ext = this._getNameAfter(item.src, ".");
		}

		return new XHRLoader(item);
	};

	Preload.prototype.getType = function(ext) {
		switch (ext) {
			case "jpeg":
			case "jpg":
			case "gif":
			case "png":
				return TW.Preload.IMAGE;
			case "ogg":
			case "mp3":
			case "wav":
				return TW.Preload.SOUND;
			case "json":
				return TW.Preload.JSON;
			case "xml":
				return TW.Preload.XML;
			case "css":
				return TW.Preload.CSS;
			case 'svg':
				return TW.Preload.SVG;
			default:
				return TW.Preload.TEXT;
		}
	};

	Preload.prototype._getNameAfter = function(path, token) {
		var dotIndex = path.lastIndexOf(token);
		var lastPiece = path.substr(dotIndex + 1);
		var endIndex = lastPiece.lastIndexOf(/[\b|\?|#|\s]/);
		return (endIndex === -1) ? lastPiece : lastPiece.substr(0, endIndex);
	};

	Preload.prototype._createImage = function() {
		return document.createElement("img");
	};

	Preload.prototype._createSVG = function() {
		var tag = document.createElement("object");
		tag.type = "image/svg+xml";
		return tag;
	};

	Preload.prototype._createAudio = function() {
		var tag = document.createElement("audio");
		tag.autoplay = false;
		tag.type = "audio/ogg";
		return tag;
	};

	Preload.prototype._createScript = function() {
		var tag = document.createElement("script");
		tag.type = "text/javascript";
		return tag;
	};

	Preload.prototype._createLink = function() {
		var tag = document.createElement("link");
		tag.type = "text/css";
		tag.rel = "stylesheet";
		return tag;
	};

	TW.Preload.Preload = Preload;
	return Preload;
});