IE7/8 + <time> tag + jQuery .clone() =? - javascript

I'll preface this by saying that I already solved this issue by fundamentally changing my approach. But in the process of solving it, I put together a test case that fascinates and vexes me.
I have a string returned from an AJAX call. The string contains HTML, most of which is useless. I want one element from the string (and all its children) inserted into the DOM. A simulation of this is:
<!DOCTYPE html>
<html>
<head>
<title>wtf?</title>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
$(document).ready(function() {
var markup = '<div class="junk"><div class="good"><time datetime="2013-03-29">March 29, 2013</time></div></div>',
output = $(markup).find('.good').clone().wrap('<div />').parent().html();
$('body').append(output);
});
</script>
</head>
<body></body>
</html>
I have a hosted copy of this file up here: http://alala.smitelli.com/temp/wtf_ie.html (won't be up forever).
What this should do is extract the .good div and the child <time> element, then insert them into the body. I do .wrap().parent() to extract the element I selected in addition to its children (see this question). The .clone() and .html() are contrivances that demonstrate the problem.
To the user, it should show today's date. And it works in Chrome, Firefox, IE9, etc.:
March 29, 2013
But in IE7 and 8, the displayed text is:
<:time datetime="2013-03-29">March 29, 2013
The opening < is shown, and a colon has somehow been inserted. The closing </time> tag looks unaffected, and is not shown escaped.
What's going on here? Is this some sort of bug, or an expected behavior? Where is the colon coming from?
EDIT: As far as suggestions to add document.createElement('time') or html5shiv, neither of those seemed to change the behavior.

Very much to my surprise, I find that if I remove jQuery from the equation in terms of actually parsing the markup, the problem goes away (on both IE7 and IE8), even without createElement('time') or a shim/shiv:
<!DOCTYPE html>
<html>
<head>
<title>wtf?</title>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
$(document).ready(function() {
var div, markup, output;
markup = '<div class="junk"><div class="good"><time datetime="2013-03-29">March 29, 2013</time></div></div>';
div = document.createElement('div');
div.innerHTML = markup;
output = $(div).find('.good').clone().wrap('<div />').parent().html();
$('body').append(output);
});
</script>
</head>
<body></body>
</html>
Live Copy | Source
The change there is that I just use the browser's own handling of innerHTML and a disconnected div to parse markup, rather than letting jQuery do it for me.
So I'd have to say this may be a problem with jQuery's handling of HTML fragments on older browsers without support for HTML5 elements. But that would be a significant claim, and significant claims require significant evidence...
But if I change these lines:
div = document.createElement('div');
div.innerHTML = markup;
output = $(div).find('.good').clone().wrap('<div />').parent().html();
to:
div = $(markup);
output = div.find('.good').clone().wrap('<div />').parent().html();
I get the problem (on both IE7 and IE8): Live Copy | Source
So it does start seeming like a jQuery issue...

Related

jQuery selector is overlooking elements from a separate context

I'm making an ajax request to a page on my site with this element as a direct child of the body tag:
<div class="container" id="wantme"><div class="content"></div></div>
There's only one .container, and I want to grab its ID which I don't know.
As far as I can tell, this code should do what I want:
$.get('/page', function(data) {
id = $('.container', data).attr('id');
});
But the .container selector fails to find anything.
I did find these two workarounds. I can find .content, and I can climb up the tree like this:
id = $('.content', data).parent().attr('id');
But I can't leap directly there.
I found this workaround elsewhere on StackOverflow that works:
html = $('<div></div>').html(data);
id = html.find('.container').attr('id');
But why is it that the seemingly obvious answer doesn't work?
UPDATED ANSWER: I'll leave my original answer at the bottom, however I'm concerned it may misbehave depending on browser. jQuery's .html() makes use of Javascript's innerHTML - some browsers choose to strip <head> and <body> tags when using innerHTML, whereas others do not.
The safest method to achieve what you're after may still be the workaround you mentioned, like so:
var data = '<!doctype html><html><body><div class="container" id="findme"><div class="content"></div></div></body></html>';
var $container = $("<div />").html(data).find(".container");
var id = $container.attr("id");
console.log(id);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
More information as to the browser-related issues can be found here.
PREVIOUS ANSWER:
When you pass HTML to a jQuery element, it will ignore the <body> tags, as well as anything outside of them. Given the data string in your JSFiddle, $(data) will create something that looks like this:
<div class="container" id="findme">
<div class="content"></div>
</div>
As you can see in the HTML above, your .container isn't inside of $(data) - it is $(data).
Because $(data) is representing your .container element, you should just be able to do $(data).attr("id") to retrieve what you're after.
var data = '<!doctype html><html><body><div class="container" id="findme"><div class="content"></div></div></body></html>';
var id = $(data).attr('id');
console.log(id);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
You are not getting the ID from $('.container', data).attr('id'); is because you are setting the value of the second parameter. What you want to do is this: $('.container ' + data).attr('id');.
Update:
If data is a string then you should convert it into a DOM element: $('.container', $(data)).attr('id');

