Reference HTML node from another element - javascript

I have this script, which I thought was relatively simple. It basically makes a tree-layout of an iframe's contents. When parts of the tree are hovered, their corresponding iframe elements are 'selected'. But it isn't working, for the life of me. Here is some quick half-pseudo code:
function traverseTree(c,o){
//c = the tree subset to put more stuff in
//o = the element to traverse
var treeNode = D.createElement('div');
c.appendChild(treeNode);
treeNode.innerHTML = o.tagName;
treeNode['refNode'] = o;
treeNode.addEventListener('mouseover',check,false);
loop(o.children){
traverseTree(treeNode,o.child);
}
}
function check(e){
alert(e.target.refNode);
}
The problem occurs with e.target.refNode. It only gives an element reference with the first node (or the HTML tag). The rest are undefined. But, when I check treeNode.refNode right after setting it, it is always right.
EDIT:
So, I made a quick test:
<html>
<head>
<title>Test</title>
<script>
window.onload = function(){
createTree(document.getElementById('tree'),
document.getElementById('page').contentWindow.document);
}
function createTree(c,o){
//get list of children
var children = o.childNodes;
//loop through them and display them
for (var i=0;i<children.length;i++){
if (typeof(children[i].tagName) !== 'undefined'){
//Create a tree node
var node = document.createElement('div');
if (children[i].childNodes.length > 0){
node.style.borderLeft = '1px dotted black';
node.style.marginLeft = '15px';
}
c.appendChild(node);
//Reference to the actual node
node.refNode = children[i];
//Add events
node.addEventListener('mouseover',selectNode,false);
//Display what type of tag it is
node.innerHTML += "<"+children[i].tagName.toLowerCase()+">";
//Add in its child nodes
createTree(node,children[i]);
//ending tag... CRASHES THE PROGRAM
node.innerHTML += "</"+children[i].tagName.toLowerCase()+">";
}
}
}
function selectNode(e){
document.getElementById('info').innerHTML = e.target.refNode;
}
</script>
</head>
<body>
<iframe id='page' src='some_page.html'></iframe>
<div id='info' style='border-bottom:1px solid red;margin-bottom:10px;'></div>
<div id='tree'></div>
</body>
</html>
and I figured out that adding innerHTML after appending the child tree nodes was taking away the children's refNode property. So, that's where the problem is occurring.

So, I guess the solution would just be to change .innerHtml to .appendChild(document.createTextNode(...)). My script is only for local use, and only built for FF3+, so other than that, there should be no problems.

You cannot store objects in attributes of elements. What you see if you check the attribute is the string-representation of the assigned node. If you would use jquery, you could implement that with jQuery.data()

Related

Javascript code leaking memory to DOM nodes - where is the Javascript reference

