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