Skip to content

Conversation

@elijahmanor
Copy link
Contributor

Clearing a Cloned Element's Style Causes the Original Element's Style to be Cleared

Introduction

In IE9 and IE10 you can run into the situation when clearing ("") a cloned element's style will also clear out the original element's corresponding style.

The actual jQuery Core Ticket #8908 isolates the bug when using the background-image style.

While I was trying to recreate and research this issue I had a feeling that there may be other styles that have the same issue, so I've created a test suite checking 252 CSS1-3 styles and testing various assertions.

Along the way, I also found some additional inconsistencies. These findings are technically unrelated to the #8908 issue so I will list those inconsistencies somewhere else and we can discuss those later.

Source Element's Style Cleared When Cloned Element's Style Cleared

IE9 & IE10

After testing 252 various styles only the following 8 ended up being problematic.

  • background-attachment
  • background-color
  • background-image
  • background-position
  • background-repeat
  • background-clip
  • background-origin
  • background-size

IE 6/7/8, Chrome, Firefox, Opera

Thankfully these browsers did not have the problem described in this issue when tested.

Proposed Solutions

1. Use Default Values when Trying to Empty Style

The following fix resolves the weird issue by substituting and empty string with it's default value. An object is used to store the style name and associated default value. If the value happens to be an empty string then it will look in the object for a matching key and if there is a match it will use that instead.

// css.js
+ // These styles were identified to be problems in IE9/10
+ var defaultValue = {
+     "background-attachment": "scroll",
+     "background-color": "transparent",
+     "background-image": "none",
+     "background-position": "0% 0%",
+     "background-repeat": "repeat",
+     "background-clip": "border-box",
+     "background-origin": "padding-box",
+     "background-size": "auto auto"
+ };

+ value = ( value === "" && defaultValue[ name ] !== undefined ) ? defaultValue[ name ] : value;

try {
    style[ name ] = value;
} catch(e) {}

Pros

  • This is very explicit in describing which styles are problematic in IE

Cons

  • There is extra code executed when clearing the style for a nonIE browser. This could be a problem because updating a style via the .css() method is very common
  • There is a considerable amount of code to account for all problematic styles and this will bloat the jQuery source

2. Strange Side Effect That Breaks the Clone Connection