I have the following JavaScript code below running on my Chrome Browser. It inserts 2 buttons, one to insert a select Element with 100,000 options if the list does not already exist, and another button which deletes the list if it does exist.
<!doctype html>
<html>
<head>
</head>
<body>
<script type="text/javascript">
insertButtons()
function insertSelectList(){
let selectList = document.querySelector("#selectList")
if(selectList!=null)return;
selectList = document.createElement("select")
selectList.setAttribute("id", "selectList")
for(let i=0; i<100000; i++){
let option = document.createElement("option")
option.setAttribute("value", i)
option.innerText = i
selectList.appendChild(option)
}
document.body.appendChild(selectList)
}
function removeSelectList(){
let selectList = document.querySelector("#selectList")
if(selectList==null)return;
selectList.remove()
}
function insertButtons(){
let insertListBtn = document.createElement("button")
let removeListBtn = document.createElement("button")
insertListBtn.innerText = "Insert List"
removeListBtn.innerText = "Remove List"
insertListBtn.addEventListener("click", insertSelectList)
removeListBtn.addEventListener("click", removeSelectList)
document.body.appendChild(insertListBtn)
document.body.appendChild(removeListBtn)
}
</script>
</body>
</html>
My script seems to be leaking memory as per the chrome DEV tools memory tab's memory snapshot functionality. Using the 2 buttons, I create and delete a Select list a few times, and after doing this, the memory usage of my webpage shoots up (see image linked below for reference). The select lists that I have created and subsequently deleted seem to be hanging out in memory. When we view the second snapshot in comparison with the first, there is an increase in the option and select elements.
As per my understanding when an HTML node is removed from the DOM tree and there is no javascript reference to the node, it should be freed up by the garbage collector. Can someone please tell me why these select nodes and their option children are sticking around in memory? I dont see a javascript reference to the select nodes.
Chrome Dev Tools memory snapshots,
I don't know the reason of this behaviour (in fact, there are some related Chromium bugs open - you can find them easily if you search). But let me show you what I found.
One of the conditions for your detached elements be garbage collected is that there should be no variables referencing them. Example:
<script type="text/javascript">
// Note these variables outside the function!
// Your DOM elements will be referenced even tho they're removed
let div;
let span;
const append = () => {
div = document.createElement('div');
span = document.createElement('span');
div.appendChild(span);
span.remove()
div.remove();
}
document.addEventListener('click', append);
</script>
As a result, your detached elements will be still in memory:
So let's move them inside our function:
<script type="text/javascript">
const append = () => {
let div = document.createElement('div');
let span = document.createElement('span');
div.appendChild(span);
span.remove()
div.remove();
}
document.addEventListener('click', append);
</script>
Awesome! Function executed, internal variables not exist anymore, detached elements are not in memory:
However, I can find some weird behaviour if I replace div and span elements with select and option:
<script type="text/javascript">
const append = () => {
let select = document.createElement('select');
let option = document.createElement('option');
select.appendChild(option);
option.remove()
select.remove();
}
document.addEventListener('click', append);
</script>
Memory contains quite a lot of detached elements, including multiple Detached InternalNode, Detached HTMLSlotElement, Detached HTMLDivElement etc:
Honestly, it looks like a bug for me. Related to internal select tag implementation. But I might be wrong and missing something.
Just a couple of more observations:
Without a child option it works as expected:
const append = () => {
let select = document.createElement('select');
document.body.append(select);
select.remove();
}
If function is executed not as a listener for some event (like document click event) but executed directly instead - everything works as expected as well:
<script type="text/javascript">
const append = () => {
let select = document.createElement('select');
let option = document.createElement('option');
select.appendChild(option);
document.body.append(select);
option.remove()
select.remove();
};
append();
</script>

It is possible to comment and uncomment block of code from DOM tree using JQuery? [duplicate]

