How do I recurse DOM nodes to an arbitrary depth in Javascript? - javascript

I am really having trouble getting my head around crossbrowser recursion in the DOM. I want to get only the text content of a node, but not any HTML tags or other information. Through trial and error, I found that the textContent and innerText attributes don't hold across all browsers, so I have to use the data attribute.
Now the function I have so far is this:
getTextContentXBrowser: function(nodeIn) {
// Currently goes down two levels. Need to abstract further to handle arbitrary number of levels
var tempString = '';
for (i=0, len=nodeIn.childNodes.length; i < len; i++) {
if (nodeIn.childNodes[i].firstChild !== null) {
tempString += nodeIn.childNodes[i].firstChild.data;
} else {
if (nodeIn.childNodes[i].data && nodeIn.childNodes[i].data !== '\n') {
tempString += nodeIn.childNodes[i].data;
}
}
}
return tempString;
},
It's written in object notation, but otherwise it's a pretty standard unremarkable function. It goes down two levels, which is almost good enough for what I want to do, but I want to "set it and forget it" if possible.
I've been at it for four hours and I haven't been able to abstract this to an arbitrary number of levels. Is recursion even my best choice here? Am I missing a better option? How would I convert the above function to recurse?
Thanks for any help!
Update: I rewrote it per dsfq's model, but for some reason, it goes one level down and is unable to go back up afterwards. I realized that my problem previously was that I wasn't concatenating in the second if clause, but this seems to have stopped me short of the goal. Here is my updated function:
getTextContentXBrowser: function(nodeIn) {
var tempString = '';
for (i=0, len=nodeIn.childNodes.length; i < len; i++) {
if (nodeIn.childNodes[i].data) {
tempString += nodeIn.childNodes[i].data;
} else if (nodeIn.childNodes[i].firstChild) {
tempString += this.getTextContentXBrowser(nodeIn.childNodes[i]);
}
}
return tempString.replace(/ /g,'').replace(/\n/g,'');
},
Anyone see what I'm missing?

Have you considered doing this with jQuery?
getTextContentXBrowser: function(nodeIn) {
return $(nodeIn).text();
}
As simple as that!

It can be really simple function calling itself to to replace nodes with its contents. For example:
function flatten(node) {
for (var c = node.childNodes, i = c.length; i--;) {
if (c[i].nodeType == 1) {
c[i].parentNode.replaceChild(document.createTextNode(flatten(c[i]).innerHTML), c[i]);
}
}
}
Looks like in your case you getTextContentXBrowser is a method of some object, so you will need to call it from inside itself properly (in my example I just use function).
Demo: http://jsfiddle.net/7tyYA/
Note that this function replaces nodes with a text in place. If you want a function that just returns a text without modifying actual node right away consider this example with another version of the script:
Demo 2: http://jsfiddle.net/7tyYA/1/

Related

Type Writter effect with HTML tags inside the text

