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 });