This sounds a little crazy, but I'm wondering whether possible to get reference to comment element so that I can dynamically replace it other content with JavaScript.
<html>
<head>
</head>
<body>
<div id="header"></div>
<div id="content"></div>
<!-- sidebar place holder: some id-->
</body>
</html>
In above page, can I get reference to the comment block and replace it with some content in local storage?
I know that I can have a div place holder. Just wondering whether it applies to comment block.
Thanks.
var findComments = function(el) {
var arr = [];
for(var i = 0; i < el.childNodes.length; i++) {
var node = el.childNodes[i];
if(node.nodeType === 8) {
arr.push(node);
} else {
arr.push.apply(arr, findComments(node));
}
}
return arr;
};
var commentNodes = findComments(document);
// whatever you were going to do with the comment...
console.log(commentNodes[0].nodeValue);
It seems there are legitimate (performance) concerns about using comments as placeholders - for one, there's no CSS selector that can match comment nodes, so you won't be able to query them with e.g. document.querySelectorAll(), which makes it both complex and slow to locate comment elements.
My question then was, is there another element I can place inline, that doesn't have any visible side-effects? I've seen some people using the <meta> tag, but I looked into that, and using that in <body> isn't valid markup.
So I settled on the <script> tag.
Use a custom type attribute, so it won't actually get executed as a script, and use data-attributes for any initialization data required by the script that's going to initialize your placeholders.
For example:
<script type="placeholder/foo" data-stuff="whatevs"></script>
Then simply query those tags - e.g.:
document.querySelectorAll('script[type="placeholder/foo"]')
Then replace them as needed - here's a plain DOM example.
Note that placeholder in this example isn't any defined "real" thing - you should replace that with e.g. vendor-name to make sure your type doesn't collide with anything "real".
Building off of hyperslug's answer, you can make it go faster by using a stack instead of function recursion. As shown in this jsPerf, function recursion is 42% slower on my Chrome 36 on Windows and 71% with IE11 in IE8 compatibility mode. It appears to run about 20% slower in IE11 in edge mode but faster in all other cases tested.
function getComments(context) {
var foundComments = [];
var elementPath = [context];
while (elementPath.length > 0) {
var el = elementPath.pop();
for (var i = 0; i < el.childNodes.length; i++) {
var node = el.childNodes[i];
if (node.nodeType === Node.COMMENT_NODE) {
foundComments.push(node);
} else {
elementPath.push(node);
}
}
}
return foundComments;
}
Or as done in TypeScript:
public static getComments(context: any): Comment[] {
const foundComments = [];
const elementPath = [context];
while (elementPath.length > 0) {
const el = elementPath.pop();
for (let i = 0; i < el.childNodes.length; i++) {
const node = el.childNodes[i];
if (node.nodeType === Node.COMMENT_NODE) {
foundComments.push(node);
} else {
elementPath.push(node);
}
}
}
return foundComments;
}
There is an API for document nodes traversal: Document#createNodeIterator():
var nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_COMMENT
);
// Replace all comment nodes with a div
while(nodeIterator.nextNode()){
var commentNode = nodeIterator.referenceNode;
var id = (commentNode.textContent.split(":")[1] || "").trim();
var div = document.createElement("div");
div.id = id;
commentNode.parentNode.replaceChild(div, commentNode);
}
#header,
#content,
#some_id{
margin: 1em 0;
padding: 0.2em;
border: 2px grey solid;
}
#header::after,
#content::after,
#some_id::after{
content: "DIV with ID=" attr(id);
}
<html>
<head>
</head>
<body>
<div id="header"></div>
<div id="content"></div>
<!-- sidebar placeholder: some_id -->
</body>
</html>
Edit: use a NodeIterator instead of a TreeWalker
If you use jQuery, you can do the following to get all comment nodes
comments = $('*').contents().filter(function(){ return this.nodeType===8; })
If you only want the comments nodes of the body, use
comments = $('body').find('*').contents().filter(function(){
return this.nodeType===8;
})
If you want the comment strings as an array you can then use map:
comment_strings = comments.map(function(){return this.nodeValue;})
Using document.evaluate and xPath:
function getAllComments(node) {
const xPath = "//comment()",
result = [];
let query = document.evaluate(xPath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0, length = query.snapshotLength; i < length; ++i) {
result.push(query.snapshotItem(i));
}
return result;
}
getAllComments(document.documentElement);
from my testing, using xPath is faster than treeWalker:
https://jsben.ch/Feagf
This is an old question, but here's my two cents on DOM "placeholders"
IMO a comment element is perfect for the job (valid html, not visible, and not misleading in any way).
However, traversing the dom looking for comments is not necessary if you build your code the other way around.
I would suggest using the following method:
Mark the places you want to "control" with markup of your choice (e.g a div element with a specific class)
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
Find the placeholders the usual way (querySelector/classSelector etc)
var placeholders = document.querySelectorAll('placeholder');
Replace them with comments and keep reference of those comments:
var refArray = [];
[...placeholders].forEach(function(placeholder){
var comment = document.createComment('this is a placeholder');
refArray.push( placeholder.parentNode.replaceChild(comment, placeholder) );
});
at this stage your rendered markup should look like this:
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
Now you can access each of those comments directly with your built refArray and do whatevere it is you wanna do... for example:
replace the second comment with a headline
let headline = document.createElement('h1');
headline.innerText = "I am a headline!";
refArray[1].parentNode.replaceChild(headline,refArray[1]);
If you just want to get an array of all comments from a document or part of a document, then this is the most efficient way I've found to do that in modern JavaScript.
function getComments (root) {
var treeWalker = document.createTreeWalker(
root,
NodeFilter.SHOW_COMMENT,
{
"acceptNode": function acceptNode (node) {
return NodeFilter.FILTER_ACCEPT;
}
}
);
// skip the first node which is the node specified in the `root`
var currentNode = treeWalker.nextNode();
var nodeList = [];
while (currentNode) {
nodeList.push(currentNode);
currentNode = treeWalker.nextNode();
}
return nodeList;
}
I am getting over 50,000 operations per second in Chrome 80 and the stack and recursion methods both get less than 5,000 operations per second in Chrome 80. I had tens of thousands of complex documents to process in node.js and this worked the best for me.
https://jsperf.com/getcomments/6

Chrome: How to insert two consecutive spans into editable body without nesting them

