Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

AMD-ified #18

Closed
wants to merge 3 commits into from

2 participants

@nick-jonas

I added AMD-ified files that can be included for projects that use AMD systems like RequireJS.

nick-jonas added some commits
@nick-jonas nick-jonas AMD-ified version
Version that is compatible with AMD projects.
c063b3c
@nick-jonas nick-jonas Typo. ae979f2
@nick-jonas nick-jonas typo 328805b
@joelfillmore
Owner

This is a good idea, but I'd rather add support to the existing code rather than having duplicate files with an AMD wrapper. There is an example of that approach in the following page (under the Conditional AMD Support header):
http://dailyjs.com/2011/12/22/framework/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 3, 2013
  1. @nick-jonas

    AMD-ified version

    nick-jonas authored
    Version that is compatible with AMD projects.
  2. @nick-jonas

    Typo.

    nick-jonas authored
  3. @nick-jonas

    typo

    nick-jonas authored
This page is out of date. Refresh to see the latest.
View
5 README
@@ -3,6 +3,11 @@ PxLoader is a Javascript library that helps you download images, sound files or
Details and examples:
http://thinkpixellab.com/pxloader/
+AMD
+---
+For people using asynchronous module definitions like RequireJS, AMD-ified modules have been included. Please see the dependency list in pxloader.js and match the paths to the config in your project. For Image/Sound/Video, it's just a matter of uncommenting in the dependency list. (added by @nick-jonas)
+
+
The MIT License
Copyright (c) 2012 Pixel Lab
View
353 amd/pxloader.js
@@ -0,0 +1,353 @@
+/**
+ * PixelLab Resource Loader
+ * Loads resources while providing progress updates.
+ */
+define(
+ [
+ 'pxloadertags'
+ // ,'pxloaderimage'
+ // ,'pxloadersound'
+ // ,'pxloadervideo'
+ ],
+function(PxLoaderTags/*, PxLoaderImage, PxLoaderSound, PxLoaderVideo*/){
+ var PxLoader = function(settings){
+ // merge settings with defaults
+ settings = settings || {};
+
+ // add methods
+ if(typeof PxLoaderImage !== 'undefined'){
+ this.addImage = function(url, tags, priority) {
+ var imageLoader = new PxLoaderImage(url, tags, priority);
+ this.add(imageLoader);
+ // return the img element to the caller
+ return imageLoader.img;
+ };
+ }
+
+ if(typeof PxLoaderSound !== 'undefined'){
+ this.addSound = function(id, url, tags, priority) {
+ var soundLoader = new PxLoaderSound(id, url, tags, priority);
+ this.add(soundLoader);
+ return soundLoader.sound;
+ };
+ }
+
+
+ if(typeof PxLoaderVideo !== 'undefined'){
+ this.addVideo = function(url, tags, priority) {
+ var videoLoader = new PxLoaderVideo(url, tags, priority);
+ this.add(videoLoader);
+ // return the vid element to the caller
+ return videoLoader.vid;
+ };
+ }
+
+ // how frequently we poll resources for progress
+ if (settings.statusInterval == null) {
+ settings.statusInterval = 5000; // every 5 seconds by default
+ }
+
+ // delay before logging since last progress change
+ if (settings.loggingDelay == null) {
+ settings.loggingDelay = 20 * 1000; // log stragglers after 20 secs
+ }
+
+ // stop waiting if no progress has been made in the moving time window
+ if (settings.noProgressTimeout == null) {
+ settings.noProgressTimeout = Infinity; // do not stop waiting by default
+ }
+
+ var entries = [],
+ // holds resources to be loaded with their status
+ progressListeners = [],
+ timeStarted, progressChanged = +new Date;
+
+ /**
+ * The status of a resource
+ * @enum {number}
+ */
+ var ResourceState = {
+ QUEUED: 0,
+ WAITING: 1,
+ LOADED: 2,
+ ERROR: 3,
+ TIMEOUT: 4
+ };
+
+ // places non-array values into an array.
+ var ensureArray = function(val) {
+ if (val == null) {
+ return [];
+ }
+
+ if (Object.prototype.toString.call(val) == '[object Array]') {
+ return val;
+ }
+
+ return [val];
+ };
+
+ // add an entry to the list of resources to be loaded
+ this.add = function(resource) {
+
+ // ensure tags are in an object
+ resource.tags = new PxLoaderTags(resource.tags);
+
+ // ensure priority is set
+ if (resource.priority == null) {
+ resource.priority = Infinity;
+ }
+
+ entries.push({
+ resource: resource,
+ status: ResourceState.QUEUED
+ });
+ };
+
+ this.addProgressListener = function(callback, tags) {
+ progressListeners.push({
+ callback: callback,
+ tags: new PxLoaderTags(tags)
+ });
+ };
+
+ this.addCompletionListener = function(callback, tags) {
+ progressListeners.push({
+ tags: new PxLoaderTags(tags),
+ callback: function(e) {
+ if (e.completedCount === e.totalCount) {
+ callback();
+ }
+ }
+ });
+ };
+
+ // creates a comparison function for resources
+ var getResourceSort = function(orderedTags) {
+
+ // helper to get the top tag's order for a resource
+ orderedTags = ensureArray(orderedTags);
+ var getTagOrder = function(entry) {
+ var resource = entry.resource,
+ bestIndex = Infinity;
+ for (var i = 0; i < resource.tags.length; i++) {
+ for (var j = 0; j < Math.min(orderedTags.length, bestIndex); j++) {
+ if (resource.tags[i] == orderedTags[j] && j < bestIndex) {
+ bestIndex = j;
+ if (bestIndex === 0) break;
+ }
+ if (bestIndex === 0) break;
+ }
+ }
+ return bestIndex;
+ };
+ return function(a, b) {
+ // check tag order first
+ var aOrder = getTagOrder(a),
+ bOrder = getTagOrder(b);
+ if (aOrder < bOrder) return -1;
+ if (aOrder > bOrder) return 1;
+
+ // now check priority
+ if (a.priority < b.priority) return -1;
+ if (a.priority > b.priority) return 1;
+ return 0;
+ }
+ };
+
+ this.start = function(orderedTags) {
+ timeStarted = +new Date;
+
+ // first order the resources
+ var compareResources = getResourceSort(orderedTags);
+ entries.sort(compareResources);
+
+ // trigger requests for each resource
+ for (var i = 0, len = entries.length; i < len; i++) {
+ var entry = entries[i];
+ entry.status = ResourceState.WAITING;
+ entry.resource.start(this);
+ }
+
+ // do an initial status check soon since items may be loaded from the cache
+ setTimeout(statusCheck, 100);
+ };
+
+ var statusCheck = function() {
+ var checkAgain = false,
+ noProgressTime = (+new Date) - progressChanged,
+ timedOut = (noProgressTime >= settings.noProgressTimeout),
+ shouldLog = (noProgressTime >= settings.loggingDelay);
+
+ for (var i = 0, len = entries.length; i < len; i++) {
+ var entry = entries[i];
+ if (entry.status !== ResourceState.WAITING) {
+ continue;
+ }
+
+ // see if the resource has loaded
+ if (entry.resource.checkStatus) {
+ entry.resource.checkStatus();
+ }
+
+ // if still waiting, mark as timed out or make sure we check again
+ if (entry.status === ResourceState.WAITING) {
+ if (timedOut) {
+ entry.resource.onTimeout();
+ } else {
+ checkAgain = true;
+ }
+ }
+ }
+
+ // log any resources that are still pending
+ if (shouldLog && checkAgain) {
+ log();
+ }
+
+ if (checkAgain) {
+ setTimeout(statusCheck, settings.statusInterval);
+ }
+ };
+
+ this.isBusy = function() {
+ for (var i = 0, len = entries.length; i < len; i++) {
+ if (entries[i].status === ResourceState.QUEUED || entries[i].status === ResourceState.WAITING) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ var onProgress = function(resource, statusType) {
+ // find the entry for the resource
+ var entry = null;
+ for (var i = 0, len = entries.length; i < len; i++) {
+ if (entries[i].resource === resource) {
+ entry = entries[i];
+ break;
+ }
+ }
+
+ // we have already updated the status of the resource
+ if (entry == null || entry.status !== ResourceState.WAITING) {
+ return;
+ }
+ entry.status = statusType;
+ progressChanged = +new Date;
+
+ var numResourceTags = resource.tags.length;
+
+ // fire callbacks for interested listeners
+ for (var i = 0, numListeners = progressListeners.length; i < numListeners; i++) {
+ var listener = progressListeners[i],
+ shouldCall;
+
+ if (listener.tags.length === 0) {
+ // no tags specified so always tell the listener
+ shouldCall = true;
+ } else {
+ // listener only wants to hear about certain tags
+ shouldCall = resource.tags.contains(listener.tags);
+ }
+
+ if (shouldCall) {
+ sendProgress(entry, listener);
+ }
+ }
+ };
+
+ this.onLoad = function(resource) {
+ onProgress(resource, ResourceState.LOADED);
+ };
+ this.onError = function(resource) {
+ onProgress(resource, ResourceState.ERROR);
+ };
+ this.onTimeout = function(resource) {
+ onProgress(resource, ResourceState.TIMEOUT);
+ };
+
+ // sends a progress report to a listener
+ var sendProgress = function(updatedEntry, listener) {
+ // find stats for all the resources the caller is interested in
+ var completed = 0,
+ total = 0;
+ for (var i = 0, len = entries.length; i < len; i++) {
+ var entry = entries[i],
+ includeResource = false;
+
+ if (listener.tags.length === 0) {
+ // no tags specified so always tell the listener
+ includeResource = true;
+ } else {
+ includeResource = entry.resource.tags.contains(listener.tags);
+ }
+
+ if (includeResource) {
+ total++;
+ if (entry.status === ResourceState.LOADED || entry.status === ResourceState.ERROR || entry.status === ResourceState.TIMEOUT) {
+ completed++;
+ }
+ }
+ }
+
+ listener.callback({
+ // info about the resource that changed
+ resource: updatedEntry.resource,
+
+ // should we expose StatusType instead?
+ loaded: (updatedEntry.status === ResourceState.LOADED),
+ error: (updatedEntry.status === ResourceState.ERROR),
+ timeout: (updatedEntry.status === ResourceState.TIMEOUT),
+
+ // updated stats for all resources
+ completedCount: completed,
+ totalCount: total
+ });
+ };
+
+ // prints the status of each resource to the console
+ var log = this.log = function(showAll) {
+ if (!window.console) {
+ return;
+ }
+
+ var elapsedSeconds = Math.round((+new Date - timeStarted) / 1000);
+ window.console.log('PxLoader elapsed: ' + elapsedSeconds + ' sec');
+
+ for (var i = 0, len = entries.length; i < len; i++) {
+ var entry = entries[i];
+ if (!showAll && entry.status !== ResourceState.WAITING) {
+ continue;
+ }
+
+ var message = 'PxLoader: #' + i + ' ' + entry.resource.getName();
+ switch(entry.status) {
+ case ResourceState.QUEUED:
+ message += ' (Not Started)';
+ break;
+ case ResourceState.WAITING:
+ message += ' (Waiting)';
+ break;
+ case ResourceState.LOADED:
+ message += ' (Loaded)';
+ break;
+ case ResourceState.ERROR:
+ message += ' (Error)';
+ break;
+ case ResourceState.TIMEOUT:
+ message += ' (Timeout)';
+ break;
+ }
+
+ if (entry.resource.tags.length > 0) {
+ message += ' Tags: [' + entry.resource.tags.join(',') + ']';
+ }
+
+ window.console.log(message);
+ }
+ };
+ };
+
+ return PxLoader;
+});
View
98 amd/pxloaderimage.js
@@ -0,0 +1,98 @@
+// @depends PxLoader.js
+/**
+ * PxLoader plugin to load images
+ */
+ define([],
+function(){
+ var PxLoaderImage = function(url, tags, priority) {
+ var self = this,
+ loader = null;
+
+ this.img = new Image();
+ this.tags = tags;
+ this.priority = priority;
+
+ var onReadyStateChange = function() {
+ if (self.img.readyState == 'complete') {
+ removeEventHandlers();
+ loader.onLoad(self);
+ }
+ };
+
+ var onLoad = function() {
+ removeEventHandlers();
+ loader.onLoad(self);
+ };
+
+ var onError = function() {
+ removeEventHandlers();
+ loader.onError(self);
+ };
+
+ var removeEventHandlers = function() {
+ self.unbind('load', onLoad);
+ self.unbind('readystatechange', onReadyStateChange);
+ self.unbind('error', onError);
+ };
+
+ this.start = function(pxLoader) {
+ // we need the loader ref so we can notify upon completion
+ loader = pxLoader;
+
+ // NOTE: Must add event listeners before the src is set. We
+ // also need to use the readystatechange because sometimes
+ // load doesn't fire when an image is in the cache.
+ self.bind('load', onLoad);
+ self.bind('readystatechange', onReadyStateChange);
+ self.bind('error', onError);
+
+ self.img.src = url;
+ };
+
+ // called by PxLoader to check status of image (fallback in case
+ // the event listeners are not triggered).
+ this.checkStatus = function() {
+ if (self.img.complete) {
+ removeEventHandlers();
+ loader.onLoad(self);
+ }
+ };
+
+ // called by PxLoader when it is no longer waiting
+ this.onTimeout = function() {
+ removeEventHandlers();
+ if (self.img.complete) {
+ loader.onLoad(self);
+ } else {
+ loader.onTimeout(self);
+ }
+ };
+
+ // returns a name for the resource that can be used in logging
+ this.getName = function() {
+ return url;
+ };
+
+ // cross-browser event binding
+ this.bind = function(eventName, eventHandler) {
+ if (self.img.addEventListener) {
+ self.img.addEventListener(eventName, eventHandler, false);
+ } else if (self.img.attachEvent) {
+ self.img.attachEvent('on' + eventName, eventHandler);
+ }
+ };
+
+ // cross-browser event un-binding
+ this.unbind = function(eventName, eventHandler) {
+ if (self.img.removeEventListener) {
+ self.img.removeEventListener(eventName, eventHandler, false);
+ } else if (self.img.detachEvent) {
+ self.img.detachEvent('on' + eventName, eventHandler);
+ }
+ };
+
+ };
+
+ return PxLoaderImage;
+});
+
View
88 amd/pxloadersound.js
@@ -0,0 +1,88 @@
+// @depends PxLoader.js
+/**
+ * PxLoader plugin to load sound using SoundManager2
+ */
+
+define([],
+function(){
+var PxLoaderSound = function(id, url, tags, priority) {
+ var self = this,
+ loader = null;
+
+ this.tags = tags;
+ this.priority = priority;
+ this.sound = soundManager['createSound']({
+ 'id': id,
+ 'url': url,
+ 'autoLoad': false,
+ 'onload': function() {
+ loader.onLoad(self);
+ },
+
+ // HTML5-only event: Fires when a browser has chosen to stop downloading.
+ // "The user agent is intentionally not currently fetching media data,
+ // but does not have the entire media resource downloaded."
+ 'onsuspend': function() {
+ loader.onTimeout(self);
+ },
+
+ // Fires at a regular interval when a sound is loading and new data
+ // has been received.
+ 'whileloading': function() {
+ var bytesLoaded = this['bytesLoaded'],
+ bytesTotal = this['bytesTotal'];
+
+ // TODO: provide percentage complete updates to loader?
+ // see if we have loaded the file
+ if (bytesLoaded > 0 && (bytesLoaded === bytesTotal)) {
+ loader.onLoad(self);
+ }
+ }
+ });
+
+ this.start = function(pxLoader) {
+ // we need the loader ref so we can notify upon completion
+ loader = pxLoader;
+
+ // On iOS, soundManager2 uses a global audio object so we can't
+ // preload multiple sounds. We'll have to hope they load quickly
+ // when we need to play them. Unfortunately, SM2 doesn't expose
+ // a property to indicate its using a global object. For now we'll
+ // use the same test they do: only when on an iDevice
+ var iDevice = navigator.userAgent.match(/(ipad|iphone|ipod)/i);
+ if (iDevice) {
+ loader.onTimeout(self);
+ } else {
+ this.sound['load']();
+ }
+ };
+
+ this.checkStatus = function() {
+ switch(self.sound['readyState']) {
+ case 0:
+ // uninitialised
+ break;
+ case 1:
+ // loading
+ break;
+ case 2:
+ // failed/error
+ loader.onError(self);
+ break;
+ case 3:
+ // loaded/success
+ loader.onLoad(self);
+ break;
+ }
+ };
+
+ this.onTimeout = function() {
+ loader.onTimeout(self);
+ };
+
+ this.getName = function() {
+ return url;
+ };
+};
+return PxLoaderSound;
+});
View
58 amd/pxloadertags.js
@@ -0,0 +1,58 @@
+// Tag object to handle tag intersection; once created not meant to be changed
+// Performance rationale: http://jsperf.com/lists-indexof-vs-in-operator/3
+define(
+ [],
+function(){
+ var PxLoaderTags = function(values) {
+
+ this.array = [];
+ this.object = {};
+ this.value = null; // single value
+ this.length = 0;
+
+ if (values !== null && values !== undefined) {
+ if (Array.isArray(values)) {
+ this.array = values;
+ } else if (typeof values === 'object') {
+ for (var key in values) {
+ this.array.push(key);
+ }
+ } else {
+ this.array.push(values);
+ this.value = values;
+ }
+
+ this.length = this.array.length;
+
+ // convert array values to object with truthy values, used by contains function below
+ for (var i = 0; i < this.length; i++) {
+ this.object[this.array[i]] = true;
+ }
+ }
+
+ // compare this object with another; return true if they share at least one value
+ this.contains = function(other) {
+ if (this.length === 0 || other.length === 0) {
+ return false;
+ } else if (this.length === 1 && this.value !== null) {
+ if (other.length === 1) {
+ return this.value === other.value;
+ } else {
+ return other.object.hasOwnProperty(this.value);
+ }
+ } else if (other.length < this.length) {
+ return other.contains(this); // better to loop through the smaller object
+ } else {
+ for (var key in this.object) {
+ if (other.object[key]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ };
+
+ return PxLoaderTags;
+
+});
View
106 amd/pxloadervideo.js
@@ -0,0 +1,106 @@
+// @depends PxLoader.js
+/**
+ * PxLoader plugin to load video elements
+ */
+define([],
+function(){
+var PxLoaderVideo = function(url, tags, priority) {
+ var self = this;
+ var loader = null;
+
+ try {
+ this.vid = new Video();
+ } catch(e) {
+ this.vid = document.createElement('video');
+ }
+
+ this.tags = tags;
+ this.priority = priority;
+
+ var onReadyStateChange = function() {
+ if (self.vid.readyState != 4) {
+ return;
+ }
+
+ removeEventHandlers();
+ loader.onLoad(self);
+ };
+
+ var onLoad = function() {
+ removeEventHandlers();
+ loader.onLoad(self);
+ };
+
+ var onError = function() {
+ removeEventHandlers();
+ loader.onError(self);
+ };
+
+ var removeEventHandlers = function() {
+ self.unbind('load', onLoad);
+ self.unbind('canplaythrough', onReadyStateChange);
+ self.unbind('error', onError);
+ };
+
+ this.start = function(pxLoader) {
+ // we need the loader ref so we can notify upon completion
+ loader = pxLoader;
+
+ // NOTE: Must add event listeners before the src is set. We
+ // also need to use the readystatechange because sometimes
+ // load doesn't fire when an video is in the cache.
+ self.bind('load', onLoad);
+ self.bind('canplaythrough', onReadyStateChange);
+ self.bind('error', onError);
+
+ self.vid.src = url;
+ };
+
+ // called by PxLoader to check status of video (fallback in case
+ // the event listeners are not triggered).
+ this.checkStatus = function() {
+ if (self.vid.readyState != 4) {
+ return;
+ }
+
+ removeEventHandlers();
+ loader.onLoad(self);
+ };
+
+ // called by PxLoader when it is no longer waiting
+ this.onTimeout = function() {
+ removeEventHandlers();
+ if (self.vid.readyState != 4) {
+ loader.onLoad(self);
+ } else {
+ loader.onTimeout(self);
+ }
+ };
+
+ // returns a name for the resource that can be used in logging
+ this.getName = function() {
+ return url;
+ };
+
+ // cross-browser event binding
+ this.bind = function(eventName, eventHandler) {
+ if (self.vid.addEventListener) {
+ self.vid.addEventListener(eventName, eventHandler, false);
+ } else if (self.vid.attachEvent) {
+ self.vid.attachEvent('on' + eventName, eventHandler);
+ }
+ };
+
+ // cross-browser event un-binding
+ this.unbind = function(eventName, eventHandler) {
+ if (self.vid.removeEventListener) {
+ self.vid.removeEventListener(eventName, eventHandler, false);
+ } else if (self.vid.detachEvent) {
+ self.vid.detachEvent('on' + eventName, eventHandler);
+ }
+ };
+};
+
+return PxLoaderVideo;
+
+});
Something went wrong with that request. Please try again.