Skip to content

Loading…

#4087 - insertAfter, insertBefore, etc do not work when destination is original element #1033

Closed
wants to merge 2 commits into from

4 participants

@PaulBRamos

No description provided.

@gibson042 gibson042 commented on the diff
src/manipulation.js
@@ -272,7 +272,8 @@ jQuery.fn.extend({
// Make sure that the elements are removed from the DOM before they are inserted
// this can help fix replacing a parent with child elements
- if ( !isFunc && typeof value !== "string" ) {
+ // #4087 - don't detach if original and target are the same (element is removed otherwise)
@gibson042 jQuery Foundation member

I think this entire special-case block and corresponding expense of .index can be eliminated if the jQuery( this ).remove() on L296 moves after the .before/.append. Did you try anything like that?

I didn't, but I can definitely give it a shot.

@markelog jQuery Foundation member

jQuery.remove() not only remove node from DOM, but also remove all data from the element,
so with this change, if we have, for example, events attached to node that replaces itself, all events from that node will be lost.

@PaulBRamos how you feel about this suggestion?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@gibson042 gibson042 commented on the diff
src/manipulation.js
@@ -525,7 +527,7 @@ jQuery.buildFragment = function( args, context, scripts ) {
if ( !fragment ) {
fragment = context.createDocumentFragment();
- jQuery.clean( args, context, fragment, scripts );
+ jQuery.clean( args, context, fragment, scripts, originalContext[0] );
@gibson042 jQuery Foundation member

That's a dangerous expression (see L506)... I'm not saying it's wrong, but you should throw a lot at this to test for robustness.

I was a bit worried about it myself, but didn't see any issues during testing. Can you think of a scenario where this might be an issue?

Ahh, nevermind, I missed the reference to L506. I'll explore that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@gibson042 gibson042 commented on the diff
src/manipulation.js
@@ -783,6 +785,11 @@ jQuery.extend({
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 ) ) ) {
+ // #4087 - dom manipulation not working when destination is original element (element is removed)
+ if ( originalContext && originalContext == elem ) {
+ continue;
@gibson042 jQuery Foundation member

This is going to have implications with respect to managing embedded <script>s, so test it extra thoroughly. Also, we use === instead of == except for the special case of window.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@gibson042
jQuery Foundation member

This approach has promise, but also a fair amount of risk and potential pitfalls. I have to wonder about alternatives like creating temporary proxy nodes (that wouldn't get detached when their analogs were pulled into a fragment), or—potentially more performant—a callback for domManip/buildFragment that lets the caller see the set of preexisting nodes about to be swept into a fragment and perform some prep work before that happens. In this case, the callback would look something like:

function( fragmentChildren ) {
    self.each(function() {
        jQuery._data( this, "sub", isDisconnected( this ) || !jQuery.inArray( this, fragmentChildren ) ?
            this :
            this.parentNode.insertBefore( this.ownerDocument.createTextNode(""), this )
        );
    });
}
@PaulBRamos

I definitely see your concerns. I had considered the method you mentioned above, but went with this solution to minimize the footprint of the change. I'll explore this alternative if you think it's more promising.

@dmethvin
jQuery Foundation member

@PaulBRamos Thanks for looking at this ticket! As you can tell it's been sitting around for a while but your solution looks like it could be pretty elegant. It's definitely worth doing some experiments to see if there is a better/smaller way.

@gibson042
jQuery Foundation member

@PaulBRamos I'd certainly like to see how it goes. Now that I've pulled the rug out from under these changes with 22f58bd anyway, how about giving it a shot with a new branch from master?

@PaulBRamos

@gibson042 That's exactly what I'm doing. :)

@dmethvin
jQuery Foundation member

Where does this stand now? It seemed like the technique was showing some promise.

@PaulBRamos

I'm about to do a pull request for a newer version of this fix.

@PaulBRamos

new pull request for this issue: #1047

@PaulBRamos PaulBRamos closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 18, 2012
  1. @PaulBRamos

    #4087 - DOM Manipulation (insertAfter, insertBefore, appendTo, etc) n…

    PaulBRamos committed
    …ot working when destination is original element
  2. @PaulBRamos
Showing with 43 additions and 3 deletions.
  1. +10 −3 src/manipulation.js
  2. +33 −0 test/unit/manipulation.js
View
13 src/manipulation.js
@@ -272,7 +272,8 @@ jQuery.fn.extend({
// Make sure that the elements are removed from the DOM before they are inserted
// this can help fix replacing a parent with child elements
- if ( !isFunc && typeof value !== "string" ) {
+ // #4087 - don't detach if original and target are the same (element is removed otherwise)
@gibson042 jQuery Foundation member

I think this entire special-case block and corresponding expense of .index can be eliminated if the jQuery( this ).remove() on L296 moves after the .before/.append. Did you try anything like that?

I didn't, but I can definitely give it a shot.

@markelog jQuery Foundation member

jQuery.remove() not only remove node from DOM, but also remove all data from the element,
so with this change, if we have, for example, events attached to node that replaces itself, all events from that node will be lost.

@PaulBRamos how you feel about this suggestion?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ if ( !isFunc && typeof value !== "string" && jQuery( value ).index( this ) == -1 ) {
value = jQuery( value ).detach();
}
@@ -498,6 +499,7 @@ function cloneFixAttributes( src, dest ) {
jQuery.buildFragment = function( args, context, scripts ) {
var fragment, cacheable, cachehit,
+ originalContext = context,
first = args[ 0 ];
// Set context from what may come in as undefined or a jQuery collection or a node
@@ -525,7 +527,7 @@ jQuery.buildFragment = function( args, context, scripts ) {
if ( !fragment ) {
fragment = context.createDocumentFragment();
- jQuery.clean( args, context, fragment, scripts );
+ jQuery.clean( args, context, fragment, scripts, originalContext[0] );
@gibson042 jQuery Foundation member

That's a dangerous expression (see L506)... I'm not saying it's wrong, but you should throw a lot at this to test for robustness.

I was a bit worried about it myself, but didn't see any issues during testing. Can you think of a scenario where this might be an issue?

Ahh, nevermind, I missed the reference to L506. I'll explore that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
// Update the cache, but only store false
// unless this is a second parsing of the same content
@@ -649,7 +651,7 @@ jQuery.extend({
return clone;
},
- clean: function( elems, context, fragment, scripts ) {
+ clean: function( elems, context, fragment, scripts, originalContext ) {
var i, j, elem, tag, wrap, depth, parent, div, hasBody, tbody, handleScript, jsTags,
safe = context === document && safeFragment,
ret = [];
@@ -783,6 +785,11 @@ jQuery.extend({
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 ) ) ) {
+ // #4087 - dom manipulation not working when destination is original element (element is removed)
+ if ( originalContext && originalContext == elem ) {
+ continue;
@gibson042 jQuery Foundation member

This is going to have implications with respect to managing embedded <script>s, so test it extra thoroughly. Also, we use === instead of == except for the special case of window.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+
// Append to fragment and handle embedded scripts
fragment.appendChild( elem );
if ( typeof elem.getElementsByTagName !== "undefined" ) {
View
33 test/unit/manipulation.js
@@ -2037,6 +2037,39 @@ test("manipulate mixed jQuery and text (#12384, #12346)", function() {
equal( div.find("*").length, 3, "added 2 paragraphs after inner div" );
});
+test("insertAfter, insertBefore, etc do not work when destination is original element. Element is removed (#4087)", function() {
+ expect(10);
+
+ var elems;
+
+ jQuery.each([
+ "appendTo",
+ "prependTo",
+ "insertBefore",
+ "insertAfter",
+ "replaceAll"
+ ], function( index, name ) {
+ elems = jQuery( [
+ "<ul id='test4087-complex'><li class='test4087'><div>c1</div>h1</li><li><div>c2</div>h2</li></ul>",
+ "<div id='test4087-simple'><div class='test4087-1'>1<div class='test4087-2'>2</div><div class='test4087-3'>3</div></div></div>",
+ "<div id='test4087-multiple'><div class='test4087-multiple'>1</div><div class='test4087-multiple'>2</div></div>"
+ ] ).appendTo( "#qunit-fixture" );
+
+ // complex case based on http://jsfiddle.net/pbramos/gZ7vB/
+ jQuery( "#test4087-complex div" )[ name ]( "#test4087-complex li:first-child div:last-child" );
+ equal( jQuery( "#test4087-complex li:first-child div" ).length, 2, name +" a node to itself, complex case." );
+
+ // simple case
+ jQuery( ".test4087-1" )[ name ]( ".test4087-1" );
+ equal( jQuery( ".test4087-1" ).length, 1, name +" a node to itself, simple case. " );
+
+ // clean for next test
+ jQuery( "#test4087-complex" ).remove();
+ jQuery( "#test4087-simple" ).remove();
+ jQuery( "#test4087-multiple" ).remove();
+ });
+});
+
testIframeWithCallback( "buildFragment works even if document[0] is iframe's window object in IE9/10 (#12266)", "manipulation/iframe-denied.html", function( test ) {
expect( 1 );
Something went wrong with that request. Please try again.