/libs/plugins/jquery.tmpl.js
JavaScript | 486 lines | 409 code | 19 blank | 58 comment | 74 complexity | 8ffddc974be8ff3982295fd8645759b5 MD5 | raw file
1/* 2 * jQuery Templating Plugin 3 * Copyright 2010, John Resig 4 * Dual licensed under the MIT or GPL Version 2 licenses. 5 */ 6(function( jQuery, undefined ){ 7 var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /, 8 newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = []; 9 10 function newTmplItem( options, parentItem, fn, data ) { 11 // Returns a template item data structure for a new rendered instance of a template (a 'template item'). 12 // The content field is a hierarchical array of strings and nested items (to be 13 // removed and replaced by nodes field of dom elements, once inserted in DOM). 14 var newItem = { 15 data: data || (parentItem ? parentItem.data : {}), 16 _wrap: parentItem ? parentItem._wrap : null, 17 tmpl: null, 18 parent: parentItem || null, 19 nodes: [], 20 calls: tiCalls, 21 nest: tiNest, 22 wrap: tiWrap, 23 html: tiHtml, 24 update: tiUpdate 25 }; 26 if ( options ) { 27 jQuery.extend( newItem, options, { nodes: [], parent: parentItem } ); 28 } 29 if ( fn ) { 30 // Build the hierarchical content to be used during insertion into DOM 31 newItem.tmpl = fn; 32 newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem ); 33 newItem.key = ++itemKey; 34 // Keep track of new template item, until it is stored as jQuery Data on DOM element 35 (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem; 36 } 37 return newItem; 38 } 39 40 // Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core). 41 jQuery.each({ 42 appendTo: "append", 43 prependTo: "prepend", 44 insertBefore: "before", 45 insertAfter: "after", 46 replaceAll: "replaceWith" 47 }, function( name, original ) { 48 jQuery.fn[ name ] = function( selector ) { 49 var ret = [], insert = jQuery( selector ), elems, i, l, tmplItems, 50 parent = this.length === 1 && this[0].parentNode; 51 52 appendToTmplItems = newTmplItems || {}; 53 if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { 54 insert[ original ]( this[0] ); 55 ret = this; 56 } else { 57 for ( i = 0, l = insert.length; i < l; i++ ) { 58 cloneIndex = i; 59 elems = (i > 0 ? this.clone(true) : this).get(); 60 jQuery.fn[ original ].apply( jQuery(insert[i]), elems ); 61 ret = ret.concat( elems ); 62 } 63 cloneIndex = 0; 64 ret = this.pushStack( ret, name, insert.selector ); 65 } 66 tmplItems = appendToTmplItems; 67 appendToTmplItems = null; 68 jQuery.tmpl.complete( tmplItems ); 69 return ret; 70 }; 71 }); 72 73 jQuery.fn.extend({ 74 // Use first wrapped element as template markup. 75 // Return wrapped set of template items, obtained by rendering template against data. 76 tmpl: function( data, options, parentItem ) { 77 return jQuery.tmpl( this[0], data, options, parentItem ); 78 }, 79 80 // Find which rendered template item the first wrapped DOM element belongs to 81 tmplItem: function() { 82 return jQuery.tmplItem( this[0] ); 83 }, 84 85 // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template. 86 template: function( name ) { 87 return jQuery.template( name, this[0] ); 88 }, 89 90 domManip: function( args, table, callback, options ) { 91 // This appears to be a bug in the appendTo, etc. implementation 92 // it should be doing .call() instead of .apply(). See #6227 93 if ( args[0] && args[0].nodeType ) { 94 var dmArgs = jQuery.makeArray( arguments ), argsLength = args.length, i = 0, tmplItem; 95 while ( i < argsLength && !(tmplItem = jQuery.data( args[i++], "tmplItem" ))) {} 96 if ( argsLength > 1 ) { 97 dmArgs[0] = [jQuery.makeArray( args )]; 98 } 99 if ( tmplItem && cloneIndex ) { 100 dmArgs[2] = function( fragClone ) { 101 // Handler called by oldManip when rendered template has been inserted into DOM. 102 jQuery.tmpl.afterManip( this, fragClone, callback ); 103 }; 104 } 105 oldManip.apply( this, dmArgs ); 106 } else { 107 oldManip.apply( this, arguments ); 108 } 109 cloneIndex = 0; 110 if ( !appendToTmplItems ) { 111 jQuery.tmpl.complete( newTmplItems ); 112 } 113 return this; 114 } 115 }); 116 117 jQuery.extend({ 118 // Return wrapped set of template items, obtained by rendering template against data. 119 tmpl: function( tmpl, data, options, parentItem ) { 120 var ret, topLevel = !parentItem; 121 if ( topLevel ) { 122 // This is a top-level tmpl call (not from a nested template using {{tmpl}}) 123 parentItem = topTmplItem; 124 tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl ); 125 wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level 126 } else if ( !tmpl ) { 127 // The template item is already associated with DOM - this is a refresh. 128 // Re-evaluate rendered template for the parentItem 129 tmpl = parentItem.tmpl; 130 newTmplItems[parentItem.key] = parentItem; 131 parentItem.nodes = []; 132 if ( parentItem.wrapped ) { 133 updateWrapped( parentItem, parentItem.wrapped ); 134 } 135 // Rebuild, without creating a new template item 136 return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) )); 137 } 138 if ( !tmpl ) { 139 return []; // Could throw... 140 } 141 if ( typeof data === "function" ) { 142 data = data.call( parentItem || {} ); 143 } 144 if ( options && options.wrapped ) { 145 updateWrapped( options, options.wrapped ); 146 } 147 ret = jQuery.isArray( data ) ? 148 jQuery.map( data, function( dataItem ) { 149 return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null; 150 }) : 151 [ newTmplItem( options, parentItem, tmpl, data ) ]; 152 return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret; 153 }, 154 155 // Return rendered template item for an element. 156 tmplItem: function( elem ) { 157 var tmplItem; 158 if ( elem instanceof jQuery ) { 159 elem = elem[0]; 160 } 161 while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {} 162 return tmplItem || topTmplItem; 163 }, 164 165 // Set: 166 // Use $.template( name, tmpl ) to cache a named template, 167 // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc. 168 // Use $( "selector" ).template( name ) to provide access by name to a script block template declaration. 169 170 // Get: 171 // Use $.template( name ) to access a cached template. 172 // Also $( selectorToScriptBlock ).template(), or $.template( null, templateString ) 173 // will return the compiled template, without adding a name reference. 174 // If templateString includes at least one HTML tag, $.template( templateString ) is equivalent 175 // to $.template( null, templateString ) 176 template: function( name, tmpl ) { 177 if (tmpl) { 178 // Compile template and associate with name 179 if ( typeof tmpl === "string" ) { 180 // This is an HTML string being passed directly in. 181 tmpl = buildTmplFn( tmpl ) 182 } else if ( tmpl instanceof jQuery ) { 183 tmpl = tmpl[0] || {}; 184 } 185 if ( tmpl.nodeType ) { 186 // If this is a template block, use cached copy, or generate tmpl function and cache. 187 tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML )); 188 } 189 return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl; 190 } 191 // Return named compiled template 192 return name ? (typeof name !== "string" ? jQuery.template( null, name ): 193 (jQuery.template[name] || 194 // If not in map, treat as a selector. (If integrated with core, use quickExpr.exec) 195 jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )))) : null; 196 }, 197 198 encode: function( text ) { 199 // Do HTML encoding replacing < > & and ' and " by corresponding entities. 200 return ("" + text).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'"); 201 } 202 }); 203 204 jQuery.extend( jQuery.tmpl, { 205 tag: { 206 "tmpl": { 207 _default: { $2: "null" }, 208 open: "if($notnull_1){_=_.concat($item.nest($1,$2));}" 209 // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions) 210 // This means that {{tmpl foo}} treats foo as a template (which IS a function). 211 // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}. 212 }, 213 "wrap": { 214 _default: { $2: "null" }, 215 open: "$item.calls(_,$1,$2);_=[];", 216 close: "call=$item.calls();_=call._.concat($item.wrap(call,_));" 217 }, 218 "each": { 219 _default: { $2: "$index, $value" }, 220 open: "if($notnull_1){$.each($1a,function($2){with(this){", 221 close: "}});}" 222 }, 223 "if": { 224 open: "if(($notnull_1) && $1a){", 225 close: "}" 226 }, 227 "else": { 228 _default: { $1: "true" }, 229 open: "}else if(($notnull_1) && $1a){" 230 }, 231 "html": { 232 // Unecoded expression evaluation. 233 open: "if($notnull_1){_.push($1a);}" 234 }, 235 "=": { 236 // Encoded expression evaluation. Abbreviated form is ${}. 237 _default: { $1: "$data" }, 238 open: "if($notnull_1){_.push($.encode($1a));}" 239 }, 240 "!": { 241 // Comment tag. Skipped by parser 242 open: "" 243 } 244 }, 245 246 // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events 247 complete: function( items ) { 248 newTmplItems = {}; 249 }, 250 251 // Call this from code which overrides domManip, or equivalent 252 // Manage cloning/storing template items etc. 253 afterManip: function afterManip( elem, fragClone, callback ) { 254 // Provides cloned fragment ready for fixup prior to and after insertion into DOM 255 var content = fragClone.nodeType === 11 ? 256 jQuery.makeArray(fragClone.childNodes) : 257 fragClone.nodeType === 1 ? [fragClone] : []; 258 259 // Return fragment to original caller (e.g. append) for DOM insertion 260 callback.call( elem, fragClone ); 261 262 // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data. 263 storeTmplItems( content ); 264 cloneIndex++; 265 } 266 }); 267 268 //========================== Private helper functions, used by code above ========================== 269 270 function build( tmplItem, nested, content ) { 271 // Convert hierarchical content into flat string array 272 // and finally return array of fragments ready for DOM insertion 273 var frag, ret = content ? jQuery.map( content, function( item ) { 274 return (typeof item === "string") ? 275 // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM. 276 (tmplItem.key ? item.replace( /(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : item) : 277 // This is a child template item. Build nested template. 278 build( item, tmplItem, item._ctnt ); 279 }) : 280 // If content is not defined, insert tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}. 281 tmplItem; 282 if ( nested ) { 283 return ret; 284 } 285 286 // top-level template 287 ret = ret.join(""); 288 289 // Support templates which have initial or final text nodes, or consist only of text 290 // Also support HTML entities within the HTML markup. 291 ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) { 292 frag = jQuery( middle ).get(); 293 294 storeTmplItems( frag ); 295 if ( before ) { 296 frag = unencode( before ).concat(frag); 297 } 298 if ( after ) { 299 frag = frag.concat(unencode( after )); 300 } 301 }); 302 return frag ? frag : unencode( ret ); 303 } 304 305 function unencode( text ) { 306 // Use createElement, since createTextNode will not render HTML entities correctly 307 var el = document.createElement( "div" ); 308 el.innerHTML = text; 309 return jQuery.makeArray(el.childNodes); 310 } 311 312 // Generate a reusable function that will serve to render a template against data 313 function buildTmplFn( markup ) { 314 return new Function("jQuery","$item", 315 "var $=jQuery,call,_=[],$data=$item.data;" + 316 317 // Introduce the data as local variables using with(){} 318 "with($data){_.push('" + 319 320 // Convert the template into pure JavaScript 321 jQuery.trim(markup) 322 .replace( /([\\'])/g, "\\$1" ) 323 .replace( /[\r\t\n]/g, " " ) 324 .replace( /\$\{([^\}]*)\}/g, "{{= $1}}" ) 325 .replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g, 326 function( all, slash, type, fnargs, target, parens, args ) { 327 var tag = jQuery.tmpl.tag[ type ], def, expr, exprAutoFnDetect; 328 if ( !tag ) { 329 throw "Template command not found: " + type; 330 } 331 def = tag._default || []; 332 if ( parens && !/\w$/.test(target)) { 333 target += parens; 334 parens = ""; 335 } 336 if ( target ) { 337 target = unescape( target ); 338 args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : ""); 339 // Support for target being things like a.toLowerCase(); 340 // In that case don't call with template item as 'this' pointer. Just evaluate... 341 expr = parens ? (target.indexOf(".") > -1 ? target + parens : ("(" + target + ").call($item" + args)) : target; 342 exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))"; 343 } else { 344 exprAutoFnDetect = expr = def.$1 || "null"; 345 } 346 fnargs = unescape( fnargs ); 347 return "');" + 348 tag[ slash ? "close" : "open" ] 349 .split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" ) 350 .split( "$1a" ).join( exprAutoFnDetect ) 351 .split( "$1" ).join( expr ) 352 .split( "$2" ).join( fnargs ? 353 fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) { 354 params = params ? ("," + params + ")") : (parens ? ")" : ""); 355 return params ? ("(" + name + ").call($item" + params) : all; 356 }) 357 : (def.$2||"") 358 ) + 359 "_.push('"; 360 }) + 361 "');}return _;" 362 ); 363 } 364 function updateWrapped( options, wrapped ) { 365 // Build the wrapped content. 366 options._wrap = build( options, true, 367 // Suport imperative scenario in which options.wrapped can be set to a selector or an HTML string. 368 jQuery.isArray( wrapped ) ? wrapped : [htmlExpr.test( wrapped ) ? wrapped : jQuery( wrapped ).html()] 369 ).join(""); 370 } 371 372 function unescape( args ) { 373 return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null; 374 } 375 function outerHtml( elem ) { 376 var div = document.createElement("div"); 377 div.appendChild( elem.cloneNode(true) ); 378 return div.innerHTML; 379 } 380 381 // Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance. 382 function storeTmplItems( content ) { 383 var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m; 384 for ( i = 0, l = content.length; i < l; i++ ) { 385 if ( (elem = content[i]).nodeType !== 1 ) { 386 continue; 387 } 388 elems = elem.getElementsByTagName("*"); 389 for ( m = elems.length - 1; m >= 0; m-- ) { 390 processItemKey( elems[m] ); 391 } 392 processItemKey( elem ); 393 } 394 function processItemKey( el ) { 395 var pntKey, pntNode = el, pntItem, tmplItem, key; 396 // Ensure that each rendered template inserted into the DOM has its own template item, 397 if ( (key = el.getAttribute( tmplItmAtt ))) { 398 while ( pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { } 399 if ( pntKey !== key ) { 400 // The next ancestor with a _tmplitem expando is on a different key than this one. 401 // So this is a top-level element within this template item 402 // Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment. 403 pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0)) : 0; 404 if ( !(tmplItem = newTmplItems[key]) ) { 405 // The item is for wrapped content, and was copied from the temporary parent wrappedItem. 406 tmplItem = wrappedItems[key]; 407 tmplItem = newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode], null, true ); 408 tmplItem.key = ++itemKey; 409 newTmplItems[itemKey] = tmplItem; 410 } 411 if ( cloneIndex ) { 412 cloneTmplItem( key ); 413 } 414 } 415 el.removeAttribute( tmplItmAtt ); 416 } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) { 417 // This was a rendered element, cloned during append or appendTo etc. 418 // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem. 419 cloneTmplItem( tmplItem.key ); 420 newTmplItems[tmplItem.key] = tmplItem; 421 pntNode = jQuery.data( el.parentNode, "tmplItem" ); 422 pntNode = pntNode ? pntNode.key : 0; 423 } 424 if ( tmplItem ) { 425 pntItem = tmplItem; 426 // Find the template item of the parent element. 427 // (Using !=, not !==, since pntItem.key is number, and pntNode may be a string) 428 while ( pntItem && pntItem.key != pntNode ) { 429 // Add this element as a top-level node for this rendered template item, as well as for any 430 // ancestor items between this item and the item of its parent element 431 pntItem.nodes.push( el ); 432 pntItem = pntItem.parent; 433 } 434 // Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering... 435 delete tmplItem._ctnt; 436 delete tmplItem._wrap; 437 // Store template item as jQuery data on the element 438 jQuery.data( el, "tmplItem", tmplItem ); 439 } 440 function cloneTmplItem( key ) { 441 key = key + keySuffix; 442 tmplItem = newClonedItems[key] = 443 (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true )); 444 } 445 } 446 } 447 448 //---- Helper functions for template item ---- 449 450 function tiCalls( content, tmpl, data, options ) { 451 if ( !content ) { 452 return stack.pop(); 453 } 454 stack.push({ _: content, tmpl: tmpl, item:this, data: data, options: options }); 455 } 456 457 function tiNest( tmpl, data, options ) { 458 // nested template, using {{tmpl}} tag 459 return jQuery.tmpl( jQuery.template( tmpl ), data, options, this ); 460 } 461 462 function tiWrap( call, wrapped ) { 463 // nested template, using {{wrap}} tag 464 var options = call.options || {}; 465 options.wrapped = wrapped; 466 // Apply the template, which may incorporate wrapped content, 467 return jQuery.tmpl( jQuery.template( call.tmpl ), call.data, options, call.item ); 468 } 469 470 function tiHtml( filter, textOnly ) { 471 var wrapped = this._wrap; 472 return jQuery.map( 473 jQuery( jQuery.isArray( wrapped ) ? wrapped.join("") : wrapped ).filter( filter || "*" ), 474 function(e) { 475 return textOnly ? 476 e.innerText || e.textContent : 477 e.outerHTML || outerHtml(e); 478 }); 479 } 480 481 function tiUpdate() { 482 var coll = this.nodes; 483 jQuery.tmpl( null, null, null, this).insertBefore( coll[0] ); 484 jQuery( coll ).remove(); 485 } 486})( jQuery );