This solution is a little more strange and not so obvious. I ran into this fix as I was building up my unit tests and asserting a bunch of things I thought should be true. One of those assertions was verifying that when setting a style using the $elem.css( "someStyle", "someValue" ) method that it actually worked. So, I used the getter ($elem.css( "someValue" ) method, but by doing so I found the problem going away just by calling the getter! And with that the following solution came about.

You may notice a slightly strange piece of code ( window.getComputedStyle( elem, null ) || {} ), but that was needed to get around an Opera bug I had when working with XML nodes. Technically I could have made the whole fix into one statement, but the minification process does a decent job of squishing down the fix to look like (b.nodeType===1&&a.getComputedStyle&&(i=(a.getComputedStyle(b,null)||{}).backgroundImage).

// clone.js
jQuery.extend({
    clone: function( elem, dataAndEvents, deepDataAndEvents ) {
        var srcElements,
            destElements,
            i,
-           clone;
+           clone,
+           fix8908;

        if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
+           // Fixes #8909 - By accessing one of the element's computed styles it breaks the
+           // connection with the cloned element's styles in IE9/10
+           if ( elem.nodeType === 1 && window.getComputedStyle ) {
+               fix8908 = ( window.getComputedStyle( elem, null ) || {} ).backgroundImage;
+           }
+
            clone = elem.cloneNode( true );
        // IE<=8 does not properly clone detached, unknown element nodes
        } else {
            ...
        }
        ...
    },
    ...
});

Pros

  • The amount of code for this fix is relatively small
  • This code is limited to the clone execution path, which happens infrequently when compared to the .css() method approach in option Accumulated, not yet pulled commits #1.

Cons

  • This relies on a weird quirk in IE9/10 to get around the problem, but it seems to be the most succinct.

3. Have IE9 & IE10 Fix this Bug

It would be ideal if no patch had to be made to jQuery at all and IE9 and IE10 could apply a fix instead. Seeing that the issue is found in IE9 though may rule out this option entirely.

Pros

  • No patch for jQuery would be needed

Cons

  • Even if the bug was fixed immediately for IE9 & IE10 there would still be an issue with versions of IE9 that didn't have the IE fix.

Unit Test

The following Unit Test exposes the problem in IE9 and IE10. In browsers that don't support these styles I am skipping over them and asserting that they are not an issue.

test( "Clearing a Cloned Element's Style Shouldn't Clear the Original Element's Style (#8908)", function() {
    expect( 8 );

    var baseUrl = document.location.href.replace( /([^\/]*)$/, "" );
    var styles = [
        { name: "background-attachment", value: [ "fixed" ], expected: [ "scroll" ] },
        { name: "background-color", value: [ "rgb(255, 0, 0)", "rgb(255,0,0)" ], expected: [ "transparent" ] },
        { name: "background-image", value: [ 'url("test.png")', 'url(' + baseUrl + 'test.png)', 'url("' + baseUrl + 'test.png")' ], expected: [ "none" ] },
        { name: "background-position", value: [ "5% 5%" ], expected: [ "0% 0%" ] },
        { name: "background-repeat", value: [ "repeat-y" ], expected: [ "repeat" ] },
        { name: "background-clip", value: [ "content-box" ], expected: [ "border-box" ] },
        { name: "background-origin", value: [ "content-box" ], expected: [ "padding-box" ] },
        { name: "background-size", value: [ "80px 60px" ], expected: [] }
    ];

    jQuery.each( styles, function(index, style) {
        var $source, source, $clone;

        style.expected = style.expected.concat( [ "", "auto" ] );
        $source = jQuery( "<div />" );
        source = $source[ 0 ];
        if ( source.style[ style.name ] === undefined ) {
            ok( true, style.name +  ": style isn't supported and therefore not an issue" );
            return true;
        }
        $source.css( style.name, style.value[0] );
        $clone = $source.clone();
        $clone.css( style.name, "" );

        ok( ~jQuery.inArray( $source.css( style.name ), style.value ),
            "Clearning clone.css() doesn't affect source.css(): " + style.name +
            "; result: " + $source.css( style.name ) +
            "; expected: " + style.value.join( "," ) );
    });
});

Conclusion

Based on the research I have done testing 252 CSS styles I have isolated this issue to only 8 styles in IE9 and IE10.

I've recommended solving this issue 3 different ways, but in the end the 2nd option seems best to me since it only is executed in the clone execution path and is significantly smaller than the 1st option. The 3rd option of having IE fix this issue seems too late since the issue also affects IE9.

@dmethvin
Copy link
Member

dmethvin commented Aug 8, 2012

Nice detective work! 👍 I think we'll have to land something in 1.9/2.0 since IE9 is not gonna get fixed and I think it's even too late for an IE10 fix at this point since they have gone RTM with Windows 8.

The call to getComputedStyle on the clone source element worries me from a performance perspective because I think that will cause a reflow in many cases. It would be good to perf test it and if needed use a feature test for the bug to ensure that only the guilty browsers are hurt by any performance hit.

There's a very similar-sounding bug regarding .clone() in IE7 ... I wonder if it could be solved the same way? http://bugs.jquery.com/ticket/9777

The commits are kind of messed up, probably because you submitted the earlier pull from your master, but we can cherry-pick d5d8622 off there. If you haven't already you should probably clean up your master if you haven't already. http://stackoverflow.com/questions/1628088/how-to-reset-my-local-repository-to-be-just-like-the-remote-repository-head

@elijahmanor
Copy link
Contributor Author

Yeah, I wondered if the PR came in clean or not. Arg. Yeah, I'll try what is listed on that SO link.

The fix comes down to adding the following before the call to elem.cloneNode( true );

// Fixes #8909 - By accessing one of the element's computed styles it breaks the
// connection with the cloned element's styles in IE9/10
if ( elem.nodeType === 1 && window.getComputedStyle ) {
    fix8908 = ( window.getComputedStyle( elem, null ) || {} ).backgroundImage;
}

As one of the cons I mentioned performance because it would run that code for any browser. I wasn't sure a good way to isolate just to IE9/10.

Good point about causing a reflow. I'm not sure about that one, but I can try to dig into that further.

I'll take a look at #9777 as you mentioned. It maybe be related, but as for the above bug it didn't show itself in IE7.

I figured this PR would probably wait since 1.8 is about finalized. Thanks for your feedback.

If you have any ideas on how to isolate this to IE9/10 I am all ears ;)

Copy link
Member

Choose a reason for hiding this comment

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

I'd like to avoid having numbered identifiers. This seems like something that belongs in the support module.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rwldrn Yeah, I wasn't too thrilled about that either, but the fix depends on getting a style off of the object returned from getComputedStyle(). The reason I went ahead and saved it off to a variable was to get around the Expected an assignment or function call and instead saw an expression. JSHint error. I could save it off to an existing variable such as i, but that felt weird too.

I am thinking of pulling out a feature detection into the jQuery.support module to test for this strange bug so that it can be scoped to only IE9/10 and not affect the performance of other browsers.

With this support feature, I will still need to save the style to a variable for JSHint to like it. Would you recommend renaming the variable, reusing another variable, or something else?

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 a strange thing... It's really bothersome that IE10 actually released a browser that allows a clone's style property to retain reference to the source element.

Anyway, yeah this belongs in jQuery.support, maybe called jQuery.support.whenWillTheyLearn ;)

Copy link
Member

Choose a reason for hiding this comment

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

Instead of having the ticket number in the variable name, usually we mention the ticket in a short comment. So you can just pick a short name or even reuse i if it isn't used in the code path.

@elijahmanor
Copy link
Contributor Author

Changes Since the Initial Pull Request

Source Code Changes

I make a new test in the support module called clearCloneStyle that is true if the browser clears only the clone's style appropriate, but is false if it incorrectly clear's the original element's style as IE9 and IE10 do with 8 different styles. I picked the shortest style name of the 8 to test to keep the code short. The feature test intentionally doesn't use jQuery in order to keep the test as fast as possible.