Losing a node reference? (javascript)

So I have this JS code :
var pElt = document.createElement("p");
var aElt = document.createElement("a");
aElt.textContent = "Text";
aElt.href = "#";
pElt.appendChild(aElt);
aElt.style.color = "red";
pElt.innerHTML += "<span> and more text</span>";
//aElt.style.color = "red";
document.getElementById("content").appendChild(pElt);
console.log(aElt); // always show the red attribute
There's probably some answer around here, but I cannot even describe the problem ; so I went with "losing node reference", even though it's not what happens here. (edit: in fact, that's what happens here, silly :))
So... Please try the code as it is. It works, the link is red, everyone is happy. Now comment the "aElt.style.color = "red";" line, then uncomment the other one, two lines below.
...
It does not work, the link still appear in black. What I thought is that the pointer linked to my node was either not valid anymore or the aElt was moved to a different memory address. But when I type "console.log(aElt)", it outputs the node correctly (well... I think it does), so I don't get why I can't access it after the .innerHTML change.
What interests me is what happens under the hood :)
Thanks!
index.html :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Question!</title>
</head>
<body>
<div id="content"></div>
<script src="script.js"></script>
</body>
</html>
When you overwrite the content of the <p> element by setting it's innerHTML, you're effectively turning the <a> back into HTML text, appending the <span> (as text), and then recreating new DOM nodes in the <p>. Your old reference still refers to the original <a> you created.
You could instead create that <span> the same way you created the <a>, and append that node to the <p> instead of overwriting .innerHTML.

Deleting and inserting Code in a DIV via jQuery