I am trying to achieve typing effect which i did but the problem is that the text i have and is being typed inside a <p> tag as it's inner HTML has also some HTML tags like <span></span> and because the typing is char by char it gets typed out is <span> instead of rendered as the element. Is there any way to achieve the effect while keeping the HTML tags as they are? I have highlighted words that are wrapped in spans what i ultimately want to achieve is have the words typed out as highlighted...
function typeWriter() {
if (i < txt.length) {
document.getElementById("content_html").innerHTML += txt.charAt(i);
i++;
setTimeout(typeWriter, self.typing.speed);
}
}
typeWriter();
Here is an example of a text i am trying to type out :
<p>Surface chemistry deals with phenomena that occur at the surfaces or interfaces. Many important phenomena, noticeable amongst this being corrosion, electrode processes, heterogeneous catalysis, dissolution, and crystallization occur at interfaces. The subject of surface chemistry finds many applications in industry, analytical work, and daily life situations.</p>
It contains a <p> tag which can in the future change to any other HTML tag since this is coming from the server...
I don't fully understand your question, but you can also achieve a typing animation with pure CSS as shown in this Codepen which could remove some of the issues you are having.
animation: typing 7s steps(15, end), /* # of steps = # of chars */
blink-caret .5s step-end infinite alternate;
I belive you are looking for
document.getElementById("demo").innerText = "<p>asd</p>";
instead of innerHTML
This way it will render exacly "<p>asd</p>" instead of creating an element
Try This out Where I Am Rendering The Whole Text After You Have Appended Everything In It.
<script>
var i = 0;
txt = "<p>test this out</p>";
function typeWriter() {
if (i < txt.length) {
document.getElementById("content_html").innerHTML += txt.charAt(i);
i++;
setTimeout(typeWriter, 100);
} else {
document.getElementById("content_html").innerHTML = document.getElementById("content_html").innerText;
}
}
typeWriter();
</script>
So I wanted to make it myself because task is interesting. I know that #little_coder has already provide the best answer in comments to question.
This is what I have done:
js:
// text
txt = "asd<p>test this out<span>with more </span>and between text<span class='second'>second</span></p>afe";
//shared array
var instructions = [] ;
// typeWriter
var i = 0; //
var j = 0;
var elem = '';
var elem_value = '';
var speed = 50;
function typeWriter() {
if(j < instructions.length){
if (typeof instructions[j][1] == 'string'){
if (i < txt.length) {
instructions[j][0].innerHTML += instructions[j][1].charAt(i);
i++;
setTimeout(typeWriter, speed);
}else{
j=j+1;
i = 0;
setTimeout(typeWriter, speed);
}
}
else if(typeof instructions[j][1] == 'object'){
console.log("ins", instructions[j][0]);
instructions[j][0].appendChild(instructions[j][1]);
j=j+1;
i=0;
typeWriter();
}
}
}
//
// recreateNode
parser = new DOMParser();
function recreateNode(list, container){
doc = parser.parseFromString(list, "text/html");
doc.body.childNodes.forEach(function(a){
console.log(a);
if(a.nodeName == '#text'){
instructions.push([container, a.nodeValue])
}
else{ // if there is element to create
b = a.cloneNode(true); // handle deep elements
c = a.cloneNode(false); // this way I can get ONLY the element with attributes and classes
/* container.appendChild(c) */; // I append only element
instructions.push([container, c]);
recreateNode(b.innerHTML, c); // b will be appended to c
}
});
}
// init
parent = document.getElementById("content_html")
recreateNode(txt, parent);
typeWriter();
First I created recreateNode that creates instructions array that stores steps for recreating html structure of the text. Using this instructions in typeWriter I make typing effect or create html element. Keeping in instructions container value I know where to put next text.
Everything would be ok but when it comes to .appendChild or at least I think this is the cause... appending elements takes time that makes typing effect not being fluent.
js fiddle: https://jsfiddle.net/an5gzL7f/3/
Thanks in advance for any tips how it could work fast
I just got through a similar challenge while updating TypeIt (https://typeitjs.com) so that it can handle typing nested HTML. In short, the approach I used was repeatedly traversing over all the childNodes of the parent element, pulling out each nested node and expanding it into an array of queue items to be later typed.
It got a little hairy working through it all, but if it's helpful, you're welcome to search through the code to see how I'm doing it:
https://github.com/alexmacarthur/typeit/blob/master/src/helpers/chunkStrings.js#L98
A lot of it is very TypeIt-specific -- particularly how I construct objects whose properties contain the information needed to reconstruct those elements.
The most elegant solution is probably the one using the CSS only. However it comes with the downside, that if the text has a line break or is not written in a monospace font, it's basically unpracticable. I took the liberty to write a patch of code to accompany the rest of the solutions that are all useful in their own way.
The downside to my solution is that i used the textContent property, which will not render as HTML. I refuse to use innerHTML for security reasons. So adding <a> tags is probably not the best idea at the moment.
The goal of my approach is to use the window.requestAnimationFrame hook, because it's so powerful. And the rest of the code is still a bit cluttered but at least it's self explanatory.
let paragraphNode = document.querySelector('p');
let textObject = {
actionCounter: 0,
typeDelay: 6,
cursorDelay: 22,
cursorShow: false,
currentIndex: 0,
stringLength: paragraphNode.textContent.length,
textContent: paragraphNode.textContent
}
paragraphNode.textContent = "";
const writeText = ()=>{
textObject.actionCounter ++;
if( textObject.actionCounter % textObject.typeDelay == 0 ){
textObject.currentIndex += 1;
}
let string = textObject.textContent.substring(0, textObject.currentIndex);
if( textObject.actionCounter % textObject.cursorDelay == 0 ){
textObject.cursorShow = !textObject.cursorShow;
}
paragraphNode.textContent = (textObject.cursorShow) ? string + "|" : string;
window.requestAnimationFrame(writeText);
}
window.requestAnimationFrame(writeText);
Here is the codepen example i worked out. The only thing missing at the time of writing is a person brave enough to replace textContent with innerHTML and possibly check if the next letter is a '<' and add the whole tag at once. Might be tricky though. I strongly recommend against it.
typewriter on codepen

Best way to create string filter comparation?

My goal's create a filter search function, in particular actually I'm using .indexOf method that allow me to check if two string are equal. The problem's that if I've the compare string with space break like this: Hair Cut.
Example:
String to search: Hair
String contained in the object: Hair Cut
var cerca = $('#filter_service').val();
for(var i = 0; i < GlobalVariables.availableServices.length; i++) {
if (cerca.toLowerCase().contains(GlobalVariables.availableServices[i].name.toLowerCase()) != -1) {
console.log(GlobalVariables.availableServices[i].name)
}
}
How you can see I valorize the variable cerca that contains the string Hair in the example. I compare this with an object variable, how I said, the problem is if I insert the string Hair I get no response in console, also if I insert the string with break space like the compare string Hair Cut I get the console response.
How I can print a result also when the variable cerca is equal to the first character of the compair string? In particular Hai?
I don't know if I was clear, hope yes.
.contains() is for checking DOM element children. You said above that you are using .indexOf to check, but it doesn't look like you use it in your code?
var cerca = $('#filter_service').val();
var searchIn;
for(var i = 0; i < GlobalVariables.availableServices.length; i++) {
searchIn = GlobalVariables.availableServices[i].name.toLowerCase().split(' ');
for (j = 0; j < searchIn.length; j++) {
if (cerca.toLowerCase().split(' ').indexOf(searchIn[j].toLowerCase()) >= 0) {
console.log(GlobalVariables.availableServices[i].name);
}
}
}
$('#filter_service').on('input', function() {
var inputStr = $('#filter_service').val();
var similar = [];
for (i = 0; i < GlobalVariables.availableServices.length; i++) {
if (GlobalVariables.availableServices[i].name.toLowerCase().indexOf(inputStr.toLowerCase) >= 0) {
similar[similar.length] = GlobalVariables.availableServices[i].name;
}
}
// At this point, you can do whatever you want with the similar service
// names (all of the possible result names are included in the array, similar[].)
});
I can't test that code right now, but in theory, it should work.
Here is a JSFiddle demo:
http://jsfiddle.net/MrGarretto/vrp5pghr/
EDIT: Updated and fixed my errors
EDIT 2: Added the 'possible results' solution
EDIT 3: Added a JSFiddle

Remove all child nodes but leave the text content of the node in javascript (no framework)

I'm trying to remove all child elements from a node but leave the actual text content of the node. I.e. go from this:
<h3>
MY TEXT
<a href='...'>Link</a>
<a href='...'>Link</a>
<select>
<option>Value</option>
<option>Value</option>
</select>
</h3>
to this:
<h3>
MY TEXT
</h3>
I know that there are a million easy ways to do this in jQuery, but it's not an option for this project... I've got to use plain old javascript.
This:
var obj = document.getElementById("myID");
if ( obj.hasChildNodes() ){
while ( obj.childNodes){
obj.removeChild( obj.firstChild );
}
}
obviously results in just <h3></h3>, and when I tried:
var h3 = content_block.getElementsByTagName('h3')[0];
var h3_children = h3.getElementsByTagName('*');
for(var i=0;i<h3_children.length;i++){
h3_children[i].parentNode.removeChild(h3_children[i]);
}
It gets hung up part way through. I figured it was having trouble removing the options, but altering the for loop to skip removal unless h3_children[i].parentNode==h3 (i.e. only remove first-level child-elements) stops after removing the first <a> element.
I'm sure I'm missing something super obvious here, but perhaps it's time to turn to the crowd. How can I remove all child elements but leave the first-level textNodes alone? And why doesn't the above approach work?
EDITS
There are a couple of working solutions posted, which is great, but I'm still a little mystified as to why looping through and removing h3.getElementsByTagName('*') doesn't work. A similar approach(adapted from Blender) likewise does not complete the process of removing child nodes. Any thoughts as to why this would be?
var h3=content_block.getElementsByTagName("h3")[0];
for(var i=0;i<h3.childNodes.length;i++)
{
if(h3.childNodes[i].nodeType==3)//TEXT_NODE
{
continue;
}
else
{
h3.removeChild(h3.childNodes[i]);
i--;
}
}
JSFiddle demo
Edit:
Combined the i-- to make it look shorter:
var h3=content_block.getElementsByTagName("h3")[0];
for(var i=0;i<h3.childNodes.length;i++)
{
if(h3.childNodes[i].nodeType==3)//TEXT_NODE
continue;
else
h3.removeChild(h3.childNodes[i--]);
}
Edit #2:
Pointed out by #SomeGuy, make it even shorter:
var h3=content_block.getElementsByTagName("h3")[0];
for(var i=0;i<h3.childNodes.length;i++)
{
if(h3.childNodes[i].nodeType!=3)//not TEXT_NODE
h3.removeChild(h3.childNodes[i--]);
}
The brackets can be removed too, but that would be "less readable" and "confusing", so I keep it there.
You can check properties .nodeType or .nodeName for each node.
Text nodes have these properties set to:
.nodeType == 3
.nodeName == '#text'`
For instance:
var e = obj.firstChild
while (e) {
if (e.nodeType == 3) {
e = e.nextSibling
} else {
var n = e.nextSibling
obj.removeChild(e)
e = n
}
}
try this. I am assuming you will keep ant text here.
var h3 = document.getElementsByTagName('h3')[0];
if (h3.hasChildNodes()) {
for (var i = h3.childNodes.length - 1; i >= 0; i--) {
if (h3.childNodes[i].nodeName != "#text")
h3.removeChild(h3.childNodes[i]);
}
}
Hope it will work.
Well, The thing I used (inspired from the answers here) is somewhat like:
var h3 = document.getElementsByTagName("h3")[0];
Array.protoype.filter.call(h3.childNodes, function(child){
if (child.nodeType != 3) {
h3.removeChild(child);
}
});

Avoiding having to write the same word over and over again

I'm very new to javascript so this question might sound stupid. But what is the correct syntax of replacing certain words inside variables and functions. For example, I have this function:
function posTelegram(p){
var data = telegramData;
$("#hotspotTelegram").css("left", xposTelegram[p] +"px");
if (p < data[0] || p > data[1]) {
$("#hotspotTelegram").hide()
} else {
$("#hotspotTelegram").show()
}
};
There is the word "telegram" repeating a lot and every time I make a new hotspot I'm manually inserting the word to replace "telegram" in each line. What would be a smarter way of writing that code so that I only need to write "telegram" once?
Group similar / related data in to data structures instead of having a variable for each bit.
Cache results of calling jQuery
Use an argument
function posGeneral(p, word){
// Don't have a variable for each of these, make them properties of an object
var data = generalDataThing[word].data;
// Don't search the DOM for the same thing over and over, use a variable
var hotspot = $("#hotspot" + word);
hotspot.css("left", generalDataThing[word].xpos[p] +"px");
if (p < data[0] || p > data[1]) {
hotspot.hide()
} else {
hotspot.show()
}
};
You can't always avoid this kind of repetition (this is general to all programing languages).
Sometimes, you can make generic functions or generic classes, for example a class which would embed all your data :
Thing = function(key, xpos) {
this.$element = $('#hotspot'+key);
this.xpos = xpos;
};
Thing.prototype.pos = function (p, data) {
this.$element.css("left", this.xpos[p] +"px");
if (p < this.data[0] || p > this.data[1]) {
this.$element.hide()
} else {
this.$element.show()
}
};
And we could imagine that this could be called like this :
var telegramThing = new Thing('telegram', xposTelegram);
...
telegramThing.pos(p, data);
But it's really hard to make a more concrete proposition without more information regarding your exact problem.
I recommend you read a little about OOP and javascript, as it may help you make complex programs more clear, simple, and easier to maintain.
For example, using a Thing class here would enable
not defining more than once the "#hotspotTelegram" string in your code
reusing the logic and avoid making the same code with another thing than "telegram"
not having the Thing logic in your main application logic (usually in another Thing.js file)
But don't abstract too much, it would have the opposite effects. And if you don't use objects, try to keep meaningful variable names.
var t = "Telegram";
var $_tg = $('#hotspotTelegram');
$_tg.css("left", "xpos"+t[p] + "px"); // not sure about this line, lol
$_tg.hide();
$_tg.show();
etc.
you can create a selector as variable, something like this
function posTelegram(p){
var data = telegramData;
var $sel = $("#hotspotTelegram");
$sel.css("left", xposTelegram[p] +"px");
if (p < data[0] || p > data[1]) {
$sel.hide()
} else {
$sel.show()
}
};

Javascript search for tag and get it's innerHTML

It's probably something really simple, but I'm just learning.
There's a page with 3 blockquote tags on it, and I'd need to get the innerHTML of the one containing a certain string. I don't know how to search/match a string and get the innerHTML of the tag containing the matched result.
Any help would be appreciated!
var searchString = 'The stuff in innerHTML';
var elements = document.getElementsByTagName('blockquote')
for (var i = 0; i < elements.length; i++) {
if (elements[i].innerHTML.indexOf(searchString) !== -1) {
alert('Match');
break;
}
}
:)
Btw there would be a much nicer method if you'd be using Prorotype JS (which is much better than jQuery btw):
var el = $$('blockquote').find(function(el) {
return el.innerHTML.indexOf('The string you are looking for.') !== -1;
});
You could of course also use regular expressions to find the string, which might be more useful (use el.match() for that).
If you need to search through every <blockquote> on the page, try this:
function findBlockquoteContainingHtml(matchString) {
var blockquoteElements = document.getElementsByTagName('BLOCKQUOTE');
var i;
for (i = 0; i < blockquoteElements.length; i++) {
if (blockquoteElements[i].innerHTML.indexOf(matchString) >= 0) {
return blockquoteElements[i].innerHTML;
}
}
return null;
}
Assign an id to the blockquote elements then you can get the innerHTML like this:
HTML:
<blockquote id="bq1">Foo</blockquote>
JS:
var quote1 = document.getElementById('bq1').innerHTML;
Be careful using innerHTML to search for text within a tag, as that may also search for text in attributes or tags as well.
You can find all blockquote elements using:
var elems = document.getElementsByTagName("blockquote")
You can then look through their innerHTML, but I would recommend instead looking through their textContent/innerText (sadly, this is not standardized across browser, it seems):
for (i in elems) {
var text = elems[i].textContent || elems[i].innerText;
if (text.match(/foo/)) {
alert(elems[i].innerHTML);
}
}

Categories

Resources