Adam Wulf

The personal site, tutorials, and projects of Adam Wulf

The undocumented life of JavaScript’s parentNode property – Internet Explorer edition!

July 21st, 2009 · 6 Comments

Did you know that the parentNode property in JavaScript doesn’t always point to the element’s parent??! And not only that, but that very same node that’s lying to you also appears multiple times in the DOM.

I didn’t want to have to tell you this way, but there’s just no way around it: in Internet Explorer 7 (I’ve left testing 6 and 8 as an exercise for the reader :-) ) the parentNode property can flat out lie to you – specifically – with pasted content in a rich text editor like TinyMCE. That’s right, a bold-faced chain-yanking tall-tale’d lie, and I’ll prove it to you.

Documented Behavior

According to the W3C documentation, “the parentNode property returns the parent node of a node.” And based on that, you would expect the following code to alert “true!” for as many times as the node has children:

var node = document.getElementById('someElement');
for(var i=0;i<node.childNodes.length;i++){
     alert(node == node.childNodes[i].parentNode);
}

This is true for the vast majority of cases, but is not always the case. I’ve found an exception in how Internet Explorer 7 handles pasted content inside rich text areas like TinyMCE. Even jQuery will select children who’s parentNode is incorrect!

Just get to the Demo!

I’ve set up a demo page that can consistently repeat the bug. For the demo to work, you’ll need to:

  • use Internet Explorer 7
  • open the attached rtf in WordPad
  • paste into the TinyMCE content area
  • press the “validate” button on the bottom toolbar

Besides IE’s fearsome use of the DIR tag, you’ll notice:

  • I’ve painted duplicate nodes red in TinyMCE
  • An brief XML output of the DOM is output into a text area beneath TinyMCE showing the duplicate node
  • A count of each node is displayed as an attribute in the text area. Notice the count=2 in the nodes at the bottom of the textarea! Yikes!

If you’re not in the mood to fire up the demo yourself, just click the link below to see what the DOM ends up looking like. Take special notice of the count=2 on the nodes at the bottom:

View IE7′s invalid DOM after a paste

Try It Yourself!

For the demo, I created a simple TinyMCE plugin that validates the editor’s DOM. Paste the magic text in IE7, and it’ll mark any invalid nodes in red and also updates a textarea with a simplified XML representation of the DOM. The plugin uses both native JavaScript functions and jQuery to find nodes with invalid parentNodes.

View the plugin code

Download the plugin code

A TinyMCE Bug?

An obvious question is: is this a TinyMCE bug? and the answer is “No.” There is no way to manually add the exact same node to multiple places in the DOM. Instead, the browser simply moves the node to the new place, and removes it from its original location. Even if Spocke tried to create this bug, he couldn’t :)

If you believe me, skip to the conclusion. If you don’t believe me, open up this second demo that uses the following code to append a node without calling removeChild() first. Even without specifically removing a node before appending it somewhere else, there still only exists one node in the document:

$(function(){
var node = document.getElementById("copyMe");
var target = document.getElementById("target");
target.appendChild(node);
});

In Conclusion

As far as parentNode is concerned, if:

  • you’re writing a plugin for TinyMCE or similar
  • you’re dealing with (possibly) pasted content
  • relying on parentNode

then:

  • validate! If(myNode != myNode.childNodes[i].parentNode) is true then you have a problem!

I hope you enjoyed the latest the-undocumented-life-of post! Also a fun read: The undocumented life of jQuery’s .append()!

Close
/**
 * $Id: editor_plugin_src.js 520 2008-01-07 16:30:32Z adam.wulf $
 *
 * @author Adam Wulf http://welcome.totheinter.net/
 */

