Broke the logic for .clean() wrap out into a separate, static, data structure. Also...
[jquery.git] / src / manipulation.js
1 var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
2         rleadingWhitespace = /^\s+/,
3         rxhtmlTag = /(<(\w+)[^>]*?)\/>/g,
4         rselfClosing = /^(?:abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i,
5         rtagName = /<(\w+)/,
6         rtbody = /<tbody/i,
7         fcloseTag = function(all, front, tag){
8                 return rselfClosing.test(tag) ?
9                         all :
10                         front + "></" + tag + ">";
11         },
12         wrapMap = {
13                 option: [ 1, "<select multiple='multiple'>", "</select>" ],
14                 legend: [ 1, "<fieldset>", "</fieldset>" ],
15                 thead: [ 1, "<table>", "</table>" ],
16                 tr: [ 2, "<table><tbody>", "</tbody></table>" ],
17                 td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
18                 col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
19                 _default: [ 0, "", "" ]
20         };
21
22 wrapMap.optgroup = wrapMap.option;
23 wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
24 wrapMap.th = wrapMap.td;
25
26 // IE can't serialize <link> and <script> tags normally
27 if ( !jQuery.support.htmlSerialize ) {
28         wrapMap._default = [ 1, "div<div>", "</div>" ];
29 }
30
31 jQuery.fn.extend({
32         text: function( text ) {
33                 if ( typeof text !== "object" && text !== undefined )
34                         return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
35
36                 var ret = "";
37
38                 jQuery.each( text || this, function(){
39                         jQuery.each( this.childNodes, function(){
40                                 if ( this.nodeType !== 8 ) {
41                                         ret += this.nodeType !== 1 ?
42                                                 this.nodeValue :
43                                                 jQuery.fn.text( [ this ] );
44                                 }
45                         });
46                 });
47
48                 return ret;
49         },
50
51         wrapAll: function( html ) {
52                 if ( jQuery.isFunction( html ) ) {
53                         return this.each(function() {
54                                 jQuery(this).wrapAll( html.apply(this, arguments) );
55                         });
56                 }
57
58                 if ( this[0] ) {
59                         // The elements to wrap the target around
60                         var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone();
61
62                         if ( this[0].parentNode ) {
63                                 wrap.insertBefore( this[0] );
64                         }
65
66                         wrap.map(function(){
67                                 var elem = this;
68
69                                 while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
70                                         elem = elem.firstChild;
71                                 }
72
73                                 return elem;
74                         }).append(this);
75                 }
76
77                 return this;
78         },
79
80         wrapInner: function( html ) {
81                 return this.each(function(){
82                         jQuery( this ).contents().wrapAll( html );
83                 });
84         },
85
86         wrap: function( html ) {
87                 return this.each(function(){
88                         jQuery( this ).wrapAll( html );
89                 });
90         },
91
92         append: function() {
93                 return this.domManip(arguments, true, function(elem){
94                         if ( this.nodeType === 1 ) {
95                                 this.appendChild( elem );
96                         }
97                 });
98         },
99
100         prepend: function() {
101                 return this.domManip(arguments, true, function(elem){
102                         if ( this.nodeType === 1 ) {
103                                 this.insertBefore( elem, this.firstChild );
104                         }
105                 });
106         },
107
108         before: function() {
109                 return this.domManip(arguments, false, function(elem){
110                         this.parentNode.insertBefore( elem, this );
111                 });
112         },
113
114         after: function() {
115                 return this.domManip(arguments, false, function(elem){
116                         this.parentNode.insertBefore( elem, this.nextSibling );
117                 });
118         },
119
120         clone: function( events ) {
121                 // Do the clone
122                 var ret = this.map(function(){
123                         if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
124                                 // IE copies events bound via attachEvent when
125                                 // using cloneNode. Calling detachEvent on the
126                                 // clone will also remove the events from the orignal
127                                 // In order to get around this, we use innerHTML.
128                                 // Unfortunately, this means some modifications to
129                                 // attributes in IE that are actually only stored
130                                 // as properties will not be copied (such as the
131                                 // the name attribute on an input).
132                                 var html = this.outerHTML, ownerDocument = this.ownerDocument;
133                                 if ( !html ) {
134                                         var div = ownerDocument.createElement("div");
135                                         div.appendChild( this.cloneNode(true) );
136                                         html = div.innerHTML;
137                                 }
138
139                                 return jQuery.clean([html.replace(rinlinejQuery, "")
140                                         .replace(rleadingWhitespace, "")], ownerDocument)[0];
141                         } else {
142                                 return this.cloneNode(true);
143                         }
144                 });
145
146                 // Copy the events from the original to the clone
147                 if ( events === true ) {
148                         var orig = this.find("*").andSelf(), i = 0;
149
150                         ret.find("*").andSelf().each(function(){
151                                 if ( this.nodeName !== orig[i].nodeName ) { return; }
152
153                                 var events = jQuery.data( orig[i], "events" );
154
155                                 for ( var type in events ) {
156                                         for ( var handler in events[ type ] ) {
157                                                 jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
158                                         }
159                                 }
160
161                                 i++;
162                         });
163                 }
164
165                 // Return the cloned set
166                 return ret;
167         },
168
169         html: function( value ) {
170                 if ( value === undefined ) {
171                         return this[0] ?
172                                 this[0].innerHTML.replace(rinlinejQuery, "") :
173                                 null;
174
175                 // See if we can take a shortcut and just use innerHTML
176                 } else if ( typeof value === "string" && !/<script/i.test( value ) &&
177                         !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
178
179                         for ( var i = 0, l = this.length; i < l; i++ ) {
180                                 // Remove element nodes and prevent memory leaks
181                                 if ( this[i].nodeType === 1 ) {
182                                         cleanData( this[i].getElementsByTagName("*") );
183                                 }
184
185                                 this[i].innerHTML = value;
186                         }
187
188                 } else {
189                         this.empty().append( value );
190                 }
191
192                 return this;
193         },
194
195         replaceWith: function( value ) {
196                 return this.after( value ).remove();
197         },
198
199         detach: function( selector ) {
200                 return this.remove( selector, true );
201         },
202
203         domManip: function( args, table, callback ) {
204                 var results, first, value = args[0], scripts = [];
205
206                 if ( jQuery.isFunction(value) ) {
207                         return this.each(function() {
208                                 args[0] = value.call(this);
209                                 return jQuery(this).domManip( args, table, callback );
210                         });
211                 }
212
213                 if ( this[0] ) {
214                         // If we're in a fragment, just use that instead of building a new one
215                         if ( args[0] && args[0].parentNode && args[0].parentNode.nodeType === 11 ) {
216                                 results = { fragment: args[0].parentNode };
217                         } else {
218                                 results = buildFragment( args, this[0], scripts );
219                         }
220
221                         first = results.fragment.firstChild;
222
223                         if ( first ) {
224                                 table = table && jQuery.nodeName( first, "tr" );
225
226                                 for ( var i = 0, l = this.length; i < l; i++ ) {
227                                         callback.call(
228                                                 table ?
229                                                         root(this[i], first) :
230                                                         this[i],
231                                                 results.cacheable || this.length > 1 || i > 0 ?
232                                                         results.fragment.cloneNode(true) :
233                                                         results.fragment
234                                         );
235                                 }
236                         }
237
238                         if ( scripts ) {
239                                 jQuery.each( scripts, evalScript );
240                         }
241                 }
242
243                 return this;
244
245                 function root( elem, cur ) {
246                         return jQuery.nodeName(elem, "table") ?
247                                 (elem.getElementsByTagName("tbody")[0] ||
248                                 elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
249                                 elem;
250                 }
251         }
252 });
253
254 function buildFragment(args, nodes, scripts){
255         var fragment, cacheable, cached, cacheresults, doc;
256
257         if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && args[0].indexOf("<option") < 0 ) {
258                 cacheable = true;
259                 cacheresults = jQuery.fragments[ args[0] ];
260                 if ( cacheresults ) {
261                         if ( cacheresults !== 1 ) {
262                                 fragment = cacheresults;
263                         }
264                         cached = true;
265                 }
266         }
267
268         if ( !fragment ) {
269                 doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document);
270                 fragment = doc.createDocumentFragment();
271                 jQuery.clean( args, doc, fragment, scripts );
272         }
273
274         if ( cacheable ) {
275                 jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1;
276         }
277
278         return { fragment: fragment, cacheable: cacheable };
279 }
280
281 jQuery.fragments = {};
282
283 jQuery.each({
284         appendTo: "append",
285         prependTo: "prepend",
286         insertBefore: "before",
287         insertAfter: "after",
288         replaceAll: "replaceWith"
289 }, function(name, original){
290         jQuery.fn[ name ] = function( selector ) {
291                 var ret = [], insert = jQuery( selector );
292
293                 for ( var i = 0, l = insert.length; i < l; i++ ) {
294                         var elems = (i > 0 ? this.clone(true) : this).get();
295                         jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
296                         ret = ret.concat( elems );
297                 }
298
299                 return this.pushStack( ret, name, selector );
300         };
301 });
302
303 jQuery.each({
304         // keepData is for internal use only--do not document
305         remove: function( selector, keepData ) {
306                 if ( !selector || jQuery.multiFilter( selector, [ this ] ).length ) {
307                         if ( !keepData && this.nodeType === 1 ) {
308                                 cleanData( this.getElementsByTagName("*") );
309                                 cleanData( [ this ] );
310                         }
311
312                         if ( this.parentNode ) {
313                                  this.parentNode.removeChild( this );
314                         }
315                 }
316         },
317
318         empty: function() {
319                 // Remove element nodes and prevent memory leaks
320                 if ( this.nodeType === 1 ) {
321                         cleanData( this.getElementsByTagName("*") );
322                 }
323
324                 // Remove any remaining nodes
325                 while ( this.firstChild ) {
326                         this.removeChild( this.firstChild );
327                 }
328         }
329 }, function(name, fn){
330         jQuery.fn[ name ] = function(){
331                 return this.each( fn, arguments );
332         };
333 });
334
335 jQuery.extend({
336         clean: function( elems, context, fragment, scripts ) {
337                 context = context || document;
338
339                 // !context.createElement fails in IE with an error but returns typeof 'object'
340                 if ( typeof context.createElement === "undefined" ) {
341                         context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
342                 }
343
344                 var ret = [], div = context.createElement("div");
345
346                 jQuery.each(elems, function(i, elem){
347                         if ( typeof elem === "number" ) {
348                                 elem += '';
349                         }
350
351                         if ( !elem ) { return; }
352
353                         // Convert html string into DOM nodes
354                         if ( typeof elem === "string" ) {
355                                 // Fix "XHTML"-style tags in all browsers
356                                 elem = elem.replace(rxhtmlTag, fcloseTag);
357
358                                 // Trim whitespace, otherwise indexOf won't work as expected
359                                 var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
360                                         wrap = wrapMap[ tag ] || wrapMap._default,
361                                         depth = wrap[0];
362
363                                 // Go to html and back, then peel off extra wrappers
364                                 div.innerHTML = wrap[1] + elem + wrap[2];
365
366                                 // Move to the right depth
367                                 while ( depth-- ) {
368                                         div = div.lastChild;
369                                 }
370
371                                 // Remove IE's autoinserted <tbody> from table fragments
372                                 if ( !jQuery.support.tbody ) {
373
374                                         // String was a <table>, *may* have spurious <tbody>
375                                         var hasBody = rtbody.test(elem),
376                                                 tbody = tag === "table" && !hasBody ?
377                                                         div.firstChild && div.firstChild.childNodes :
378
379                                                         // String was a bare <thead> or <tfoot>
380                                                         wrap[1] == "<table>" && !hasBody ?
381                                                                 div.childNodes :
382                                                                 [];
383
384                                         for ( var j = tbody.length - 1; j >= 0 ; --j ) {
385                                                 if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
386                                                         tbody[ j ].parentNode.removeChild( tbody[ j ] );
387                                                 }
388                                         }
389
390                                 }
391
392                                 // IE completely kills leading whitespace when innerHTML is used
393                                 if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
394                                         div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
395                                 }
396
397                                 elem = jQuery.makeArray( div.childNodes );
398                         }
399
400                         if ( elem.nodeType ) {
401                                 ret.push( elem );
402                         } else {
403                                 ret = jQuery.merge( ret, elem );
404                         }
405
406                 });
407
408                 if ( fragment ) {
409                         for ( var i = 0; ret[i]; i++ ) {
410                                 if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
411                                         scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
412                                 } else {
413                                         if ( ret[i].nodeType === 1 ) {
414                                                 ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
415                                         }
416                                         fragment.appendChild( ret[i] );
417                                 }
418                         }
419                 }
420
421                 return ret;
422         }
423 });
424
425 function cleanData( elems ) {
426         for ( var i = 0, l = elems.length; i < l; i++ ) {
427                 var id = elems[i][expando];
428                 if ( id ) {
429                         delete jQuery.cache[ id ];
430                 }
431         }
432 }