I am trying to insert two consecutive spans into an editable body element on Chrome. My problem is that the 2nd span is ending up inside the first span instead of next to it.
I have simplified my example, but in real life, the end user might have moved the cursor or selected some text in between the two inserts.
<html>
<head>
<script>
function load(){
insert("<span style='color:red'>hello</span>");
insert("<span>goodbye</span>");
}
function insert(sHtml){
var oSel = window.getSelection();
var oRange = oSel.rangeCount > 0 ? oSel.getRangeAt(0) : void 0;
if(!oRange){
oRange = window.document.createRange();
oRange.selectNodeContents(window.document.body);
}
var newFrag = oRange.createContextualFragment(sHtml);
oRange.insertNode(newFrag);
oRange.collapse(false);
oSel.removeAllRanges()
oSel.addRange(oRange);
}
</script>
</head>
<body onload="load()">
</body>
</html>
You're doing strange and complex things to insert your nodes. Why using the selection ?
Using jquery, you could define your insert function like this :
function insert(html) {
$("body").append(html);
}
Without jquery, you would have to add the node and set a text in the node :
var newNode = document.createElement("span");
newNode.setAttribute("style", "color:red");
newNode.appendChild(document.createTextNode("hello"));
document.body.append(newNode);

Search for non-nested nodes with specific attribute set

