1 /*! 2 Copyright 2010 British Broadcasting Corporation 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 (function() { 17 var glowMap, 18 defaultBase, 19 document = window.document, 20 thisScriptSrc = ( document.body || document.getElementsByTagName('head')[0] ).lastChild.src; 21 22 // get default base from last script element 23 defaultBase = thisScriptSrc.slice( 0, thisScriptSrc.lastIndexOf('/') ) + '/../'; 24 25 // track when document is ready, must run before the page is finished loading 26 if (!document.readyState) { 27 if (document.addEventListener) { // like Mozilla 28 document.addEventListener('DOMContentLoaded', 29 function () { 30 document.removeEventListener('DOMContentLoaded', arguments.callee, false); 31 document.readyState = 'complete'; 32 }, 33 false 34 ); 35 } 36 } 37 38 /** 39 @public 40 @name Glow 41 @function 42 @description Creates an instance of the Glow JavaScript Library. 43 @param {string} [version] 44 @param {object} [opts] 45 @param {string} [opts.base] The path to the base folder, in which the Glow versions are kept. 46 @param {boolean} [opts.debug] Have all filenames modified to point to debug versions. 47 */ 48 window.Glow = function(version, opts) { /*debug*///log.info('new Glow("'+Array.prototype.join.call(arguments, '", "')+'")'); 49 opts = opts || {}; 50 51 var glowInstance, 52 debug = (opts.debug)? '.debug' : '', 53 base = opts.base || defaultBase; 54 55 glowMap = { 56 versions: ['2.0.0-alpha1', '@'+'SRC@'], 57 '2.0.0-alpha1': { 58 'core': ['core'+debug+'.js'], 59 'widgets': ['core', 'widgets'+debug+'.js', 'widgets'+debug+'.css'] 60 } 61 }; 62 63 if (opts._map) { glowMap = opts._map; } // for testing purposes map can be overridden 64 65 version = getVersion(version); /*debug*///log.info('Version is "'+version+'"'); 66 67 if (Glow._build.instances[version]) { /*debug*///log.info('instance for "'+version+'" already exists.'); 68 return Glow._build.instances[version]; 69 } 70 71 // opts.base should be formatted like a directory 72 if (base.slice(-1) !== '/') { 73 base += '/'; 74 } 75 76 glowInstance = createGlowInstance(version, base); 77 Glow._build.instances[version] = glowInstance; 78 79 glowInstance.UID = 'glow' + Math.floor(Math.random() * (1<<30)); 80 81 glowInstance.load('core'); // core is always loaded; 82 83 return glowInstance; 84 } 85 86 /** 87 @private 88 @name getVersion 89 @function 90 @param {string} version A (possibly imprecise) version identifier, like "2". 91 @description Find the most recent, available version of glow that matches. 92 */ 93 var getVersion = function(version) { /*debug*///log.info('getVersion("'+version+'")'); 94 var versions = glowMap.versions, 95 matchThis = version + '.'; 96 97 // TODO: an empty version means: the very latest version 98 99 var i = versions.length; 100 while (i--) { 101 if ( ( versions[i] + '.').indexOf(matchThis) === 0 ) { 102 return versions[i]; 103 } 104 } 105 106 throw new Error('Version "'+version+'" does not exist'); 107 } 108 109 /** 110 @private 111 @name getMap 112 @function 113 @description Find the file map for a given version. 114 @param {string} version Resolved identifier, like '2.0.0'. 115 @returns {object} A map of package names to files list. 116 */ 117 var getMap = function(version, debug) { /*debug*///log.info('getMap("'+version+'")'); 118 var versions = glowMap.versions, 119 map = null, 120 versionFound = false; 121 122 var i = versions.length; 123 while (--i > -1) { 124 if (glowMap[versions[i]]) { map = glowMap[versions[i]]; } 125 if (versions[i] === version) { versionFound = true; } 126 if (versionFound && map) { return map; } 127 } 128 129 throw new Error('No map available for version "' + version + '".'); 130 } 131 132 /** 133 @private 134 @name injectJs 135 @function 136 @description Start asynchronously loading an external JavaScript file. 137 */ 138 var injectJs = function(src) { /*debug*///log.info('injectJs("'+src+'")'); 139 var head, 140 script; 141 142 head = document.getElementsByTagName('head')[0]; 143 script = document.createElement('script'); 144 script.src = src; 145 script.type = 'text/javascript'; 146 147 head.insertBefore(script, head.firstChild); // rather than appendChild() to avoid IE bug when injecting SCRIPTs after BASE tag opens. see: http://shauninman.com/archive/2007/04/13/operation_aborted 148 } 149 150 /** 151 @private 152 @name injectCss 153 @function 154 @description Start asynchronously loading an external CSS file. 155 */ 156 var injectCss = function(src) { /*debug*///log.info('injectCss("'+src+'")'); 157 var head, 158 link; 159 160 head = document.getElementsByTagName('head')[0]; 161 link = document.createElement('link'); 162 link.href = src; 163 link.type = 'text/css'; 164 link.rel = 'stylesheet'; 165 166 head.insertBefore(link, head.firstChild); 167 } 168 169 /** @private */ 170 Glow._build = { 171 provided: [], // provided but not yet complete 172 instances: {} // built 173 } 174 175 /** 176 @private 177 @name Glow.provide 178 @function 179 @param {function} builder A function to run, given an instance of glow, and will add a feature to glow. 180 @description Provide a builder function to Glow as part of a package. 181 */ 182 Glow.provide = function(builder) { /*debug*///log.info('Glow.provide('+typeof builder+')'); 183 Glow._build.provided.push(builder); 184 } 185 186 /** 187 @private 188 @name Glow.complete 189 @function 190 @param {string} name The name of the completed package. 191 @param {string} version The version of the completed package. 192 @description Signals that no more builder functions will be provided by this package. 193 */ 194 Glow.complete = function(name, version) { /*debug*///log.info('complete('+name+', '+version+')'); 195 var glow, 196 loading, 197 builders; 198 199 // now that we have the name and version we can move the builders out of provided cache 200 glow = Glow._build.instances[version]; 201 if (!glow) { /*debug*///log.info('Cannot complete, unknown version of glow: '+version); 202 throw new Error('Cannot complete, unknown version of glow: '+version); 203 } 204 glow._build.builders[name] = Glow._build.provided; 205 Glow._build.provided = []; 206 207 // shortcuts 208 loading = glow._build.loading; 209 builders = glow._build.builders; 210 211 // try to build packages, in the same order they were loaded 212 for (var i = 0; i < loading.length; i++) { // loading.length may change during loop 213 if (!builders[loading[i]]) { /*debug*///log.info(loading[i]+' has no builders.'); 214 break; 215 } 216 217 // run the builders for this package in the same order they were loaded 218 for (var j = 0, jlen = builders[loading[i]].length; j < jlen; j++) { /*debug*///log.info('running builder '+j+ ' for '+loading[i]+' version '+glow.version); 219 builders[loading[i]][j](glow); // builder will modify glow 220 } 221 222 // remove this package from the loaded and builders list, now that it's built 223 if (glow._removeReadyBlock) { glow._removeReadyBlock('glow_loading_'+loading[i]); } 224 builders[loading[i]] = undefined; 225 loading.splice(i, 1); 226 i--; 227 228 229 } 230 231 // try to run onLoaded callbacks 232 glow._release(); 233 } 234 235 /** 236 @name createGlowInstance 237 @private 238 @function 239 @description Creates an instance of the Glow library. 240 @param {string} version 241 @param {string} base 242 */ 243 var createGlowInstance = function(version, base) { /*debug*///log.info('new glow("'+Array.prototype.join.call(arguments, '", "')+'")'); 244 var glow = function(nodeListContents) { 245 return new glow.NodeList(nodeListContents); 246 }; 247 248 glow.version = version; 249 glow.base = base; 250 glow.map = getMap(version); 251 glow._build = { 252 loading: [], // names of packages requested but not yet built, in same order as requested. 253 builders: {}, // completed but not yet built (waiting on dependencies). Like _build.builders[packageName]: [function, function, ...]. 254 history: {}, // names of every package ever loaded for this instance 255 callbacks: [] 256 }; 257 258 // copy properties from glowInstanceMembers 259 for (var prop in glowInstanceMembers) { 260 glow[prop] = glowInstanceMembers[prop]; 261 } 262 263 return glow; 264 } 265 266 267 /** 268 @name glowInstanceMembers 269 @private 270 @description All members of this object will be copied onto little-glow instances 271 @type {Object} 272 */ 273 var glowInstanceMembers = { 274 /** 275 @public 276 @name glow#load 277 @function 278 @description Add a package to this instance of the Glow library. 279 @param {string[]} ... The names of 1 or more packages to add. 280 */ 281 load: function() { /*debug*///log.info('glow.load("'+Array.prototype.join.call(arguments, '", "')+'") for version '+this.version); 282 var name = '', 283 src, 284 depends; 285 286 for (var i = 0, len = arguments.length; i < len; i++) { 287 name = arguments[i]; 288 289 if (this._build.history[name]) { /*debug*///log.info('already loaded package "'+name+'" for version '+this.version+', skipping.'); 290 continue; 291 } 292 293 this._build.history[name] = true; 294 295 // packages have dependencies, listed in the map: a single js file, css files, or even other packages 296 depends = this.map[name]; /*debug*///log.info('depends for '+name+' '+this.version+': "'+depends.join('", "')+'"'); 297 for (var j = 0, lenj = depends.length; j < lenj; j++) { 298 299 if (depends[j].slice(-3) === '.js') { /*debug*///log.info('dependent js: "'+depends[j]+'"'); 300 src = this.base + this.version + '/' + depends[j]; 301 302 // readyBlocks are removed in _release() 303 if (this._addReadyBlock) { this._addReadyBlock('glow_loading_'+name); } // provided by core 304 this._build.loading.push(name); 305 306 injectJs(src); 307 } 308 else if (depends[j].slice(-4) === '.css') { /*debug*///log.info('dependent css "'+depends[j]+'"'); 309 src = this.base + this.version + '/' + depends[j]; 310 injectCss(src); 311 } 312 else { /*debug*///log.info('dependent package: "'+depends[j]+'"'); 313 this.load(depends[j]); // recursively load dependency packages 314 } 315 } 316 } 317 318 return this; 319 }, 320 /** 321 @public 322 @name glow#loaded 323 @function 324 @param {function} onLoadCallback Called when all the packages load. 325 @description Do something when all the packages load. 326 */ 327 loaded: function(onLoadCallback) { /*debug*///log.info('glow.loaded('+typeof onLoadCallback+') for version '+this.version); 328 this._build.callbacks.push(onLoadCallback); 329 if (this._addReadyBlock) { this._addReadyBlock('glow_loading_loadedcallback'); } 330 331 this._release(); 332 333 return this; 334 }, 335 /** 336 @private 337 @name glow#_release 338 @function 339 @description If all loaded packages are now built, then run the onLoaded callbacks. 340 */ 341 _release: function() { /*debug*///log.info('glow._release("'+this.version+'")'); 342 var callback; 343 344 if (this._build.loading.length !== 0) { /*debug*///log.info('waiting for '+this._build.loading.length+' to finish.'); 345 return; 346 } 347 /*debug*///log.info('running '+this._build.callbacks.length+' loaded callbacks for version "'+this.version+'"'); 348 349 // run and remove each available _onloaded callback 350 while (callback = this._build.callbacks.shift()) { 351 callback(this); 352 if (this._removeReadyBlock) { this._removeReadyBlock('glow_loading_loadedcallback'); } 353 } 354 }, 355 /** 356 @name glow#ready 357 @function 358 @param {function} onReadyCallback Called when all the packages load and the DOM is available. 359 @description Do something when all the packages load and the DOM is ready. 360 */ 361 ready: function(onReadyCallback) { /*debug*///log.info('(ember) glow#ready('+typeof onReadyCallback+') for version '+this.version+'. There are '+this._build.loading.length+' loaded packages waiting to be built.'); 362 this.loaded(function(glow) { 363 glow.ready( function() { onReadyCallback(glow); } ); 364 }); 365 366 return this; 367 } 368 } 369 })(); 370