/*
* jQuery Templating Plugin * NOTE: Created for demonstration purposes. * Copyright 2010, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. */
(function( jQuery, undefined ){
var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", newTmplItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0; function newTmplItem( options, parentItem, fn, data ) { // Returns a template item data structure for a new rendered instance of a template (a 'template item'). // The content field is a hierarchical array of strings and nested items (to be // removed and replaced by nodes field of dom elements, once inserted in DOM). var newItem = { data: data || (parentItem ? parentItem.data : {}), tmpl: null, parent: parentItem || null, nodes: [], nest: nest }; if ( options ) { jQuery.extend( newItem, options, { nodes: [], parent: parentItem } ); fn = fn || (typeof options.tmpl === "function" ? options.tmpl : null); } if ( fn ) { // Build the hierarchical content to be used during insertion into DOM newItem.tmpl = fn; newItem.content = newItem.tmpl( jQuery, newItem ); newItem.key = ++itemKey; // Keep track of new template item, until it is stored as jQuery Data on DOM element newTmplItems[itemKey] = newItem; } return newItem; } // Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core). jQuery.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { var ret = [], insert = jQuery( selector ), parent = this.length === 1 && this[0].parentNode; appendToTmplItems = newTmplItems || {}; if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { insert[ original ]( this[0] ); ret = this; } else { for ( var i = 0, l = insert.length; i < l; i++ ) { cloneIndex = i; var elems = (i > 0 ? this.clone(true) : this).get(); jQuery.fn[ original ].apply( jQuery(insert[i]), elems ); ret = ret.concat( elems ); } cloneIndex = 0; ret = this.pushStack( ret, name, insert.selector ); } var tmplItems = appendToTmplItems; appendToTmplItems = null; jQuery.tmpl.complete( tmplItems ); return ret; }; }); jQuery.fn.extend({ // Use first wrapped element as template markup. // Return wrapped set of template items, obtained by rendering template against data. tmpl: function( data, options, parentItem ) { return jQuery.tmpl( this[0], data, options, parentItem, parentItem === undefined ); }, // Find which rendered template item the first wrapped DOM element belongs to tmplItem: function() { return jQuery.tmplItem( this[0] ); }, // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template. templates: function( name ) { return jQuery.templates( name, this[0] ); }, domManip: function( args, table, callback, options ) { // This appears to be a bug in the appendTo, etc. implementation // it should be doing .call() instead of .apply(). See #6227 if ( args[0].nodeType ) { var dmArgs = jQuery.makeArray( arguments ), argsLength = args.length, i = 0, tmplItem; while ( i < argsLength && !(tmplItem = jQuery.data( args[i++], "tmplItem" ))) {}; if ( argsLength > 1 ) { dmArgs[0] = [jQuery.makeArray( args )]; } if ( tmplItem && cloneIndex ) { dmArgs[2] = function( fragClone ) { // Handler called by oldManip when rendered template has been inserted into DOM. jQuery.tmpl.afterManip( this, fragClone, callback ); } } oldManip.apply( this, dmArgs ); } else { oldManip.apply( this, arguments ); } cloneIndex = 0; if ( !appendToTmplItems ) { jQuery.tmpl.complete( newTmplItems ); } return this; } }); jQuery.extend({ // Return wrapped set of template items, obtained by rendering template against data. tmpl: function( tmpl, data, options, parentItem ) { var ret, topLevel = !parentItem; if ( topLevel ) { // This is a top-level tmpl call (not from a nested template using {{tmpl}}) parentItem = topTmplItem; tmpl = jQuery.templates[tmpl] || jQuery.templates( null, tmpl ); } else if ( !tmpl ) { // The template item is already associated with DOM - this is a refresh. // Re-evaluate rendered template for the parentItem tmpl = parentItem.tmpl; newTmplItems[parentItem.key] = parentItem; parentItem.nodes = []; // Rebuild, without creating a new template item return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) )); } if ( !tmpl ) { return []; // Could throw... } if ( typeof data === "function" ) { data = data.call( parentItem.data || {}, parentItem ); } ret = jQuery.isArray( data ) ? jQuery.map( data, function( dataItem ) { return newTmplItem( options, parentItem, tmpl, dataItem ); }) : [ newTmplItem( options, parentItem, tmpl, data ) ]; return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret; }, // Return rendered template item for an element. tmplItem: function( elem ) { var tmplItem; if ( elem instanceof jQuery ) { elem = tmpl[0]; } while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {} return tmplItem || topTmplItem; }, // Set: // Use $.templates( name, tmpl ) to cache a named template, // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc. // Use $( "selector" ).templates( name ) to provide access by name to a script block template declaration. // Get: // Use $.templates( name ) to access a cached template. // Also $( selectorToScriptBlock ).templates(), or $.templates( null, templateString ) // will return the compiled template, without adding a name reference. templates: function( name, tmpl ) { if (tmpl) { // Compile template and associate with name if ( typeof tmpl === "string" ) { // This is an HTML string being passed directly in. tmpl = buildTmplFn( tmpl ) } else if ( tmpl instanceof jQuery ) { tmpl = tmpl[0] || {}; } if ( tmpl.nodeType ) { // If this is a template block, use cached copy, or generate tmpl function and cache. tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML )); } return name ? (jQuery.templates[name] = tmpl) : tmpl; } // Return named compiled template return typeof name !== "string" ? null : (jQuery.templates[name] || // If not in map, treat as a selector. jQuery.templates( null, jQuery( name ))); }, encode: function( text ) { // Do HTML encoding replacing < > & and ' and " by corresponding entities. return ("" + text).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'"); } }); jQuery.extend( jQuery.tmpl, { tags: { "tmpl": { _default: { $2: "null" }, prefix: "if($notnull_1){_=_.concat($item.nest($1,$2));}" // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions) // This means that {{tmpl foo}} treats foo as a template (which IS a function). // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}. }, "each": { _default: { $2: "$index, $value" }, prefix: "if($notnull_1){$.each($1a,function($2){with(this){", suffix: "}});}" }, "if": { prefix: "if(($notnull_1) && $1a){", suffix: "}" }, "else": { prefix: "}else{" }, "html": { prefix: "if($notnull_1){_.push($1a);}" }, "=": { _default: { $1: "$data" }, prefix: "if($notnull_1){_.push($.encode($1a));}" } }, // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events complete: function( items ) { newTmplItems = {}; }, // Call this from code which overrides domManip, or equivalent // Manage cloning/storing template items etc. afterManip: function afterManip( elem, fragClone, callback ) { // Provides cloned fragment ready for fixup prior to and after insertion into DOM var content = fragClone.nodeType === 11 ? jQuery.makeArray(fragClone.childNodes) : fragClone.nodeType === 1 ? [fragClone] : []; // Return fragment to original caller (e.g. append) for DOM insertion callback.call( elem, fragClone ); // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data. storeTmplItems( content ); cloneIndex++; } }); //========================== Private helper functions, used by code above ========================== function build( tmplItem, parent, content ) { // Convert hierarchical content into flat string array // and finally return array of fragments ready for DOM insertion var frag, ret = jQuery.map( content, function( item ) { return (typeof item === "string") ? // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM. item.replace( /(<\w+)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : // This is a child template item. Build nested template. build( item, tmplItem, item.content ); }); if ( parent ) { // nested template return ret; } // top-level template ret = ret.join(""); // Support templates which have initial or final text nodes, or consist only of text // Also support HTML entities within the HTML markup. ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) { frag = jQuery( middle ).get(); // For now use get(), since buildFragment is not current public
// frag = jQuery.buildFragment( [middle] ); // If buildFragment was public, could do these two lines instead // frag = frag.cacheable ? frag.fragment.cloneNode(true) : frag.fragment;
storeTmplItems( frag ); if ( before ) { frag = unencode( before ).concat(frag); } if ( after ) { frag = frag.concat(unencode( after )); } }); return frag ? frag : unencode( ret ); } function unencode( text ) { // createTextNode will not render HTML entities correctly var el = document.createElement( "div" ); el.innerHTML = text; return jQuery.makeArray(el.childNodes); } // Generate a reusable function that will serve to render a template against data function buildTmplFn( markup ) { return new Function("jQuery","$item", "var $=jQuery,_=[],$data=$item.data;" + // Introduce the data as local variables using with(){} "with($data){_.push('" + // Convert the template into pure JavaScript $.trim(markup) .replace( /([\\'])/g, "\\$1" ) .replace( /[\r\t\n]/g, " " ) .replace( /\${([^}]*)}/g, "{{= $1}}" ) .replace( /{{(\/?)(\w+|.)(?:\(((?:.(?!}}))*?)?\))?(?:\s+(.*?)?)?(\((.*?)\))?\s*}}/g, function( all, slash, type, fnargs, target, parens, args ) { var cmd = jQuery.tmpl.tags[ type ], def, expr; if ( !cmd ) { throw "Template command not found: " + type; } def = cmd._default || []; if ( target ) { target = unescape( target ); args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : ""); if ( parens && target.indexOf(".") > -1 ) { // Support for target being things like a.toLowerCase(); // In that case don't call with template item as 'this' pointer. Just evaluate... target += parens; args = ""; } expr = args ? ("(" + target + ").call($item" + args) : target; exprAutoFnDetect = args ? expr: "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))"; } else { expr = def["$1"] || "null"; } fnargs = unescape( fnargs ); return "');" + cmd[ slash ? "suffix" : "prefix" ] .split( "$notnull_1" ).join( "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" ) .split( "$1a" ).join( exprAutoFnDetect ) .split( "$1" ).join( expr ) .split( "$2" ).join( fnargs ? fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) { params = params ? ("," + params + ")") : (parens ? ")" : ""); return params ? ("(" + name + ").call($item" + params) : all; }) : (def["$2"]||"") ) + "_.push('"; }) + "');}return _;" ); } function unescape( args ) { return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null; } function nest( tmpl, data, options ) { // nested template, using {{tmpl}} tag return jQuery.tmpl( typeof tmpl === "string" ? jQuery.templates( tmpl ) : tmpl, data, options, this ); } // Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance. function storeTmplItems( content ) { var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}; for ( var i = 0, l = content.length; i < l; i++ ) { if ( (elem = content[i]).nodeType !== 1 ) { continue; } elems = elem.getElementsByTagName("*"); for ( var j = 0, m = elems.length; j < m; j++) { processItemKey( elems[j] ); } processItemKey( elem ); } function processItemKey( el ) { var pntKey, pntNode = el, pntItem, pntNodeItem, tmplItem, key; // Ensure that each rendered template inserted into the DOM has its own template item, if ( key = el.getAttribute( tmplItmAtt )) { while ((pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { } if ( pntKey !== key ) { // The next ancestor with a _tmplitem expando is on a different key than this one. // So this is a top-level element within this template item tmplItem = newTmplItems[key]; if ( cloneIndex ) { cloneTmplItem( key ); } pntNodeItem = el.parentNode; pntNodeItem = pntNodeItem.nodeType === 11 ? 0 : (pntNodeItem.getAttribute( tmplItmAtt ) || 0); } el.removeAttribute( tmplItmAtt ); } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) { // This was a rendered element, cloned during append or appendTo etc. // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem. cloneTmplItem( tmplItem.key ); newTmplItems[tmplItem.key] = tmplItem; pntNodeItem = jQuery.data( el.parentNode, "tmplItem" ); pntNodeItem = pntNodeItem ? pntNodeItem.key : 0; } if ( tmplItem ) { pntItem = tmplItem; // Find the template item of the parent element while ( pntItem && pntItem.key != pntNodeItem ) { // Add this element as a top-level node for this rendered template item, as well as for any // ancestor items between this item and the item of its parent element pntItem.nodes.push( el ); pntItem = pntItem.parent; } delete tmplItem.content; // Could keep this available. Currently deleting to reduce API surface area, and memory use... // Store template item as jQuery data on the element jQuery.data( el, "tmplItem", tmplItem ); } function cloneTmplItem( key ) { key = key + keySuffix; tmplItem = newClonedItems[key] = (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true )); } } }
})(jQuery);