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