support.clearCloneStyle = (function() {
    var source = document.createElement( "div" ),
        styleName = "backgroundClip",
        value = "content-box",
        clone;

    source.style[ styleName ] = value;
    clone = source.cloneNode( true );
    clone.style[ styleName ] = "";

    return source.style[ styleName ] === value;
})();

I am using the support.clearCloneStyle property in the fix right before the clone in manipulation.js

...
if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
+    // Fixes #8909 - By accessing one of the element's computed styles it breaks the
+    // connection with the cloned element's styles in IE9/10
+    if ( !jQuery.support.clearCloneStyle && elem.nodeType === 1 &&
+        window.getComputedStyle ) {
+        i = ( window.getComputedStyle( elem, null ) || {} ).backgroundPosition;
+    }

    clone = elem.cloneNode( true );
    ...
}
...

As I was retesting the code in IE6/7/8/9/10 I noticed that one of the styles (backgroundPosition) was not passing in IE9 :( The weird getComputedStyle call fixed all of the 8 broken styles in IE10, but only 7 of them in IE9. So, I had to add the following code in the style method of the css.js file to fix that outlier.

...         
// If a hook was provided, use that value, otherwise just set the specified value
if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
+    // IE9/10 Clearing Cloned Style Clear's Original Style. Fixes bug #8908
+    value = !jQuery.support.clearCloneStyle && value === "" && 
+        name.match( /backgroundPosition/ ) ? "0% 0%" : value;

    // Wrapped to prevent IE from throwing errors when 'invalid' values are provided
    // Fixes bug #5509
    try {
        style[ name ] = value;
    } catch(e) {}
}
...

Browsers Tested

I have run the above changes through the following browsers

Windows

  • Chrome 22d/21.0b/20/19/18/17/16/15/14
  • Firefox 15.0b/14/13/12/11/10/9/8/7/6/5/4/3.6/3
  • IE 10/9/8/7/6
  • Opera 12.5b/12.0/11.6/11.5/11.1/10.0
  • Safari 5.1/5.0/4.0

Mac

  • Chrome 20.0b/19/18/17/16/14
  • Firefox 14.0b/13/12/11/10/9/8/7/6/5
  • Opera 12/11.6/11.1
  • Safari 5.1/5.0/4.0

Unit Test Changes

As I was testing the code in all the above browsers I added another assertion to the Unit Test to check if the style that was being cleared on the clone element was indeed being reset. Because of that addition assertion I made the following changes...

  • Added a unit test to verify that the clone was indeed reset correctly
  • Add the #ff0000 value to match the backgroundColor for Opera 11.1
  • When a background-image is cleared in Firefox it sets the value to the full image path that it is inheriting
  • Added the -1000px 0% value to match the backgroundPosition for Firefox 5.0
  • Switched background-clip to padding-box because Safari 5.0 doesn't support that value

Summary of Changes

  • I added the jQuery.support.clearCloneStyle check and used it before the elem.cloneNode( true ) to limit when a call to getComputedStyle occurs (IE9 / 10)
  • I had to add another fix in the style method to fix the backgroundPosition style that wasn't fixed by the getComputedStyle above. This code path is also limited by the jQuery.support.clearCloneStyle check.
  • I added another assertion to the Unit Test to verify that clearing out the style indeed resets it to an expected default value.

Copy link

Choose a reason for hiding this comment

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

Is this a regex because of backgroundPositionX and backgroundPositionY? Is that value still work for those? Also, can you move the regex out to it's own variable at the top of the file with the other regex's so it can get cached?

jaubourg and others added 19 commits October 19, 2012 01:13
We only care about the result of parseJSON so there's no reason to feature detect the entire test.
Test expects input elements having name='id', name='name', and name='target'. Additionally, these should have id='id', id='name', and id='target' respectively. No element was provided with id='id' or name='id', but rather one element had two name attributes (illegal) with the values 'id' and 'name' respectively.
@elijahmanor
Copy link
Contributor Author

@dmethvin I just pulled in the changes and fixed the differences. Unfortunately I saw you said "rebase" after I pushed ;( So there are tons of commits shown.

@timmywil
Copy link
Member

Didn't actually close.

@timmywil timmywil closed this Nov 16, 2012
@dmethvin
Copy link
Member

I haven't landed this yet, I just created a branch for @elijahmanor to review.

@dmethvin dmethvin reopened this Nov 16, 2012
@timmywil
Copy link
Member

Woops, read the close, thought it needed to be closes/closed. :/

@dmethvin dmethvin closed this in 5904468 Nov 18, 2012
@dmethvin
Copy link
Member

I realized there was no need to fix for backgroundPositionX/Y since they're not standard or supported across browsers (Firefox is the holdout, possibly Opera as well).

mescoda pushed a commit to mescoda/jquery that referenced this pull request Nov 4, 2014
@lock lock bot locked as resolved and limited conversation to collaborators Jan 21, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.