resource-loader
Advanced tools
Comparing version 1.8.0 to 2.0.0
@@ -1,1 +0,1 @@ | ||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Loader=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module,exports){"use strict";var has=Object.prototype.hasOwnProperty,prefix="~";function Events(){}if(Object.create){Events.prototype=Object.create(null);if(!(new Events).__proto__)prefix=false}function EE(fn,context,once){this.fn=fn;this.context=context;this.once=once||false}function EventEmitter(){this._events=new Events;this._eventsCount=0}EventEmitter.prototype.eventNames=function eventNames(){var names=[],events,name;if(this._eventsCount===0)return names;for(name in events=this._events){if(has.call(events,name))names.push(prefix?name.slice(1):name)}if(Object.getOwnPropertySymbols){return names.concat(Object.getOwnPropertySymbols(events))}return names};EventEmitter.prototype.listeners=function listeners(event,exists){var evt=prefix?prefix+event:event,available=this._events[evt];if(exists)return!!available;if(!available)return[];if(available.fn)return[available.fn];for(var i=0,l=available.length,ee=new Array(l);i<l;i++){ee[i]=available[i].fn}return ee};EventEmitter.prototype.emit=function emit(event,a1,a2,a3,a4,a5){var evt=prefix?prefix+event:event;if(!this._events[evt])return false;var listeners=this._events[evt],len=arguments.length,args,i;if(listeners.fn){if(listeners.once)this.removeListener(event,listeners.fn,undefined,true);switch(len){case 1:return listeners.fn.call(listeners.context),true;case 2:return listeners.fn.call(listeners.context,a1),true;case 3:return listeners.fn.call(listeners.context,a1,a2),true;case 4:return listeners.fn.call(listeners.context,a1,a2,a3),true;case 5:return listeners.fn.call(listeners.context,a1,a2,a3,a4),true;case 6:return listeners.fn.call(listeners.context,a1,a2,a3,a4,a5),true}for(i=1,args=new Array(len-1);i<len;i++){args[i-1]=arguments[i]}listeners.fn.apply(listeners.context,args)}else{var length=listeners.length,j;for(i=0;i<length;i++){if(listeners[i].once)this.removeListener(event,listeners[i].fn,undefined,true);switch(len){case 1:listeners[i].fn.call(listeners[i].context);break;case 2:listeners[i].fn.call(listeners[i].context,a1);break;case 3:listeners[i].fn.call(listeners[i].context,a1,a2);break;case 4:listeners[i].fn.call(listeners[i].context,a1,a2,a3);break;default:if(!args)for(j=1,args=new Array(len-1);j<len;j++){args[j-1]=arguments[j]}listeners[i].fn.apply(listeners[i].context,args)}}}return true};EventEmitter.prototype.on=function on(event,fn,context){var listener=new EE(fn,context||this),evt=prefix?prefix+event:event;if(!this._events[evt])this._events[evt]=listener,this._eventsCount++;else if(!this._events[evt].fn)this._events[evt].push(listener);else this._events[evt]=[this._events[evt],listener];return this};EventEmitter.prototype.once=function once(event,fn,context){var listener=new EE(fn,context||this,true),evt=prefix?prefix+event:event;if(!this._events[evt])this._events[evt]=listener,this._eventsCount++;else if(!this._events[evt].fn)this._events[evt].push(listener);else this._events[evt]=[this._events[evt],listener];return this};EventEmitter.prototype.removeListener=function removeListener(event,fn,context,once){var evt=prefix?prefix+event:event;if(!this._events[evt])return this;if(!fn){if(--this._eventsCount===0)this._events=new Events;else delete this._events[evt];return this}var listeners=this._events[evt];if(listeners.fn){if(listeners.fn===fn&&(!once||listeners.once)&&(!context||listeners.context===context)){if(--this._eventsCount===0)this._events=new Events;else delete this._events[evt]}}else{for(var i=0,events=[],length=listeners.length;i<length;i++){if(listeners[i].fn!==fn||once&&!listeners[i].once||context&&listeners[i].context!==context){events.push(listeners[i])}}if(events.length)this._events[evt]=events.length===1?events[0]:events;else if(--this._eventsCount===0)this._events=new Events;else delete this._events[evt]}return this};EventEmitter.prototype.removeAllListeners=function removeAllListeners(event){var evt;if(event){evt=prefix?prefix+event:event;if(this._events[evt]){if(--this._eventsCount===0)this._events=new Events;else delete this._events[evt]}}else{this._events=new Events;this._eventsCount=0}return this};EventEmitter.prototype.off=EventEmitter.prototype.removeListener;EventEmitter.prototype.addListener=EventEmitter.prototype.on;EventEmitter.prototype.setMaxListeners=function setMaxListeners(){return this};EventEmitter.prefixed=prefix;EventEmitter.EventEmitter=EventEmitter;if("undefined"!==typeof module){module.exports=EventEmitter}},{}],2:[function(require,module,exports){"use strict";module.exports=function parseURI(str,opts){opts=opts||{};var o={key:["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],q:{name:"queryKey",parser:/(?:^|&)([^&=]*)=?([^&]*)/g},parser:{strict:/^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,loose:/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/}};var m=o.parser[opts.strictMode?"strict":"loose"].exec(str);var uri={};var i=14;while(i--)uri[o.key[i]]=m[i]||"";uri[o.q.name]={};uri[o.key[12]].replace(o.q.parser,function($0,$1,$2){if($1)uri[o.q.name][$1]=$2});return uri}},{}],3:[function(require,module,exports){"use strict";var parseUri=require("parse-uri");var async=require("./async");var Resource=require("./Resource");var EventEmitter=require("eventemitter3");var DEFAULT_CONCURRENCY=10;var MAX_PROGRESS=100;function Loader(baseUrl,concurrency){EventEmitter.call(this);concurrency=concurrency||DEFAULT_CONCURRENCY;this.baseUrl=baseUrl||"";this.progress=0;this.loading=false;this._progressChunk=0;this._beforeMiddleware=[];this._afterMiddleware=[];this._boundLoadResource=this._loadResource.bind(this);this._buffer=[];this._numToLoad=0;this._queue=async.queue(this._boundLoadResource,concurrency);this.resources={}}Loader.prototype=Object.create(EventEmitter.prototype);Loader.prototype.constructor=Loader;module.exports=Loader;Loader.prototype.add=Loader.prototype.enqueue=function(name,url,options,cb){if(Array.isArray(name)){for(var i=0;i<name.length;++i){this.add(name[i])}return this}if(typeof name==="object"){cb=url||name.callback||name.onComplete;options=name;url=name.url;name=name.name||name.key||name.url}if(typeof url!=="string"){cb=options;options=url;url=name}if(typeof url!=="string"){throw new Error("No url passed to add resource to loader.")}if(typeof options==="function"){cb=options;options=null}if(this.resources[name]){throw new Error('Resource with name "'+name+'" already exists.')}url=this._prepareUrl(url);this.resources[name]=new Resource(name,url,options);if(typeof cb==="function"){this.resources[name].once("afterMiddleware",cb)}this._numToLoad++;if(this._queue.started){this._queue.push(this.resources[name]);this._progressChunk=(MAX_PROGRESS-this.progress)/(this._queue.length()+this._queue.running())}else{this._buffer.push(this.resources[name]);this._progressChunk=MAX_PROGRESS/this._buffer.length}return this};Loader.prototype.before=Loader.prototype.pre=function(fn){this._beforeMiddleware.push(fn);return this};Loader.prototype.after=Loader.prototype.use=function(fn){this._afterMiddleware.push(fn);return this};Loader.prototype.reset=function(){this.progress=0;this.loading=false;this._progressChunk=0;this._buffer.length=0;this._numToLoad=0;this._queue.kill();this._queue.started=false;for(var k in this.resources){var res=this.resources[k];res.off("complete",this._onLoad,this);if(res.isLoading){res.abort()}}this.resources={};return this};Loader.prototype.load=function(cb){if(typeof cb==="function"){this.once("complete",cb)}if(this._queue.started){return this}this.emit("start",this);this.loading=true;for(var i=0;i<this._buffer.length;++i){this._queue.push(this._buffer[i])}this._buffer.length=0;return this};Loader.prototype._prepareUrl=function(url){var parsedUrl=parseUri(url,{strictMode:true});if(parsedUrl.protocol||!parsedUrl.path||parsedUrl.path.indexOf("//")===0){return url}if(this.baseUrl.length&&this.baseUrl.lastIndexOf("/")!==this.baseUrl.length-1&&url.charAt(0)!=="/"){return this.baseUrl+"/"+url}return this.baseUrl+url};Loader.prototype._loadResource=function(resource,dequeue){var self=this;resource._dequeue=dequeue;async.eachSeries(this._beforeMiddleware,function(fn,next){fn.call(self,resource,function(){next(resource.isComplete?{}:null)})},function(){if(resource.isComplete){self._onLoad(resource)}else{resource.once("complete",self._onLoad,self);resource.load()}})};Loader.prototype._onComplete=function(){this.loading=false;this.emit("complete",this,this.resources)};Loader.prototype._onLoad=function(resource){var self=this;async.eachSeries(this._afterMiddleware,function(fn,next){fn.call(self,resource,next)},function(){resource.emit("afterMiddleware",resource);self._numToLoad--;self.progress+=self._progressChunk;self.emit("progress",self,resource);if(resource.error){self.emit("error",resource.error,self,resource)}else{self.emit("load",self,resource)}if(self._numToLoad===0){self.progress=100;self._onComplete()}});resource._dequeue()};Loader.LOAD_TYPE=Resource.LOAD_TYPE;Loader.XHR_RESPONSE_TYPE=Resource.XHR_RESPONSE_TYPE},{"./Resource":4,"./async":5,eventemitter3:1,"parse-uri":2}],4:[function(require,module,exports){"use strict";var EventEmitter=require("eventemitter3");var parseUri=require("parse-uri");var useXdr=!!(window.XDomainRequest&&!("withCredentials"in new XMLHttpRequest));var tempAnchor=null;var STATUS_NONE=0;var STATUS_OK=200;var STATUS_EMPTY=204;function Resource(name,url,options){EventEmitter.call(this);options=options||{};if(typeof name!=="string"||typeof url!=="string"){throw new Error("Both name and url are required for constructing a resource.")}this.name=name;this.url=url;this.isDataUrl=this.url.indexOf("data:")===0;this.data=null;this.crossOrigin=options.crossOrigin===true?"anonymous":options.crossOrigin;this.loadType=options.loadType||this._determineLoadType();this.xhrType=options.xhrType;this.metadata=options.metadata||{};this.error=null;this.xhr=null;this.isJson=false;this.isXml=false;this.isImage=false;this.isAudio=false;this.isVideo=false;this.isComplete=false;this.isLoading=false;this._dequeue=null;this._boundComplete=this.complete.bind(this);this._boundOnError=this._onError.bind(this);this._boundOnProgress=this._onProgress.bind(this);this._boundXhrOnError=this._xhrOnError.bind(this);this._boundXhrOnAbort=this._xhrOnAbort.bind(this);this._boundXhrOnLoad=this._xhrOnLoad.bind(this);this._boundXdrOnTimeout=this._xdrOnTimeout.bind(this)}Resource.prototype=Object.create(EventEmitter.prototype);Resource.prototype.constructor=Resource;module.exports=Resource;Resource.prototype.complete=function(){if(this.data&&this.data.removeEventListener){this.data.removeEventListener("error",this._boundOnError,false);this.data.removeEventListener("load",this._boundComplete,false);this.data.removeEventListener("progress",this._boundOnProgress,false);this.data.removeEventListener("canplaythrough",this._boundComplete,false)}if(this.xhr){if(this.xhr.removeEventListener){this.xhr.removeEventListener("error",this._boundXhrOnError,false);this.xhr.removeEventListener("abort",this._boundXhrOnAbort,false);this.xhr.removeEventListener("progress",this._boundOnProgress,false);this.xhr.removeEventListener("load",this._boundXhrOnLoad,false)}else{this.xhr.onerror=null;this.xhr.ontimeout=null;this.xhr.onprogress=null;this.xhr.onload=null}}if(this.isComplete){throw new Error("Complete called again for an already completed resource.")}this.isComplete=true;this.isLoading=false;this.emit("complete",this)};Resource.prototype.abort=function(message){if(this.error){return}this.error=new Error(message);if(this.xhr){this.xhr.abort()}else if(this.xdr){this.xdr.abort()}else if(this.data){if(typeof this.data.src!=="undefined"){this.data.src=""}else{while(this.data.firstChild){this.data.removeChild(this.data.firstChild)}}}this.complete()};Resource.prototype.load=function(cb){if(this.isLoading){return}if(this.isComplete){if(cb){var self=this;setTimeout(function(){cb(self)},1)}return}else if(cb){this.once("complete",cb)}this.isLoading=true;this.emit("start",this);if(this.crossOrigin===false||typeof this.crossOrigin!=="string"){this.crossOrigin=this._determineCrossOrigin(this.url)}switch(this.loadType){case Resource.LOAD_TYPE.IMAGE:this._loadElement("image");break;case Resource.LOAD_TYPE.AUDIO:this._loadSourceElement("audio");break;case Resource.LOAD_TYPE.VIDEO:this._loadSourceElement("video");break;case Resource.LOAD_TYPE.XHR:default:if(useXdr&&this.crossOrigin){this._loadXdr()}else{this._loadXhr()}break}};Resource.prototype._loadElement=function(type){if(this.metadata.loadElement){this.data=this.metadata.loadElement}else if(type==="image"&&typeof window.Image!=="undefined"){this.data=new Image}else{this.data=document.createElement(type)}if(this.crossOrigin){this.data.crossOrigin=this.crossOrigin}if(!this.metadata.skipSource){this.data.src=this.url}var typeName="is"+type[0].toUpperCase()+type.substring(1);if(this[typeName]===false){this[typeName]=true}this.data.addEventListener("error",this._boundOnError,false);this.data.addEventListener("load",this._boundComplete,false);this.data.addEventListener("progress",this._boundOnProgress,false)};Resource.prototype._loadSourceElement=function(type){if(this.metadata.loadElement){this.data=this.metadata.loadElement}else if(type==="audio"&&typeof window.Audio!=="undefined"){this.data=new Audio}else{this.data=document.createElement(type)}if(this.data===null){this.abort("Unsupported element "+type);return}if(!this.metadata.skipSource){if(navigator.isCocoonJS){this.data.src=Array.isArray(this.url)?this.url[0]:this.url}else if(Array.isArray(this.url)){for(var i=0;i<this.url.length;++i){this.data.appendChild(this._createSource(type,this.url[i]))}}else{this.data.appendChild(this._createSource(type,this.url))}}this["is"+type[0].toUpperCase()+type.substring(1)]=true;this.data.addEventListener("error",this._boundOnError,false);this.data.addEventListener("load",this._boundComplete,false);this.data.addEventListener("progress",this._boundOnProgress,false);this.data.addEventListener("canplaythrough",this._boundComplete,false);this.data.load()};Resource.prototype._loadXhr=function(){if(typeof this.xhrType!=="string"){this.xhrType=this._determineXhrType()}var xhr=this.xhr=new XMLHttpRequest;xhr.open("GET",this.url,true);if(this.xhrType===Resource.XHR_RESPONSE_TYPE.JSON||this.xhrType===Resource.XHR_RESPONSE_TYPE.DOCUMENT){xhr.responseType=Resource.XHR_RESPONSE_TYPE.TEXT}else{xhr.responseType=this.xhrType}xhr.addEventListener("error",this._boundXhrOnError,false);xhr.addEventListener("abort",this._boundXhrOnAbort,false);xhr.addEventListener("progress",this._boundOnProgress,false);xhr.addEventListener("load",this._boundXhrOnLoad,false);xhr.send()};Resource.prototype._loadXdr=function(){if(typeof this.xhrType!=="string"){this.xhrType=this._determineXhrType()}var xdr=this.xhr=new XDomainRequest;xdr.timeout=5e3;xdr.onerror=this._boundXhrOnError;xdr.ontimeout=this._boundXdrOnTimeout;xdr.onprogress=this._boundOnProgress;xdr.onload=this._boundXhrOnLoad;xdr.open("GET",this.url,true);setTimeout(function(){xdr.send()},0)};Resource.prototype._createSource=function(type,url,mime){if(!mime){mime=type+"/"+url.substr(url.lastIndexOf(".")+1)}var source=document.createElement("source");source.src=url;source.type=mime;return source};Resource.prototype._onError=function(event){this.abort("Failed to load element using "+event.target.nodeName)};Resource.prototype._onProgress=function(event){if(event&&event.lengthComputable){this.emit("progress",this,event.loaded/event.total)}};Resource.prototype._xhrOnError=function(){var xhr=this.xhr;this.abort(reqType(xhr)+" Request failed. Status: "+xhr.status+', text: "'+xhr.statusText+'"')};Resource.prototype._xhrOnAbort=function(){this.abort(reqType(this.xhr)+" Request was aborted by the user.")};Resource.prototype._xdrOnTimeout=function(){this.abort(reqType(this.xhr)+" Request timed out.")};Resource.prototype._xhrOnLoad=function(){var xhr=this.xhr;var status=typeof xhr.status==="undefined"?xhr.status:STATUS_OK;if(status===STATUS_OK||status===STATUS_EMPTY||status===STATUS_NONE&&xhr.responseText.length>0){if(this.xhrType===Resource.XHR_RESPONSE_TYPE.TEXT){this.data=xhr.responseText}else if(this.xhrType===Resource.XHR_RESPONSE_TYPE.JSON){try{this.data=JSON.parse(xhr.responseText);this.isJson=true}catch(e){this.abort("Error trying to parse loaded json:",e);return}}else if(this.xhrType===Resource.XHR_RESPONSE_TYPE.DOCUMENT){try{if(window.DOMParser){var domparser=new DOMParser;this.data=domparser.parseFromString(xhr.responseText,"text/xml")}else{var div=document.createElement("div");div.innerHTML=xhr.responseText;this.data=div}this.isXml=true}catch(e){this.abort("Error trying to parse loaded xml:",e);return}}else{this.data=xhr.response||xhr.responseText}}else{this.abort("["+xhr.status+"]"+xhr.statusText+":"+xhr.responseURL);return}this.complete()};Resource.prototype._determineCrossOrigin=function(url,loc){if(url.indexOf("data:")===0){return""}loc=loc||window.location;if(!tempAnchor){tempAnchor=document.createElement("a")}tempAnchor.href=url;url=parseUri(tempAnchor.href,{strictMode:true});var samePort=!url.port&&loc.port===""||url.port===loc.port;var protocol=url.protocol?url.protocol+":":"";if(url.host!==loc.hostname||!samePort||protocol!==loc.protocol){return"anonymous"}return""};Resource.prototype._determineXhrType=function(){return Resource._xhrTypeMap[this._getExtension()]||Resource.XHR_RESPONSE_TYPE.TEXT};Resource.prototype._determineLoadType=function(){return Resource._loadTypeMap[this._getExtension()]||Resource.LOAD_TYPE.XHR};Resource.prototype._getExtension=function(){var url=this.url;var ext="";if(this.isDataUrl){var slashIndex=url.indexOf("/");ext=url.substring(slashIndex+1,url.indexOf(";",slashIndex))}else{var queryStart=url.indexOf("?");if(queryStart!==-1){url=url.substring(0,queryStart)}ext=url.substring(url.lastIndexOf(".")+1)}return ext.toLowerCase()};Resource.prototype._getMimeFromXhrType=function(type){switch(type){case Resource.XHR_RESPONSE_TYPE.BUFFER:return"application/octet-binary";case Resource.XHR_RESPONSE_TYPE.BLOB:return"application/blob";case Resource.XHR_RESPONSE_TYPE.DOCUMENT:return"application/xml";case Resource.XHR_RESPONSE_TYPE.JSON:return"application/json";case Resource.XHR_RESPONSE_TYPE.DEFAULT:case Resource.XHR_RESPONSE_TYPE.TEXT:default:return"text/plain"}};function reqType(xhr){return xhr.toString().replace("object ","")}Resource.LOAD_TYPE={XHR:1,IMAGE:2,AUDIO:3,VIDEO:4};Resource.XHR_RESPONSE_TYPE={DEFAULT:"text",BUFFER:"arraybuffer",BLOB:"blob",DOCUMENT:"document",JSON:"json",TEXT:"text"};Resource._loadTypeMap={gif:Resource.LOAD_TYPE.IMAGE,png:Resource.LOAD_TYPE.IMAGE,bmp:Resource.LOAD_TYPE.IMAGE,jpg:Resource.LOAD_TYPE.IMAGE,jpeg:Resource.LOAD_TYPE.IMAGE,tif:Resource.LOAD_TYPE.IMAGE,tiff:Resource.LOAD_TYPE.IMAGE,webp:Resource.LOAD_TYPE.IMAGE,tga:Resource.LOAD_TYPE.IMAGE,"svg+xml":Resource.LOAD_TYPE.IMAGE};Resource._xhrTypeMap={xhtml:Resource.XHR_RESPONSE_TYPE.DOCUMENT,html:Resource.XHR_RESPONSE_TYPE.DOCUMENT,htm:Resource.XHR_RESPONSE_TYPE.DOCUMENT,xml:Resource.XHR_RESPONSE_TYPE.DOCUMENT,tmx:Resource.XHR_RESPONSE_TYPE.DOCUMENT,tsx:Resource.XHR_RESPONSE_TYPE.DOCUMENT,svg:Resource.XHR_RESPONSE_TYPE.DOCUMENT,gif:Resource.XHR_RESPONSE_TYPE.BLOB,png:Resource.XHR_RESPONSE_TYPE.BLOB,bmp:Resource.XHR_RESPONSE_TYPE.BLOB,jpg:Resource.XHR_RESPONSE_TYPE.BLOB,jpeg:Resource.XHR_RESPONSE_TYPE.BLOB,tif:Resource.XHR_RESPONSE_TYPE.BLOB,tiff:Resource.XHR_RESPONSE_TYPE.BLOB,webp:Resource.XHR_RESPONSE_TYPE.BLOB,tga:Resource.XHR_RESPONSE_TYPE.BLOB,json:Resource.XHR_RESPONSE_TYPE.JSON,text:Resource.XHR_RESPONSE_TYPE.TEXT,txt:Resource.XHR_RESPONSE_TYPE.TEXT};Resource.setExtensionLoadType=function(extname,loadType){setExtMap(Resource._loadTypeMap,extname,loadType)};Resource.setExtensionXhrType=function(extname,xhrType){setExtMap(Resource._xhrTypeMap,extname,xhrType)};function setExtMap(map,extname,val){if(extname&&extname.indexOf(".")===0){extname=extname.substring(1)}if(!extname){return}map[extname]=val}},{eventemitter3:1,"parse-uri":2}],5:[function(require,module,exports){"use strict";module.exports={eachSeries:asyncEachSeries,queue:asyncQueue};function _noop(){}function asyncEachSeries(array,iterator,callback){var i=0;var len=array.length;(function next(err){if(err||i===len){if(callback){callback(err)}return}iterator(array[i++],next)})()}function onlyOnce(fn){return function onceWrapper(){if(fn===null){throw new Error("Callback was already called.")}var callFn=fn;fn=null;callFn.apply(this,arguments)}}function asyncQueue(worker,concurrency){if(concurrency==null){concurrency=1}else if(concurrency===0){throw new Error("Concurrency must not be zero")}var workers=0;var q={_tasks:[],concurrency:concurrency,saturated:_noop,unsaturated:_noop,buffer:concurrency/4,empty:_noop,drain:_noop,error:_noop,started:false,paused:false,push:function(data,callback){_insert(data,false,callback)},kill:function(){q.drain=_noop;q._tasks=[]},unshift:function(data,callback){_insert(data,true,callback)},process:function(){while(!q.paused&&workers<q.concurrency&&q._tasks.length){var task=q._tasks.shift();if(q._tasks.length===0){q.empty()}workers+=1;if(workers===q.concurrency){q.saturated()}worker(task.data,onlyOnce(_next(task)))}},length:function(){return q._tasks.length},running:function(){return workers},idle:function(){return q._tasks.length+workers===0},pause:function(){if(q.paused===true){return}q.paused=true},resume:function(){if(q.paused===false){return}q.paused=false;for(var w=1;w<=q.concurrency;w++){q.process()}}};function _insert(data,insertAtFront,callback){if(callback!=null&&typeof callback!=="function"){throw new Error("task callback must be a function")}q.started=true;if(data==null&&q.idle()){setTimeout(function(){q.drain()},1);return}var item={data:data,callback:typeof callback==="function"?callback:_noop};if(insertAtFront){q._tasks.unshift(item)}else{q._tasks.push(item)}setTimeout(function(){q.process()},1)}function _next(task){return function(){workers-=1;task.callback.apply(task,arguments);if(arguments[0]!=null){q.error(arguments[0],task.data)}if(workers<=q.concurrency-q.buffer){q.unsaturated()}if(q.idle()){q.drain()}q.process()}}return q}},{}],6:[function(require,module,exports){"use strict";module.exports={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encodeBinary:function(input){var output="";var bytebuffer;var encodedCharIndexes=new Array(4);var inx=0;var jnx=0;var paddingBytes=0;while(inx<input.length){bytebuffer=new Array(3);for(jnx=0;jnx<bytebuffer.length;jnx++){if(inx<input.length){bytebuffer[jnx]=input.charCodeAt(inx++)&255}else{bytebuffer[jnx]=0}}encodedCharIndexes[0]=bytebuffer[0]>>2;encodedCharIndexes[1]=(bytebuffer[0]&3)<<4|bytebuffer[1]>>4;encodedCharIndexes[2]=(bytebuffer[1]&15)<<2|bytebuffer[2]>>6;encodedCharIndexes[3]=bytebuffer[2]&63;paddingBytes=inx-(input.length-1);switch(paddingBytes){case 2:encodedCharIndexes[3]=64;encodedCharIndexes[2]=64;break;case 1:encodedCharIndexes[3]=64;break;default:break}for(jnx=0;jnx<encodedCharIndexes.length;jnx++){output+=this._keyStr.charAt(encodedCharIndexes[jnx])}}return output}}},{}],7:[function(require,module,exports){"use strict";module.exports=require("./Loader");module.exports.Resource=require("./Resource");module.exports.middleware={caching:{memory:require("./middlewares/caching/memory")},parsing:{blob:require("./middlewares/parsing/blob")}};module.exports.async=require("./async")},{"./Loader":3,"./Resource":4,"./async":5,"./middlewares/caching/memory":8,"./middlewares/parsing/blob":9}],8:[function(require,module,exports){"use strict";var cache={};module.exports=function(){return function(resource,next){if(cache[resource.url]){resource.data=cache[resource.url];resource.complete()}else{resource.once("complete",function(){cache[this.url]=this.data})}next()}}},{}],9:[function(require,module,exports){"use strict";var Resource=require("../../Resource");var b64=require("../../b64");var Url=window.URL||window.webkitURL;module.exports=function(){return function(resource,next){if(!resource.data){next();return}if(resource.xhr&&resource.xhrType===Resource.XHR_RESPONSE_TYPE.BLOB){if(!window.Blob||typeof resource.data==="string"){var type=resource.xhr.getResponseHeader("content-type");if(type&&type.indexOf("image")===0){resource.data=new Image;resource.data.src="data:"+type+";base64,"+b64.encodeBinary(resource.xhr.responseText);resource.isImage=true;resource.data.onload=function(){resource.data.onload=null;next()};return}}else if(resource.data.type.indexOf("image")===0){var src=Url.createObjectURL(resource.data);resource.blob=resource.data;resource.data=new Image;resource.data.src=src;resource.isImage=true;resource.data.onload=function(){Url.revokeObjectURL(src);resource.data.onload=null;next()};return}}next()}}},{"../../Resource":4,"../../b64":6}]},{},[7])(7)}); | ||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Loader=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var MiniSignalBinding=function(){function MiniSignalBinding(fn,once,thisArg){if(once===undefined)once=false;_classCallCheck(this,MiniSignalBinding);this._fn=fn;this._once=once;this._thisArg=thisArg;this._next=this._prev=this._owner=null}_createClass(MiniSignalBinding,[{key:"detach",value:function detach(){if(this._owner===null)return false;this._owner.detach(this);return true}}]);return MiniSignalBinding}();function _addMiniSignalBinding(self,node){if(!self._head){self._head=node;self._tail=node}else{self._tail._next=node;node._prev=self._tail;self._tail=node}node._owner=self;return node}var MiniSignal=function(){function MiniSignal(){_classCallCheck(this,MiniSignal);this._head=this._tail=undefined}_createClass(MiniSignal,[{key:"handlers",value:function handlers(){var exists=arguments.length<=0||arguments[0]===undefined?false:arguments[0];var node=this._head;if(exists)return!!node;var ee=[];while(node){ee.push(node);node=node._next}return ee}},{key:"has",value:function has(node){if(!(node instanceof MiniSignalBinding)){throw new Error("MiniSignal#has(): First arg must be a MiniSignalBinding object.")}return node._owner===this}},{key:"dispatch",value:function dispatch(){var node=this._head;if(!node)return false;while(node){if(node._once)this.detach(node);node._fn.apply(node._thisArg,arguments);node=node._next}return true}},{key:"add",value:function add(fn){var thisArg=arguments.length<=1||arguments[1]===undefined?null:arguments[1];if(typeof fn!=="function"){throw new Error("MiniSignal#add(): First arg must be a Function.")}return _addMiniSignalBinding(this,new MiniSignalBinding(fn,false,thisArg))}},{key:"once",value:function once(fn){var thisArg=arguments.length<=1||arguments[1]===undefined?null:arguments[1];if(typeof fn!=="function"){throw new Error("MiniSignal#once(): First arg must be a Function.")}return _addMiniSignalBinding(this,new MiniSignalBinding(fn,true,thisArg))}},{key:"detach",value:function detach(node){if(!(node instanceof MiniSignalBinding)){throw new Error("MiniSignal#detach(): First arg must be a MiniSignalBinding object.")}if(node._owner!==this)return this;if(node._prev)node._prev._next=node._next;if(node._next)node._next._prev=node._prev;if(node===this._head){this._head=node._next;if(node._next===null){this._tail=null}}else if(node===this._tail){this._tail=node._prev;this._tail._next=null}node._owner=null;return this}},{key:"detachAll",value:function detachAll(){var node=this._head;if(!node)return this;this._head=this._tail=null;while(node){node._owner=null;node=node._next}return this}}]);return MiniSignal}();MiniSignal.MiniSignalBinding=MiniSignalBinding;exports["default"]=MiniSignal;module.exports=exports["default"]},{}],2:[function(require,module,exports){"use strict";module.exports=function parseURI(str,opts){opts=opts||{};var o={key:["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],q:{name:"queryKey",parser:/(?:^|&)([^&=]*)=?([^&]*)/g},parser:{strict:/^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,loose:/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/}};var m=o.parser[opts.strictMode?"strict":"loose"].exec(str);var uri={};var i=14;while(i--)uri[o.key[i]]=m[i]||"";uri[o.q.name]={};uri[o.key[12]].replace(o.q.parser,function($0,$1,$2){if($1)uri[o.q.name][$1]=$2});return uri}},{}],3:[function(require,module,exports){"use strict";exports.__esModule=true;var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};var _miniSignals=require("mini-signals");var _miniSignals2=_interopRequireDefault(_miniSignals);var _parseUri=require("parse-uri");var _parseUri2=_interopRequireDefault(_parseUri);var _async=require("./async");var async=_interopRequireWildcard(_async);var _Resource=require("./Resource");var _Resource2=_interopRequireDefault(_Resource);function _interopRequireWildcard(obj){if(obj&&obj.__esModule){return obj}else{var newObj={};if(obj!=null){for(var key in obj){if(Object.prototype.hasOwnProperty.call(obj,key))newObj[key]=obj[key]}}newObj.default=obj;return newObj}}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var MAX_PROGRESS=100;var rgxExtractUrlHash=/(#[\w\-]+)?$/;var Loader=function(){function Loader(){var _this=this;var baseUrl=arguments.length>0&&arguments[0]!==undefined?arguments[0]:"";var concurrency=arguments.length>1&&arguments[1]!==undefined?arguments[1]:10;_classCallCheck(this,Loader);this.baseUrl=baseUrl;this.progress=0;this.loading=false;this.defaultQueryString="";this._beforeMiddleware=[];this._afterMiddleware=[];this._boundLoadResource=function(r,d){return _this._loadResource(r,d)};this._queue=async.queue(this._boundLoadResource,concurrency);this._queue.pause();this.resources={};this.onProgress=new _miniSignals2.default;this.onError=new _miniSignals2.default;this.onLoad=new _miniSignals2.default;this.onStart=new _miniSignals2.default;this.onComplete=new _miniSignals2.default}Loader.prototype.add=function add(name,url,options,cb){if(Array.isArray(name)){for(var i=0;i<name.length;++i){this.add(name[i])}return this}if((typeof name==="undefined"?"undefined":_typeof(name))==="object"){cb=url||name.callback||name.onComplete;options=name;url=name.url;name=name.name||name.key||name.url}if(typeof url!=="string"){cb=options;options=url;url=name}if(typeof url!=="string"){throw new Error("No url passed to add resource to loader.")}if(typeof options==="function"){cb=options;options=null}if(this.loading&&(!options||!options.parentResource)){throw new Error("Cannot add resources while the loader is running.")}if(this.resources[name]){throw new Error('Resource named "'+name+'" already exists.')}url=this._prepareUrl(url);this.resources[name]=new _Resource2.default(name,url,options);if(typeof cb==="function"){this.resources[name].onAfterMiddleware.once(cb)}if(this.loading){var parent=options.parentResource;var fullChunk=parent.progressChunk*(parent.children.length+1);var eachChunk=fullChunk/(parent.children.length+2);parent.children.push(this.resources[name]);parent.progressChunk=eachChunk;for(var _i=0;_i<parent.children.length;++_i){parent.children[_i].progressChunk=eachChunk}}this._queue.push(this.resources[name]);return this};Loader.prototype.pre=function pre(fn){this._beforeMiddleware.push(fn);return this};Loader.prototype.use=function use(fn){this._afterMiddleware.push(fn);return this};Loader.prototype.reset=function reset(){this.progress=0;this.loading=false;this._queue.kill();this._queue.pause();for(var k in this.resources){var res=this.resources[k];if(res._onLoadBinding){res._onLoadBinding.detach()}if(res.isLoading){res.abort()}}this.resources={};return this};Loader.prototype.load=function load(cb){if(typeof cb==="function"){this.onComplete.once(cb)}if(this.loading){return this}var chunk=100/this._queue._tasks.length;for(var i=0;i<this._queue._tasks.length;++i){this._queue._tasks[i].data.progressChunk=chunk}this.loading=true;this.onStart.dispatch(this);this._queue.resume();return this};Loader.prototype._prepareUrl=function _prepareUrl(url){var parsedUrl=(0,_parseUri2.default)(url,{strictMode:true});var result=void 0;if(parsedUrl.protocol||!parsedUrl.path||url.indexOf("//")===0){result=url}else if(this.baseUrl.length&&this.baseUrl.lastIndexOf("/")!==this.baseUrl.length-1&&url.charAt(0)!=="/"){result=this.baseUrl+"/"+url}else{result=this.baseUrl+url}if(this.defaultQueryString){var hash=rgxExtractUrlHash.exec(result)[0];result=result.substr(0,result.length-hash.length);if(result.indexOf("?")!==-1){result+="&"+this.defaultQueryString}else{result+="?"+this.defaultQueryString}result+=hash}return result};Loader.prototype._loadResource=function _loadResource(resource,dequeue){var _this2=this;resource._dequeue=dequeue;async.eachSeries(this._beforeMiddleware,function(fn,next){fn.call(_this2,resource,function(){next(resource.isComplete?{}:null)})},function(){if(resource.isComplete){_this2._onLoad(resource)}else{resource._onLoadBinding=resource.onComplete.once(_this2._onLoad,_this2);resource.load()}})};Loader.prototype._onComplete=function _onComplete(){this.loading=false;this.onComplete.dispatch(this,this.resources)};Loader.prototype._onLoad=function _onLoad(resource){var _this3=this;resource._onLoadBinding=null;async.eachSeries(this._afterMiddleware,function(fn,next){fn.call(_this3,resource,next)},function(){resource.onAfterMiddleware.dispatch(resource);_this3.progress+=resource.progressChunk;_this3.onProgress.dispatch(_this3,resource);if(resource.error){_this3.onError.dispatch(resource.error,_this3,resource)}else{_this3.onLoad.dispatch(_this3,resource)}resource._dequeue();if(_this3._queue.idle()){_this3.progress=MAX_PROGRESS;_this3._onComplete()}})};return Loader}();exports.default=Loader},{"./Resource":4,"./async":5,"mini-signals":1,"parse-uri":2}],4:[function(require,module,exports){"use strict";exports.__esModule=true;var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _parseUri=require("parse-uri");var _parseUri2=_interopRequireDefault(_parseUri);var _miniSignals=require("mini-signals");var _miniSignals2=_interopRequireDefault(_miniSignals);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var useXdr=!!(window.XDomainRequest&&!("withCredentials"in new XMLHttpRequest));var tempAnchor=null;var STATUS_NONE=0;var STATUS_OK=200;var STATUS_EMPTY=204;function _noop(){}var Resource=function(){Resource.setExtensionLoadType=function setExtensionLoadType(extname,loadType){setExtMap(Resource._loadTypeMap,extname,loadType)};Resource.setExtensionXhrType=function setExtensionXhrType(extname,xhrType){setExtMap(Resource._xhrTypeMap,extname,xhrType)};function Resource(name,url,options){_classCallCheck(this,Resource);if(typeof name!=="string"||typeof url!=="string"){throw new Error("Both name and url are required for constructing a resource.")}options=options||{};this._flags=0;this._setFlag(Resource.STATUS_FLAGS.DATA_URL,url.indexOf("data:")===0);this.name=name;this.url=url;this.data=null;this.crossOrigin=options.crossOrigin===true?"anonymous":options.crossOrigin;this.loadType=options.loadType||this._determineLoadType();this.xhrType=options.xhrType;this.metadata=options.metadata||{};this.error=null;this.xhr=null;this.children=[];this.type=Resource.TYPE.UNKNOWN;this.progressChunk=0;this._dequeue=_noop;this._onLoadBinding=null;this._boundComplete=this.complete.bind(this);this._boundOnError=this._onError.bind(this);this._boundOnProgress=this._onProgress.bind(this);this._boundXhrOnError=this._xhrOnError.bind(this);this._boundXhrOnAbort=this._xhrOnAbort.bind(this);this._boundXhrOnLoad=this._xhrOnLoad.bind(this);this._boundXdrOnTimeout=this._xdrOnTimeout.bind(this);this.onStart=new _miniSignals2.default;this.onProgress=new _miniSignals2.default;this.onComplete=new _miniSignals2.default;this.onAfterMiddleware=new _miniSignals2.default}Resource.prototype.complete=function complete(){if(this.data&&this.data.removeEventListener){this.data.removeEventListener("error",this._boundOnError,false);this.data.removeEventListener("load",this._boundComplete,false);this.data.removeEventListener("progress",this._boundOnProgress,false);this.data.removeEventListener("canplaythrough",this._boundComplete,false)}if(this.xhr){if(this.xhr.removeEventListener){this.xhr.removeEventListener("error",this._boundXhrOnError,false);this.xhr.removeEventListener("abort",this._boundXhrOnAbort,false);this.xhr.removeEventListener("progress",this._boundOnProgress,false);this.xhr.removeEventListener("load",this._boundXhrOnLoad,false)}else{this.xhr.onerror=null;this.xhr.ontimeout=null;this.xhr.onprogress=null;this.xhr.onload=null}}if(this.isComplete){throw new Error("Complete called again for an already completed resource.")}this._setFlag(Resource.STATUS_FLAGS.COMPLETE,true);this._setFlag(Resource.STATUS_FLAGS.LOADING,false);this.onComplete.dispatch(this)};Resource.prototype.abort=function abort(message){if(this.error){return}this.error=new Error(message);if(this.xhr){this.xhr.abort()}else if(this.xdr){this.xdr.abort()}else if(this.data){if(this.data.src){this.data.src=Resource.EMPTY_GIF}else{while(this.data.firstChild){this.data.removeChild(this.data.firstChild)}}}this.complete()};Resource.prototype.load=function load(cb){var _this=this;if(this.isLoading){return}if(this.isComplete){if(cb){setTimeout(function(){return cb(_this)},1)}return}else if(cb){this.onComplete.once(cb)}this._setFlag(Resource.STATUS_FLAGS.LOADING,true);this.onStart.dispatch(this);if(this.crossOrigin===false||typeof this.crossOrigin!=="string"){this.crossOrigin=this._determineCrossOrigin(this.url)}switch(this.loadType){case Resource.LOAD_TYPE.IMAGE:this.type=Resource.TYPE.IMAGE;this._loadElement("image");break;case Resource.LOAD_TYPE.AUDIO:this.type=Resource.TYPE.AUDIO;this._loadSourceElement("audio");break;case Resource.LOAD_TYPE.VIDEO:this.type=Resource.TYPE.VIDEO;this._loadSourceElement("video");break;case Resource.LOAD_TYPE.XHR:default:if(useXdr&&this.crossOrigin){this._loadXdr()}else{this._loadXhr()}break}};Resource.prototype._hasFlag=function _hasFlag(flag){return!!(this._flags&flag)};Resource.prototype._setFlag=function _setFlag(flag,value){this._flags=value?this._flags|flag:this._flags&~flag};Resource.prototype._loadElement=function _loadElement(type){if(this.metadata.loadElement){this.data=this.metadata.loadElement}else if(type==="image"&&typeof window.Image!=="undefined"){this.data=new Image}else{this.data=document.createElement(type)}if(this.crossOrigin){this.data.crossOrigin=this.crossOrigin}if(!this.metadata.skipSource){this.data.src=this.url}this.data.addEventListener("error",this._boundOnError,false);this.data.addEventListener("load",this._boundComplete,false);this.data.addEventListener("progress",this._boundOnProgress,false)};Resource.prototype._loadSourceElement=function _loadSourceElement(type){if(this.metadata.loadElement){this.data=this.metadata.loadElement}else if(type==="audio"&&typeof window.Audio!=="undefined"){this.data=new Audio}else{this.data=document.createElement(type)}if(this.data===null){this.abort("Unsupported element: "+type);return}if(!this.metadata.skipSource){if(navigator.isCocoonJS){this.data.src=Array.isArray(this.url)?this.url[0]:this.url}else if(Array.isArray(this.url)){for(var i=0;i<this.url.length;++i){this.data.appendChild(this._createSource(type,this.url[i]))}}else{this.data.appendChild(this._createSource(type,this.url))}}this.data.addEventListener("error",this._boundOnError,false);this.data.addEventListener("load",this._boundComplete,false);this.data.addEventListener("progress",this._boundOnProgress,false);this.data.addEventListener("canplaythrough",this._boundComplete,false);this.data.load()};Resource.prototype._loadXhr=function _loadXhr(){if(typeof this.xhrType!=="string"){this.xhrType=this._determineXhrType()}var xhr=this.xhr=new XMLHttpRequest;xhr.open("GET",this.url,true);if(this.xhrType===Resource.XHR_RESPONSE_TYPE.JSON||this.xhrType===Resource.XHR_RESPONSE_TYPE.DOCUMENT){xhr.responseType=Resource.XHR_RESPONSE_TYPE.TEXT}else{xhr.responseType=this.xhrType}xhr.addEventListener("error",this._boundXhrOnError,false);xhr.addEventListener("abort",this._boundXhrOnAbort,false);xhr.addEventListener("progress",this._boundOnProgress,false);xhr.addEventListener("load",this._boundXhrOnLoad,false);xhr.send()};Resource.prototype._loadXdr=function _loadXdr(){if(typeof this.xhrType!=="string"){this.xhrType=this._determineXhrType()}var xdr=this.xhr=new XDomainRequest;xdr.timeout=5e3;xdr.onerror=this._boundXhrOnError;xdr.ontimeout=this._boundXdrOnTimeout;xdr.onprogress=this._boundOnProgress;xdr.onload=this._boundXhrOnLoad;xdr.open("GET",this.url,true);setTimeout(function(){return xdr.send()},1)};Resource.prototype._createSource=function _createSource(type,url,mime){if(!mime){mime=type+"/"+url.substr(url.lastIndexOf(".")+1)}var source=document.createElement("source");source.src=url;source.type=mime;return source};Resource.prototype._onError=function _onError(event){this.abort("Failed to load element using: "+event.target.nodeName)};Resource.prototype._onProgress=function _onProgress(event){if(event&&event.lengthComputable){this.onProgress.dispatch(this,event.loaded/event.total)}};Resource.prototype._xhrOnError=function _xhrOnError(){var xhr=this.xhr;this.abort(reqType(xhr)+" Request failed. Status: "+xhr.status+', text: "'+xhr.statusText+'"')};Resource.prototype._xhrOnAbort=function _xhrOnAbort(){this.abort(reqType(this.xhr)+" Request was aborted by the user.")};Resource.prototype._xdrOnTimeout=function _xdrOnTimeout(){this.abort(reqType(this.xhr)+" Request timed out.")};Resource.prototype._xhrOnLoad=function _xhrOnLoad(){var xhr=this.xhr;var status=typeof xhr.status==="undefined"?xhr.status:STATUS_OK;if(status===STATUS_OK||status===STATUS_EMPTY||status===STATUS_NONE&&xhr.responseText.length>0){if(this.xhrType===Resource.XHR_RESPONSE_TYPE.TEXT){this.data=xhr.responseText;this.type=Resource.TYPE.TEXT}else if(this.xhrType===Resource.XHR_RESPONSE_TYPE.JSON){try{this.data=JSON.parse(xhr.responseText);this.type=Resource.TYPE.JSON}catch(e){this.abort("Error trying to parse loaded json: "+e);return}}else if(this.xhrType===Resource.XHR_RESPONSE_TYPE.DOCUMENT){try{if(window.DOMParser){var domparser=new DOMParser;this.data=domparser.parseFromString(xhr.responseText,"text/xml")}else{var div=document.createElement("div");div.innerHTML=xhr.responseText;this.data=div}this.type=Resource.TYPE.XML}catch(e){this.abort("Error trying to parse loaded xml: "+e);return}}else{this.data=xhr.response||xhr.responseText}}else{this.abort("["+xhr.status+"] "+xhr.statusText+": "+xhr.responseURL);return}this.complete()};Resource.prototype._determineCrossOrigin=function _determineCrossOrigin(url,loc){if(url.indexOf("data:")===0){return""}loc=loc||window.location;if(!tempAnchor){tempAnchor=document.createElement("a")}tempAnchor.href=url;url=(0,_parseUri2.default)(tempAnchor.href,{strictMode:true});var samePort=!url.port&&loc.port===""||url.port===loc.port;var protocol=url.protocol?url.protocol+":":"";if(url.host!==loc.hostname||!samePort||protocol!==loc.protocol){return"anonymous"}return""};Resource.prototype._determineXhrType=function _determineXhrType(){return Resource._xhrTypeMap[this._getExtension()]||Resource.XHR_RESPONSE_TYPE.TEXT};Resource.prototype._determineLoadType=function _determineLoadType(){return Resource._loadTypeMap[this._getExtension()]||Resource.LOAD_TYPE.XHR};Resource.prototype._getExtension=function _getExtension(){var url=this.url;var ext="";if(this.isDataUrl){var slashIndex=url.indexOf("/");ext=url.substring(slashIndex+1,url.indexOf(";",slashIndex))}else{var queryStart=url.indexOf("?");if(queryStart!==-1){url=url.substring(0,queryStart)}ext=url.substring(url.lastIndexOf(".")+1)}return ext.toLowerCase()};Resource.prototype._getMimeFromXhrType=function _getMimeFromXhrType(type){switch(type){case Resource.XHR_RESPONSE_TYPE.BUFFER:return"application/octet-binary";case Resource.XHR_RESPONSE_TYPE.BLOB:return"application/blob";case Resource.XHR_RESPONSE_TYPE.DOCUMENT:return"application/xml";case Resource.XHR_RESPONSE_TYPE.JSON:return"application/json";case Resource.XHR_RESPONSE_TYPE.DEFAULT:case Resource.XHR_RESPONSE_TYPE.TEXT:default:return"text/plain"}};_createClass(Resource,[{key:"isDataUrl",get:function get(){return this._hasFlag(Resource.STATUS_FLAGS.DATA_URL)}},{key:"isComplete",get:function get(){return this._hasFlag(Resource.STATUS_FLAGS.COMPLETE)}},{key:"isLoading",get:function get(){return this._hasFlag(Resource.STATUS_FLAGS.LOADING)}}]);return Resource}();exports.default=Resource;Resource.STATUS_FLAGS={NONE:0,DATA_URL:1<<0,COMPLETE:1<<1,LOADING:1<<2};Resource.TYPE={UNKNOWN:0,JSON:1,XML:2,IMAGE:3,AUDIO:4,VIDEO:5,TEXT:6};Resource.LOAD_TYPE={XHR:1,IMAGE:2,AUDIO:3,VIDEO:4};Resource.XHR_RESPONSE_TYPE={DEFAULT:"text",BUFFER:"arraybuffer",BLOB:"blob",DOCUMENT:"document",JSON:"json",TEXT:"text"};Resource._loadTypeMap={gif:Resource.LOAD_TYPE.IMAGE,png:Resource.LOAD_TYPE.IMAGE,bmp:Resource.LOAD_TYPE.IMAGE,jpg:Resource.LOAD_TYPE.IMAGE,jpeg:Resource.LOAD_TYPE.IMAGE,tif:Resource.LOAD_TYPE.IMAGE,tiff:Resource.LOAD_TYPE.IMAGE,webp:Resource.LOAD_TYPE.IMAGE,tga:Resource.LOAD_TYPE.IMAGE,svg:Resource.LOAD_TYPE.IMAGE,"svg+xml":Resource.LOAD_TYPE.IMAGE,mp3:Resource.LOAD_TYPE.AUDIO,ogg:Resource.LOAD_TYPE.AUDIO,wav:Resource.LOAD_TYPE.AUDIO,mp4:Resource.LOAD_TYPE.VIDEO,webm:Resource.LOAD_TYPE.VIDEO};Resource._xhrTypeMap={xhtml:Resource.XHR_RESPONSE_TYPE.DOCUMENT,html:Resource.XHR_RESPONSE_TYPE.DOCUMENT,htm:Resource.XHR_RESPONSE_TYPE.DOCUMENT,xml:Resource.XHR_RESPONSE_TYPE.DOCUMENT,tmx:Resource.XHR_RESPONSE_TYPE.DOCUMENT,svg:Resource.XHR_RESPONSE_TYPE.DOCUMENT,tsx:Resource.XHR_RESPONSE_TYPE.DOCUMENT,gif:Resource.XHR_RESPONSE_TYPE.BLOB,png:Resource.XHR_RESPONSE_TYPE.BLOB,bmp:Resource.XHR_RESPONSE_TYPE.BLOB,jpg:Resource.XHR_RESPONSE_TYPE.BLOB,jpeg:Resource.XHR_RESPONSE_TYPE.BLOB,tif:Resource.XHR_RESPONSE_TYPE.BLOB,tiff:Resource.XHR_RESPONSE_TYPE.BLOB,webp:Resource.XHR_RESPONSE_TYPE.BLOB,tga:Resource.XHR_RESPONSE_TYPE.BLOB,json:Resource.XHR_RESPONSE_TYPE.JSON,text:Resource.XHR_RESPONSE_TYPE.TEXT,txt:Resource.XHR_RESPONSE_TYPE.TEXT,ttf:Resource.XHR_RESPONSE_TYPE.BUFFER,otf:Resource.XHR_RESPONSE_TYPE.BUFFER};Resource.EMPTY_GIF="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";function setExtMap(map,extname,val){if(extname&&extname.indexOf(".")===0){extname=extname.substring(1)}if(!extname){return}map[extname]=val}function reqType(xhr){return xhr.toString().replace("object ","")}},{"mini-signals":1,"parse-uri":2}],5:[function(require,module,exports){"use strict";exports.__esModule=true;exports.eachSeries=eachSeries;exports.queue=queue;function _noop(){}function eachSeries(array,iterator,callback){var i=0;var len=array.length;(function next(err){if(err||i===len){if(callback){callback(err)}return}iterator(array[i++],next)})()}function onlyOnce(fn){return function onceWrapper(){if(fn===null){throw new Error("Callback was already called.")}var callFn=fn;fn=null;callFn.apply(this,arguments)}}function queue(worker,concurrency){if(concurrency==null){concurrency=1}else if(concurrency===0){throw new Error("Concurrency must not be zero")}var workers=0;var q={_tasks:[],concurrency:concurrency,saturated:_noop,unsaturated:_noop,buffer:concurrency/4,empty:_noop,drain:_noop,error:_noop,started:false,paused:false,push:function push(data,callback){_insert(data,false,callback)},kill:function kill(){workers=0;q.drain=_noop;q.started=false;q._tasks=[]},unshift:function unshift(data,callback){_insert(data,true,callback)},process:function process(){while(!q.paused&&workers<q.concurrency&&q._tasks.length){var task=q._tasks.shift();if(q._tasks.length===0){q.empty()}workers+=1;if(workers===q.concurrency){q.saturated()}worker(task.data,onlyOnce(_next(task)))}},length:function length(){return q._tasks.length},running:function running(){return workers},idle:function idle(){return q._tasks.length+workers===0},pause:function pause(){if(q.paused===true){return}q.paused=true},resume:function resume(){if(q.paused===false){return}q.paused=false;for(var w=1;w<=q.concurrency;w++){q.process()}}};function _insert(data,insertAtFront,callback){if(callback!=null&&typeof callback!=="function"){throw new Error("task callback must be a function")}q.started=true;if(data==null&&q.idle()){setTimeout(function(){return q.drain()},1);return}var item={data:data,callback:typeof callback==="function"?callback:_noop};if(insertAtFront){q._tasks.unshift(item)}else{q._tasks.push(item)}setTimeout(function(){return q.process()},1)}function _next(task){return function next(){workers-=1;task.callback.apply(task,arguments);if(arguments[0]!=null){q.error(arguments[0],task.data)}if(workers<=q.concurrency-q.buffer){q.unsaturated()}if(q.idle()){q.drain()}q.process()}}return q}},{}],6:[function(require,module,exports){"use strict";exports.__esModule=true;exports.encodeBinary=encodeBinary;var _keyStr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";function encodeBinary(input){var output="";var inx=0;while(inx<input.length){var bytebuffer=[0,0,0];var encodedCharIndexes=[0,0,0,0];for(var jnx=0;jnx<bytebuffer.length;++jnx){if(inx<input.length){bytebuffer[jnx]=input.charCodeAt(inx++)&255}else{bytebuffer[jnx]=0}}encodedCharIndexes[0]=bytebuffer[0]>>2;encodedCharIndexes[1]=(bytebuffer[0]&3)<<4|bytebuffer[1]>>4;encodedCharIndexes[2]=(bytebuffer[1]&15)<<2|bytebuffer[2]>>6;encodedCharIndexes[3]=bytebuffer[2]&63;var paddingBytes=inx-(input.length-1);switch(paddingBytes){case 2:encodedCharIndexes[3]=64;encodedCharIndexes[2]=64;break;case 1:encodedCharIndexes[3]=64;break;default:break}for(var _jnx=0;_jnx<encodedCharIndexes.length;++_jnx){output+=_keyStr.charAt(encodedCharIndexes[_jnx])}}return output}},{}],7:[function(require,module,exports){"use strict";var _Loader=require("./Loader");var _Loader2=_interopRequireDefault(_Loader);var _Resource=require("./Resource");var _Resource2=_interopRequireDefault(_Resource);var _async=require("./async");var async=_interopRequireWildcard(_async);var _b=require("./b64");var b64=_interopRequireWildcard(_b);function _interopRequireWildcard(obj){if(obj&&obj.__esModule){return obj}else{var newObj={};if(obj!=null){for(var key in obj){if(Object.prototype.hasOwnProperty.call(obj,key))newObj[key]=obj[key]}}newObj.default=obj;return newObj}}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}_Loader2.default.Resource=_Resource2.default;_Loader2.default.async=async;_Loader2.default.base64=b64;module.exports=_Loader2.default},{"./Loader":3,"./Resource":4,"./async":5,"./b64":6}]},{},[7])(7)}); |
{ | ||
"name": "resource-loader", | ||
"version": "1.8.0", | ||
"main": "./src/index.js", | ||
"version": "2.0.0", | ||
"main": "./lib/index.js", | ||
"description": "A generic asset loader, made with web games in mind.", | ||
"author": "Chad Engler <chad@pantherdev.com>", | ||
"license": "MIT", | ||
"homepage": "https://github.com/englercj/asset-loader", | ||
"homepage": "https://github.com/englercj/resource-loader", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/englercj/asset-loader.git" | ||
"url": "https://github.com/englercj/resource-loader.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/englercj/asset-loader/issues" | ||
"url": "https://github.com/englercj/resource-loader/issues" | ||
}, | ||
@@ -19,17 +19,19 @@ "keywords": [], | ||
"dist", | ||
"lib", | ||
"src" | ||
], | ||
"scripts": { | ||
"test": "npm run build && testem ci", | ||
"start": "npm run build", | ||
"travis": "npm run lint && npm test", | ||
"clean": "rm -rf ./dist && mkdir dist", | ||
"build": "browserify -d -t babelify -s Loader -e ./src/index.js -o ./dist/resource-loader.js", | ||
"minify": "uglifyjs --output ./dist/resource-loader.min.js -- ./dist/resource-loader.js", | ||
"build": "npm run clean && browserify -d -s Loader -e ./src/index.js -o ./dist/resource-loader.js && npm run minify", | ||
"dev": "npm run clean && watchify -d -s Loader -e ./src/index.js -o ./dist/resource-loader.js", | ||
"lint": "eslint src/ && eslint test/", | ||
"docs": "jsdoc -c ./gulp/util/jsdoc.conf.json" | ||
"dev": "watchify -d -t babelify -s Loader -e ./src/index.js -o ./dist/resource-loader.js", | ||
"lint": "eslint src/ test/", | ||
"start": "npm run clean && npm run build", | ||
"test": "npm run test-dev -- --single-run", | ||
"test-dev": "karma start test/karma.conf.js", | ||
"docs": "jsdoc -c jsdoc.conf.json -R README.md", | ||
"prepublish": "npm run build && npm run minify" | ||
}, | ||
"dependencies": { | ||
"eventemitter3": "^2.0.0", | ||
"mini-signals": "^1.1.1", | ||
"parse-uri": "^1.0.0" | ||
@@ -39,13 +41,26 @@ }, | ||
"@englercj/code-style": "^1.0.6", | ||
"babel-preset-es2015": "^6.16.0", | ||
"babelify": "^7.3.0", | ||
"browserify": "^13.1.0", | ||
"chai": "^3.5.0", | ||
"eslint": "^3.1.1", | ||
"ink-docstrap": "^1.2.1", | ||
"jsdoc": "^3.4.0", | ||
"sinon": "^1.17.5", | ||
"eslint": "^3.7.1", | ||
"ink-docstrap": "^1.3.0", | ||
"jsdoc": "^3.4.2", | ||
"karma": "^1.3.0", | ||
"karma-chrome-launcher": "^2.0.0", | ||
"karma-firefox-launcher": "^1.0.0", | ||
"karma-mocha": "^1.2.0", | ||
"karma-mocha-reporter": "^2.2.0", | ||
"karma-sinon-chai": "^1.2.4", | ||
"mocha": "^3.1.2", | ||
"sinon": "^1.17.6", | ||
"sinon-chai": "^2.8.0", | ||
"testem": "^1.10.2", | ||
"uglify-js": "^2.7.0", | ||
"uglify-js": "^2.7.3", | ||
"watchify": "^3.7.0" | ||
}, | ||
"babel": { | ||
"presets": [ | ||
["es2015", { "loose": true }] | ||
] | ||
} | ||
} |
@@ -9,20 +9,19 @@ # Resource Loader [![Build Status](https://travis-ci.org/englercj/resource-loader.svg?branch=master)](https://travis-ci.org/englercj/resource-loader) | ||
// ctor | ||
var loader = new Loader(); | ||
const loader = new Loader(); | ||
loader | ||
// chainable `add` to enqueue a resource | ||
// Chainable `add` to enqueue a resource | ||
.add(name, url, options) | ||
// chainable `before` to add a middleware that runs for each resource, *before* loading a resource. | ||
// this is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). | ||
.before(cachingMiddleware); | ||
// Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource. | ||
// This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc). | ||
.pre(cachingMiddleware) | ||
// chainable `after` to add a middleware that runs for each resource, *after* loading a resource. | ||
// this is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). | ||
.after(parsingMiddleware); | ||
// Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. | ||
// This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc). | ||
.use(parsingMiddleware) | ||
// `load` method loads the queue of resources, and calls the passed in callback called once all | ||
// The `load` method loads the queue of resources, and calls the passed in callback called once all | ||
// resources have loaded. | ||
.load(function (loader, resources) { | ||
.load((loader, resources) => { | ||
// resources is an object where the key is the name of the resource loaded and the value is the resource object. | ||
@@ -36,7 +35,7 @@ // They have a couple default properties: | ||
// throughout the process multiple events can happen. | ||
loader.on('progress', ...); // called once per loaded/errored file | ||
loader.on('error', ...); // called once per errored file | ||
loader.on('load', ...); // called once per loaded file | ||
loader.on('complete', ...); // called once when the queued resources all load. | ||
// throughout the process multiple signals can be dispatched. | ||
loader.onProgress.add(() => {}); // called once per loaded/errored file | ||
loader.onError.add(() => {}); // called once per errored file | ||
loader.onLoad.add(() => {}); // called once per loaded file | ||
loader.onComplete.add(() => {}); // called once when the queued resources all load. | ||
``` | ||
@@ -66,1 +65,8 @@ | ||
- Opera 12.1+ | ||
## Upgrading to v2 | ||
- No more events, all signals now | ||
- No more isJson, isXml, etc. Now use `res.type === Resource.TYPE.JSON`, etc. | ||
- Removed `before` (in favor of `pre`) and `after` (in favor of `use`). | ||
- If a middleware adds more resources, it *must* pass in the parent resource in options for `.add()`. |
@@ -1,3 +0,1 @@ | ||
'use strict'; | ||
/** | ||
@@ -7,8 +5,2 @@ * Smaller version of the async library constructs. | ||
*/ | ||
module.exports = { | ||
eachSeries: asyncEachSeries, | ||
queue: asyncQueue | ||
}; | ||
function _noop() { /* empty */ } | ||
@@ -23,5 +15,5 @@ | ||
*/ | ||
function asyncEachSeries(array, iterator, callback) { | ||
var i = 0; | ||
var len = array.length; | ||
export function eachSeries(array, iterator, callback) { | ||
let i = 0; | ||
const len = array.length; | ||
@@ -53,3 +45,3 @@ (function next(err) { | ||
var callFn = fn; | ||
const callFn = fn; | ||
@@ -68,3 +60,3 @@ fn = null; | ||
*/ | ||
function asyncQueue(worker, concurrency) { | ||
export function queue(worker, concurrency) { | ||
if (concurrency == null) { // eslint-disable-line no-eq-null,eqeqeq | ||
@@ -77,6 +69,6 @@ concurrency = 1; | ||
var workers = 0; | ||
var q = { | ||
let workers = 0; | ||
const q = { | ||
_tasks: [], | ||
concurrency: concurrency, | ||
concurrency, | ||
saturated: _noop, | ||
@@ -90,15 +82,17 @@ unsaturated: _noop, | ||
paused: false, | ||
push: function (data, callback) { | ||
push(data, callback) { | ||
_insert(data, false, callback); | ||
}, | ||
kill: function () { | ||
kill() { | ||
workers = 0; | ||
q.drain = _noop; | ||
q.started = false; | ||
q._tasks = []; | ||
}, | ||
unshift: function (data, callback) { | ||
unshift(data, callback) { | ||
_insert(data, true, callback); | ||
}, | ||
process: function () { | ||
process() { | ||
while (!q.paused && workers < q.concurrency && q._tasks.length) { | ||
var task = q._tasks.shift(); | ||
const task = q._tasks.shift(); | ||
@@ -118,12 +112,12 @@ if (q._tasks.length === 0) { | ||
}, | ||
length: function () { | ||
length() { | ||
return q._tasks.length; | ||
}, | ||
running: function () { | ||
running() { | ||
return workers; | ||
}, | ||
idle: function () { | ||
idle() { | ||
return q._tasks.length + workers === 0; | ||
}, | ||
pause: function () { | ||
pause() { | ||
if (q.paused === true) { | ||
@@ -135,3 +129,3 @@ return; | ||
}, | ||
resume: function () { | ||
resume() { | ||
if (q.paused === false) { | ||
@@ -145,6 +139,6 @@ return; | ||
// worker to preserve full concurrency after pause | ||
for (var w = 1; w <= q.concurrency; w++) { | ||
for (let w = 1; w <= q.concurrency; w++) { | ||
q.process(); | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -161,5 +155,3 @@ | ||
// call drain immediately if there are no tasks | ||
setTimeout(function () { | ||
q.drain(); | ||
}, 1); | ||
setTimeout(() => q.drain(), 1); | ||
@@ -169,5 +161,5 @@ return; | ||
var item = { | ||
data: data, | ||
callback: typeof callback === 'function' ? callback : _noop | ||
const item = { | ||
data, | ||
callback: typeof callback === 'function' ? callback : _noop, | ||
}; | ||
@@ -182,9 +174,7 @@ | ||
setTimeout(function () { | ||
q.process(); | ||
}, 1); | ||
setTimeout(() => q.process(), 1); | ||
} | ||
function _next(task) { | ||
return function () { | ||
return function next() { | ||
workers -= 1; | ||
@@ -191,0 +181,0 @@ |
107
src/b64.js
@@ -1,68 +0,63 @@ | ||
/* eslint no-magic-numbers: 0 */ | ||
'use strict'; | ||
const _keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; | ||
module.exports = { | ||
// private property | ||
_keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', | ||
export function encodeBinary(input) { | ||
let output = ''; | ||
let inx = 0; | ||
encodeBinary: function (input) { | ||
var output = ''; | ||
var bytebuffer; | ||
var encodedCharIndexes = new Array(4); | ||
var inx = 0; | ||
var jnx = 0; | ||
var paddingBytes = 0; | ||
while (inx < input.length) { | ||
// Fill byte buffer array | ||
const bytebuffer = [0, 0, 0]; | ||
const encodedCharIndexes = [0, 0, 0, 0]; | ||
while (inx < input.length) { | ||
// Fill byte buffer array | ||
bytebuffer = new Array(3); | ||
for (jnx = 0; jnx < bytebuffer.length; jnx++) { | ||
if (inx < input.length) { | ||
// throw away high-order byte, as documented at: | ||
// https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data | ||
bytebuffer[jnx] = input.charCodeAt(inx++) & 0xff; | ||
} | ||
else { | ||
bytebuffer[jnx] = 0; | ||
} | ||
for (let jnx = 0; jnx < bytebuffer.length; ++jnx) { | ||
if (inx < input.length) { | ||
// throw away high-order byte, as documented at: | ||
// https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data | ||
bytebuffer[jnx] = input.charCodeAt(inx++) & 0xff; | ||
} | ||
else { | ||
bytebuffer[jnx] = 0; | ||
} | ||
} | ||
// Get each encoded character, 6 bits at a time | ||
// index 1: first 6 bits | ||
encodedCharIndexes[0] = bytebuffer[0] >> 2; | ||
// index 2: second 6 bits (2 least significant bits from input byte 1 + 4 most significant bits from byte 2) | ||
encodedCharIndexes[1] = ((bytebuffer[0] & 0x3) << 4) | (bytebuffer[1] >> 4); | ||
// index 3: third 6 bits (4 least significant bits from input byte 2 + 2 most significant bits from byte 3) | ||
encodedCharIndexes[2] = ((bytebuffer[1] & 0x0f) << 2) | (bytebuffer[2] >> 6); | ||
// index 3: forth 6 bits (6 least significant bits from input byte 3) | ||
encodedCharIndexes[3] = bytebuffer[2] & 0x3f; | ||
// Get each encoded character, 6 bits at a time | ||
// index 1: first 6 bits | ||
encodedCharIndexes[0] = bytebuffer[0] >> 2; | ||
// Determine whether padding happened, and adjust accordingly | ||
paddingBytes = inx - (input.length - 1); | ||
switch (paddingBytes) { | ||
case 2: | ||
// Set last 2 characters to padding char | ||
encodedCharIndexes[3] = 64; | ||
encodedCharIndexes[2] = 64; | ||
break; | ||
// index 2: second 6 bits (2 least significant bits from input byte 1 + 4 most significant bits from byte 2) | ||
encodedCharIndexes[1] = ((bytebuffer[0] & 0x3) << 4) | (bytebuffer[1] >> 4); | ||
case 1: | ||
// Set last character to padding char | ||
encodedCharIndexes[3] = 64; | ||
break; | ||
// index 3: third 6 bits (4 least significant bits from input byte 2 + 2 most significant bits from byte 3) | ||
encodedCharIndexes[2] = ((bytebuffer[1] & 0x0f) << 2) | (bytebuffer[2] >> 6); | ||
default: | ||
break; // No padding - proceed | ||
} | ||
// index 3: forth 6 bits (6 least significant bits from input byte 3) | ||
encodedCharIndexes[3] = bytebuffer[2] & 0x3f; | ||
// Now we will grab each appropriate character out of our keystring | ||
// based on our index array and append it to the output string | ||
for (jnx = 0; jnx < encodedCharIndexes.length; jnx++) { | ||
output += this._keyStr.charAt(encodedCharIndexes[jnx]); | ||
} | ||
// Determine whether padding happened, and adjust accordingly | ||
const paddingBytes = inx - (input.length - 1); | ||
switch (paddingBytes) { | ||
case 2: | ||
// Set last 2 characters to padding char | ||
encodedCharIndexes[3] = 64; | ||
encodedCharIndexes[2] = 64; | ||
break; | ||
case 1: | ||
// Set last character to padding char | ||
encodedCharIndexes[3] = 64; | ||
break; | ||
default: | ||
break; // No padding - proceed | ||
} | ||
return output; | ||
// Now we will grab each appropriate character out of our keystring | ||
// based on our index array and append it to the output string | ||
for (let jnx = 0; jnx < encodedCharIndexes.length; ++jnx) { | ||
output += _keyStr.charAt(encodedCharIndexes[jnx]); | ||
} | ||
} | ||
}; | ||
return output; | ||
} |
@@ -1,15 +0,10 @@ | ||
/* eslint global-require: 0 */ | ||
'use strict'; | ||
import Loader from './Loader'; | ||
import Resource from './Resource'; | ||
import * as async from './async'; | ||
import * as b64 from './b64'; | ||
module.exports = require('./Loader'); | ||
module.exports.Resource = require('./Resource'); | ||
module.exports.middleware = { | ||
caching: { | ||
memory: require('./middlewares/caching/memory') | ||
}, | ||
parsing: { | ||
blob: require('./middlewares/parsing/blob') | ||
} | ||
}; | ||
Loader.Resource = Resource; | ||
Loader.async = async; | ||
Loader.base64 = b64; | ||
module.exports.async = require('./async'); | ||
module.exports = Loader; // eslint-disable-line no-undef |
@@ -1,11 +0,9 @@ | ||
'use strict'; | ||
import Signal from 'mini-signals'; | ||
import parseUri from 'parse-uri'; | ||
import * as async from './async'; | ||
import Resource from './Resource'; | ||
var parseUri = require('parse-uri'); | ||
var async = require('./async'); | ||
var Resource = require('./Resource'); | ||
var EventEmitter = require('eventemitter3'); | ||
// some constants | ||
var DEFAULT_CONCURRENCY = 10; | ||
var MAX_PROGRESS = 100; | ||
const MAX_PROGRESS = 100; | ||
const rgxExtractUrlHash = /(#[\w\-]+)?$/; | ||
@@ -16,478 +14,543 @@ /** | ||
* @class | ||
* @param {string} [baseUrl=''] - The base url for all resources loaded by this loader. | ||
* @param {number} [concurrency=10] - The number of resources to load concurrently. | ||
*/ | ||
function Loader(baseUrl, concurrency) { | ||
EventEmitter.call(this); | ||
concurrency = concurrency || DEFAULT_CONCURRENCY; | ||
export default class Loader { | ||
/** | ||
* The base url for all resources loaded by this loader. | ||
* | ||
* @member {string} | ||
* @param {string} [baseUrl=''] - The base url for all resources loaded by this loader. | ||
* @param {number} [concurrency=10] - The number of resources to load concurrently. | ||
*/ | ||
this.baseUrl = baseUrl || ''; | ||
constructor(baseUrl = '', concurrency = 10) { | ||
/** | ||
* The base url for all resources loaded by this loader. | ||
* | ||
* @member {string} | ||
*/ | ||
this.baseUrl = baseUrl; | ||
/** | ||
* The progress percent of the loader going through the queue. | ||
* | ||
* @member {number} | ||
*/ | ||
this.progress = 0; | ||
/** | ||
* The progress percent of the loader going through the queue. | ||
* | ||
* @member {number} | ||
*/ | ||
this.progress = 0; | ||
/** | ||
* Loading state of the loader, true if it is currently loading resources. | ||
* | ||
* @member {boolean} | ||
*/ | ||
this.loading = false; | ||
/** | ||
* Loading state of the loader, true if it is currently loading resources. | ||
* | ||
* @member {boolean} | ||
*/ | ||
this.loading = false; | ||
/** | ||
* The percentage of total progress that a single resource represents. | ||
* | ||
* @member {number} | ||
*/ | ||
this._progressChunk = 0; | ||
/** | ||
* A querystring to append to every URL added to the loader. | ||
* | ||
* This should be a valid query string *without* the question-mark (`?`). The loader will | ||
* also *not* escape values for you. Make sure to escape your parameters with | ||
* [`encodeURIComponent`](https://mdn.io/encodeURIComponent) before assigning this property. | ||
* | ||
* @example | ||
* | ||
* ```js | ||
* const loader = new Loader(); | ||
* | ||
* loader.defaultQueryString = 'user=me&password=secret'; | ||
* | ||
* // This will request 'image.png?user=me&password=secret' | ||
* loader.add('image.png').load(); | ||
* | ||
* loader.reset(); | ||
* | ||
* // This will request 'image.png?v=1&user=me&password=secret' | ||
* loader.add('iamge.png?v=1').load(); | ||
* ``` | ||
*/ | ||
this.defaultQueryString = ''; | ||
/** | ||
* The middleware to run before loading each resource. | ||
* | ||
* @member {function[]} | ||
*/ | ||
this._beforeMiddleware = []; | ||
/** | ||
* The middleware to run before loading each resource. | ||
* | ||
* @member {function[]} | ||
*/ | ||
this._beforeMiddleware = []; | ||
/** | ||
* The middleware to run after loading each resource. | ||
* | ||
* @member {function[]} | ||
*/ | ||
this._afterMiddleware = []; | ||
/** | ||
* The middleware to run after loading each resource. | ||
* | ||
* @member {function[]} | ||
*/ | ||
this._afterMiddleware = []; | ||
/** | ||
* The `_loadResource` function bound with this object context. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._boundLoadResource = this._loadResource.bind(this); | ||
/** | ||
* The `_loadResource` function bound with this object context. | ||
* | ||
* @private | ||
* @member {function} | ||
* @param {Resource} r - The resource to load | ||
* @param {Function} d - The dequeue function | ||
* @return {undefined} | ||
*/ | ||
this._boundLoadResource = (r, d) => this._loadResource(r, d); | ||
/** | ||
* The resource buffer that fills until `load` is called to start loading resources. | ||
* | ||
* @private | ||
* @member {Resource[]} | ||
*/ | ||
this._buffer = []; | ||
/** | ||
* The resources waiting to be loaded. | ||
* | ||
* @private | ||
* @member {Resource[]} | ||
*/ | ||
this._queue = async.queue(this._boundLoadResource, concurrency); | ||
/** | ||
* Used to track load completion. | ||
* | ||
* @private | ||
* @member {number} | ||
*/ | ||
this._numToLoad = 0; | ||
this._queue.pause(); | ||
/** | ||
* The resources waiting to be loaded. | ||
* | ||
* @private | ||
* @member {Resource[]} | ||
*/ | ||
this._queue = async.queue(this._boundLoadResource, concurrency); | ||
/** | ||
* All the resources for this loader keyed by name. | ||
* | ||
* @member {object<string, Resource>} | ||
*/ | ||
this.resources = {}; | ||
/** | ||
* All the resources for this loader keyed by name. | ||
* | ||
* @member {object<string, Resource>} | ||
*/ | ||
this.resources = {}; | ||
/** | ||
* Dispatched once per loaded or errored resource. | ||
* | ||
* The callback looks like {@link Loader.OnProgressSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onProgress = new Signal(); | ||
/** | ||
* Emitted once per loaded or errored resource. | ||
* | ||
* @event progress | ||
* @memberof Loader# | ||
*/ | ||
/** | ||
* Dispatched once per errored resource. | ||
* | ||
* The callback looks like {@link Loader.OnErrorSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onError = new Signal(); | ||
/** | ||
* Emitted once per errored resource. | ||
* | ||
* @event error | ||
* @memberof Loader# | ||
*/ | ||
/** | ||
* Dispatched once per loaded resource. | ||
* | ||
* The callback looks like {@link Loader.OnLoadSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onLoad = new Signal(); | ||
/** | ||
* Emitted once per loaded resource. | ||
* | ||
* @event load | ||
* @memberof Loader# | ||
*/ | ||
/** | ||
* Dispatched when the loader begins to process the queue. | ||
* | ||
* The callback looks like {@link Loader.OnStartSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onStart = new Signal(); | ||
/** | ||
* Emitted when the loader begins to process the queue. | ||
* | ||
* @event start | ||
* @memberof Loader# | ||
*/ | ||
/** | ||
* Dispatched when the queued resources all load. | ||
* | ||
* The callback looks like {@link Loader.OnCompleteSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onComplete = new Signal(); | ||
/** | ||
* Emitted when the queued resources all load. | ||
* | ||
* @event complete | ||
* @memberof Loader# | ||
*/ | ||
} | ||
/** | ||
* When the progress changes the loader and resource are disaptched. | ||
* | ||
* @memberof Loader | ||
* @callback OnProgressSignal | ||
* @param {Loader} loader - The loader the progress is advancing on. | ||
* @param {Resource} resource - The resource that has completed or failed to cause the progress to advance. | ||
*/ | ||
Loader.prototype = Object.create(EventEmitter.prototype); | ||
Loader.prototype.constructor = Loader; | ||
module.exports = Loader; | ||
/** | ||
* When an error occurrs the loader and resource are disaptched. | ||
* | ||
* @memberof Loader | ||
* @callback OnErrorSignal | ||
* @param {Loader} loader - The loader the error happened in. | ||
* @param {Resource} resource - The resource that caused the error. | ||
*/ | ||
/** | ||
* Adds a resource (or multiple resources) to the loader queue. | ||
* | ||
* This function can take a wide variety of different parameters. The only thing that is always | ||
* required the url to load. All the following will work: | ||
* | ||
* ```js | ||
* loader | ||
* // normal param syntax | ||
* .add('key', 'http://...', function () {}) | ||
* .add('http://...', function () {}) | ||
* .add('http://...') | ||
* | ||
* // object syntax | ||
* .add({ | ||
* name: 'key2', | ||
* url: 'http://...' | ||
* }, function () {}) | ||
* .add({ | ||
* url: 'http://...' | ||
* }, function () {}) | ||
* .add({ | ||
* name: 'key3', | ||
* url: 'http://...' | ||
* onComplete: function () {} | ||
* }) | ||
* .add({ | ||
* url: 'https://...', | ||
* onComplete: function () {}, | ||
* crossOrigin: true | ||
* }) | ||
* | ||
* // you can also pass an array of objects or urls or both | ||
* .add([ | ||
* { name: 'key4', url: 'http://...', onComplete: function () {} }, | ||
* { url: 'http://...', onComplete: function () {} }, | ||
* 'http://...' | ||
* ]) | ||
* | ||
* // and you can use both params and options | ||
* .add('key', 'http://...', { crossOrigin: true }, function () {}) | ||
* .add('http://...', { crossOrigin: true }, function () {}); | ||
* ``` | ||
* | ||
* @alias enqueue | ||
* @param {string} [name] - The name of the resource to load, if not passed the url is used. | ||
* @param {string} [url] - The url for this resource, relative to the baseUrl of this loader. | ||
* @param {object} [options] - The options for the load. | ||
* @param {boolean} [options.crossOrigin] - Is this request cross-origin? Default is to determine automatically. | ||
* @param {Resource.XHR_LOAD_TYPE} [options.loadType=Resource.LOAD_TYPE.XHR] - How should this resource be loaded? | ||
* @param {Resource.XHR_RESPONSE_TYPE} [options.xhrType=Resource.XHR_RESPONSE_TYPE.DEFAULT] - How should the data being | ||
* loaded be interpreted when using XHR? | ||
* @param {function} [cb] - Function to call when this specific resource completes loading. | ||
* @return {Loader} Returns itself. | ||
*/ | ||
Loader.prototype.add = Loader.prototype.enqueue = function (name, url, options, cb) { | ||
// special case of an array of objects or urls | ||
if (Array.isArray(name)) { | ||
for (var i = 0; i < name.length; ++i) { | ||
this.add(name[i]); | ||
} | ||
/** | ||
* When a load completes the loader and resource are disaptched. | ||
* | ||
* @memberof Loader | ||
* @callback OnLoadSignal | ||
* @param {Loader} loader - The loader that laoded the resource. | ||
* @param {Resource} resource - The resource that has completed loading. | ||
*/ | ||
return this; | ||
} | ||
/** | ||
* When the loader starts loading resources it dispatches this callback. | ||
* | ||
* @memberof Loader | ||
* @callback OnStartSignal | ||
* @param {Loader} loader - The loader that has started loading resources. | ||
*/ | ||
// if an object is passed instead of params | ||
if (typeof name === 'object') { | ||
cb = url || name.callback || name.onComplete; | ||
options = name; | ||
url = name.url; | ||
name = name.name || name.key || name.url; | ||
/** | ||
* When the loader completes loading resources it dispatches this callback. | ||
* | ||
* @memberof Loader | ||
* @callback OnCompleteSignal | ||
* @param {Loader} loader - The loader that has finished loading resources. | ||
*/ | ||
} | ||
// case where no name is passed shift all args over by one. | ||
if (typeof url !== 'string') { | ||
cb = options; | ||
options = url; | ||
url = name; | ||
} | ||
/** | ||
* Adds a resource (or multiple resources) to the loader queue. | ||
* | ||
* This function can take a wide variety of different parameters. The only thing that is always | ||
* required the url to load. All the following will work: | ||
* | ||
* ```js | ||
* loader | ||
* // normal param syntax | ||
* .add('key', 'http://...', function () {}) | ||
* .add('http://...', function () {}) | ||
* .add('http://...') | ||
* | ||
* // object syntax | ||
* .add({ | ||
* name: 'key2', | ||
* url: 'http://...' | ||
* }, function () {}) | ||
* .add({ | ||
* url: 'http://...' | ||
* }, function () {}) | ||
* .add({ | ||
* name: 'key3', | ||
* url: 'http://...' | ||
* onComplete: function () {} | ||
* }) | ||
* .add({ | ||
* url: 'https://...', | ||
* onComplete: function () {}, | ||
* crossOrigin: true | ||
* }) | ||
* | ||
* // you can also pass an array of objects or urls or both | ||
* .add([ | ||
* { name: 'key4', url: 'http://...', onComplete: function () {} }, | ||
* { url: 'http://...', onComplete: function () {} }, | ||
* 'http://...' | ||
* ]) | ||
* | ||
* // and you can use both params and options | ||
* .add('key', 'http://...', { crossOrigin: true }, function () {}) | ||
* .add('http://...', { crossOrigin: true }, function () {}); | ||
* ``` | ||
* | ||
* @param {string} [name] - The name of the resource to load, if not passed the url is used. | ||
* @param {string} [url] - The url for this resource, relative to the baseUrl of this loader. | ||
* @param {object} [options] - The options for the load. | ||
* @param {boolean} [options.crossOrigin] - Is this request cross-origin? Default is to determine automatically. | ||
* @param {Resource.LOAD_TYPE} [options.loadType=Resource.LOAD_TYPE.XHR] - How should this resource be loaded? | ||
* @param {Resource.XHR_RESPONSE_TYPE} [options.xhrType=Resource.XHR_RESPONSE_TYPE.DEFAULT] - How should | ||
* the data being loaded be interpreted when using XHR? | ||
* @param {object} [options.metadata] - Extra configuration for middleware and the Resource object. | ||
* @param {HTMLImageElement|HTMLAudioElement|HTMLVideoElement} [options.metadata.loadElement=null] - The | ||
* element to use for loading, instead of creating one. | ||
* @param {boolean} [options.metadata.skipSource=false] - Skips adding source(s) to the load element. This | ||
* is useful if you want to pass in a `loadElement` that you already added load sources to. | ||
* @param {function} [cb] - Function to call when this specific resource completes loading. | ||
* @return {Loader} Returns itself. | ||
*/ | ||
add(name, url, options, cb) { | ||
// special case of an array of objects or urls | ||
if (Array.isArray(name)) { | ||
for (let i = 0; i < name.length; ++i) { | ||
this.add(name[i]); | ||
} | ||
// now that we shifted make sure we have a proper url. | ||
if (typeof url !== 'string') { | ||
throw new Error('No url passed to add resource to loader.'); | ||
} | ||
return this; | ||
} | ||
// options are optional so people might pass a function and no options | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = null; | ||
} | ||
// if an object is passed instead of params | ||
if (typeof name === 'object') { | ||
cb = url || name.callback || name.onComplete; | ||
options = name; | ||
url = name.url; | ||
name = name.name || name.key || name.url; | ||
} | ||
// check if resource already exists. | ||
if (this.resources[name]) { | ||
throw new Error('Resource with name "' + name + '" already exists.'); | ||
} | ||
// case where no name is passed shift all args over by one. | ||
if (typeof url !== 'string') { | ||
cb = options; | ||
options = url; | ||
url = name; | ||
} | ||
// add base url if this isn't an absolute url | ||
url = this._prepareUrl(url); | ||
// now that we shifted make sure we have a proper url. | ||
if (typeof url !== 'string') { | ||
throw new Error('No url passed to add resource to loader.'); | ||
} | ||
// create the store the resource | ||
this.resources[name] = new Resource(name, url, options); | ||
// options are optional so people might pass a function and no options | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = null; | ||
} | ||
if (typeof cb === 'function') { | ||
this.resources[name].once('afterMiddleware', cb); | ||
} | ||
// if loading already you can only add resources that have a parent. | ||
if (this.loading && (!options || !options.parentResource)) { | ||
throw new Error('Cannot add resources while the loader is running.'); | ||
} | ||
this._numToLoad++; | ||
// check if resource already exists. | ||
if (this.resources[name]) { | ||
throw new Error(`Resource named "${name}" already exists.`); | ||
} | ||
// if already loading add it to the worker queue | ||
if (this._queue.started) { | ||
this._queue.push(this.resources[name]); | ||
this._progressChunk = (MAX_PROGRESS - this.progress) / (this._queue.length() + this._queue.running()); | ||
} | ||
// otherwise buffer it to be added to the queue later | ||
else { | ||
this._buffer.push(this.resources[name]); | ||
this._progressChunk = MAX_PROGRESS / this._buffer.length; | ||
} | ||
// add base url if this isn't an absolute url | ||
url = this._prepareUrl(url); | ||
return this; | ||
}; | ||
// create the store the resource | ||
this.resources[name] = new Resource(name, url, options); | ||
/** | ||
* Sets up a middleware function that will run *before* the | ||
* resource is loaded. | ||
* | ||
* @alias pre | ||
* @method before | ||
* @param {function} fn - The middleware function to register. | ||
* @return {Loader} Returns itself. | ||
*/ | ||
Loader.prototype.before = Loader.prototype.pre = function (fn) { | ||
this._beforeMiddleware.push(fn); | ||
if (typeof cb === 'function') { | ||
this.resources[name].onAfterMiddleware.once(cb); | ||
} | ||
return this; | ||
}; | ||
// if loading make sure to adjust progress chunks for that parent and its children | ||
if (this.loading) { | ||
const parent = options.parentResource; | ||
const fullChunk = parent.progressChunk * (parent.children.length + 1); // +1 for parent | ||
const eachChunk = fullChunk / (parent.children.length + 2); // +2 for parent & new child | ||
/** | ||
* Sets up a middleware function that will run *after* the | ||
* resource is loaded. | ||
* | ||
* @alias use | ||
* @method after | ||
* @param {function} fn - The middleware function to register. | ||
* @return {Loader} Returns itself. | ||
*/ | ||
Loader.prototype.after = Loader.prototype.use = function (fn) { | ||
this._afterMiddleware.push(fn); | ||
parent.children.push(this.resources[name]); | ||
parent.progressChunk = eachChunk; | ||
return this; | ||
}; | ||
for (let i = 0; i < parent.children.length; ++i) { | ||
parent.children[i].progressChunk = eachChunk; | ||
} | ||
} | ||
/** | ||
* Resets the queue of the loader to prepare for a new load. | ||
* | ||
* @return {Loader} Returns itself. | ||
*/ | ||
Loader.prototype.reset = function () { | ||
// this.baseUrl = baseUrl || ''; | ||
// add the resource to the queue | ||
this._queue.push(this.resources[name]); | ||
this.progress = 0; | ||
return this; | ||
} | ||
this.loading = false; | ||
/** | ||
* Sets up a middleware function that will run *before* the | ||
* resource is loaded. | ||
* | ||
* @method before | ||
* @param {function} fn - The middleware function to register. | ||
* @return {Loader} Returns itself. | ||
*/ | ||
pre(fn) { | ||
this._beforeMiddleware.push(fn); | ||
this._progressChunk = 0; | ||
return this; | ||
} | ||
// this._beforeMiddleware.length = 0; | ||
// this._afterMiddleware.length = 0; | ||
/** | ||
* Sets up a middleware function that will run *after* the | ||
* resource is loaded. | ||
* | ||
* @alias use | ||
* @method after | ||
* @param {function} fn - The middleware function to register. | ||
* @return {Loader} Returns itself. | ||
*/ | ||
use(fn) { | ||
this._afterMiddleware.push(fn); | ||
this._buffer.length = 0; | ||
return this; | ||
} | ||
this._numToLoad = 0; | ||
/** | ||
* Resets the queue of the loader to prepare for a new load. | ||
* | ||
* @return {Loader} Returns itself. | ||
*/ | ||
reset() { | ||
this.progress = 0; | ||
this.loading = false; | ||
this._queue.kill(); | ||
this._queue.started = false; | ||
this._queue.kill(); | ||
this._queue.pause(); | ||
// abort all resource loads | ||
for (var k in this.resources) { | ||
var res = this.resources[k]; | ||
// abort all resource loads | ||
for (const k in this.resources) { | ||
const res = this.resources[k]; | ||
res.off('complete', this._onLoad, this); | ||
if (res._onLoadBinding) { | ||
res._onLoadBinding.detach(); | ||
} | ||
if (res.isLoading) { | ||
res.abort(); | ||
if (res.isLoading) { | ||
res.abort(); | ||
} | ||
} | ||
} | ||
this.resources = {}; | ||
this.resources = {}; | ||
return this; | ||
}; | ||
/** | ||
* Starts loading the queued resources. | ||
* | ||
* @fires start | ||
* @param {function} [cb] - Optional callback that will be bound to the `complete` event. | ||
* @return {Loader} Returns itself. | ||
*/ | ||
Loader.prototype.load = function (cb) { | ||
// register complete callback if they pass one | ||
if (typeof cb === 'function') { | ||
this.once('complete', cb); | ||
} | ||
// if the queue has already started we are done here | ||
if (this._queue.started) { | ||
return this; | ||
} | ||
// notify of start | ||
this.emit('start', this); | ||
/** | ||
* Starts loading the queued resources. | ||
* | ||
* @param {function} [cb] - Optional callback that will be bound to the `complete` event. | ||
* @return {Loader} Returns itself. | ||
*/ | ||
load(cb) { | ||
// register complete callback if they pass one | ||
if (typeof cb === 'function') { | ||
this.onComplete.once(cb); | ||
} | ||
// update loading state | ||
this.loading = true; | ||
// if the queue has already started we are done here | ||
if (this.loading) { | ||
return this; | ||
} | ||
// start the internal queue | ||
for (var i = 0; i < this._buffer.length; ++i) { | ||
this._queue.push(this._buffer[i]); | ||
} | ||
// distribute progress chunks | ||
const chunk = 100 / this._queue._tasks.length; | ||
// empty the buffer | ||
this._buffer.length = 0; | ||
for (let i = 0; i < this._queue._tasks.length; ++i) { | ||
this._queue._tasks[i].data.progressChunk = chunk; | ||
} | ||
return this; | ||
}; | ||
// update loading state | ||
this.loading = true; | ||
/** | ||
* Prepares a url for usage based on the configuration of this object | ||
* | ||
* @private | ||
* @param {string} url - The url to prepare. | ||
* @return {string} The prepared url. | ||
*/ | ||
Loader.prototype._prepareUrl = function (url) { | ||
var parsedUrl = parseUri(url, { strictMode: true }); | ||
// notify of start | ||
this.onStart.dispatch(this); | ||
// absolute url, just use it as is. | ||
if (parsedUrl.protocol || !parsedUrl.path || parsedUrl.path.indexOf('//') === 0) { | ||
return url; | ||
} | ||
// start loading | ||
this._queue.resume(); | ||
// if baseUrl doesn't end in slash and url doesn't start with slash, then add a slash inbetween | ||
if (this.baseUrl.length | ||
&& this.baseUrl.lastIndexOf('/') !== this.baseUrl.length - 1 | ||
&& url.charAt(0) !== '/' | ||
) { | ||
return this.baseUrl + '/' + url; | ||
return this; | ||
} | ||
return this.baseUrl + url; | ||
}; | ||
/** | ||
* Prepares a url for usage based on the configuration of this object | ||
* | ||
* @private | ||
* @param {string} url - The url to prepare. | ||
* @return {string} The prepared url. | ||
*/ | ||
_prepareUrl(url) { | ||
const parsedUrl = parseUri(url, { strictMode: true }); | ||
let result; | ||
/** | ||
* Loads a single resource. | ||
* | ||
* @private | ||
* @param {Resource} resource - The resource to load. | ||
* @param {function} dequeue - The function to call when we need to dequeue this item. | ||
*/ | ||
Loader.prototype._loadResource = function (resource, dequeue) { | ||
var self = this; | ||
// absolute url, just use it as is. | ||
if (parsedUrl.protocol || !parsedUrl.path || url.indexOf('//') === 0) { | ||
result = url; | ||
} | ||
// if baseUrl doesn't end in slash and url doesn't start with slash, then add a slash inbetween | ||
else if (this.baseUrl.length | ||
&& this.baseUrl.lastIndexOf('/') !== this.baseUrl.length - 1 | ||
&& url.charAt(0) !== '/' | ||
) { | ||
result = `${this.baseUrl}/${url}`; | ||
} | ||
else { | ||
result = this.baseUrl + url; | ||
} | ||
resource._dequeue = dequeue; | ||
// if we need to add a default querystring, there is a bit more work | ||
if (this.defaultQueryString) { | ||
const hash = rgxExtractUrlHash.exec(result)[0]; | ||
// run before middleware | ||
async.eachSeries( | ||
this._beforeMiddleware, | ||
function (fn, next) { | ||
fn.call(self, resource, function () { | ||
// if the before middleware marks the resource as complete, | ||
// break and don't process any more before middleware | ||
next(resource.isComplete ? {} : null); | ||
}); | ||
}, | ||
function () { | ||
// resource.on('progress', self.emit.bind(self, 'progress')); | ||
result = result.substr(0, result.length - hash.length); | ||
if (resource.isComplete) { | ||
self._onLoad(resource); | ||
if (result.indexOf('?') !== -1) { | ||
result += `&${this.defaultQueryString}`; | ||
} | ||
else { | ||
resource.once('complete', self._onLoad, self); | ||
resource.load(); | ||
result += `?${this.defaultQueryString}`; | ||
} | ||
result += hash; | ||
} | ||
); | ||
}; | ||
/** | ||
* Called once each resource has loaded. | ||
* | ||
* @fires complete | ||
* @private | ||
*/ | ||
Loader.prototype._onComplete = function () { | ||
this.loading = false; | ||
return result; | ||
} | ||
this.emit('complete', this, this.resources); | ||
}; | ||
/** | ||
* Loads a single resource. | ||
* | ||
* @private | ||
* @param {Resource} resource - The resource to load. | ||
* @param {function} dequeue - The function to call when we need to dequeue this item. | ||
*/ | ||
_loadResource(resource, dequeue) { | ||
resource._dequeue = dequeue; | ||
/** | ||
* Called each time a resources is loaded. | ||
* | ||
* @fires progress | ||
* @fires error | ||
* @fires load | ||
* @private | ||
* @param {Resource} resource - The resource that was loaded | ||
*/ | ||
Loader.prototype._onLoad = function (resource) { | ||
var self = this; | ||
// run before middleware | ||
async.eachSeries( | ||
this._beforeMiddleware, | ||
(fn, next) => { | ||
fn.call(this, resource, () => { | ||
// if the before middleware marks the resource as complete, | ||
// break and don't process any more before middleware | ||
next(resource.isComplete ? {} : null); | ||
}); | ||
}, | ||
() => { | ||
if (resource.isComplete) { | ||
this._onLoad(resource); | ||
} | ||
else { | ||
resource._onLoadBinding = resource.onComplete.once(this._onLoad, this); | ||
resource.load(); | ||
} | ||
} | ||
); | ||
} | ||
// run middleware, this *must* happen before dequeue so sub-assets get added properly | ||
async.eachSeries( | ||
this._afterMiddleware, | ||
function (fn, next) { | ||
fn.call(self, resource, next); | ||
}, | ||
function () { | ||
resource.emit('afterMiddleware', resource); | ||
/** | ||
* Called once each resource has loaded. | ||
* | ||
* @private | ||
*/ | ||
_onComplete() { | ||
this.loading = false; | ||
self._numToLoad--; | ||
this.onComplete.dispatch(this, this.resources); | ||
} | ||
self.progress += self._progressChunk; | ||
self.emit('progress', self, resource); | ||
/** | ||
* Called each time a resources is loaded. | ||
* | ||
* @private | ||
* @param {Resource} resource - The resource that was loaded | ||
*/ | ||
_onLoad(resource) { | ||
resource._onLoadBinding = null; | ||
if (resource.error) { | ||
self.emit('error', resource.error, self, resource); | ||
} | ||
else { | ||
self.emit('load', self, resource); | ||
} | ||
// run middleware, this *must* happen before dequeue so sub-assets get added properly | ||
async.eachSeries( | ||
this._afterMiddleware, | ||
(fn, next) => { | ||
fn.call(this, resource, next); | ||
}, | ||
() => { | ||
resource.onAfterMiddleware.dispatch(resource); | ||
// do completion check | ||
if (self._numToLoad === 0) { | ||
self.progress = 100; | ||
self._onComplete(); | ||
} | ||
} | ||
); | ||
this.progress += resource.progressChunk; | ||
this.onProgress.dispatch(this, resource); | ||
// remove this resource from the async queue | ||
resource._dequeue(); | ||
}; | ||
if (resource.error) { | ||
this.onError.dispatch(resource.error, this, resource); | ||
} | ||
else { | ||
this.onLoad.dispatch(this, resource); | ||
} | ||
Loader.LOAD_TYPE = Resource.LOAD_TYPE; | ||
Loader.XHR_RESPONSE_TYPE = Resource.XHR_RESPONSE_TYPE; | ||
// remove this resource from the async queue | ||
resource._dequeue(); | ||
// do completion check | ||
if (this._queue.idle()) { | ||
this.progress = MAX_PROGRESS; | ||
this._onComplete(); | ||
} | ||
} | ||
); | ||
} | ||
} |
@@ -1,8 +0,6 @@ | ||
'use strict'; | ||
// a simple in-memory cache for resources | ||
var cache = {}; | ||
const cache = {}; | ||
module.exports = function () { | ||
return function (resource, next) { | ||
export function memoryMiddlewareFactory() { | ||
return function memoryMiddleware(resource, next) { | ||
// if cached, then set data and complete the resource | ||
@@ -15,5 +13,3 @@ if (cache[resource.url]) { | ||
else { | ||
resource.once('complete', function () { | ||
cache[this.url] = this.data; | ||
}); | ||
resource.onComplete.once(() => (cache[this.url] = this.data)); | ||
} | ||
@@ -23,2 +19,2 @@ | ||
}; | ||
}; | ||
} |
@@ -1,12 +0,9 @@ | ||
'use strict'; | ||
import Resource from '../../Resource'; | ||
import b64 from '../../b64'; | ||
var Resource = require('../../Resource'); | ||
var b64 = require('../../b64'); | ||
const Url = window.URL || window.webkitURL; | ||
var Url = window.URL || window.webkitURL; | ||
// a middleware for transforming XHR loaded Blobs into more useful objects | ||
module.exports = function () { | ||
return function (resource, next) { | ||
export function blobMiddlewareFactory() { | ||
return function blobMiddleware(resource, next) { | ||
if (!resource.data) { | ||
@@ -22,3 +19,3 @@ next(); | ||
if (!window.Blob || typeof resource.data === 'string') { | ||
var type = resource.xhr.getResponseHeader('content-type'); | ||
const type = resource.xhr.getResponseHeader('content-type'); | ||
@@ -28,8 +25,8 @@ // this is an image, convert the binary string into a data url | ||
resource.data = new Image(); | ||
resource.data.src = 'data:' + type + ';base64,' + b64.encodeBinary(resource.xhr.responseText); | ||
resource.data.src = `data:${type};base64,${b64.encodeBinary(resource.xhr.responseText)}`; | ||
resource.isImage = true; | ||
resource.type = Resource.TYPE.IMAGE; | ||
// wait until the image loads and then callback | ||
resource.data.onload = function () { | ||
resource.data.onload = () => { | ||
resource.data.onload = null; | ||
@@ -46,3 +43,3 @@ | ||
else if (resource.data.type.indexOf('image') === 0) { | ||
var src = Url.createObjectURL(resource.data); | ||
const src = Url.createObjectURL(resource.data); | ||
@@ -53,6 +50,7 @@ resource.blob = resource.data; | ||
resource.isImage = true; | ||
resource.type = Resource.TYPE.IMAGE; | ||
// cleanup the no longer used blob after the image loads | ||
resource.data.onload = function () { | ||
// TODO: Is this correct? Will the image be invalid after revoking? | ||
resource.data.onload = () => { | ||
Url.revokeObjectURL(src); | ||
@@ -71,2 +69,2 @@ resource.data.onload = null; | ||
}; | ||
}; | ||
} |
1491
src/Resource.js
@@ -1,160 +0,301 @@ | ||
'use strict'; | ||
import parseUri from 'parse-uri'; | ||
import Signal from 'mini-signals'; | ||
var EventEmitter = require('eventemitter3'); | ||
var parseUri = require('parse-uri'); | ||
// tests is CORS is supported in XHR, if not we need to use XDR | ||
var useXdr = !!(window.XDomainRequest && !('withCredentials' in (new XMLHttpRequest()))); | ||
var tempAnchor = null; | ||
const useXdr = !!(window.XDomainRequest && !('withCredentials' in (new XMLHttpRequest()))); | ||
let tempAnchor = null; | ||
// some status constants | ||
var STATUS_NONE = 0; | ||
var STATUS_OK = 200; | ||
var STATUS_EMPTY = 204; | ||
const STATUS_NONE = 0; | ||
const STATUS_OK = 200; | ||
const STATUS_EMPTY = 204; | ||
// noop | ||
function _noop() { /* empty */ } | ||
/** | ||
* Manages the state and loading of a single resource represented by | ||
* a single URL. | ||
* Manages the state and loading of a resource and all child resources. | ||
* | ||
* @class | ||
* @param {string} name - The name of the resource to load. | ||
* @param {string|string[]} url - The url for this resource, for audio/video loads you can pass an array of sources. | ||
* @param {object} [options] - The options for the load. | ||
* @param {string|boolean} [options.crossOrigin] - Is this request cross-origin? Default is to determine automatically. | ||
* @param {Resource.LOAD_TYPE} [options.loadType=Resource.LOAD_TYPE.XHR] - How should this resource be loaded? | ||
* @param {Resource.XHR_RESPONSE_TYPE} [options.xhrType=Resource.XHR_RESPONSE_TYPE.DEFAULT] - How should the data being | ||
* loaded be interpreted when using XHR? | ||
* @param {object} [options.metadata] - Extra info for middleware. | ||
*/ | ||
function Resource(name, url, options) { | ||
EventEmitter.call(this); | ||
options = options || {}; | ||
if (typeof name !== 'string' || typeof url !== 'string') { | ||
throw new Error('Both name and url are required for constructing a resource.'); | ||
} | ||
export default class Resource { | ||
/** | ||
* The name of this resource. | ||
* Sets the load type to be used for a specific extension. | ||
* | ||
* @member {string} | ||
* @readonly | ||
* @static | ||
* @param {string} extname - The extension to set the type for, e.g. "png" or "fnt" | ||
* @param {Resource.LOAD_TYPE} loadType - The load type to set it to. | ||
*/ | ||
this.name = name; | ||
static setExtensionLoadType(extname, loadType) { | ||
setExtMap(Resource._loadTypeMap, extname, loadType); | ||
} | ||
/** | ||
* The url used to load this resource. | ||
* Sets the load type to be used for a specific extension. | ||
* | ||
* @member {string} | ||
* @readonly | ||
* @static | ||
* @param {string} extname - The extension to set the type for, e.g. "png" or "fnt" | ||
* @param {Resource.XHR_RESPONSE_TYPE} xhrType - The xhr type to set it to. | ||
*/ | ||
this.url = url; | ||
static setExtensionXhrType(extname, xhrType) { | ||
setExtMap(Resource._xhrTypeMap, extname, xhrType); | ||
} | ||
/** | ||
* Stores whether or not this url is a data url. | ||
* | ||
* @member {boolean} | ||
* @readonly | ||
* @param {string} name - The name of the resource to load. | ||
* @param {string|string[]} url - The url for this resource, for audio/video loads you can pass | ||
* an array of sources. | ||
* @param {object} [options] - The options for the load. | ||
* @param {string|boolean} [options.crossOrigin] - Is this request cross-origin? Default is to | ||
* determine automatically. | ||
* @param {Resource.LOAD_TYPE} [options.loadType=Resource.LOAD_TYPE.XHR] - How should this resource | ||
* be loaded? | ||
* @param {Resource.XHR_RESPONSE_TYPE} [options.xhrType=Resource.XHR_RESPONSE_TYPE.DEFAULT] - How | ||
* should the data being loaded be interpreted when using XHR? | ||
* @param {object} [options.metadata] - Extra configuration for middleware and the Resource object. | ||
* @param {HTMLImageElement|HTMLAudioElement|HTMLVideoElement} [options.metadata.loadElement=null] - The | ||
* element to use for loading, instead of creating one. | ||
* @param {boolean} [options.metadata.skipSource=false] - Skips adding source(s) to the load element. This | ||
* is useful if you want to pass in a `loadElement` that you already added load sources to. | ||
*/ | ||
this.isDataUrl = this.url.indexOf('data:') === 0; | ||
constructor(name, url, options) { | ||
if (typeof name !== 'string' || typeof url !== 'string') { | ||
throw new Error('Both name and url are required for constructing a resource.'); | ||
} | ||
/** | ||
* The data that was loaded by the resource. | ||
* | ||
* @member {any} | ||
*/ | ||
this.data = null; | ||
options = options || {}; | ||
/** | ||
* Is this request cross-origin? If unset, determined automatically. | ||
* | ||
* @member {string} | ||
*/ | ||
this.crossOrigin = options.crossOrigin === true ? 'anonymous' : options.crossOrigin; | ||
/** | ||
* The state flags of this resource. | ||
* | ||
* @member {number} | ||
*/ | ||
this._flags = 0; | ||
/** | ||
* The method of loading to use for this resource. | ||
* | ||
* @member {Resource.LOAD_TYPE} | ||
*/ | ||
this.loadType = options.loadType || this._determineLoadType(); | ||
// set data url flag, needs to be set early for some _determineX checks to work. | ||
this._setFlag(Resource.STATUS_FLAGS.DATA_URL, url.indexOf('data:') === 0); | ||
/** | ||
* The type used to load the resource via XHR. If unset, determined automatically. | ||
* | ||
* @member {string} | ||
*/ | ||
this.xhrType = options.xhrType; | ||
/** | ||
* The name of this resource. | ||
* | ||
* @member {string} | ||
* @readonly | ||
*/ | ||
this.name = name; | ||
/** | ||
* Extra info for middleware, and controlling specifics about how the resource loads. | ||
* | ||
* Note that if you pass in a `loadElement`, the Resource class takes ownership of it. | ||
* Meaning it will modify it as it sees fit. | ||
* | ||
* @member {object} | ||
* @property {HTMLImageElement|HTMLAudioElement|HTMLVideoElement} [loadElement=null] - The | ||
* element to use for loading, instead of creating one. | ||
* @property {boolean} [skipSource=false] - Skips adding source(s) to the load element. This | ||
* is useful if you want to pass in a `loadElement` that you already added load sources | ||
* to. | ||
*/ | ||
this.metadata = options.metadata || {}; | ||
/** | ||
* The url used to load this resource. | ||
* | ||
* @member {string} | ||
* @readonly | ||
*/ | ||
this.url = url; | ||
/** | ||
* The error that occurred while loading (if any). | ||
* | ||
* @member {Error} | ||
* @readonly | ||
*/ | ||
this.error = null; | ||
/** | ||
* The data that was loaded by the resource. | ||
* | ||
* @member {any} | ||
*/ | ||
this.data = null; | ||
/** | ||
* The XHR object that was used to load this resource. This is only set | ||
* when `loadType` is `Resource.LOAD_TYPE.XHR`. | ||
* | ||
* @member {XMLHttpRequest} | ||
*/ | ||
this.xhr = null; | ||
/** | ||
* Is this request cross-origin? If unset, determined automatically. | ||
* | ||
* @member {string} | ||
*/ | ||
this.crossOrigin = options.crossOrigin === true ? 'anonymous' : options.crossOrigin; | ||
/** | ||
* Describes if this resource was loaded as json. Only valid after the resource | ||
* has completely loaded. | ||
* | ||
* @member {boolean} | ||
*/ | ||
this.isJson = false; | ||
/** | ||
* The method of loading to use for this resource. | ||
* | ||
* @member {Resource.LOAD_TYPE} | ||
*/ | ||
this.loadType = options.loadType || this._determineLoadType(); | ||
/** | ||
* Describes if this resource was loaded as xml. Only valid after the resource | ||
* has completely loaded. | ||
* | ||
* @member {boolean} | ||
*/ | ||
this.isXml = false; | ||
/** | ||
* The type used to load the resource via XHR. If unset, determined automatically. | ||
* | ||
* @member {string} | ||
*/ | ||
this.xhrType = options.xhrType; | ||
/** | ||
* Describes if this resource was loaded as an image tag. Only valid after the resource | ||
* has completely loaded. | ||
* | ||
* @member {boolean} | ||
*/ | ||
this.isImage = false; | ||
/** | ||
* Extra info for middleware, and controlling specifics about how the resource loads. | ||
* | ||
* Note that if you pass in a `loadElement`, the Resource class takes ownership of it. | ||
* Meaning it will modify it as it sees fit. | ||
* | ||
* @member {object} | ||
* @property {HTMLImageElement|HTMLAudioElement|HTMLVideoElement} [loadElement=null] - The | ||
* element to use for loading, instead of creating one. | ||
* @property {boolean} [skipSource=false] - Skips adding source(s) to the load element. This | ||
* is useful if you want to pass in a `loadElement` that you already added load sources | ||
* to. | ||
*/ | ||
this.metadata = options.metadata || {}; | ||
/** | ||
* Describes if this resource was loaded as an audio tag. Only valid after the resource | ||
* has completely loaded. | ||
* | ||
* @member {boolean} | ||
*/ | ||
this.isAudio = false; | ||
/** | ||
* The error that occurred while loading (if any). | ||
* | ||
* @member {Error} | ||
* @readonly | ||
*/ | ||
this.error = null; | ||
/** | ||
* The XHR object that was used to load this resource. This is only set | ||
* when `loadType` is `Resource.LOAD_TYPE.XHR`. | ||
* | ||
* @member {XMLHttpRequest} | ||
* @readonly | ||
*/ | ||
this.xhr = null; | ||
/** | ||
* The child resources this resource owns. | ||
* | ||
* @member {Resource[]} | ||
* @readonly | ||
*/ | ||
this.children = []; | ||
/** | ||
* The resource type. | ||
* | ||
* @member {Resource.TYPE} | ||
* @readonly | ||
*/ | ||
this.type = Resource.TYPE.UNKNOWN; | ||
/** | ||
* The progress chunk owned by this resource. | ||
* | ||
* @member {number} | ||
* @readonly | ||
*/ | ||
this.progressChunk = 0; | ||
/** | ||
* The `dequeue` method that will be used a storage place for the async queue dequeue method | ||
* used privately by the loader. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._dequeue = _noop; | ||
/** | ||
* Used a storage place for the on load binding used privately by the loader. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._onLoadBinding = null; | ||
/** | ||
* The `complete` function bound to this resource's context. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._boundComplete = this.complete.bind(this); | ||
/** | ||
* The `_onError` function bound to this resource's context. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._boundOnError = this._onError.bind(this); | ||
/** | ||
* The `_onProgress` function bound to this resource's context. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._boundOnProgress = this._onProgress.bind(this); | ||
// xhr callbacks | ||
this._boundXhrOnError = this._xhrOnError.bind(this); | ||
this._boundXhrOnAbort = this._xhrOnAbort.bind(this); | ||
this._boundXhrOnLoad = this._xhrOnLoad.bind(this); | ||
this._boundXdrOnTimeout = this._xdrOnTimeout.bind(this); | ||
/** | ||
* Dispatched when the resource beings to load. | ||
* | ||
* The callback looks like {@link Resource.OnStartSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onStart = new Signal(); | ||
/** | ||
* Dispatched each time progress of this resource load updates. | ||
* Not all resources types and loader systems can support this event | ||
* so sometimes it may not be available. If the resource | ||
* is being loaded on a modern browser, using XHR, and the remote server | ||
* properly sets Content-Length headers, then this will be available. | ||
* | ||
* The callback looks like {@link Resource.OnProgressSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onProgress = new Signal(); | ||
/** | ||
* Dispatched once this resource has loaded, if there was an error it will | ||
* be in the `error` property. | ||
* | ||
* The callback looks like {@link Resource.OnCompleteSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onComplete = new Signal(); | ||
/** | ||
* Dispatched after this resource has had all the *after* middleware run on it. | ||
* | ||
* The callback looks like {@link Resource.OnCompleteSignal}. | ||
* | ||
* @member {Signal} | ||
*/ | ||
this.onAfterMiddleware = new Signal(); | ||
/** | ||
* When the resource starts to load. | ||
* | ||
* @memberof Resource | ||
* @callback OnStartSignal | ||
* @param {Resource} resource - The resource that the event happened on. | ||
*/ | ||
/** | ||
* When the resource reports loading progress. | ||
* | ||
* @memberof Resource | ||
* @callback OnProgressSignal | ||
* @param {Resource} resource - The resource that the event happened on. | ||
* @param {number} percentage - The progress of the load in the range [0, 1]. | ||
*/ | ||
/** | ||
* When the resource finishes loading. | ||
* | ||
* @memberof Resource | ||
* @callback OnCompleteSignal | ||
* @param {Resource} resource - The resource that the event happened on. | ||
*/ | ||
} | ||
/** | ||
* Describes if this resource was loaded as a video tag. Only valid after the resource | ||
* has completely loaded. | ||
* Stores whether or not this url is a data url. | ||
* | ||
* @member {boolean} | ||
* @readonly | ||
*/ | ||
this.isVideo = false; | ||
get isDataUrl() { | ||
return this._hasFlag(Resource.STATUS_FLAGS.DATA_URL); | ||
} | ||
@@ -166,4 +307,7 @@ /** | ||
* @member {boolean} | ||
* @readonly | ||
*/ | ||
this.isComplete = false; | ||
get isComplete() { | ||
return this._hasFlag(Resource.STATUS_FLAGS.COMPLETE); | ||
} | ||
@@ -175,625 +319,606 @@ /** | ||
* @member {boolean} | ||
* @readonly | ||
*/ | ||
this.isLoading = false; | ||
get isLoading() { | ||
return this._hasFlag(Resource.STATUS_FLAGS.LOADING); | ||
} | ||
/** | ||
* The `dequeue` method that will be used a storage place for the async queue dequeue method | ||
* used privately by the loader. | ||
* Marks the resource as complete. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._dequeue = null; | ||
complete() { | ||
// TODO: Clean this up in a wrapper or something...gross.... | ||
if (this.data && this.data.removeEventListener) { | ||
this.data.removeEventListener('error', this._boundOnError, false); | ||
this.data.removeEventListener('load', this._boundComplete, false); | ||
this.data.removeEventListener('progress', this._boundOnProgress, false); | ||
this.data.removeEventListener('canplaythrough', this._boundComplete, false); | ||
} | ||
/** | ||
* The `complete` function bound to this resource's context. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._boundComplete = this.complete.bind(this); | ||
if (this.xhr) { | ||
if (this.xhr.removeEventListener) { | ||
this.xhr.removeEventListener('error', this._boundXhrOnError, false); | ||
this.xhr.removeEventListener('abort', this._boundXhrOnAbort, false); | ||
this.xhr.removeEventListener('progress', this._boundOnProgress, false); | ||
this.xhr.removeEventListener('load', this._boundXhrOnLoad, false); | ||
} | ||
else { | ||
this.xhr.onerror = null; | ||
this.xhr.ontimeout = null; | ||
this.xhr.onprogress = null; | ||
this.xhr.onload = null; | ||
} | ||
} | ||
/** | ||
* The `_onError` function bound to this resource's context. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._boundOnError = this._onError.bind(this); | ||
if (this.isComplete) { | ||
throw new Error('Complete called again for an already completed resource.'); | ||
} | ||
/** | ||
* The `_onProgress` function bound to this resource's context. | ||
* | ||
* @private | ||
* @member {function} | ||
*/ | ||
this._boundOnProgress = this._onProgress.bind(this); | ||
this._setFlag(Resource.STATUS_FLAGS.COMPLETE, true); | ||
this._setFlag(Resource.STATUS_FLAGS.LOADING, false); | ||
// xhr callbacks | ||
this._boundXhrOnError = this._xhrOnError.bind(this); | ||
this._boundXhrOnAbort = this._xhrOnAbort.bind(this); | ||
this._boundXhrOnLoad = this._xhrOnLoad.bind(this); | ||
this._boundXdrOnTimeout = this._xdrOnTimeout.bind(this); | ||
this.onComplete.dispatch(this); | ||
} | ||
/** | ||
* Emitted when the resource beings to load. | ||
* Aborts the loading of this resource, with an optional message. | ||
* | ||
* @event start | ||
* @memberof Resource# | ||
* @param {string} message - The message to use for the error | ||
*/ | ||
abort(message) { | ||
// abort can be called multiple times, ignore subsequent calls. | ||
if (this.error) { | ||
return; | ||
} | ||
/** | ||
* Emitted each time progress of this resource load updates. | ||
* Not all resources types and loader systems can support this event | ||
* so sometimes it may not be available. If the resource | ||
* is being loaded on a modern browser, using XHR, and the remote server | ||
* properly sets Content-Length headers, then this will be available. | ||
* | ||
* @event progress | ||
* @memberof Resource# | ||
*/ | ||
// store error | ||
this.error = new Error(message); | ||
// abort the actual loading | ||
if (this.xhr) { | ||
this.xhr.abort(); | ||
} | ||
else if (this.xdr) { | ||
this.xdr.abort(); | ||
} | ||
else if (this.data) { | ||
// single source | ||
if (this.data.src) { | ||
this.data.src = Resource.EMPTY_GIF; | ||
} | ||
// multi-source | ||
else { | ||
while (this.data.firstChild) { | ||
this.data.removeChild(this.data.firstChild); | ||
} | ||
} | ||
} | ||
// done now. | ||
this.complete(); | ||
} | ||
/** | ||
* Emitted once this resource has loaded, if there was an error it will | ||
* be in the `error` property. | ||
* Kicks off loading of this resource. This method is asynchronous. | ||
* | ||
* @event complete | ||
* @memberof Resource# | ||
* @param {function} [cb] - Optional callback to call once the resource is loaded. | ||
*/ | ||
} | ||
load(cb) { | ||
if (this.isLoading) { | ||
return; | ||
} | ||
Resource.prototype = Object.create(EventEmitter.prototype); | ||
Resource.prototype.constructor = Resource; | ||
module.exports = Resource; | ||
if (this.isComplete) { | ||
if (cb) { | ||
setTimeout(() => cb(this), 1); | ||
} | ||
/** | ||
* Marks the resource as complete. | ||
* | ||
* @fires complete | ||
*/ | ||
Resource.prototype.complete = function () { | ||
// TODO: Clean this up in a wrapper or something...gross.... | ||
if (this.data && this.data.removeEventListener) { | ||
this.data.removeEventListener('error', this._boundOnError, false); | ||
this.data.removeEventListener('load', this._boundComplete, false); | ||
this.data.removeEventListener('progress', this._boundOnProgress, false); | ||
this.data.removeEventListener('canplaythrough', this._boundComplete, false); | ||
} | ||
if (this.xhr) { | ||
if (this.xhr.removeEventListener) { | ||
this.xhr.removeEventListener('error', this._boundXhrOnError, false); | ||
this.xhr.removeEventListener('abort', this._boundXhrOnAbort, false); | ||
this.xhr.removeEventListener('progress', this._boundOnProgress, false); | ||
this.xhr.removeEventListener('load', this._boundXhrOnLoad, false); | ||
return; | ||
} | ||
else { | ||
this.xhr.onerror = null; | ||
this.xhr.ontimeout = null; | ||
this.xhr.onprogress = null; | ||
this.xhr.onload = null; | ||
else if (cb) { | ||
this.onComplete.once(cb); | ||
} | ||
} | ||
if (this.isComplete) { | ||
throw new Error('Complete called again for an already completed resource.'); | ||
} | ||
this._setFlag(Resource.STATUS_FLAGS.LOADING, true); | ||
this.isComplete = true; | ||
this.isLoading = false; | ||
this.onStart.dispatch(this); | ||
this.emit('complete', this); | ||
}; | ||
// if unset, determine the value | ||
if (this.crossOrigin === false || typeof this.crossOrigin !== 'string') { | ||
this.crossOrigin = this._determineCrossOrigin(this.url); | ||
} | ||
/** | ||
* Aborts the loading of this resource, with an optional message. | ||
* | ||
* @param {string} message - The message to use for the error | ||
*/ | ||
Resource.prototype.abort = function (message) { | ||
// abort can be called multiple times, ignore subsequent calls. | ||
if (this.error) { | ||
return; | ||
} | ||
switch (this.loadType) { | ||
case Resource.LOAD_TYPE.IMAGE: | ||
this.type = Resource.TYPE.IMAGE; | ||
this._loadElement('image'); | ||
break; | ||
// store error | ||
this.error = new Error(message); | ||
case Resource.LOAD_TYPE.AUDIO: | ||
this.type = Resource.TYPE.AUDIO; | ||
this._loadSourceElement('audio'); | ||
break; | ||
// abort the actual loading | ||
if (this.xhr) { | ||
this.xhr.abort(); | ||
} | ||
else if (this.xdr) { | ||
this.xdr.abort(); | ||
} | ||
else if (this.data) { | ||
// single source | ||
if (typeof this.data.src !== 'undefined') { | ||
this.data.src = ''; | ||
case Resource.LOAD_TYPE.VIDEO: | ||
this.type = Resource.TYPE.VIDEO; | ||
this._loadSourceElement('video'); | ||
break; | ||
case Resource.LOAD_TYPE.XHR: | ||
/* falls through */ | ||
default: | ||
if (useXdr && this.crossOrigin) { | ||
this._loadXdr(); | ||
} | ||
else { | ||
this._loadXhr(); | ||
} | ||
break; | ||
} | ||
// multi-source | ||
else { | ||
while (this.data.firstChild) { | ||
this.data.removeChild(this.data.firstChild); | ||
} | ||
} | ||
} | ||
// done now. | ||
this.complete(); | ||
}; | ||
/** | ||
* Checks if the flag is set. | ||
* | ||
* @private | ||
* @param {number} flag - The flag to check. | ||
* @return {boolean} True if the flag is set. | ||
*/ | ||
_hasFlag(flag) { | ||
return !!(this._flags & flag); | ||
} | ||
/** | ||
* Kicks off loading of this resource. This method is asynchronous. | ||
* | ||
* @fires start | ||
* @param {function} [cb] - Optional callback to call once the resource is loaded. | ||
*/ | ||
Resource.prototype.load = function (cb) { | ||
if (this.isLoading) { | ||
return; | ||
/** | ||
* (Un)Sets the flag. | ||
* | ||
* @private | ||
* @param {number} flag - The flag to (un)set. | ||
* @param {boolean} value - Whether to set or (un)set the flag. | ||
*/ | ||
_setFlag(flag, value) { | ||
this._flags = value ? (this._flags | flag) : (this._flags & ~flag); | ||
} | ||
if (this.isComplete) { | ||
if (cb) { | ||
var self = this; | ||
/** | ||
* Loads this resources using an element that has a single source, | ||
* like an HTMLImageElement. | ||
* | ||
* @private | ||
* @param {string} type - The type of element to use. | ||
*/ | ||
_loadElement(type) { | ||
if (this.metadata.loadElement) { | ||
this.data = this.metadata.loadElement; | ||
} | ||
else if (type === 'image' && typeof window.Image !== 'undefined') { | ||
this.data = new Image(); | ||
} | ||
else { | ||
this.data = document.createElement(type); | ||
} | ||
setTimeout(function () { | ||
cb(self); | ||
}, 1); | ||
if (this.crossOrigin) { | ||
this.data.crossOrigin = this.crossOrigin; | ||
} | ||
return; | ||
} | ||
else if (cb) { | ||
this.once('complete', cb); | ||
} | ||
if (!this.metadata.skipSource) { | ||
this.data.src = this.url; | ||
} | ||
this.isLoading = true; | ||
this.emit('start', this); | ||
// if unset, determine the value | ||
if (this.crossOrigin === false || typeof this.crossOrigin !== 'string') { | ||
this.crossOrigin = this._determineCrossOrigin(this.url); | ||
this.data.addEventListener('error', this._boundOnError, false); | ||
this.data.addEventListener('load', this._boundComplete, false); | ||
this.data.addEventListener('progress', this._boundOnProgress, false); | ||
} | ||
switch (this.loadType) { | ||
case Resource.LOAD_TYPE.IMAGE: | ||
this._loadElement('image'); | ||
break; | ||
/** | ||
* Loads this resources using an element that has multiple sources, | ||
* like an HTMLAudioElement or HTMLVideoElement. | ||
* | ||
* @private | ||
* @param {string} type - The type of element to use. | ||
*/ | ||
_loadSourceElement(type) { | ||
if (this.metadata.loadElement) { | ||
this.data = this.metadata.loadElement; | ||
} | ||
else if (type === 'audio' && typeof window.Audio !== 'undefined') { | ||
this.data = new Audio(); | ||
} | ||
else { | ||
this.data = document.createElement(type); | ||
} | ||
case Resource.LOAD_TYPE.AUDIO: | ||
this._loadSourceElement('audio'); | ||
break; | ||
if (this.data === null) { | ||
this.abort(`Unsupported element: ${type}`); | ||
case Resource.LOAD_TYPE.VIDEO: | ||
this._loadSourceElement('video'); | ||
break; | ||
return; | ||
} | ||
case Resource.LOAD_TYPE.XHR: | ||
/* falls through */ | ||
default: | ||
if (useXdr && this.crossOrigin) { | ||
this._loadXdr(); | ||
if (!this.metadata.skipSource) { | ||
// support for CocoonJS Canvas+ runtime, lacks document.createElement('source') | ||
if (navigator.isCocoonJS) { | ||
this.data.src = Array.isArray(this.url) ? this.url[0] : this.url; | ||
} | ||
else if (Array.isArray(this.url)) { | ||
for (let i = 0; i < this.url.length; ++i) { | ||
this.data.appendChild(this._createSource(type, this.url[i])); | ||
} | ||
} | ||
else { | ||
this._loadXhr(); | ||
this.data.appendChild(this._createSource(type, this.url)); | ||
} | ||
break; | ||
} | ||
}; | ||
} | ||
/** | ||
* Loads this resources using an element that has a single source, | ||
* like an HTMLImageElement. | ||
* | ||
* @private | ||
* @param {string} type - The type of element to use. | ||
*/ | ||
Resource.prototype._loadElement = function (type) { | ||
if (this.metadata.loadElement) { | ||
this.data = this.metadata.loadElement; | ||
} | ||
else if (type === 'image' && typeof window.Image !== 'undefined') { | ||
this.data = new Image(); | ||
} | ||
else { | ||
this.data = document.createElement(type); | ||
} | ||
this.data.addEventListener('error', this._boundOnError, false); | ||
this.data.addEventListener('load', this._boundComplete, false); | ||
this.data.addEventListener('progress', this._boundOnProgress, false); | ||
this.data.addEventListener('canplaythrough', this._boundComplete, false); | ||
if (this.crossOrigin) { | ||
this.data.crossOrigin = this.crossOrigin; | ||
this.data.load(); | ||
} | ||
if (!this.metadata.skipSource) { | ||
this.data.src = this.url; | ||
} | ||
/** | ||
* Loads this resources using an XMLHttpRequest. | ||
* | ||
* @private | ||
*/ | ||
_loadXhr() { | ||
// if unset, determine the value | ||
if (typeof this.xhrType !== 'string') { | ||
this.xhrType = this._determineXhrType(); | ||
} | ||
var typeName = 'is' + type[0].toUpperCase() + type.substring(1); | ||
const xhr = this.xhr = new XMLHttpRequest(); | ||
if (this[typeName] === false) { | ||
this[typeName] = true; | ||
} | ||
// set the request type and url | ||
xhr.open('GET', this.url, true); | ||
this.data.addEventListener('error', this._boundOnError, false); | ||
this.data.addEventListener('load', this._boundComplete, false); | ||
this.data.addEventListener('progress', this._boundOnProgress, false); | ||
}; | ||
/** | ||
* Loads this resources using an element that has multiple sources, | ||
* like an HTMLAudioElement or HTMLVideoElement. | ||
* | ||
* @private | ||
* @param {string} type - The type of element to use. | ||
*/ | ||
Resource.prototype._loadSourceElement = function (type) { | ||
if (this.metadata.loadElement) { | ||
this.data = this.metadata.loadElement; | ||
} | ||
else if (type === 'audio' && typeof window.Audio !== 'undefined') { | ||
this.data = new Audio(); | ||
} | ||
else { | ||
this.data = document.createElement(type); | ||
} | ||
if (this.data === null) { | ||
this.abort('Unsupported element ' + type); | ||
return; | ||
} | ||
if (!this.metadata.skipSource) { | ||
// support for CocoonJS Canvas+ runtime, lacks document.createElement('source') | ||
if (navigator.isCocoonJS) { | ||
this.data.src = Array.isArray(this.url) ? this.url[0] : this.url; | ||
// load json as text and parse it ourselves. We do this because some browsers | ||
// *cough* safari *cough* can't deal with it. | ||
if (this.xhrType === Resource.XHR_RESPONSE_TYPE.JSON || this.xhrType === Resource.XHR_RESPONSE_TYPE.DOCUMENT) { | ||
xhr.responseType = Resource.XHR_RESPONSE_TYPE.TEXT; | ||
} | ||
else if (Array.isArray(this.url)) { | ||
for (var i = 0; i < this.url.length; ++i) { | ||
this.data.appendChild(this._createSource(type, this.url[i])); | ||
} | ||
} | ||
else { | ||
this.data.appendChild(this._createSource(type, this.url)); | ||
xhr.responseType = this.xhrType; | ||
} | ||
} | ||
this['is' + type[0].toUpperCase() + type.substring(1)] = true; | ||
xhr.addEventListener('error', this._boundXhrOnError, false); | ||
xhr.addEventListener('abort', this._boundXhrOnAbort, false); | ||
xhr.addEventListener('progress', this._boundOnProgress, false); | ||
xhr.addEventListener('load', this._boundXhrOnLoad, false); | ||
this.data.addEventListener('error', this._boundOnError, false); | ||
this.data.addEventListener('load', this._boundComplete, false); | ||
this.data.addEventListener('progress', this._boundOnProgress, false); | ||
this.data.addEventListener('canplaythrough', this._boundComplete, false); | ||
this.data.load(); | ||
}; | ||
/** | ||
* Loads this resources using an XMLHttpRequest. | ||
* | ||
* @private | ||
*/ | ||
Resource.prototype._loadXhr = function () { | ||
// if unset, determine the value | ||
if (typeof this.xhrType !== 'string') { | ||
this.xhrType = this._determineXhrType(); | ||
xhr.send(); | ||
} | ||
var xhr = this.xhr = new XMLHttpRequest(); | ||
/** | ||
* Loads this resources using an XDomainRequest. This is here because we need to support IE9 (gross). | ||
* | ||
* @private | ||
*/ | ||
_loadXdr() { | ||
// if unset, determine the value | ||
if (typeof this.xhrType !== 'string') { | ||
this.xhrType = this._determineXhrType(); | ||
} | ||
// set the request type and url | ||
xhr.open('GET', this.url, true); | ||
const xdr = this.xhr = new XDomainRequest(); | ||
// load json as text and parse it ourselves. We do this because some browsers | ||
// *cough* safari *cough* can't deal with it. | ||
if (this.xhrType === Resource.XHR_RESPONSE_TYPE.JSON || this.xhrType === Resource.XHR_RESPONSE_TYPE.DOCUMENT) { | ||
xhr.responseType = Resource.XHR_RESPONSE_TYPE.TEXT; | ||
} | ||
else { | ||
xhr.responseType = this.xhrType; | ||
} | ||
// XDomainRequest has a few quirks. Occasionally it will abort requests | ||
// A way to avoid this is to make sure ALL callbacks are set even if not used | ||
// More info here: http://stackoverflow.com/questions/15786966/xdomainrequest-aborts-post-on-ie-9 | ||
xdr.timeout = 5000; | ||
xhr.addEventListener('error', this._boundXhrOnError, false); | ||
xhr.addEventListener('abort', this._boundXhrOnAbort, false); | ||
xhr.addEventListener('progress', this._boundOnProgress, false); | ||
xhr.addEventListener('load', this._boundXhrOnLoad, false); | ||
xdr.onerror = this._boundXhrOnError; | ||
xdr.ontimeout = this._boundXdrOnTimeout; | ||
xdr.onprogress = this._boundOnProgress; | ||
xdr.onload = this._boundXhrOnLoad; | ||
xhr.send(); | ||
}; | ||
xdr.open('GET', this.url, true); | ||
/** | ||
* Loads this resources using an XDomainRequest. This is here because we need to support IE9 (gross). | ||
* | ||
* @private | ||
*/ | ||
Resource.prototype._loadXdr = function () { | ||
// if unset, determine the value | ||
if (typeof this.xhrType !== 'string') { | ||
this.xhrType = this._determineXhrType(); | ||
// Note: The xdr.send() call is wrapped in a timeout to prevent an | ||
// issue with the interface where some requests are lost if multiple | ||
// XDomainRequests are being sent at the same time. | ||
// Some info here: https://github.com/photonstorm/phaser/issues/1248 | ||
setTimeout(() => xdr.send(), 1); | ||
} | ||
var xdr = this.xhr = new XDomainRequest(); | ||
/** | ||
* Creates a source used in loading via an element. | ||
* | ||
* @private | ||
* @param {string} type - The element type (video or audio). | ||
* @param {string} url - The source URL to load from. | ||
* @param {string} [mime] - The mime type of the video | ||
* @return {HTMLSourceElement} The source element. | ||
*/ | ||
_createSource(type, url, mime) { | ||
if (!mime) { | ||
mime = `${type}/${url.substr(url.lastIndexOf('.') + 1)}`; | ||
} | ||
// XDomainRequest has a few quirks. Occasionally it will abort requests | ||
// A way to avoid this is to make sure ALL callbacks are set even if not used | ||
// More info here: http://stackoverflow.com/questions/15786966/xdomainrequest-aborts-post-on-ie-9 | ||
xdr.timeout = 5000; | ||
const source = document.createElement('source'); | ||
xdr.onerror = this._boundXhrOnError; | ||
xdr.ontimeout = this._boundXdrOnTimeout; | ||
xdr.onprogress = this._boundOnProgress; | ||
xdr.onload = this._boundXhrOnLoad; | ||
source.src = url; | ||
source.type = mime; | ||
xdr.open('GET', this.url, true); | ||
return source; | ||
} | ||
// Note: The xdr.send() call is wrapped in a timeout to prevent an | ||
// issue with the interface where some requests are lost if multiple | ||
// XDomainRequests are being sent at the same time. | ||
// Some info here: https://github.com/photonstorm/phaser/issues/1248 | ||
setTimeout(function () { | ||
xdr.send(); | ||
}, 0); | ||
}; | ||
/** | ||
* Called if a load errors out. | ||
* | ||
* @param {Event} event - The error event from the element that emits it. | ||
* @private | ||
*/ | ||
_onError(event) { | ||
this.abort(`Failed to load element using: ${event.target.nodeName}`); | ||
} | ||
/** | ||
* Creates a source used in loading via an element. | ||
* | ||
* @private | ||
* @param {string} type - The element type (video or audio). | ||
* @param {string} url - The source URL to load from. | ||
* @param {string} [mime] - The mime type of the video | ||
* @return {HTMLSourceElement} The source element. | ||
*/ | ||
Resource.prototype._createSource = function (type, url, mime) { | ||
if (!mime) { | ||
mime = type + '/' + url.substr(url.lastIndexOf('.') + 1); | ||
/** | ||
* Called if a load progress event fires for xhr/xdr. | ||
* | ||
* @private | ||
* @param {XMLHttpRequestProgressEvent|Event} event - Progress event. | ||
*/ | ||
_onProgress(event) { | ||
if (event && event.lengthComputable) { | ||
this.onProgress.dispatch(this, event.loaded / event.total); | ||
} | ||
} | ||
var source = document.createElement('source'); | ||
/** | ||
* Called if an error event fires for xhr/xdr. | ||
* | ||
* @private | ||
* @param {XMLHttpRequestErrorEvent|Event} event - Error event. | ||
*/ | ||
_xhrOnError() { | ||
const xhr = this.xhr; | ||
source.src = url; | ||
source.type = mime; | ||
this.abort(`${reqType(xhr)} Request failed. Status: ${xhr.status}, text: "${xhr.statusText}"`); | ||
} | ||
return source; | ||
}; | ||
/** | ||
* Called if an abort event fires for xhr. | ||
* | ||
* @private | ||
* @param {XMLHttpRequestAbortEvent} event - Abort Event | ||
*/ | ||
_xhrOnAbort() { | ||
this.abort(`${reqType(this.xhr)} Request was aborted by the user.`); | ||
} | ||
/** | ||
* Called if a load errors out. | ||
* | ||
* @param {Event} event - The error event from the element that emits it. | ||
* @private | ||
*/ | ||
Resource.prototype._onError = function (event) { | ||
this.abort('Failed to load element using ' + event.target.nodeName); | ||
}; | ||
/** | ||
* Called if a load progress event fires for xhr/xdr. | ||
* | ||
* @fires progress | ||
* @private | ||
* @param {XMLHttpRequestProgressEvent|Event} event - Progress event. | ||
*/ | ||
Resource.prototype._onProgress = function (event) { | ||
if (event && event.lengthComputable) { | ||
this.emit('progress', this, event.loaded / event.total); | ||
/** | ||
* Called if a timeout event fires for xdr. | ||
* | ||
* @private | ||
* @param {Event} event - Timeout event. | ||
*/ | ||
_xdrOnTimeout() { | ||
this.abort(`${reqType(this.xhr)} Request timed out.`); | ||
} | ||
}; | ||
/** | ||
* Called if an error event fires for xhr/xdr. | ||
* | ||
* @private | ||
* @param {XMLHttpRequestErrorEvent|Event} event - Error event. | ||
*/ | ||
Resource.prototype._xhrOnError = function () { | ||
var xhr = this.xhr; | ||
/** | ||
* Called when data successfully loads from an xhr/xdr request. | ||
* | ||
* @private | ||
* @param {XMLHttpRequestLoadEvent|Event} event - Load event | ||
*/ | ||
_xhrOnLoad() { | ||
const xhr = this.xhr; | ||
const status = typeof xhr.status === 'undefined' ? xhr.status : STATUS_OK; // XDR has no `.status`, assume 200. | ||
this.abort(reqType(xhr) + ' Request failed. Status: ' + xhr.status + ', text: "' + xhr.statusText + '"'); | ||
}; | ||
// status can be 0 when using the `file://` protocol so we also check if a response is set | ||
if (status === STATUS_OK | ||
|| status === STATUS_EMPTY | ||
|| (status === STATUS_NONE && xhr.responseText.length > 0) | ||
) { | ||
// if text, just return it | ||
if (this.xhrType === Resource.XHR_RESPONSE_TYPE.TEXT) { | ||
this.data = xhr.responseText; | ||
this.type = Resource.TYPE.TEXT; | ||
} | ||
// if json, parse into json object | ||
else if (this.xhrType === Resource.XHR_RESPONSE_TYPE.JSON) { | ||
try { | ||
this.data = JSON.parse(xhr.responseText); | ||
this.type = Resource.TYPE.JSON; | ||
} | ||
catch (e) { | ||
this.abort(`Error trying to parse loaded json: ${e}`); | ||
/** | ||
* Called if an abort event fires for xhr. | ||
* | ||
* @private | ||
* @param {XMLHttpRequestAbortEvent} event - Abort Event | ||
*/ | ||
Resource.prototype._xhrOnAbort = function () { | ||
this.abort(reqType(this.xhr) + ' Request was aborted by the user.'); | ||
}; | ||
return; | ||
} | ||
} | ||
// if xml, parse into an xml document or div element | ||
else if (this.xhrType === Resource.XHR_RESPONSE_TYPE.DOCUMENT) { | ||
try { | ||
if (window.DOMParser) { | ||
const domparser = new DOMParser(); | ||
/** | ||
* Called if a timeout event fires for xdr. | ||
* | ||
* @private | ||
* @param {Event} event - Timeout event. | ||
*/ | ||
Resource.prototype._xdrOnTimeout = function () { | ||
this.abort(reqType(this.xhr) + ' Request timed out.'); | ||
}; | ||
this.data = domparser.parseFromString(xhr.responseText, 'text/xml'); | ||
} | ||
else { | ||
const div = document.createElement('div'); | ||
/** | ||
* Called when data successfully loads from an xhr/xdr request. | ||
* | ||
* @private | ||
* @param {XMLHttpRequestLoadEvent|Event} event - Load event | ||
*/ | ||
Resource.prototype._xhrOnLoad = function () { | ||
var xhr = this.xhr; | ||
var status = typeof xhr.status === 'undefined' ? xhr.status : STATUS_OK; // XDR has no `.status`, assume 200. | ||
div.innerHTML = xhr.responseText; | ||
// status can be 0 when using the file:// protocol, also check if a response was found | ||
if (status === STATUS_OK || status === STATUS_EMPTY || (status === STATUS_NONE && xhr.responseText.length > 0)) { | ||
// if text, just return it | ||
if (this.xhrType === Resource.XHR_RESPONSE_TYPE.TEXT) { | ||
this.data = xhr.responseText; | ||
} | ||
// if json, parse into json object | ||
else if (this.xhrType === Resource.XHR_RESPONSE_TYPE.JSON) { | ||
try { | ||
this.data = JSON.parse(xhr.responseText); | ||
this.isJson = true; | ||
} | ||
catch (e) { | ||
this.abort('Error trying to parse loaded json:', e); | ||
this.data = div; | ||
} | ||
return; | ||
} | ||
} | ||
// if xml, parse into an xml document or div element | ||
else if (this.xhrType === Resource.XHR_RESPONSE_TYPE.DOCUMENT) { | ||
try { | ||
if (window.DOMParser) { | ||
var domparser = new DOMParser(); | ||
this.data = domparser.parseFromString(xhr.responseText, 'text/xml'); | ||
this.type = Resource.TYPE.XML; | ||
} | ||
else { | ||
var div = document.createElement('div'); | ||
catch (e) { | ||
this.abort(`Error trying to parse loaded xml: ${e}`); | ||
div.innerHTML = xhr.responseText; | ||
this.data = div; | ||
return; | ||
} | ||
this.isXml = true; | ||
} | ||
catch (e) { | ||
this.abort('Error trying to parse loaded xml:', e); | ||
return; | ||
// other types just return the response | ||
else { | ||
this.data = xhr.response || xhr.responseText; | ||
} | ||
} | ||
// other types just return the response | ||
else { | ||
this.data = xhr.response || xhr.responseText; | ||
this.abort(`[${xhr.status}] ${xhr.statusText}: ${xhr.responseURL}`); | ||
return; | ||
} | ||
} | ||
else { | ||
this.abort('[' + xhr.status + ']' + xhr.statusText + ':' + xhr.responseURL); | ||
return; | ||
this.complete(); | ||
} | ||
this.complete(); | ||
}; | ||
/** | ||
* Sets the `crossOrigin` property for this resource based on if the url | ||
* for this resource is cross-origin. If crossOrigin was manually set, this | ||
* function does nothing. | ||
* | ||
* @private | ||
* @param {string} url - The url to test. | ||
* @param {object} [loc=window.location] - The location object to test against. | ||
* @return {string} The crossOrigin value to use (or empty string for none). | ||
*/ | ||
_determineCrossOrigin(url, loc) { | ||
// data: and javascript: urls are considered same-origin | ||
if (url.indexOf('data:') === 0) { | ||
return ''; | ||
} | ||
/** | ||
* Sets the `crossOrigin` property for this resource based on if the url | ||
* for this resource is cross-origin. If crossOrigin was manually set, this | ||
* function does nothing. | ||
* | ||
* @private | ||
* @param {string} url - The url to test. | ||
* @param {object} [loc=window.location] - The location object to test against. | ||
* @return {string} The crossOrigin value to use (or empty string for none). | ||
*/ | ||
Resource.prototype._determineCrossOrigin = function (url, loc) { | ||
// data: and javascript: urls are considered same-origin | ||
if (url.indexOf('data:') === 0) { | ||
return ''; | ||
} | ||
// default is window.location | ||
loc = loc || window.location; | ||
// default is window.location | ||
loc = loc || window.location; | ||
if (!tempAnchor) { | ||
tempAnchor = document.createElement('a'); | ||
} | ||
if (!tempAnchor) { | ||
tempAnchor = document.createElement('a'); | ||
} | ||
// let the browser determine the full href for the url of this resource and then | ||
// parse with the node url lib, we can't use the properties of the anchor element | ||
// because they don't work in IE9 :( | ||
tempAnchor.href = url; | ||
url = parseUri(tempAnchor.href, { strictMode: true }); | ||
// let the browser determine the full href for the url of this resource and then | ||
// parse with the node url lib, we can't use the properties of the anchor element | ||
// because they don't work in IE9 :( | ||
tempAnchor.href = url; | ||
url = parseUri(tempAnchor.href, { strictMode: true }); | ||
const samePort = (!url.port && loc.port === '') || (url.port === loc.port); | ||
const protocol = url.protocol ? `${url.protocol}:` : ''; | ||
var samePort = (!url.port && loc.port === '') || (url.port === loc.port); | ||
var protocol = url.protocol ? url.protocol + ':' : ''; | ||
// if cross origin | ||
if (url.host !== loc.hostname || !samePort || protocol !== loc.protocol) { | ||
return 'anonymous'; | ||
} | ||
// if cross origin | ||
if (url.host !== loc.hostname || !samePort || protocol !== loc.protocol) { | ||
return 'anonymous'; | ||
return ''; | ||
} | ||
return ''; | ||
}; | ||
/** | ||
* Determines the responseType of an XHR request based on the extension of the | ||
* resource being loaded. | ||
* | ||
* @private | ||
* @return {Resource.XHR_RESPONSE_TYPE} The responseType to use. | ||
*/ | ||
_determineXhrType() { | ||
return Resource._xhrTypeMap[this._getExtension()] || Resource.XHR_RESPONSE_TYPE.TEXT; | ||
} | ||
/** | ||
* Determines the responseType of an XHR request based on the extension of the | ||
* resource being loaded. | ||
* | ||
* @private | ||
* @return {Resource.XHR_RESPONSE_TYPE} The responseType to use. | ||
*/ | ||
Resource.prototype._determineXhrType = function () { | ||
return Resource._xhrTypeMap[this._getExtension()] || Resource.XHR_RESPONSE_TYPE.TEXT; | ||
}; | ||
/** | ||
* Determines the loadType of a resource based on the extension of the | ||
* resource being loaded. | ||
* | ||
* @private | ||
* @return {Resource.LOAD_TYPE} The loadType to use. | ||
*/ | ||
_determineLoadType() { | ||
return Resource._loadTypeMap[this._getExtension()] || Resource.LOAD_TYPE.XHR; | ||
} | ||
Resource.prototype._determineLoadType = function () { | ||
return Resource._loadTypeMap[this._getExtension()] || Resource.LOAD_TYPE.XHR; | ||
}; | ||
/** | ||
* Extracts the extension (sans '.') of the file being loaded by the resource. | ||
* | ||
* @private | ||
* @return {string} The extension. | ||
*/ | ||
_getExtension() { | ||
let url = this.url; | ||
let ext = ''; | ||
Resource.prototype._getExtension = function () { | ||
var url = this.url; | ||
var ext = ''; | ||
if (this.isDataUrl) { | ||
const slashIndex = url.indexOf('/'); | ||
if (this.isDataUrl) { | ||
var slashIndex = url.indexOf('/'); | ||
ext = url.substring(slashIndex + 1, url.indexOf(';', slashIndex)); | ||
} | ||
else { | ||
const queryStart = url.indexOf('?'); | ||
ext = url.substring(slashIndex + 1, url.indexOf(';', slashIndex)); | ||
} | ||
else { | ||
var queryStart = url.indexOf('?'); | ||
if (queryStart !== -1) { | ||
url = url.substring(0, queryStart); | ||
} | ||
if (queryStart !== -1) { | ||
url = url.substring(0, queryStart); | ||
ext = url.substring(url.lastIndexOf('.') + 1); | ||
} | ||
ext = url.substring(url.lastIndexOf('.') + 1); | ||
return ext.toLowerCase(); | ||
} | ||
return ext.toLowerCase(); | ||
}; | ||
/** | ||
* Determines the mime type of an XHR request based on the responseType of | ||
* resource being loaded. | ||
* | ||
* @private | ||
* @param {Resource.XHR_RESPONSE_TYPE} type - The type to get a mime type for. | ||
* @return {string} The mime type to use. | ||
*/ | ||
_getMimeFromXhrType(type) { | ||
switch (type) { | ||
case Resource.XHR_RESPONSE_TYPE.BUFFER: | ||
return 'application/octet-binary'; | ||
/** | ||
* Determines the mime type of an XHR request based on the responseType of | ||
* resource being loaded. | ||
* | ||
* @private | ||
* @param {Resource.XHR_RESPONSE_TYPE} type - The type to get a mime type for. | ||
* @return {string} The mime type to use. | ||
*/ | ||
Resource.prototype._getMimeFromXhrType = function (type) { | ||
switch (type) { | ||
case Resource.XHR_RESPONSE_TYPE.BUFFER: | ||
return 'application/octet-binary'; | ||
case Resource.XHR_RESPONSE_TYPE.BLOB: | ||
return 'application/blob'; | ||
case Resource.XHR_RESPONSE_TYPE.BLOB: | ||
return 'application/blob'; | ||
case Resource.XHR_RESPONSE_TYPE.DOCUMENT: | ||
return 'application/xml'; | ||
case Resource.XHR_RESPONSE_TYPE.DOCUMENT: | ||
return 'application/xml'; | ||
case Resource.XHR_RESPONSE_TYPE.JSON: | ||
return 'application/json'; | ||
case Resource.XHR_RESPONSE_TYPE.JSON: | ||
return 'application/json'; | ||
case Resource.XHR_RESPONSE_TYPE.DEFAULT: | ||
case Resource.XHR_RESPONSE_TYPE.TEXT: | ||
/* falls through */ | ||
default: | ||
return 'text/plain'; | ||
case Resource.XHR_RESPONSE_TYPE.DEFAULT: | ||
case Resource.XHR_RESPONSE_TYPE.TEXT: | ||
/* falls through */ | ||
default: | ||
return 'text/plain'; | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* The types of resources a resource could represent. | ||
* | ||
* @static | ||
* @readonly | ||
* @enum {number} | ||
*/ | ||
Resource.STATUS_FLAGS = { | ||
NONE: 0, | ||
DATA_URL: (1 << 0), | ||
COMPLETE: (1 << 1), | ||
LOADING: (1 << 2), | ||
}; | ||
/** | ||
* Quick helper to get string xhr type. | ||
* The types of resources a resource could represent. | ||
* | ||
* @ignore | ||
* @param {XMLHttpRequest|XDomainRequest} xhr - The request to check. | ||
* @return {string} The type. | ||
* @static | ||
* @readonly | ||
* @enum {number} | ||
*/ | ||
function reqType(xhr) { | ||
return xhr.toString().replace('object ', ''); | ||
} | ||
Resource.TYPE = { | ||
UNKNOWN: 0, | ||
JSON: 1, | ||
XML: 2, | ||
IMAGE: 3, | ||
AUDIO: 4, | ||
VIDEO: 5, | ||
TEXT: 6, | ||
}; | ||
@@ -815,3 +940,3 @@ /** | ||
/** Uses a `Video` object to load the resource. */ | ||
VIDEO: 4 | ||
VIDEO: 4, | ||
}; | ||
@@ -827,3 +952,3 @@ | ||
Resource.XHR_RESPONSE_TYPE = { | ||
/** defaults to text */ | ||
/** string */ | ||
DEFAULT: 'text', | ||
@@ -839,16 +964,27 @@ /** ArrayBuffer */ | ||
/** String */ | ||
TEXT: 'text' | ||
TEXT: 'text', | ||
}; | ||
Resource._loadTypeMap = { | ||
gif: Resource.LOAD_TYPE.IMAGE, | ||
png: Resource.LOAD_TYPE.IMAGE, | ||
bmp: Resource.LOAD_TYPE.IMAGE, | ||
jpg: Resource.LOAD_TYPE.IMAGE, | ||
jpeg: Resource.LOAD_TYPE.IMAGE, | ||
tif: Resource.LOAD_TYPE.IMAGE, | ||
tiff: Resource.LOAD_TYPE.IMAGE, | ||
webp: Resource.LOAD_TYPE.IMAGE, | ||
tga: Resource.LOAD_TYPE.IMAGE, | ||
'svg+xml': Resource.LOAD_TYPE.IMAGE | ||
// images | ||
gif: Resource.LOAD_TYPE.IMAGE, | ||
png: Resource.LOAD_TYPE.IMAGE, | ||
bmp: Resource.LOAD_TYPE.IMAGE, | ||
jpg: Resource.LOAD_TYPE.IMAGE, | ||
jpeg: Resource.LOAD_TYPE.IMAGE, | ||
tif: Resource.LOAD_TYPE.IMAGE, | ||
tiff: Resource.LOAD_TYPE.IMAGE, | ||
webp: Resource.LOAD_TYPE.IMAGE, | ||
tga: Resource.LOAD_TYPE.IMAGE, | ||
svg: Resource.LOAD_TYPE.IMAGE, | ||
'svg+xml': Resource.LOAD_TYPE.IMAGE, // for SVG data urls | ||
// audio | ||
mp3: Resource.LOAD_TYPE.AUDIO, | ||
ogg: Resource.LOAD_TYPE.AUDIO, | ||
wav: Resource.LOAD_TYPE.AUDIO, | ||
// videos | ||
mp4: Resource.LOAD_TYPE.VIDEO, | ||
webm: Resource.LOAD_TYPE.VIDEO, | ||
}; | ||
@@ -858,51 +994,49 @@ | ||
// xml | ||
xhtml: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
html: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
htm: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
xml: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
tmx: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
tsx: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
svg: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
xhtml: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
html: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
htm: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
xml: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
tmx: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
svg: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
// This was added to handle Tiled Tileset XML, but .tsx is also a TypeScript React Component. | ||
// Since it is way less likely for people to be loading TypeScript files instead of Tiled files, | ||
// this should probably be fine. | ||
tsx: Resource.XHR_RESPONSE_TYPE.DOCUMENT, | ||
// images | ||
gif: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
png: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
bmp: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
jpg: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
jpeg: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
tif: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
tiff: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
webp: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
tga: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
gif: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
png: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
bmp: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
jpg: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
jpeg: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
tif: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
tiff: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
webp: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
tga: Resource.XHR_RESPONSE_TYPE.BLOB, | ||
// json | ||
json: Resource.XHR_RESPONSE_TYPE.JSON, | ||
json: Resource.XHR_RESPONSE_TYPE.JSON, | ||
// text | ||
text: Resource.XHR_RESPONSE_TYPE.TEXT, | ||
txt: Resource.XHR_RESPONSE_TYPE.TEXT | ||
}; | ||
text: Resource.XHR_RESPONSE_TYPE.TEXT, | ||
txt: Resource.XHR_RESPONSE_TYPE.TEXT, | ||
/** | ||
* Sets the load type to be used for a specific extension. | ||
* | ||
* @static | ||
* @param {string} extname - The extension to set the type for, e.g. "png" or "fnt" | ||
* @param {Resource.LOAD_TYPE} loadType - The load type to set it to. | ||
*/ | ||
Resource.setExtensionLoadType = function (extname, loadType) { | ||
setExtMap(Resource._loadTypeMap, extname, loadType); | ||
// fonts | ||
ttf: Resource.XHR_RESPONSE_TYPE.BUFFER, | ||
otf: Resource.XHR_RESPONSE_TYPE.BUFFER, | ||
}; | ||
// We can't set the `src` attribute to empty string, so on abort we set it to this 1px transparent gif | ||
Resource.EMPTY_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='; | ||
/** | ||
* Sets the load type to be used for a specific extension. | ||
* Quick helper to set a value on one of the extension maps. Ensures there is no | ||
* dot at the start of the extension. | ||
* | ||
* @static | ||
* @param {string} extname - The extension to set the type for, e.g. "png" or "fnt" | ||
* @param {Resource.XHR_RESPONSE_TYPE} xhrType - The xhr type to set it to. | ||
* @ignore | ||
* @param {object} map - The map to set on. | ||
* @param {string} extname - The extension (or key) to set. | ||
* @param {number} val - The value to set. | ||
*/ | ||
Resource.setExtensionXhrType = function (extname, xhrType) { | ||
setExtMap(Resource._xhrTypeMap, extname, xhrType); | ||
}; | ||
function setExtMap(map, extname, val) { | ||
@@ -919,1 +1053,12 @@ if (extname && extname.indexOf('.') === 0) { | ||
} | ||
/** | ||
* Quick helper to get string xhr type. | ||
* | ||
* @ignore | ||
* @param {XMLHttpRequest|XDomainRequest} xhr - The request to check. | ||
* @return {string} The type. | ||
*/ | ||
function reqType(xhr) { | ||
return xhr.toString().replace('object ', ''); | ||
} |
Sorry, the diff of this file is too big to display
281815
3968
70
19
+ Addedmini-signals@^1.1.1
+ Addedmini-signals@1.2.0(transitive)
- Removedeventemitter3@^2.0.0
- Removedeventemitter3@2.0.3(transitive)