The short long of it is I'm working on a small library in javascript that will replace <div src="somesite"></div> with the content from the specified source. This would allow coders to create dynamic pages without having to do more work server-side without the annoyance of using iframes.
What I need is an efficent way to get the top most div nodes of a branch with an src attribute. E.G:
<div src="somesite/pagelet.htm" id="div1">
<div src="somesite/fallback.htm" id="div2"></div>
</div>
<div src="somesite/pagelet2.htm" id="div3"></div>
I want to retrieve #div1 and #div3 and ignore #div2 until later. At the moment I'm using the following function, but am wondering if there is a more efficent way to do this:
function getRootElementsByAttribute(rootEle, tag, attr) {
try {
tag = tag.toLowerCase();
if (rootEle.tagName.toLowerCase() === tag && rootEle.hasAttribute(attr)) {
return [rooEle]
}
var eles = rootEle.getElementsByTagName(tag),
nodes = [], ele, isRoot, eleParent, a;
for (a=0; a<eles.length; a++) {
ele = eles[a];
if (ele.hasAttrinute(attr)) {
isRoot = true;
eleParent = ele;
while ((eleParent = eleParent.parentNode)) {
if (eleParent.tagName.toLowerCase() === 'div' && eleParent.hasAttribute(attr)) {
isRoot = false;
break;
}
}
if (isRoot == true) nodes.push(ele)
}
}
}catch(e){}
return nodes;
}
Please no answers suggesting the use of a library. It seems overkill to import a whole library when all it would be used for is this single function
You could try to use an XPath expression to get all root divs with the attribute source using something like the following XPath expression:
/div[#src]
/div selects all divs that are on the root level. For all divs in the document use //div.
[#src] specifies that you only want nodes with the 'src' attribute.
var xmlDoc = //load your document here
var xpath = "/div[#src]"
var nodes = xmlDoc.evaluate(xpath, xmlDoc, null, XPathResult.ANY_TYPE,null);

Is it possible to get reference to comment element/block by JavaScript?

This sounds a little crazy, but I'm wondering whether possible to get reference to comment element so that I can dynamically replace it other content with JavaScript.
<html>
<head>
</head>
<body>
<div id="header"></div>
<div id="content"></div>
<!-- sidebar place holder: some id-->
</body>
</html>
In above page, can I get reference to the comment block and replace it with some content in local storage?
I know that I can have a div place holder. Just wondering whether it applies to comment block.
Thanks.
var findComments = function(el) {
var arr = [];
for(var i = 0; i < el.childNodes.length; i++) {
var node = el.childNodes[i];
if(node.nodeType === 8) {
arr.push(node);
} else {
arr.push.apply(arr, findComments(node));
}
}
return arr;
};
var commentNodes = findComments(document);
// whatever you were going to do with the comment...
console.log(commentNodes[0].nodeValue);
It seems there are legitimate (performance) concerns about using comments as placeholders - for one, there's no CSS selector that can match comment nodes, so you won't be able to query them with e.g. document.querySelectorAll(), which makes it both complex and slow to locate comment elements.
My question then was, is there another element I can place inline, that doesn't have any visible side-effects? I've seen some people using the <meta> tag, but I looked into that, and using that in <body> isn't valid markup.
So I settled on the <script> tag.
Use a custom type attribute, so it won't actually get executed as a script, and use data-attributes for any initialization data required by the script that's going to initialize your placeholders.
For example:
<script type="placeholder/foo" data-stuff="whatevs"></script>
Then simply query those tags - e.g.:
document.querySelectorAll('script[type="placeholder/foo"]')
Then replace them as needed - here's a plain DOM example.
Note that placeholder in this example isn't any defined "real" thing - you should replace that with e.g. vendor-name to make sure your type doesn't collide with anything "real".
Building off of hyperslug's answer, you can make it go faster by using a stack instead of function recursion. As shown in this jsPerf, function recursion is 42% slower on my Chrome 36 on Windows and 71% with IE11 in IE8 compatibility mode. It appears to run about 20% slower in IE11 in edge mode but faster in all other cases tested.
function getComments(context) {
var foundComments = [];
var elementPath = [context];
while (elementPath.length > 0) {
var el = elementPath.pop();
for (var i = 0; i < el.childNodes.length; i++) {
var node = el.childNodes[i];
if (node.nodeType === Node.COMMENT_NODE) {
foundComments.push(node);
} else {
elementPath.push(node);
}
}
}
return foundComments;
}
Or as done in TypeScript:
public static getComments(context: any): Comment[] {
const foundComments = [];
const elementPath = [context];
while (elementPath.length > 0) {
const el = elementPath.pop();
for (let i = 0; i < el.childNodes.length; i++) {
const node = el.childNodes[i];
if (node.nodeType === Node.COMMENT_NODE) {
foundComments.push(node);
} else {
elementPath.push(node);
}
}
}
return foundComments;
}
There is an API for document nodes traversal: Document#createNodeIterator():
var nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_COMMENT
);
// Replace all comment nodes with a div
while(nodeIterator.nextNode()){
var commentNode = nodeIterator.referenceNode;
var id = (commentNode.textContent.split(":")[1] || "").trim();
var div = document.createElement("div");
div.id = id;
commentNode.parentNode.replaceChild(div, commentNode);
}
#header,
#content,
#some_id{
margin: 1em 0;
padding: 0.2em;
border: 2px grey solid;
}
#header::after,
#content::after,
#some_id::after{
content: "DIV with ID=" attr(id);
}
<html>
<head>
</head>
<body>
<div id="header"></div>
<div id="content"></div>
<!-- sidebar placeholder: some_id -->
</body>
</html>
Edit: use a NodeIterator instead of a TreeWalker
If you use jQuery, you can do the following to get all comment nodes
comments = $('*').contents().filter(function(){ return this.nodeType===8; })
If you only want the comments nodes of the body, use
comments = $('body').find('*').contents().filter(function(){
return this.nodeType===8;
})
If you want the comment strings as an array you can then use map:
comment_strings = comments.map(function(){return this.nodeValue;})
Using document.evaluate and xPath:
function getAllComments(node) {
const xPath = "//comment()",
result = [];
let query = document.evaluate(xPath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0, length = query.snapshotLength; i < length; ++i) {
result.push(query.snapshotItem(i));
}
return result;
}
getAllComments(document.documentElement);
from my testing, using xPath is faster than treeWalker:
https://jsben.ch/Feagf
This is an old question, but here's my two cents on DOM "placeholders"
IMO a comment element is perfect for the job (valid html, not visible, and not misleading in any way).
However, traversing the dom looking for comments is not necessary if you build your code the other way around.
I would suggest using the following method:
Mark the places you want to "control" with markup of your choice (e.g a div element with a specific class)
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
Find the placeholders the usual way (querySelector/classSelector etc)
var placeholders = document.querySelectorAll('placeholder');
Replace them with comments and keep reference of those comments:
var refArray = [];
[...placeholders].forEach(function(placeholder){
var comment = document.createComment('this is a placeholder');
refArray.push( placeholder.parentNode.replaceChild(comment, placeholder) );
});
at this stage your rendered markup should look like this:
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
Now you can access each of those comments directly with your built refArray and do whatevere it is you wanna do... for example:
replace the second comment with a headline
let headline = document.createElement('h1');
headline.innerText = "I am a headline!";
refArray[1].parentNode.replaceChild(headline,refArray[1]);
If you just want to get an array of all comments from a document or part of a document, then this is the most efficient way I've found to do that in modern JavaScript.
function getComments (root) {
var treeWalker = document.createTreeWalker(
root,
NodeFilter.SHOW_COMMENT,
{
"acceptNode": function acceptNode (node) {
return NodeFilter.FILTER_ACCEPT;
}
}
);
// skip the first node which is the node specified in the `root`
var currentNode = treeWalker.nextNode();
var nodeList = [];
while (currentNode) {
nodeList.push(currentNode);
currentNode = treeWalker.nextNode();
}
return nodeList;
}
I am getting over 50,000 operations per second in Chrome 80 and the stack and recursion methods both get less than 5,000 operations per second in Chrome 80. I had tens of thousands of complex documents to process in node.js and this worked the best for me.
https://jsperf.com/getcomments/6

Categories