Skip to content

Manipulation: Detect sneaky no-content replaceWith input #2206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions src/manipulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ jQuery.extend({
return clone;
},

buildFragment: function( elems, context, scripts, selection ) {
buildFragment: function( elems, context, scripts, selection, ignored ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make buildFragment private and change it signature, 5 arguments usually considered as bad API, even for helpers. One object as an argument?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is performance-critical code, I'm afraid wrapping it in an object might have a perf penalty (and it'll almost certainly be larger).

As for making it fully private, it seems like a good idea. We should do that with almost everything that's not documented IMO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is performance-critical code, I'm afraid wrapping it in an object might have a perf penalty (and it'll almost certainly be larger).

Need proof for that statement

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The burden of proof is on the person that wants to introduce a change, i.e. here it's you. ;)

I just said it might introduce a perf penalty, I don't know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the bigger issue is why buildFragment is so complex that it needs that many args, and why we had to add another one. I thought for sure there would be an easier way to deal with this crazy edge case, but couldn't find a reasonable way around it. It is a pretty hot path in our code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In short, because of self-manipulation. .replaceWith is the worst offender, but it's basically a problem any time there's a nonempty intersection between the input and the context collection, and exacerbated by the fact that .domManip lies in between the outer method and buildFragment.

That said, the scripts parameter is probably unnecessary and safe to remove when buildFragment is made private. In fact, that's an opportunity to look at more sweeping changes... why exactly do we pluck nodes from the DOM and into a fragment in buildFragment instead of .domManip anyway? It's an operation that parseHTML has to undo!

var elem, tmp, tag, wrap, contains, j,
fragment = context.createDocumentFragment(),
nodes = [],
Expand Down Expand Up @@ -257,9 +257,11 @@ jQuery.extend({
i = 0;
while ( (elem = nodes[ i++ ]) ) {

// #4087 - If origin and destination elements are the same, and this is
// that element, do not do anything
// Skip elements already in the context collection (trac-4087)
if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
if ( ignored ) {
ignored.push( elem );
}
continue;
}

Expand Down Expand Up @@ -446,28 +448,28 @@ jQuery.fn.extend({
},

replaceWith: function() {
var arg = arguments[ 0 ];

// Make the changes, replacing each context element with the new content
this.domManip( arguments, function( elem ) {
arg = this.parentNode;
var ignored = [];

jQuery.cleanData( getAll( this ) );
// Make the changes, replacing each non-ignored context element with the new content
return this.domManip( arguments, function( elem ) {
var parent = this.parentNode;

if ( arg ) {
arg.replaceChild( elem, this );
if ( jQuery.inArray( this, ignored ) < 0 ) {
jQuery.cleanData( getAll( this ) );
if ( parent ) {
parent.replaceChild( elem, this );
}
}
});

// Force removal if there was no new content (e.g., from empty arguments)
return arg && (arg.length || arg.nodeType) ? this : this.remove();
// Force callback invocation
}, ignored );
},

detach: function( selector ) {
return this.remove( selector, true );
},

domManip: function( args, callback ) {
domManip: function( args, callback, ignored ) {

// Flatten any nested arrays
args = concat.apply( [], args );
Expand All @@ -489,19 +491,20 @@ jQuery.fn.extend({
if ( isFunction ) {
args[ 0 ] = value.call( this, index, self.html() );
}
self.domManip( args, callback );
self.domManip( args, callback, ignored );
});
}

if ( l ) {
fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );
fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this, ignored );
first = fragment.firstChild;

if ( fragment.childNodes.length === 1 ) {
fragment = first;
}

if ( first ) {
// Require either new content or an interest in ignored elements to invoke the callback
if ( first || ignored ) {
scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
hasScripts = scripts.length;

Expand Down
23 changes: 19 additions & 4 deletions test/unit/manipulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -1279,22 +1279,37 @@ test( "replaceWith(string) for more than one element", function() {
equal(jQuery("#foo p").length, 0, "verify that all the three original element have been replaced");
});

test( "Empty replaceWith (#13401; #13596)", 8, function() {
var $el = jQuery( "<div/>" ),
test( "Empty replaceWith (trac-13401; trac-13596; gh-2204)", function() {

expect( 25 );

var $el = jQuery( "<div/><div/>" ),
tests = {
"empty string": "",
"empty array": [],
"array of empty string": [ "" ],
"empty collection": jQuery( "#nonexistent" ),

// in case of jQuery(...).replaceWith();
"empty undefined": undefined
// in case of jQuery(...).replaceWith();
"undefined": undefined
};

jQuery.each( tests, function( label, input ) {
$el.html( "<a/>" ).children().replaceWith( input );
strictEqual( $el.html(), "", "replaceWith(" + label + ")" );
$el.html( "<b/>" ).children().replaceWith(function() { return input; });
strictEqual( $el.html(), "", "replaceWith(function returning " + label + ")" );
$el.html( "<i/>" ).children().replaceWith(function( i ) { i; return input; });
strictEqual( $el.html(), "", "replaceWith(other function returning " + label + ")" );
$el.html( "<p/>" ).children().replaceWith(function( i ) {
return i ?
input :
jQuery( this ).html( i + "" );
});
strictEqual( $el.eq( 0 ).html(), "<p>0</p>",
"replaceWith(function conditionally returning context)" );
strictEqual( $el.eq( 1 ).html(), "",
"replaceWith(function conditionally returning " + label + ")" );
});
});

Expand Down