1 /**	Manages views, providing methods for loading, templating, caching and swapping.
  2  *	@constructor Creates a new ViewManager instance.
  3  *	@augments puredom.EventEmitter
  4  *	@param {Object} [options]		Hashmap of options to be given to the instance.
  5  *	@param {Object} [options.init=false]				Immediately calls init(options) for you
  6  *	@param {Object} [options.viewDomPrefix=views_]		Custom DOM cache ID prefix
  7  *	@param {Object} [options.cacheBase=document.body]	Move the DOM view cache
  8  */
  9 puredom.ViewManager = function(options) {
 10 	puredom.EventEmitter.call(this);
 11 	this._htmlViews = {};
 12 	this._postProcessors = [];
 13 	if (options && options.init===true) {
 14 		this.init(options);
 15 	}
 16 };
 17 
 18 
 19 puredom.inherits(puredom.ViewManager, puredom.EventEmitter);
 20 
 21 
 22 puredom.extend(puredom.ViewManager.prototype, /** @lends puredom.ViewManager# */ {
 23 
 24 	/** ID prefix for view storage. */
 25 	viewDomPrefix : 'views_',
 26 	
 27 
 28 	/**	@private */
 29 	_regs : {
 30 		node : /<(\/?)([a-z][a-z0-9]*)(\s+[a-z0-9._-]+=(['"]).*?\4)*\s*\/?>/gim
 31 	},
 32 	
 33 
 34 	/**	Initialize the manager.
 35 	 *	@param {Object} [options]		Hashmap of options.
 36 	 *	@param {Object} [options.viewDomPrefix=views_]		Custom DOM cache ID prefix
 37 	 *	@param {Object} [options.cacheBase=document.body]	Move the DOM view cache
 38 	 */
 39 	init : function(options) {
 40 		options = options || {};
 41 		if (this.initialized!==true) {
 42 			this.initialized = true;
 43 			if (options.viewDomPrefix) {
 44 				this.viewDomPrefix = options.viewDomPrefix;
 45 			}
 46 			this.cacheBase = puredom.el({
 47 				className : 'views_cacheBase',
 48 				css : 'display: none;'
 49 			}, options.cacheBase || document.body);
 50 		}
 51 	},
 52 	
 53 
 54 	/**	Teardown and cleanup the manager. */
 55 	destroy : function() {
 56 		if (this.initialized===true) {
 57 			this.initialized = false;
 58 			try {
 59 				this.cacheBase.remove();
 60 			} catch(err) {}
 61 		}
 62 	},
 63 	
 64 
 65 	/**	@private */
 66 	log : function(msg, data) {
 67 		if (this.logging===true) {
 68 			puredom.log('ViewManager :: ' + msg, data);
 69 		}
 70 	},
 71 	
 72 
 73 	/**	Register a named view. */
 74 	addView : function(name, view) {
 75 		if (puredom.typeOf(view)==='string') {
 76 			this._htmlViews[(name+'').toLowerCase()] = view;
 77 		}
 78 		else {
 79 			this.load(name, false);
 80 		}
 81 	},
 82 	
 83 
 84 	/**	Check if a named view is registered. */
 85 	exists : function(name) {
 86 		return this._htmlViews.hasOwnProperty((name+'').toLowerCase());
 87 	},
 88 	
 89 
 90 	/**	Load a view and immediately template it. <br />
 91 	 *	See {@link puredom.ViewManager#load}
 92 	 */
 93 	template : function(name, fields, insertInto, insertBefore) {
 94 		var ui, template;
 95 		name = (name+'').toLowerCase();
 96 		fields = fields || {};
 97 		if (this._htmlViews.hasOwnProperty(name)) {
 98 			template = this._htmlViews[name];
 99 			//ui = this.buildViewFromHTML( puredom.template(template, fields) );
100 			ui = this.load(puredom.template(template, fields), insertInto, insertBefore, null, false);
101 		}
102 		return ui || false;
103 	},
104 	
105 
106 	/**	Load a view.
107 	 *	@param {String} name					The named view to load
108 	 *	@param {HTMLElement} [insertInto]		Immediately insert the view into an parent
109 	 *	@param {HTMLElement} [insertBefore]		Inject view into the parent of this node, before this node.
110 	 *	@param {Boolean} [cloneOriginal=true]	Set this to false to hijack previously rendered DOM views.
111 	 *	@param {Boolean} [caching=true]			Set this to false to turn off view caching.
112 	 *	@returns {puredom.NodeSelection} view, or false on error.
113 	 */
114 	load : function(name, insertInto, insertBefore, cloneOriginal, caching) {
115 		var ui, lookup, cached, origName=name;
116 		if (name) {
117 			cached = caching!==false && this.getCachedView(name);
118 			if (cached) {
119 				this.log('Using cached view for "'+name+'".');
120 				ui = this.buildCachedView(cached);
121 			}
122 			else if (puredom.typeOf(name)==='string') {
123 				this._regs.node.lastIndex = 0;
124 				if (this._regs.node.test(name)) {										// build from an HTML string
125 					this.log('Parsing HTML view for "'+name+'".');
126 					ui = this.buildViewFromHTML(name);
127 					//console.log('TEST:::', name, arguments);
128 					name = null;
129 				}
130 				else if (this._htmlViews.hasOwnProperty((name+'').toLowerCase())) {		// build from stored HTML
131 					this.log('Loading stored HTML view for "'+name+'".');
132 					ui = this.buildViewFromHTML(this._htmlViews[(name+'').toLowerCase()]);
133 				}
134 				else {
135 					this.log('Looking up in-DOM view for "'+name+'"...');
136 					lookup = this.getViewFromDOM(name);									// build from a DOM tree
137 					if (lookup && lookup.exists()) {
138 						this.log('Lookup succeeded, view found.');
139 						ui = this.buildViewFromDOM(lookup, cloneOriginal!==false);		// clone rendered view
140 					}
141 					else {
142 						this.log('Lookup failed, no matching view found.');
143 					}
144 				}
145 			}
146 		}
147 		
148 		if (ui && ui.exists()) {
149 			if (name) {
150 				this.log('View "'+name+'" loaded.');
151 				// cache views if not retrieved from the cache:
152 				if (caching!==false) {
153 					this.cacheView(name, ui, cloneOriginal!==false);
154 				}
155 				
156 				ui.classify('views_'+name);
157 			}
158 			
159 			if (!ui.parent().exists()) {
160 				if (insertBefore) {
161 					insertBefore.parent().insertBefore(ui, insertBefore);
162 				}
163 				if (!insertBefore && insertInto) {
164 					ui.insertInto(insertInto);
165 				}
166 			}
167 			
168 			this.postProcessView(ui);
169 			
170 			return ui;
171 		}
172 		else {
173 			puredom.log('ViewManager :: Unable to find view "'+name+'".');
174 			return false;
175 		}
176 	},
177 	
178 
179 	/**	Destroy a view. */
180 	unload : function(ui, unCache) {
181 		if (ui && ui.destroy) {
182 			ui.destroy();
183 		}
184 	},
185 	
186 	
187 	/** @private */
188 	_postProcessors : [],
189 	
190 
191 	/**	Register a post-processor function that will be run on all loaded views. <br />
192 	 *	The function gets passed the view base as a {@link puredom.NodeSelection}.
193 	 */
194 	addViewPostProcessor : function(callback) {
195 		var i, exists=false;
196 		for (i=this._postProcessors.length; i--; ) {
197 			if (this._postProcessors[i]===callback) {
198 				exists = true;
199 				break;
200 			}
201 		}
202 		if (!exists) {
203 			this._postProcessors.push(callback);
204 		}
205 	},
206 	
207 
208 	/**	Process a view after it has loaded.  Automatically called by load().
209 	 *	@private
210 	 */
211 	postProcessView : function(ui) {
212 		for (var i=0; i<this._postProcessors.length; i++) {
213 			this._postProcessors[i](ui);
214 		}
215 	},
216 	
217 	
218 	/**	@private */
219 	getViewFromDOM : function(def, customPrefix) {
220 		var el;
221 		customPrefix = customPrefix || this.viewDomPrefix;
222 		if (customPrefix.match(/[a-z0-9_-]/i)) {
223 			customPrefix = '#' + customPrefix;
224 		}
225 		def = def.replace(/[\s.\/\\]/gim, '');
226 		this.log('Lookup: prefix="'+customPrefix+'", def="'+def+'"');
227 		if (customPrefix) {
228 			el = puredom.el(customPrefix + def);
229 			if (el && el.exists()) {
230 				return el;
231 			}
232 		}
233 		return false;
234 	},
235 	
236 	
237 	/**	@private */
238 	buildViewFromHTML : function(html) {
239 		var node = puredom.el({
240 			innerHTML : html
241 		});
242 		node = node.children();
243 		node.remove();
244 		return node.exists() && node || false;
245 	},
246 	
247 	
248 	/**	@private */
249 	buildViewFromObj : function(obj) {
250 		var node;
251 		if (puredom.isArray(obj)) {
252 			node = tinyom.el({
253 				children : obj
254 			});
255 			node = node.children();
256 		}
257 		else {
258 			obj = puredom.extend({}, obj, {
259 				parent : null,
260 				insertBefore : null
261 			});
262 			node = puredom.el(obj);
263 		}
264 		return node.exists() && node || false;
265 	},
266 
267 	
268 	/**	@private */
269 	buildViewFromDOM : function(domNodes, clone) {
270 		var node, html;
271 		domNodes = puredom.el(domNodes);
272 		if (clone===false) {
273 			return domNodes;
274 		}
275 		node = domNodes.clone(true);			// deep, no parent
276 		return node.exists() && node || false;
277 	},
278 	
279 	
280 	/**	@private */
281 	_htmlViews : {},
282 	
283 	
284 	/** The root node where cached DOM nodes are stored
285 	 *	@private
286 	 */
287 	cacheBase : null,
288 	
289 
290 	/**	Attempt to retrieve a cached view
291 	 *	@private
292 	 */
293 	getCachedView : function(name) {
294 		var view = this.cacheBase.query('[data-viewname="'+name+'"]').first();
295 		return view.exists() && view || false;
296 	},
297 	
298 
299 	/** Build a view from the cache, optionally retrieving it if not passed a reference.
300 	 *	@private
301 	 */
302 	buildCachedView : function(cached) {
303 		var view;
304 		if (puredom.typeOf(cached)==='string') {
305 			cached = this.getCachedView(cached);
306 		}
307 		view = cached && this.buildViewFromDOM(cached);
308 		if (view) {
309 			view.attr('id', this.viewDomPrefix + view.attr('data-viewname'));
310 		}
311 		return view;
312 	},
313 	
314 
315 	/** Cache a view for future use.
316 	 *	@private
317 	 */
318 	cacheView : function(name, ui, copy) {
319 		/*
320 		if (copy===true) {
321 			ui = ui && this.buildViewFromDOM(ui);
322 		}
323 		this.cacheBase.query('[data-viewname='+name+']').destroy();
324 		ui.attr('data-viewname', name);
325 		ui.attr('id', null);
326 		ui._removeAllEvents();
327 		ui.insertInto(this.cacheBase);
328 		*/
329 		return false;
330 	}
331 	
332 });