(function() {
	tinymce.create('tinymce.plugins.DuplicatePlugin', {

		 /**
		  * This function will return the number
		  * of times that the findMe node occurs
		  * inside of the DOM
		  * @param n: the node to look inside of
		  * @param findMe: the node to find
		  * @return: the number of times findMe is found
		  *          inside of n
		  */
		 countOccurances : function(n, findMe){
		 	if(n == findMe) return 1;
		 	if(n.nodeType != 1) return 0;
		 	var ret = 0;
		 	for(var i=0;i<n.childNodes.length;i++){
		 		ret += this.countOccurances(n.childNodes[i], findMe);
		 	}
		 	return ret;
		 },

		 /**
		  * returns a simplified XML representation of
		  * the input node n. an attribute "count"
		  * will be added to every node, and its value
		  * will be the number of times that that node
		  * appears in the entire DOM
		  *
		  * invalid nodes are also changed to display in red text
		  *
		  * @param n: the node to convert to XML
		  * @param indent: the string of empty spaces
		  *                to be used to indent the XML
		  * @param ed: the TinyMCE Editor
		  * @return: an XML (ish) string of n
		  * @sideeffect: invalid nodes are changed to red text
		  */
		 toXML : function(n, indent, ed){
            if(n.nodeType == 3) return indent + n.nodeValue + "\n";
            var ret = "";
            if(n.nodeName.toLowerCase() == "br"){
                return ret + indent + "<BR>\n";
            }
            ret += indent + "<" + n.nodeName + " count=" + this.countOccurances(ed.getBody(), n) + ">\n";
            for(var i=0;i<n.childNodes.length;i++){
                var invalidParent = (n != n.childNodes[i].parentNode);
                if(invalidParent) ret += indent + "  <error>\n";
                ret += this.toXML(n.childNodes[i], indent + (invalidParent ? "    " : "  "), ed);
                if(invalidParent) ret += indent + "  </error>\n";

                if(invalidParent) n.childNodes[i].style.color = "red";
            }
            ret += indent + "</" + n.nodeName + ">\n";
            return ret;
        },

        /**
         * validates the Editor's <body> to try and find
         * duplicate nodes. This function relies on the user
         * copying and pasting the sample.rtf from
         * http://welcome.totheinter.net/2009/07/21/the-undocumented-life-of-javascripts-parentnode-property-internet-explorer-edition/
         *
         * A textarea with id #theContents is assumed, and
         * the XML representation of the <body> is added there.
         *
         * also, to show that jQuery also shows the problem, a
         * brief message will be shown at the top of the XML output
         * describing if jQuery also selected an invalid child.
         *
         * @param ed: TinyMCE Editor
         * @return: nothing
         * @sideeffect: contents of #theContents modified
         */
        validate : function(ed){
        	var jQueryText = "jQuery only selected valid children!\n\n";

    		var parent = $(ed.getBody()).find("p span").get(0);
    		if(parent){
				var kids = $(ed.getBody()).find("p span").children();
				for(var i=0;i<kids.length;i++){
					var kid = kids.get(i);
					if(kid.parentNode != parent){
						jQueryText = "jQuery selected invalid children as well!\n\n";
					}
				}
    		}

			$("#theContents").val(jQueryText + this.toXML(ed.getBody(), "", ed));
        },

		/**
		 * initialize the plugin.
		 *
		 * add the custom command, button,
		 * and nodeChange handler
		 */
		init : function(ed, url) {

			// Register commands
			ed.addCommand('mceFindDuplicates', function() {
				this.validate(ed);
			}, this);

			// Register buttons
			ed.addButton('validate', {
				label : 'validate',
				cmd : 'mceFindDuplicates'
			});

			ed.onNodeChange.add(function(ed, cm, n) {
				this.validate(ed);
			}, this);

		},

		getInfo : function() {
			return {
				longname : 'Duplicate',
				author : 'Adam Wulf',
				authorurl : 'http://welcome.totheinter.net',
				infourl : 'http://welcome.totheinter.net',
				version : "1.0.0"
			};
		}
	});

	// Register plugin
	tinymce.PluginManager.add('duplicate', tinymce.plugins.DuplicatePlugin);
})();
Close
<BODY count=1>
  <P count=1>
    <SPAN count=1>
      <P count=1>
        Software:
      </P>
      <FONT count=1>
        <DIR count=1>
          <DIR count=1>
            <DIR count=1>
              <DIR count=1>
                <DIR count=1>
                  <DIR count=1>
                    <DIR count=1>
                      <DIR count=1>
                        <B count=2>
                          <FONT count=2>
                            <P count=2>
                              System Software Overview:
                            </P>
                          </FONT>
                        </B>
                        <FONT count=2>
                        </FONT>
                        <FONT count=2>
                          <P count=2>
                            System Version: Mac OS X 10.5.5s
                          </P>
                        </FONT>
                      </DIR>
                    </DIR>
                  </DIR>
                </DIR>
              </DIR>
            </DIR>
          </DIR>
        </DIR>
      </FONT>
      <error>
        <B count=2>
          <FONT count=2>
            <P count=2>
              System Software Overview:
            </P>
          </FONT>
        </B>
      </error>
      <error>
        <FONT count=2>
        </FONT>
      </error>
      <error>
        <FONT count=2>
          <P count=2>
            System Version: Mac OS X 10.5.5s
          </P>
        </FONT>
      </error>
    </SPAN>
  </P>
</BODY>

Tags: javascript · programming

6 responses so far ↓

  • 1 juanmaguerrero // Jul 21, 2009 at 9:13 am

    http://is.gd/1GnUj The undocumented life of JavaScript’s parentNode property – Internet Explorer edition!

    This comment was originally posted on Twitter

  • 2 mlane // Jul 21, 2009 at 10:54 am

    The undocumented life of JavaScript’s parentNode property – Internet Explorer edition – http://bit.ly/MW0wL

    This comment was originally posted on Twitter

  • 3 marcotrulla // Jul 22, 2009 at 10:42 am

    reading: The undocumented life of JavaScript’s parentNode property – Internet Explorer edition! http://bit.ly/13Ae5o

    This comment was originally posted on Twitter

  • 4 Lori // Jul 22, 2009 at 3:03 pm

    Hmm I just tried this and the issue only occurs if you paste from WordPad. Pasting from notepad or Word returns what you expect. I think this is just how WordPad internally represents rich text and pasting in IE just faithfully retains that.

  • 5 Tim Down // Oct 15, 2009 at 12:33 pm

    This happens because of the way IE parses empty tags such as <span />. See this blog posting for more: http://tmik.co.uk/?p=184

  • 6 Rusty // Dec 17, 2009 at 7:33 pm

    Using a great tool called clipview, if you copy from Wordpad, then remove the \lang1033, you don’t get this problem. I’ve discovered this with other rich text content as well. The \lang controls seem to be what cause IE problems, though I don’t know why.

Leave a Comment or