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