From e889134058232c5e19156353c5fc3bf3b4915a94 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 19 Nov 2012 09:50:19 -0500 Subject: [PATCH] Fix #11795, #10470: keep scripts in DOM; execute only on first insertion. Close gh-864. --- src/core.js | 24 ++- src/manipulation.js | 374 ++++++++++++++++++++------------------ test/unit/manipulation.js | 239 ++++++++++++++++-------- 3 files changed, 372 insertions(+), 265 deletions(-) diff --git a/src/core.js b/src/core.js index 1b566c479..19246d1d3 100644 --- a/src/core.js +++ b/src/core.js @@ -16,6 +16,7 @@ var _$ = window.$, // Save a reference to some core methods + core_concat = Array.prototype.concat, core_push = Array.prototype.push, core_slice = Array.prototype.slice, core_indexOf = Array.prototype.indexOf, @@ -472,26 +473,31 @@ jQuery.extend({ // data: string of html // context (optional): If specified, the fragment will be created in this context, defaults to document - // scripts (optional): If true, will include scripts passed in the html string - parseHTML: function( data, context, scripts ) { - var parsed; + // keepScripts (optional): If true, will include scripts passed in the html string + parseHTML: function( data, context, keepScripts ) { if ( !data || typeof data !== "string" ) { return null; } if ( typeof context === "boolean" ) { - scripts = context; - context = 0; + keepScripts = context; + context = false; } context = context || document; + var parsed = rsingleTag.exec( data ), + scripts = !keepScripts && []; + // Single tag - if ( (parsed = rsingleTag.exec( data )) ) { + if ( parsed ) { return [ context.createElement( parsed[1] ) ]; } - parsed = jQuery.buildFragment( [ data ], context, scripts ? null : [] ); + parsed = jQuery.buildFragment( [ data ], context, scripts ); + if ( scripts ) { + jQuery( scripts ).remove(); + } return jQuery.merge( [], - (parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment).childNodes ); + ( parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment ).childNodes ); }, parseJSON: function( data ) { @@ -744,7 +750,7 @@ jQuery.extend({ } // Flatten any nested arrays - return ret.concat.apply( [], ret ); + return core_concat.apply( [], ret ); }, // A global GUID counter for objects diff --git a/src/manipulation.js b/src/manipulation.js index 473fa5c82..f9e85c9c5 100644 --- a/src/manipulation.js +++ b/src/manipulation.js @@ -26,7 +26,8 @@ var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figca rcheckableType = /^(?:checkbox|radio)$/, // checked="checked" or checked rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, - rscriptType = /\/(java|ecma)script/i, + rscriptType = /^$|\/(?:java|ecma)script/i, + rscriptTypeMasked = /^true\/(.*)/, rcleanScript = /^\s*\s*$/g, wrapMap = { option: [ 1, "" ], @@ -181,13 +182,15 @@ jQuery.fn.extend({ i = 0; for ( ; (elem = this[i]) != null; i++ ) { - if ( !selector || jQuery.filter( selector, [ elem ] ).length ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) { if ( !keepData && elem.nodeType === 1 ) { - jQuery.cleanData( elem.getElementsByTagName("*") ); - jQuery.cleanData( [ elem ] ); + jQuery.cleanData( getAll( elem ) ); } if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } elem.parentNode.removeChild( elem ); } } @@ -203,7 +206,7 @@ jQuery.fn.extend({ for ( ; (elem = this[i]) != null; i++ ) { // Remove element nodes and prevent memory leaks if ( elem.nodeType === 1 ) { - jQuery.cleanData( elem.getElementsByTagName("*") ); + jQuery.cleanData( getAll( elem, false ) ); } // Remove any remaining nodes @@ -249,7 +252,7 @@ jQuery.fn.extend({ // Remove element nodes and prevent memory leaks elem = this[i] || {}; if ( elem.nodeType === 1 ) { - jQuery.cleanData( elem.getElementsByTagName( "*" ) ); + jQuery.cleanData( getAll( elem, false ) ); elem.innerHTML = value; } } @@ -311,31 +314,27 @@ jQuery.fn.extend({ domManip: function( args, table, callback ) { // Flatten any nested arrays - args = [].concat.apply( [], args ); + args = core_concat.apply( [], args ); - var results, first, fragment, iNoClone, + var fragment, first, results, scripts, hasScripts, iNoClone, node, doc, i = 0, + l = this.length, value = args[0], - scripts = [], - l = this.length; + isFunction = jQuery.isFunction( value ); // We can't cloneNode fragments that contain checked, in WebKit - if ( !jQuery.support.checkClone && l > 1 && typeof value === "string" && rchecked.test( value ) ) { + if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { return this.each(function() { - jQuery(this).domManip( args, table, callback ); - }); - } - - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - args[0] = value.call( this, i, table ? self.html() : undefined ); + var self = jQuery( this ); + if ( isFunction ) { + args[0] = value.call( this, i, table ? self.html() : undefined ); + } self.domManip( args, table, callback ); }); } if ( this[0] ) { - results = jQuery.buildFragment( args, this, scripts ); + results = jQuery.buildFragment( args, this ); fragment = results.fragment; first = fragment.firstChild; @@ -345,48 +344,61 @@ jQuery.fn.extend({ if ( first ) { table = table && jQuery.nodeName( first, "tr" ); + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; // Use the original fragment for the last item instead of the first because it can end up // being emptied incorrectly in certain situations (#8070). // Fragments from the fragment cache must always be cloned and never used in place. for ( iNoClone = results.cacheable || l - 1; i < l; i++ ) { + node = fragment; + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } callback.call( table && jQuery.nodeName( this[i], "table" ) ? findOrAppend( this[i], "tbody" ) : this[i], - i === iNoClone ? - fragment : - jQuery.clone( fragment, true, true ) + node ); } - } - // Fix #11809: Avoid leaking memory - fragment = first = null; - - if ( scripts.length ) { - jQuery.each( scripts, function( i, elem ) { - if ( elem.src ) { - if ( jQuery.ajax ) { - jQuery.ajax({ - url: elem.src, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true - }); - } else { - jQuery.error("no ajax"); + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Hope ajax is available... + jQuery.ajax({ + url: node.src, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } } - } else { - jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "" ) ); } + } - if ( elem.parentNode ) { - elem.parentNode.removeChild( elem ); - } - }); + // Fix #11809: Avoid leaking memory + fragment = first = null; } } @@ -398,6 +410,31 @@ function findOrAppend( elem, tag ) { return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); } +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + var attr = elem.getAttributeNode("type"); + elem.type = ( attr && attr.specified ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + if ( match ) { + elem.type = match[1]; + } else { + elem.removeAttribute("type"); + } + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + function cloneCopyEvent( src, dest ) { if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { @@ -448,9 +485,14 @@ function cloneFixAttributes( src, dest ) { nodeName = dest.nodeName.toLowerCase(); - if ( nodeName === "object" ) { - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { if ( dest.parentNode ) { dest.outerHTML = src.outerHTML; } @@ -459,7 +501,7 @@ function cloneFixAttributes( src, dest ) { // element in IE9, the outerHTML strategy above is not sufficient. // If the src has innerHTML and the destination does not, // copy the src.innerHTML into the dest.innerHTML. #10324 - if ( jQuery.support.html5Clone && (src.innerHTML && !jQuery.trim(dest.innerHTML)) ) { + if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { dest.innerHTML = src.innerHTML; } @@ -485,10 +527,6 @@ function cloneFixAttributes( src, dest ) { // cloning other types of input fields } else if ( nodeName === "input" || nodeName === "textarea" ) { dest.defaultValue = src.defaultValue; - - // IE blanks contents when cloning scripts - } else if ( nodeName === "script" && dest.text !== src.text ) { - dest.text = src.text; } // Event data gets referenced instead of copied if the expando @@ -569,16 +607,26 @@ jQuery.each({ }; }); -function getAll( elem ) { - if ( typeof elem.getElementsByTagName !== "undefined" ) { - return elem.getElementsByTagName( "*" ); - - } else if ( typeof elem.querySelectorAll !== "undefined" ) { - return elem.querySelectorAll( "*" ); - - } else { - return []; +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; } // Used in clean, fixes the defaultChecked property @@ -590,10 +638,8 @@ function fixDefaultChecked( elem ) { jQuery.extend({ clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var srcElements, - destElements, - i, - clone; + var destElements, srcElements, node, i, clone, + inPage = jQuery.contains( elem.ownerDocument, elem ); if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { // Break the original-clone style connection in IE9/10 (#8909) @@ -616,191 +662,163 @@ jQuery.extend({ // proprietary methods to clear the events. Thanks to MooTools // guys for this hotness. - cloneFixAttributes( elem, clone ); - - // Using Sizzle here is crazy slow, so we use getElementsByTagName instead - srcElements = getAll( elem ); + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 destElements = getAll( clone ); + srcElements = getAll( elem ); // Weird iteration because IE will replace the length property // with an element if you are cloning the body and one of the // elements on the page has a name or id of "length" - for ( i = 0; srcElements[i]; ++i ) { + for ( i = 0; (node = srcElements[i]) != null; ++i ) { // Ensure that the destination node is not null; Fixes #9587 if ( destElements[i] ) { - cloneFixAttributes( srcElements[i], destElements[i] ); + cloneFixAttributes( node, destElements[i] ); } } } // Copy the events from the original to the clone if ( dataAndEvents ) { - cloneCopyEvent( elem, clone ); - if ( deepDataAndEvents ) { - srcElements = getAll( elem ); destElements = getAll( clone ); + srcElements = getAll( elem ); - for ( i = 0; srcElements[i]; ++i ) { - cloneCopyEvent( srcElements[i], destElements[i] ); + for ( i = 0; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); } + } else { + cloneCopyEvent( elem, clone ); } } - srcElements = destElements = null; + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; // Return the cloned set return clone; }, clean: function( elems, context, fragment, scripts ) { - var i, j, elem, tag, wrap, depth, parent, div, hasBody, tbody, handleScript, jsTags, - safe = context === document && safeFragment, - ret = []; + var elem, j, tmp, tag, wrap, tbody, + ret = [], + i = 0, + safe = context === document && safeFragment; // Ensure that context is a document if ( !context || typeof context.createDocumentFragment === "undefined" ) { context = document; } - // Use the already-created safe fragment if context permits for ( i = 0; (elem = elems[i]) != null; i++ ) { - if ( typeof elem === "number" ) { - elem += ""; - } + if ( elem || elem === 0 ) { + // Add nodes directly + if ( typeof elem === "object" ) { + jQuery.merge( ret, elem.nodeType ? [ elem ] : elem ); - if ( !elem ) { - continue; - } + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + ret.push( context.createTextNode( elem ) ); - // Convert html string into DOM nodes - if ( typeof elem === "string" ) { - if ( !rhtml.test( elem ) ) { - elem = context.createTextNode( elem ); + // Convert html into DOM nodes } else { - // Ensure a safe container in which to render the html + // Ensure a safe container safe = safe || createSafeFragment( context ); - div = div || safe.appendChild( context.createElement("div") ); + tmp = tmp || safe.appendChild( context.createElement("div") ); - // Fix "XHTML"-style tags in all browsers - elem = elem.replace(rxhtmlTag, "<$1>"); - - // Go to html and back, then peel off extra wrappers + // Deserialize a standard representation tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); wrap = wrapMap[ tag ] || wrapMap._default; - depth = wrap[0]; - div.innerHTML = wrap[1] + addMandatoryAttributes( elem ) + wrap[2]; + tmp.innerHTML = wrap[1] + addMandatoryAttributes( elem.replace( rxhtmlTag, "<$1>" ) ) + wrap[2]; - // Move to the right depth - while ( depth-- ) { - div = div.lastChild; + // Descend through wrappers to the right content + j = wrap[0]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Manually add leading whitespace removed by IE + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + ret.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); } // Remove IE's autoinserted from table fragments if ( !jQuery.support.tbody ) { // String was a , *may* have spurious - hasBody = rtbody.test(elem); - tbody = tag === "table" && !hasBody ? - div.firstChild && div.firstChild.childNodes : - - // String was a bare or - wrap[1] === "
" && !hasBody ? - div.childNodes : - []; - - for ( j = tbody.length - 1; j >= 0 ; --j ) { - if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { - tbody[ j ].parentNode.removeChild( tbody[ j ] ); + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare or + wrap[1] === "
" && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); } } } - // IE completely kills leading whitespace when innerHTML is used - if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); - } + jQuery.merge( ret, tmp.childNodes ); - elem = div.childNodes; - parent = div; + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; - // Remember the top-level container for proper cleanup - div = safe.lastChild; - } - } - - if ( elem.nodeType ) { - ret.push( elem ); - } else { - jQuery.merge( ret, elem ); - - // Fix #12392 - if ( parent ) { - - // for WebKit and IE > 9 - parent.textContent = ""; - - // for oldIE - while ( parent.firstChild ) { - parent.removeChild( parent.firstChild ); + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); } - parent = null; + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; } } } // Fix #11356: Clear elements from safeFragment - if ( div ) { - safe.removeChild( div ); + if ( tmp ) { + safe.removeChild( tmp ); } - elem = div = safe = null; - - // Reset defaultChecked for any radios and checkboxes // about to be appended to the DOM in IE 6/7 (#8060) if ( !jQuery.support.appendChecked ) { - for ( i = 0; (elem = ret[i]) != null; i++ ) { - if ( jQuery.nodeName( elem, "input" ) ) { - fixDefaultChecked( elem ); - } else if ( typeof elem.getElementsByTagName !== "undefined" ) { - jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); - } - } + jQuery.grep( getAll( ret, "input" ), fixDefaultChecked ); } - // Append elements to a provided document fragment if ( fragment ) { - // Special handling of each script element - handleScript = function( elem ) { - // Check if we consider it executable - if ( !elem.type || rscriptType.test( elem.type ) ) { - // Detach the script and store it in the scripts array (if provided) or the fragment - // Return truthy to indicate that it has been handled - return scripts ? - scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) : - fragment.appendChild( elem ); + for ( i = 0; (elem = ret[i]) != null; i++ ) { + safe = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + fragment.appendChild( elem ); + tmp = getAll( elem, "script" ); + + // Preserve script evaluation history + if ( safe ) { + setGlobalEval( tmp ); } - }; - for ( i = 0; (elem = ret[i]) != null; i++ ) { - // Check if we're done after handling an executable script - if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) { - // Append to fragment and handle embedded scripts - fragment.appendChild( elem ); - if ( typeof elem.getElementsByTagName !== "undefined" ) { - // handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration - jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript ); - - // Splice the scripts into ret after their former ancestor and advance our index beyond them - ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); - i += jsTags.length; + // Capture executables + if ( scripts ) { + for ( j = 0; (elem = tmp[j]) != null; j++ ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } } } } } + elem = tmp = safe = null; + return ret; }, diff --git a/test/unit/manipulation.js b/test/unit/manipulation.js index 19897af74..8047bdf49 100644 --- a/test/unit/manipulation.js +++ b/test/unit/manipulation.js @@ -1372,7 +1372,9 @@ test("clone()", function() { equal( jQuery(form).clone().children().length, 1, "Make sure we just get the form back." ); - equal( jQuery("body").clone().children()[0].id, "qunit-header", "Make sure cloning body works" ); + var body = jQuery("body").clone(); + equal( body.children()[0].id, "qunit-header", "Make sure cloning body works" ); + body.remove(); }); test("clone(script type=non-javascript) (#11359)", function() { @@ -1382,6 +1384,7 @@ test("clone(script type=non-javascript) (#11359)", function() { equal( dest[0].text, "Lorem ipsum dolor sit amet", "Cloning preserves script text" ); equal( dest.last().html(), src.last().html(), "Cloning preserves nested script text" ); ok( /^\s*consectetur adipiscing elit<\/scr.pt>\s*$/i.test( dest.last().html() ), "Cloning preserves nested script text" ); + dest.remove(); }); test("clone(form element) (Bug #3879, #6655)", function() { @@ -1449,100 +1452,124 @@ test("html() on empty set", function() { strictEqual( jQuery().html(), undefined, ".html() returns undefined for empty sets (#11962)" ); }); -var testHtml = function(valueObj) { - expect(35); - - jQuery["scriptorder"] = 0; +var childNodeNames = function( node ) { + return jQuery.map( node.childNodes, function( child ) { + return child.nodeName.toUpperCase(); + }).join(" "); +}; - var div = jQuery("#qunit-fixture > div"); - div.html(valueObj("test")); - var pass = true; - for ( var i = 0; i < div.size(); i++ ) { - if ( div.get(i).childNodes.length != 1 ) { - pass = false; - } - } - ok( pass, "Set HTML" ); +var testHtml = function( valueObj ) { + expect( 37 ); - div = jQuery("
").html( valueObj("
") ); + var actual, expected, tmp, + div = jQuery("
"), + fixture = jQuery("#qunit-fixture"); - equal( div.children().length, 2, "Make sure two child nodes exist." ); - equal( div.children().children().length, 1, "Make sure that a grandchild exists." ); + div.html( valueObj("
") ); + equal( div.children().length, 2, "Found children" ); + equal( div.children().children().length, 1, "Found grandchild" ); - var space = jQuery("
").html(valueObj(" "))[0].innerHTML; - ok( /^\xA0$|^ $/.test( space ), "Make sure entities are passed through correctly." ); - equal( jQuery("
").html(valueObj("&"))[0].innerHTML, "&", "Make sure entities are passed through correctly." ); + actual = []; expected = []; + tmp = jQuery("").html( valueObj("area") ).each(function() { + expected.push("AREA"); + actual.push( childNodeNames( this ) ); + }); + equal( expected.length, 1, "Expecting one parent" ); + deepEqual( actual, expected, "Found the inserted area element" ); - jQuery("#qunit-fixture").html(valueObj("")); + equal( div.html( valueObj(5) ).html(), "5", "Setting a number as html" ); + equal( div.html( valueObj(0) ).html(), "0", "Setting a zero as html" ); - equal( jQuery("#qunit-fixture").children().length, 1, "Make sure there is a child element." ); - equal( jQuery("#qunit-fixture").children()[0].nodeName.toUpperCase(), "STYLE", "And that a style element was inserted." ); + div.html( valueObj(" &") ); + equal( + div[0].innerHTML.replace( /\xA0/, " " ), + " &", + "Entities are passed through correctly" + ); - QUnit.reset(); - // using contents will get comments regular, text, and comment nodes - var j = jQuery("#nonnodes").contents(); - j.html(valueObj("bold")); + tmp = "<div>hello1</div>"; + equal( div.html( valueObj( tmp ) ).html().replace( />/g, ">" ), tmp, "Escaped html" ); + tmp = "x" + tmp; + equal( div.html( valueObj( tmp ) ).html().replace( />/g, ">" ), tmp, "Escaped html, leading x" ); + tmp = " " + tmp.slice(1); + equal( div.html( valueObj( tmp ) ).html().replace( />/g, ">" ), tmp, "Escaped html, leading space" ); + + actual = []; expected = []; tmp = {}; + jQuery("#nonnodes").contents().html( valueObj("bold") ).each(function() { + var html = jQuery( this ).html(); + tmp[ this.nodeType ] = true; + expected.push( this.nodeType === 1 ? "bold" : undefined ); + actual.push( html ? html.toLowerCase() : html ); + }); + deepEqual( actual, expected, "Set containing element, text node, comment" ); + ok( tmp[1], "element" ); + ok( tmp[3], "text node" ); + ok( tmp[8], "comment" ); + + actual = []; expected = []; + fixture.find("> div").html( valueObj("test") ).each(function() { + expected.push("B"); + actual.push( childNodeNames( this ) ); + }); + equal( expected.length, 6, "Expecting many parents" ); + deepEqual( actual, expected, "Correct childNodes after setting HTML" ); - // this is needed, or the expando added by jQuery unique will yield a different html - j.find("b").removeData(); - equal( j.html().replace(/ xmlns="[^"]+"/g, "").toLowerCase(), "bold", "Check node,textnode,comment with html()" ); + actual = []; expected = []; + fixture.html( valueObj("") ).each(function() { + expected.push("STYLE"); + actual.push( childNodeNames( this ) ); + }); + equal( expected.length, 1, "Expecting one parent" ); + deepEqual( actual, expected, "Found the inserted style element" ); - jQuery("#qunit-fixture").html(valueObj("") ); + jQuery("#qunit-fixture select").html( valueObj("") ); equal( jQuery("#qunit-fixture select").val(), "O2", "Selected option correct" ); - var $div = jQuery("
"); - equal( $div.html(valueObj( 5 )).html(), "5", "Setting a number as html" ); - equal( $div.html(valueObj( 0 )).html(), "0", "Setting a zero as html" ); - - var $div2 = jQuery("
"), insert = "<div>hello1</div>"; - equal( $div2.html(insert).html().replace(/>/g, ">"), insert, "Verify escaped insertion." ); - equal( $div2.html("x" + insert).html().replace(/>/g, ">"), "x" + insert, "Verify escaped insertion." ); - equal( $div2.html(" " + insert).html().replace(/>/g, ">"), " " + insert, "Verify escaped insertion." ); - - var map = jQuery("").html(valueObj("jQuery")); - - equal( map[0].childNodes.length, 1, "The area was inserted." ); - equal( map[0].firstChild.nodeName.toLowerCase(), "area", "The area was inserted." ); - - QUnit.reset(); - - jQuery("#qunit-fixture").html(valueObj("
")); - - var child = jQuery("#qunit-fixture").find("script"); - - equal( child.length, 2, "Make sure that two non-JavaScript script tags are left." ); - equal( child[0].type, "something/else", "Verify type of script tag." ); - equal( child[1].type, "something/else", "Verify type of script tag." ); - - jQuery("#qunit-fixture").html(valueObj("")); - jQuery("#qunit-fixture").html(valueObj("")); - jQuery("#qunit-fixture").html(valueObj("")); - - jQuery("#qunit-fixture").html(valueObj("")); - - jQuery("#qunit-fixture").html(valueObj("foo
")); - - jQuery("#qunit-fixture").html(valueObj("", + "", + "", + "", + "
", + "", + "", + "", + "", + "
" + ].join("")) + ).find("script"); + equal( tmp.length, 8, "All script tags remain." ); + equal( tmp[0].type, "something/else", "Non-evaluated type." ); + equal( tmp[1].type, "text/javascript", "Evaluated type." ); + + fixture.html( valueObj("") ); + fixture.html( valueObj("") ); + fixture.html( valueObj("") ); + fixture.html( valueObj("foo
") ); + + jQuery.scriptorder = 0; + fixture.html( valueObj([ + "", + "", + "" + ].join("")) ); + + QUnit.reset(); + fixture.html( valueObj( fixture.text() ) ); + ok( /^[^<]*[^<\s][^<]*$/.test( fixture.html() ), "Replace html with text" ); }; test("html(String)", function() { - testHtml(manipulationBareObj); + testHtml( manipulationBareObj ); }); test("html(Function)", function() { - testHtml(manipulationFunctionReturningObj); - - expect(37); - - QUnit.reset(); - - jQuery("#qunit-fixture").html(function(){ - return jQuery(this).text(); - }); - - ok( !/ 0, "Make sure text exists." ); + testHtml( manipulationFunctionReturningObj ); }); test("html(Function) with incoming value", function() { @@ -2083,3 +2110,59 @@ testIframeWithCallback( "buildFragment works even if document[0] is iframe's win ok( test.status, test.description ); }); + +test("script evaluation (#11795)", function() { + expect(11); + + var scriptsIn, scriptsOut, + fixture = jQuery("#qunit-fixture").empty(), + objGlobal = (function(){ return this; })(), + isOk = objGlobal.ok, + notOk = function() { + var args = arguments; + args[0] = !args[0]; + return isOk.apply( this, args ); + }; + + objGlobal.ok = notOk; + scriptsIn = jQuery([ + "", + "", + "", + "", + "
", + "", + "", + "", + "", + "
" + ].join("")); + scriptsIn.appendTo( jQuery("
") ); + objGlobal.ok = isOk; + + scriptsOut = fixture.append( scriptsIn ).find("script"); + equal( scriptsOut[0].type, "something/else", "Non-evaluated type." ); + equal( scriptsOut[1].type, "text/javascript", "Evaluated type." ); + deepEqual( scriptsOut.get(), fixture.find("script").get(), "All script tags remain." ); + + objGlobal.ok = notOk; + scriptsOut = scriptsOut.add( scriptsOut.clone() ).appendTo( fixture.find("div") ); + deepEqual( fixture.find("div script").get(), scriptsOut.get(), "Scripts cloned without reevaluation" ); + fixture.append( scriptsOut.detach() ); + deepEqual( fixture.find("> script").get(), scriptsOut.get(), "Scripts detached without reevaluation" ); + objGlobal.ok = isOk; +}); + +test("wrapping scripts (#10470)", function() { + expect(2); + + var script = document.createElement("script"); + script.text = script.textContent = + "ok( !document.eval10470, 'script evaluated once' ); document.eval10470 = true;"; + + document.eval10470 = false; + jQuery("#qunit-fixture").empty()[0].appendChild( script ); + jQuery("#qunit-fixture script").wrap(""); + strictEqual( script.parentNode, jQuery("#qunit-fixture > b")[0], "correctly wrapped" ); + jQuery( script ).remove(); +}); -- 2.39.5