I know this has been adressed before, but I can't seem to get it working for me.
I am trying to create a football pitch with editable players via HTML/JavaScript/jQuery.
I can produce the field the first time when loading the page without any problems. The code looks like this:
<div id="pitch" class="updateAble">
<script type="text/javascript">
appBuilder(team1, team2);
</script></div>
appBuilder() looks like this:
var appBuilder = function (team1, team2) {
team1.Display();
team2.Display(); }
It simply creates the players on the pitch for both teams. As it does. I now want to push an input-button to call a function appUpdate(), which deletes the content of #pitch and puts the appBuilder()-part in again as to renew it (if I changed or added players):
var appUpdate = function () {
var newContent = "<script type='text/javascript'>appBuilder(team1, team2);</script>";
var updateItem = $('#pitch');
updateItem.empty();
updateItem.append(newContent);}
Here is what drives me nuts: It seems to work just fine up to and including the empty()-function. So the code has to be fine.
But when I try to append newContent to the #pitch-DIV, the programm seems to completely delete everything inside <head> and <body> it recreates a clean html-file (with empty html-, head-, body-tags) and inserts my players inside <body>.
Any ideas as to why it is doing that?
Thanks in advance!
UPADTE: The solution was a rookie mistake (which is fitting, since I'm a rookie). The Team.Display()-method was trying to do a document.write() call. As I learned: If you call document.write once the document is fully loaded, it will delete your site. Thanks to jfriend for the solution! :)
If you call document.write() AFTER the document has finished loading, then it will clear the current document and create a new empty one.
What you need to do is use DOM insertion operations rather than document.write() to add/change content in the DOM once the document has already loaded.
My guess is that the .Display() method is using document.write() and you need to change the way it works to insert content into a parent node rather than write it into the current position.
Some ways to insert content:
var newNode = document.createElement("div");
node.appendChild(newNode);
node.innerHTML = "<div>My Content</div>";
Or, if you're using jQuery, you can use it's wrappers for this:
obj.append("<div>My Content</div>");
obj.html("<div>My Content</div>");
.html() would empty and fill the div at once. Have you tried that ?
updateItem.html(newContent);
I proposed a JQuery replacement for your code that does what you want, ion the style of your own typing.
Note that I kept the .html() call to mimic your "empty()" function, but it is not necessary. Simply put he code in the append, straight into the html() et get rid of the extra unnecessary remaing bit of code.
My code replacement, as a 100% functioning .html file. Hope it helps, cheers.
<html>
<header>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
var appBuilder = function (team1, team2) {
//team1.Display();
//team2.Display();
}
var team1, team2;
</script>
</header>
<body>
<div id="pitch" class="updateAble">
<script type="text/javascript">
appBuilder(team1, team2); // Original code to be updated
</script>
</div>
<script>
var appUpdate = function () {
$("#pitch").html("<!-- Old javscript code has been flushed -->").append($("<script />", {
html: "appBuilder(team1, team2); // brand new, replaced code"
}));
}
appUpdate();
</script>
</body>
</html>

JavaScript: Unable to read all elements from innerHTML content

I am trying to read the elements in an innerHTML of a div, but it seems that alternate elements are being read.
Code Block:
<!DOCTYPE html>
<html>
<body>
<script type="text/javascript">
var tdiv=document.createElement("div");
tdiv.innerHTML="<span>a1</span><span>a2</span><span>a3</span><span>a4</span><span>a5</span>";
var cn=tdiv.getElementsByTagName("*");
var len=cn.length;
console.log("length: "+len);
console.log("tdiv len: "+tdiv.getElementsByTagName("*").length);
for(var i=0;i<len;i++){
if(cn[i]){
console.log(i+": "+cn[i].nodeName+": "+cn[i].tagName);
document.body.appendChild(cn[i]);
}
}
</script>
</body>
</html>
Output:
a1a3a5
Note: a2 and a4 are missing.
I have tried using both childNodes and getElementsByTagName("*") in all the browsers, IE, FF, Chrome, Opera, Safari and I see the same behavior.
When I add a white space between all the spans then all the elements are being read. Is this an expected behavior ? If so, why ?
The returned item is a live NodeList. You are appending them to the body element, so the NodeList is shrinking with each iteration of the for loop. This is what causes it to appear like it's arbitrarily skipping elements.
Try...
while (cn.length) {
cn[0] && document.body.appendChild(cn[0]);
}
jsFiddle.
When I add a white space between all the spans then all the elements are being read. Is this an expected behavior ? If so, why ?
Yes, it's expected. It just means instead of skipping the span elements, it's skipping the text nodes introduced by the spaces. Never rely on this - it's terribly fragile.

Javascript Removing Whitespace When It Shouldn't?

I have a HTML file that has code similar to the following.
<table>
<tr>
<td id="MyCell">Hello World</td>
</tr>
</table>
I am using javascript like the following to get the value
document.getElementById(cell2.Element.id).innerText
This returns the text "Hello World" with only 1 space between hello and world. I MUST keep the same number of spaces, is there any way for that to be done?
I've tried using innerHTML, outerHTML and similar items, but I'm having no luck.
HTML is white space insensititive which means your DOM is too. Would wrapping your "Hello World" in pre block work at all?
In HTML,any spaces >1 are ignored, both in displaying text and in retrieving it via the DOM. The only guaranteed way to maintain spaces it to use a non-breaking space .
Just a tip, innerText only works in Internet Explorer, while innerHTML works in every browser... so, use innerHTML instead of innerText
The pre tag or white-space: pre in your CSS will treat all spaces as meaningful. This will also, however, turn newlines into line breaks, so be careful.
Just an opinion here and not canonical advice, but you're headed for a world or hurt if you're trying to extract exact text values from the DOM using the inner/outer HTML/TEXT properties via Javascript. Different browsers are going to return slightly different values, based on how the browser "sees" the internal document.
If you can, I'd change the HTML you're rendering to include a hidden input, something like
<table>
<tr>
<td id="MyCell">Hello World<input id="MyCell_VALUE" type="hidden" value="Hello World" /></td>
</tr>
</table>
And then grab your value in javascript something like
document.getElementById(cell2.Element.id+'_VALUE').value
The input tags were designed to hold values, and you'll be less likely to run into fidelity issues.
Also, it sounds like you're using a .NET control of some kind. It might be worth looking through the documentation (ha) or asking a slightly different question to see if the control offers an official client-side API of some kind.
Just checked it and it looks like wrapping with the pre tag should do it.
Edit: I am wrong, ignore me.
You can get a text node's nodeValue, which should correctly represent its whitespace.
Here is a function to recursively get the text within a given element (and it's library-safe, won't fail if you use something that modifies Array.prototype or whatever):
var textValue = function(element) {
if(!element.hasOwnProperty('childNodes')) {
return '';
}
var childNodes = element.childNodes, text = '', childNode;
for(var i in childNodes) {
if(childNodes.hasOwnProperty(i)) {
childNode = childNodes[i];
if(childNode.nodeType == 3) {
text += childNode.nodeValue;
} else {
text += textValue(childNode);
}
}
}
return text;
};
This is a bit hacky, but it works on my IE.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title></title>
</head>
<body>
<div id="a">a b</div>
<script>
var a = document.getElementById("a");
a.style.whiteSpace = "pre"
window.onload = function() {
alert(a.firstChild.nodeValue.length) // should show 4
}
</script>
</body>
</html>
Some notes:
You must have a doctype.
You cannot query the DOM element before window.onload has fired
You should use element.nodeValue instead of innerHTML et al to avoid bugs when the text contains things like < > & "
You cannot reset whiteSpace once IE finishes rendering the page due to what I assume is an ugly bug
If someone could format my last post correctly it would look more readable. Sorry, I messed that one up. Basically the trick is create create a throwaway pre element, then append a copy of your node to that. Then you can get innerText or textContent depending on the browser.
All browsers except IE basically do the obvious thing correctly. IE requires this hack since it only preserves white-space in pre elements, and only when you access innerText.
This following trick preserves white-space in innerText in IE
var cloned = element.cloneNode(true);
var pre = document.createElement("pre");
pre.appendChild(cloned);
var textContent = pre.textContent
? pre.textContent
: pre.innerText;
delete pre;
delete cloned;

Categories

Resources