Skip to content

Conversation

usmonster
Copy link
Contributor

I've traced the issue to element-properties.js.

Here's the gist (sorry, neither jsbin nor jsFiddle support IE8 anymore):

https://gist.github.com/usmonster/9349332

Notice I load aight after jQuery. (Loading before causes other issues... see, e.g., #24, #29.)
Note also that this could affect libraries or JS other than jQuery.

@usmonster
Copy link
Contributor Author

After some debugging, I see where the issue manifests in jQuery. When tmp.textContent is cleared, the single text node in nodes loses a bunch of properties, or they become "unknown" or something weird if using aight's shim. This explodes later on, when trying to access its .ownerDocument property.

This manifestation of the issue seems to have appeared after a bug fix introduced in jQuery 1.9 (issue, code). It seems to work fine when using jQuery 1.8.*...

...though don't get me wrong--this doesn't seem to be @jquery's bug. aight's Element.prototype.textContent shim (in element-properties.js) should probably be fixed, though I'm not yet exactly sure how to do that. This is some spooky stuff. Maybe @eligrey has an idea?

@shawnbot
Copy link
Owner

shawnbot commented Mar 4, 2014

I'm curious: does it occur when you include jquery before aight? For what it's worth, aight was never intended to work (or even tested) with jquery, specifically because d3 does most of what it does, and I personally haven't found much use for jquery when I'm using d3. There's an inherent conflict in jquery providing its own workarounds to missing features and aight shimming the browser's capabilities to play nice with d3 (which assumes ES5 and W3C DOM). If aight subverts jquery's feature detection, then I would say the two are fundamentally incompatible.

@shawnbot
Copy link
Owner

shawnbot commented Mar 4, 2014

Sorry, just re-read and saw that the load order is correct. Without understanding jquery's feature detection, it's hard to say. I'm curious to see what happens if you load aight after the window loads (and, presumably, jquery has done all that it needs to), though.

@usmonster
Copy link
Contributor Author

@shawnbot Thanks for the quick response. I've tried loading aight after document load, and the issue persists. I'm fairly sure it's not specific to jQuery or its internal magic, but it's just one manifestation of the bug. I can reproduce it on any version of IE if I take out the check in element-properties.

I'll try to post a simplified, jQuery-independent demo of the bug sometime tomorrow.

@usmonster
Copy link
Contributor Author

Here's the test, which fails on a page with aight loaded regardless of whether or not jQuery is also loaded:

var e = document.createElement('div');
e.innerHTML = " ";
var n = e.childNodes[0];
n.ownerDocument;
e.textContent = "";
n.ownerDocument;

Without the .textContent shim, the last line executes fine. With the shim, it throws an "Unspecified error." (Updated gist and issue.)

@usmonster
Copy link
Contributor Author

It also looks like it's not related to HTML entities, as replacing the second line with e.innerText = 'boom!'; results in the same error.

I think this issue is caused by the supposition that .innerText by itself is a suitable stand-in for .textContent in oldIE. Aside from the fact that the functions technically do two different things, IE8's particular implementation of Element.prototype.innerText seems to make assumptions about its children and change or add certain properties that don't necessarily exist on its implementation of TextNode (note: this analysis is incomplete, but check out the element's properties in F12 before and after accessing e.textContent).

Needs more investigation, but I think a solution would be either to remove the shim or to add some more logic to its existing get/set functions.

@usmonster
Copy link
Contributor Author

I'm about to test this, but after another quick glance at your shim it looks like the issue is that you're adding the textContent property to the prototype of Element and not to Node... So certain types of non-Element Nodes (e.g., TextNodes) will barf. I'll let you know if I can verify this.

@usmonster
Copy link
Contributor Author

Okay, after some more investigation, it looks like there are two issues with the current shim:

  1. Reusing innerText is not a good idea, since the setter does some weird recursive stuff on the childNodes of the element, and textContent doesn't really call for all that. The spec says:

    On setting, any possible children this node may have are removed and, if it the new string is not empty or null, replaced by a single Text node containing the string this attribute is set to.

  2. Since textContent is a property of ALL Nodes, a complete solution would shim all possible kinds of Node, including text nodes, comments, cdata, etc. The current shim is only added to Elements, which is insufficient.

A slightly nicer (though still incomplete) shim would look something like this:

// ...
// see http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#Node3-textContent
var nodeValue = Object.getOwnPropertyDescriptor(Text.prototype, "nodeValue");
Object.defineProperty(Text.prototype, "textContent", {
        // It won't work if you just drop in nodeValue.get
        // and nodeValue.set or the whole descriptor.
        get: function() {
                return nodeValue.get.call(this);
        },
        set: function(x) {
                return nodeValue.set.call(this,x);
        }
});
var innerText = Object.getOwnPropertyDescriptor(Element.prototype, "innerText");
Object.defineProperty(Element.prototype, "textContent", {
        get: function() {
                // It won't work if you just drop in innerText.get or the whole descriptor.
                return innerText.get.call(this);
        },
        set: function(x) {
                var c;
                while(!!(c=this.firstChild)) { this.removeChild(c); }
                if (x!==null) {
                        c=document.createTextNode(x);
                        this.appendChild(c);
                }
                c=null;
                //return innerText.set.call(this,x); // nope! you do weird things!
                return x;
        }
});
// ...

I've done some basic testing and it seems to work as expected for elements & text nodes. As it's already an improvement to what's currently in the plugin, I can either put this in a pull request now or fill out the cases to handle the remaining node types, exceptions, etc. @shawnbot, what do you think?

@shawnbot
Copy link
Owner

Thanks for looking into this. Honestly, I just grabbed this shim from a gist somewhere, and I'm realizing that, now that aight is more popular, it definitely needs to be rewritten and that I probably need a formal test suite to catch these issues.

That said, your code above looks like a good start, so go ahead and open a pull request (preferably on a branch) and note what else needs to be done, then I can merge it when it's ready.

First draft, but closer to the spec:
http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#Node3-textContent

- No longer uses Element.innerText as a setter, since there were
  undesirable side effects (recursive node modification, ew).
- Works with Text nodes as well now.
- Still needs to work with non-text, non-element nodes (e.g. cdata,
  comment, etc..), and to handle error cases as described in the spec.
- (Would be nice to have tests, but maybe that's something for the
  plugin as a whole...)
@usmonster
Copy link
Contributor Author

Attached pull request to this issue. What's left to do:

  • work with non-text, non-element nodes (e.g. comment, document fragments, etc.. list of nodeTypes here)
  • work with weird IE elements that somehow have different behaviors (e.g. script, style, etc.)
  • insert reasonable line breaks around block(-like) elements? WONTFIX
  • post before & after performance metrics (jsperf)
  • handle error cases as described in the spec

References:

usmonster added 2 commits May 22, 2014 19:31
Now works with non-text, non-element nodes (e.g. comment, document
fragments, etc.) as well as weird IE elements that somehow have
different behaviors (e.g. script, style, etc.).

Fixes shawnbot#27.
@usmonster
Copy link
Contributor Author

@shawnbot, I think I'm done with all the changes -- feel free to check out the code, poke around, comment, or just merge if it looks good.

@usmonster
Copy link
Contributor Author

It might be convenient to strategically add some newlines when recursively getting the textContent of an element (just to make the output the same as FF/Chrome), though not entirely sure if that's necessary/worth it, if I'm reading the spec correctly. It may also be a pain/impossible since it seems that all leading & trailing whitespace (including line breaks) is trimmed from most nodes in IE8.

After some more investigation, it looks like there's just no way around it without unnecessarily complicating things and/or adding significant performance penalties, so I wouldn't be too concerned about the line break issue. (For reference, jQuery's .text() does pretty much the same thing in IE8, due to the same limitations I mentioned.)

@usmonster usmonster changed the title bug in aight's shim for Element.prototype.textContent results in "Unspecified error." Incomplete Node.prototype.textContent shim causes errors Jun 13, 2014
@shawnbot shawnbot merged commit 9c724ab into shawnbot:master Jun 13, 2014
shawnbot pushed a commit that referenced this pull request Jun 13, 2014
@shawnbot
Copy link
Owner

Whew, okay! Merged and tagged v1.2.5. Thanks for this!

This was referenced Sep 13, 2014
Merged
MasterInQuestion referenced this pull request in MasterInQuestion/Markup Aug 30, 2025
	Firefox 45 below lack support for "innerText".
	Polyfill the property ("textContent") may not be viable for IE8 below. (no "defineProperty" etc.)
	Plus the unnecessary sophistry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants