I am trying to create a type writer effect that will get the nodes of an element and then display the values of those nodes sequentially at a given speed. If the node is a text node I want it to go in and sequentially display each character in that text.
HTML:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<!-- item will be appened to this layout -->
<div id="log" class="sl__chat__layout">
</div>
<!-- chat item -->
<script type="text/template" id="chatlist_item">
<div data-from="{from}" data-id="{messageId}" id="messageID">
<div id="messageBox">
<span id="message">
{message}
</span>
</div>
</div>
</script>
Javascript:
// Please use event listeners to run functions.
document.addEventListener('onLoad', function(obj) {
// obj will be empty for chat widget
// this will fire only once when the widget loads
});
document.addEventListener('onEventReceived', function(obj) {
// obj will contain information about the event
e++
typeEffect(e);
});
var speed = 50;
var e = 1;
function typeEffect(inp) {
var o = inp;
document.getElementById("messageID").id= "messageID"+o;
document.getElementById("message").id= "message"+o;
var text = $("#message"+o).text();
$("#message"+o).text('');
var i = 0;
var timer = setInterval(function() {
if(i < text.length) {
$("#message"+o).append(text.charAt(i));
i++;
}
else{
clearInterval(timer);
};
}, speed);
}
Here is an example of an element with the id "message2". As you can see it contains some text, then a span containing an image and then some more text.
<span id="message2">
Hello
<span class="emote">
<img src="https://static-cdn.jtvnw.net/emoticons/v1/1251411/1.0">
</span>
There
</span>
In my code posted above I am able to create the typewriter effect of the text. However, using the above example, I can't figure out a way to type "Hello" then the span with the image and then "There".
I have tried to get the nodes like this:
var contents = document.getElementById("message"+o).childNodes;
When I log that to the console I get: NodeList(3) [text, span.emote, text]
From there however I am having trouble accessing the nodeValues. I keep getting errors thrown. I am not sure exactly what I am doing wrong. From there I am also not sure the proper way to empty the "message"+o element and then refill it with the information.
Hopefully that explains everything!
By using $.text(), you are getting your Element's textContent, and all its markup content is gone (actually all its children).
In order to retain this content, you need to store the DOM nodes instead of just their textContent.
From there, you will have to detach the DOM tree and walk it while appending every Element, iterating slowly over each TextNode's textContent.
However, doing so is not that easy. Indeed, the fact that we will re-append the DOM nodes inside the document means that the detached DOM tree
we were walking will get broken.
To circumvent that, we thus need to create a copy of the detached DOM tree, that we will keep intact, so we can continue walking it just like if it were the original one.
And in order to know where to place our elements, we need to store each original node as a property of the cloned one.
To do so, we'll create two TreeWalkers, one for the original nodes, and one for the cloned version. By walking both at the same time, we can set our clones' .original property easily.
We then just have to go back to the root of our clones TreeWalker and start again walking it, this time being able to append the correct node to its original parentNode.
async function typeWrite(root, freq) {
// grab our element's content
const content = [...root.childNodes];
// move it to a documentFragment
const originals = document.createDocumentFragment();
originals.append.apply(originals, content);
// clone this documentFragment so can keep a clean version of the DOM tree
const clones = originals.cloneNode(true);
// every clone will have an `original` node
// clones documentFragment's one is the root Element, still in doc
clones.original = root;
// make two TreeWalkers
const originals_walker = document.createTreeWalker(originals, NodeFilter.SHOW_ALL, null);
const clones_walker = document.createTreeWalker(clones, NodeFilter.SHOW_ALL, null);
while(originals_walker.nextNode() && clones_walker.nextNode()) {
// link each original node to its clone
clones_walker.currentNode.original =
originals_walker.currentNode
}
while(clones_walker.parentNode()) {
// go back to root
}
// walk down only our clones (will stay untouched now)
while(clones_walker.nextNode()) {
const clone = clones_walker.currentNode;
const original = clone.original;
// retrieve the original parentNode (which is already in doc)
clone.parentNode.original
.append(original); // and append the original version of our currentNode
if(clone.nodeType === 3) { // TextNode
const originalText = original.textContent;
// we use a trimmed version to avoid all non visible characters
const txt = originalText.trim().replace(/\n/g, '');
original.textContent = ''; // in doc => empty for now
let i = 0;
while(i < txt.length) {
await wait(freq); // TypeWriting effect...
original.textContent += txt[i++];
}
// restore original textContent (invisible to user)
original.textContent = originalText;
}
}
}
typeWrite(message2, 200)
.catch(console.error);
function wait(time) {
return new Promise(res => setTimeout(res, time));
}
<span id="message2">
Hello
<span class="emote">
<img src="https://static-cdn.jtvnw.net/emoticons/v1/1251411/1.0">
</span>
There
</span>
Related
I'm trying to create a chrome extension, that will loop through a certain span innerText and if that value matches it will change that text to hyperlinked text with a URL connected to that specific word.
On the web page there are these tags (chat,flamingo,sample ticket, test, etc.) there is a <span> element within a span element with a class "badge-tag"
What I plan is to have an object with numerous entries, key will represent the text of tag(<span>) and the value will be the URL I want to convert this to.
The html will look something like this so I have to grab the value of document.getElementsByClassName("badge-tag").innerText.
Sample html
<span class="badge-tag"><span>facebook</span></span>
<span class="badge-tag"><span>youtube</span></span>
<span class="badge-tag"><span>twitter</span></span>
<span class="badge-tag"><span>bing</span></span>
I created this JS:
if(typeof listitems === 'undefined'){
const init = function(){
const listitems = document.getElementsByClassName("badge-tag");
const tagMap = {
"facebook":"https://google.com",
"youtube":"https://youtube.com",
"test/obj":"https://docs.gorgias.com",
"instagram/dm":"https://instagram.com"
};
// console.log(tagMap['facebook'])
if(text in tagMap){
console.log("ok")
}
Object.values(listitems).forEach(item => {
const text = item.firstChild;
const link = document.createElement('a');
link.setAttribute("href", tagMap); //test link.setAttribute("href", "https://google.com");
link.setAttribute("target", "_blank");
item.appendChild(link);
link.appendChild(text);
// document.querySelectorAll('.badge-tag a')
// .forEach(function(elem){
// elem.setAttribute('target','_blank');
// })
}
)
}
init();
}
What this code does is converts all the <span> with class "badge-tag" to hyperlinked, I need a way to connect the object tagMap with it so that only the if text is equal to object key, it will add it's respective value.
I understand that I'm missing a good chunk here, as I need For Loop, and probably something else to validate but I'm kinda stuck, any help would be appreciated even if it's just a link to an article.
This is the JsFiddle I was messing with https://jsfiddle.net/94yd2h3g/31/
Thanks!
I made only a few minor modifications to your code:
/* if(text in tagMap){
console.log("ok")
console.log(tagMap[text])
} */
if(typeof listitems === 'undefined'){
const init = function(){
const listitems = document.getElementsByClassName("badge-tag");
const tagMap = {
"facebook":"https://google.com",
"youtube":"https://youtube.com",
"test/obj":"https://docs.gorgias.com",
"instagram/dm":"https://instagram.com"
};
// console.log(tagMap['facebook'])
text = "facebook"
if(text in tagMap){
console.log("ok")
}
Object.values(listitems).forEach(item => {
const text = item.textContent;
if(text in tagMap) {
const link = document.createElement('a');
link.setAttribute("href", tagMap[text]);
link.setAttribute("target", "_blank");
while(item.firstChild)
link.appendChild(item.firstChild);
item.appendChild(link);
}
// document.querySelectorAll('.badge-tag a')
// .forEach(function(elem){
// elem.setAttribute('target','_blank');
// })
}
)
}
init();
}
The text you're looking for is best achieved with Node.textContent. I also added a loop to run through all children and add those, which should make the implementation a bit more flexible. Briefly, you can have a situation where an element's text can be made up of more than one text node and looping through .firstChild accounts for that.
Of course, there are multiple ways to skin a cat. You could create a new normalised text node and destroy the old one, use innerHTML, etc., but this is what seemed simplest to me and required the fewest changes.
I have an html element tree sample(below) that I want to return with relevant data for every match I got in database.Lets say there are 5 matches.
Do I need to create 5 given elements and populate them with javascript data?
I'm gonna run a loop, but this looks like it will be performance costly(creating all element tree for every match). Instead, can i use the given element(pic) populate it with javascript and drop it onto dom instead (x times)? If possible how ?
<!-- sample elem -->
<div class="col-12 col-md-4" style="display: none">
<div class="card my-3 mx-1">
<img src="" alt="img">
<div class="card-body">
<div class="row">
<div class="col-12 p-1">Country</div>
<div class="col-3 p-1">State</div>
<div class="col-4 p-1">City</div>
</div>
</div>
</div>
</div>
To further elabourate on my comment: it is often the repeated insertion of elements into the DOM tree that causes performance issue, because the document needs to reflow every time a new node is inserted. You should not be worried about calling/invoking document.createElement() too many times: that is the least of your concern.
Therefore, I would suggest that you use a function to create your entire sample element. You can then invoke this function to create the entire card element as you please in each iteration of the loop, and then append it to the document fragment.
Pseudo code:
function createCard() {
// Create the entire `sample element` as you would call it
const el = <something>;
return el;
}
// Create new document fragment to hold all the nodes
// At this point, we are NOT injecting them into the DOM yet
const fragment = new DocumentFragment();
// Go through your data and create new card for each data point
for (let i = 0; i < 5; i++) {
fragment.appendChild(createCard());
}
// Now this is when you insert the entire bulk of the content into the DOM
document.querySelector('#myInsertionTarget').appendChild(fragment);
A proof-of-concept code is as follow:
// Since we are creating so many `<div>` elements
// It helps to further abstract its logic into another function
function createDivElement(classes, text) {
const div = document.createElement('div');
if (classes.length)
div.classList.add(...classes);
if (text)
div.innerText = text;
return div;
}
// Call this whenever you want to create a new card
function createCard(i) {
const colCountry = createDivElement(['col-12', 'p-1'], 'Country');
const colState = createDivElement(['col-3', 'p-1'], 'State');
const colCity = createDivElement(['col-4', 'p-1'], 'City');
const row = createDivElement(['row']);
row.appendChild(colCountry);
row.appendChild(colState);
row.appendChild(colCity);
const cardBody = createDivElement(['card-body']);
cardBody.appendChild(row);
const image = document.createElement('img');
image.alt = 'img';
// Proof-of-concept image source, you can ignore this!
image.src = `https://placehold.it/100x50?text=Image%20${i+1}`;
const imageLink = document.createElement('a');
imageLink.href = '#';
imageLink.appendChild(image);
const card = createDivElement(['card', 'my-3', 'mx-1']);
card.appendChild(imageLink);
card.appendChild(cardBody);
const outer = createDivElement(['col-12', 'col-md-4']);
// outer.style.display = 'none';
outer.appendChild(card);
return outer;
}
// Create new document fragment
const fragment = new DocumentFragment();
// In each iteration of the loop, insert the new card element into fragment
for (let i = 0; i < 5; i++) {
const el = createCard(i);
fragment.appendChild(el);
}
// When you're done generating the entire set of elements
// You can then insert the fragment into your DOM (finally!)
document.querySelector('#app').appendChild(fragment);
<div id="app"></div>
Performance impact is not great for 15-20 elements and that markup alone.
However if you can prove it's slow, know that strings are faster. So a faster approach is this:
Store the markup template as a string
Create a string with the final markup - it can repeat the template as many times as it needs, obviously filled with that
Insert markup in target node
Here is how that would look like:
const products = [{ title: 'gearbox' }, { title: 'drive shaft' }, { title: 'spark plug'}]
const myTemplate = '<div class="product">{title}</div>'
const finalMarkup = products.map(({ title }) => myTemplate.replace('{title}', title))
document.getElementId('targetNode').innerHtml = finalMarkup
I trying to build a script that will clear html documents of specific html tags(Table, td, tr ) but save the data once the tag is removed.
It would function as a very specific document washer needed for work. Where all the tables are removed from the html doc and then pasted into a custom editor. I want to remove all table elements from the DOM while retaining the contents of those tables. So removing and preserving the inside of them. Each HTML is going to vary greatly part of the issue is there is no standardization with the HTML they all vary greatly in formatting.
<script>
function WashElements() {
var list = document.getElementsByTagName("table");
list.removeChild(list.childNodes[0]);
}
</script>
<html>
<head>
<script type="text/javascript" src="script.js"></script>
</head>
<body>
<table id="toc" class="toc" border="1" summary="Contents">
<tr><td><p>This table is going</p></td></tr>
</table>
</body>
</html>
here is a little html example its basically what were dealing with. We are copying out of a browser window into another editor thats on a separate browser window. I apologize for my lack of clarity this my first stack overflow question, You are all awesome!!
I think you can just create a function with a callback param to achieve what you are looking for:
// pass the cb() to save your data in your way, it can be an ajax call or use localStorage, caches etc
function clearDOMandDoStuff(cb) {
var elem = document.querySelector('YOUT_TAG'); // can be class, id, or tag
var data = elem.innerHTML; // let's say you wanna save html content
if (cb) {
cb(data);
}
elem.innerHTML = ''; // clear the element body
}
function saveData(data) {
if (window.localStorage) {
localStorage.setItem("mydata", data);
} else {
// call other saving functions
}
}
And you can wrap them up and put it in your code:
clearDOMandDoStuff(saveData);
This will do exactly what you need. Explanation in comments:
function parseHTML(html, elements){
// Parse the HTML
let parser = new DOMParser();
let htmlDoc = parser.parseFromString(html, "text/html");
// Loop through each element that should be removed
for(let i = 0; i < elements.length; i++){
// Get all elements that need to be removed
let element = htmlDoc.getElementsByTagName(elements[i]), index;
// Loop through each element
for (index = element.length - 1; index >= 0; index--) {
let parent = element[index].parentNode;
// Copy the contents of the element to be removed to its parent so it doesn't get lost
while( element[index].firstChild ) {
parent.insertBefore( element[index].firstChild, element[index] );
}
// Remove the element
element[index].parentNode.removeChild(element[index]);
}
}
// Save the result
let result = htmlDoc.documentElement.outerHTML;
// Show the result in the console
console.log(result);
}
// Array of elements to remove
const arr = ['table','tr','td'];
// HTML string to parse
const str = "<body><div><table><tr><td><p>test</p></td></tr></table></div></body>";
parseHTML(str, arr);
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
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