I try to drop empty nodes using JavaScript, because the navigator count the return line or space like a child node
So, I make this try, first it's work correctly but if I count the nodes for the second time it's give me a number ( contains the empty nodes ) despite I make removechild
My code :
function testing(){
var c = document.body.childNodes;
for(i=0;i<c.length;i++){
if(c[i].nodeName == "#text")
{
var rest = c[i].textContent;
if(rest.length == 0){
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
}
}
//this give a number contains the empty nodes
var d =document.body.childNodes.length;
alert(d);
}
My HTML code
<body onclick="testing()">
<p>test</p>
<p>test1</p>
</body>
If I try to know the length of this HTML code, the navigator give me 5 not 2 (it's count the return line and space)
So, for that I want to delete the empty nodes the take result 2
if(c[i].nodeName == "#text")
At this line you check if it is a #text node but in your html are p elements.
var rest = c[i].textContent;
if(rest.length == 0){
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
Also with those lines you check if the content of the element is empty which is also not the case in both of your p elements.
So your alert should correctly contain 2.
If your problem is that you have text elements which contain whitespaces and newlines in your body element you can check for them with regex.
Just change this line
if(rest.length == 0){
To
if(/^[\s\n]{0,}$/.test(rest)){
And for the compleatness sake:
change the following line
for(i=0;i<c.length;i++){
To
for(i=len -1;i>=0;i--){
As described in #Bindrid's answer because you can miss elements.
So the compleate code should be
function testing(){
var c = document.body.childNodes;
for(var i = c.length; i >= 0; i--) {
if(c[i].nodeName == "#text") {
var rest = c[i].textContent;
if(/^[\s\n]{0,}$/.test(rest)) {
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
}
}
//this give a number contains the empty nodes
var d =document.body.childNodes.length;
alert(d);
}
An element is considered empty when has no content, whitespaces, line return are considered as content. So, you have two way depending on what you need. if you need for true empties elements use document.querySelectorAll(':empty'), otherwise, use document.body.childNodes and check if its content is empty with String.prototype.trim.
Hope it helps.
<div>
</div>
<section></section>
function isTextNode(element) {
return element.nodeType === 3;
}
function removeEmptyTextNodes(element) {
if(!isTextNode(element)) { return; }
var content = (element.textContent || "").trim();
if(content.length > 0) { return; }
console.log("removing", element);
element.parentNode.removeChild(element);
}
Array.prototype.forEach.call(
document.body.childNodes, removeEmptyTextNodes
);
<div>
</div>
<span></span>
Sounds like you're overthinking it. You've observed the difference between childNodes and children, which is that childNodes contains all nodes, including text nodes consisting entirely of whitespace, while children is a collection of just the child nodes that are elements. That's really all there is to it.
function testing(){
alert(document.body.children.length);
var c = document.body.children;
for(i=0;i<c.length;i++){
if(c[i].innerHTML == "")
{
//I want to remove the empty nodes
document.body.removeChild(c[i]);
}
}
//this give a number contains the empty nodes
var d =document.body.children.length;
alert(d);
}
<body onclick="testing()">
<p>test</p>
<p>test1</p>
<p></p>
ss
</body>
for(i=0;i<c.length;i++){
if(c[i].nodeName == "#text")
{
var rest = c[i].textContent;
if(rest.length == 0){
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
}
}
should actually be as shown below to take in account that you are removing nodes as you run through the list. If node 7 and 8 are empty and you remove 7, 8 becomes 7 while you are in the 7 loop so 8 as the new 7 gets missed on the next loop.
var len = c.length;
for(i=len -1;i>=0;i--){
if(c[i].nodeName == "#text")
{
var rest = c[i].textContent;
if(rest.length == 0){
//I want to remove the empty nodes
document.body.removeChild(document.body.childNodes[i]);
}
}
}
Related
Maybe this function is very simple for you. but i have problems with how this functions works.please explain how to compile this function.
It is zero the count in the run loop ?
function countChars(elm) {
var i, count = 0;
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
for (i = 0, child; child = elm.childNodes[i]; i++) {
count += countChars(child);
}
return count;
}
Some Text
The DOM will look like this:
[
Textnode {"Some Text"}
]
The upper code is valid HTML, actually it is a text node. So if we want to get its length we simply take its length:
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
Lets imagine a more complicated case:
Some Text <span> nested </span>
DOM:
[
Textnode {"Some Text"},
Span {
children:[
Textnode {"nested"}
]
}
]
Now weve got four nodes. A text node and a span node, and a text node nested in the span node. And theyre all nested in some kind of body. So to get its text length, we need to iterate over the nodes, and then go deeper ( have a look into tree traversal ):
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += countChars(child);
}
return count;
So the upper example will work like this:
count=0
iterate over body:
child textnode:
count+="Some Text".length
child span:
inner count=0
iterate over span:
child textnode
inner count+="nested".length
count+=inner count
finished
/**
** This function counts the characters of a given node.
**
** #param : The current node to work on
**/
function countChars(elm) {
// Set local variables
var i, count = 0;
// Check to see if the current node is text.
// If its a text it cannot have any other children so simply
// return the text length int his node
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
// This is not a text node so driil doen in to its all
// children and loop into them to count the number of
// character they have
for (i = 0, child; child = elm.childNodes[i]; i++) {
// dive into the current child in count the number of
// characters it contains. since we use recursion the count will
// be returned form the previous run and will return the number of
// characters collect from all "previous" children
count += countChars(child);
}
return count;
}
I want to insert html tags within a text node with TreeWalker, but TreeWalker forces my html brackets into & lt; & gt; no matter what I've tried. Here is the code:
var text;
var tree = document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);
while (tree.nextNode()) {
text = tree.currentNode.nodeValue;
text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
tree.currentNode.nodeValue = text;
}
Using \< or " instead of ' won't help. My workaround is to copy all of the DOM tree to a string and to replace the html body with that. It works on very simple webpages and solves my first problem, but is a bad hack and won't work on anything more than a trivial page. I was wondering if I could just work straight with the text node rather than use a workaround. Here is the code for the (currently buggy) workaround:
var text;
var newHTML = "";
var tree = document.createTreeWalker(document.body);
while (tree.nextNode()) {
text = tree.currentNode.nodeValue;
if (tree.currentNode.nodeType == 3){
text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
}
newHTML += text
}
document.body.innerHTML = newHTML;
Edit: I realize a better workaround would be to custom tag the text nodes ((Customtag_Start_Here) etc.), copy the whole DOM to a string, and use my customs tags to identify text nodes and modify them that way. But if I don't have to, I'd rather not.
To 'change' a text node into an element, you must replace it with an element. For example:
var text = tree.currentNode;
var el = document.createElement('foo');
el.setAttribute('bar','yes');
text.parentNode.replaceChild( el, text );
If you want to retain part of the text node, and inject an element "in the middle", you need to create another text node and insert it and the element into the tree at the appropriate places in the tree.
Edit: Here's a function that might be super useful to you. :)
Given a text node, it runs a regex on the text values. For each hit that it finds it calls a custom function that you supply. If that function returns a string, then the match is replaced. However, if that function returns an object like:
{ name:"element", attrs{onmouseover:"sendWord('foo')"}, content:"foo" }
then it will split the text node around the match and inject an element in that location. You can also return an array of strings or those objects (and can recursively use arrays, strings, or objects as the content property).
Demo: http://jsfiddle.net/DpqGH/8/
function textNodeReplace(node,regex,handler) {
var mom=node.parentNode, nxt=node.nextSibling,
doc=node.ownerDocument, hits;
if (regex.global) {
while(node && (hits=regex.exec(node.nodeValue))){
regex.lastIndex = 0;
node=handleResult( node, hits, handler.apply(this,hits) );
}
} else if (hits=regex.exec(node.nodeValue))
handleResult( node, hits, handler.apply(this,hits) );
function handleResult(node,hits,results){
var orig = node.nodeValue;
node.nodeValue = orig.slice(0,hits.index);
[].concat(create(mom,results)).forEach(function(n){
mom.insertBefore(n,nxt);
});
var rest = orig.slice(hits.index+hits[0].length);
return rest && mom.insertBefore(doc.createTextNode(rest),nxt);
}
function create(el,o){
if (o.map) return o.map(function(v){ return create(el,v) });
else if (typeof o==='object') {
var e = doc.createElementNS(o.namespaceURI || el.namespaceURI,o.name);
if (o.attrs) for (var a in o.attrs) e.setAttribute(a,o.attrs[a]);
if (o.content) [].concat(create(e,o.content)).forEach(e.appendChild,e);
return e;
} else return doc.createTextNode(o+"");
}
}
It's not quite perfectly generic, as it does not support namespaces on attributes. But hopefully it's enough to get you going. :)
You would use it like so:
findAllTextNodes(document.body).forEach(function(textNode){
replaceTextNode( textNode, /\b\w+/g, function(match){
return {
name:'element',
attrs:{onmouseover:"sendWord('"+match[0]+"')"},
content:match[0]
};
});
});
function findAllTextNodes(node){
var walker = node.ownerDocument.createTreeWalker(node,NodeFilter.SHOW_TEXT);
var textNodes = [];
while (walker.nextNode())
if (walker.currentNode.parentNode.tagName!='SCRIPT')
textNodes.push(walker.currentNode);
return textNodes;
}
or if you want something closer to your original regex:
replaceTextNode( textNode, /(^|\W)(\w+)/g, function(match){
return [
match[1], // might be an empty string
{
name:'element',
attrs:{onmouseover:"sendWord('"+match[2]+"')"},
content:match[2]
}
];
});
Function that returns the parent element of any text node including partial match of passed string:
function findElByText(text, mainNode) {
let textEl = null;
const traverseNodes = function (n) {
if (textEl) {
return;
}
for (var nodes = n.childNodes, i = nodes.length; i--;) {
if (textEl) {
break;
}
var n = nodes[i], nodeType = n.nodeType;
// Its a text node, check if it matches string
if (nodeType == 3) {
if (n.textContent.includes(text)) {
textEl = n.parentElement;
break;
}
}
else if (nodeType == 1 || nodeType == 9 || nodeType == 11) {
traverseNodes(n);
}
}
}
traverseNodes(mainNode);
return textEl;
}
Usage:
findElByText('Some string in document', document.body);
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);
}
});
I've got an in page text search using JS, which is here:
$.fn.eoTextSearch = function(pat) {
var out = []
var textNodes = function(n) {
if (!window['Node']) {
window.Node = new Object();
Node.ELEMENT_NODE = 1;
Node.ATTRIBUTE_NODE = 2;
Node.TEXT_NODE = 3;
Node.CDATA_SECTION_NODE = 4;
Node.ENTITY_REFERENCE_NODE = 5;
Node.ENTITY_NODE = 6;
Node.PROCESSING_INSTRUCTION_NODE = 7;
Node.COMMENT_NODE = 8;
Node.DOCUMENT_NODE = 9;
Node.DOCUMENT_TYPE_NODE = 10;
Node.DOCUMENT_FRAGMENT_NODE = 11;
Node.NOTATION_NODE = 12;
}
if (n.nodeType == Node.TEXT_NODE) {
var t = typeof pat == 'string' ?
n.nodeValue.indexOf(pat) != -1 :
pat.test(n.nodeValue);
if (t) {
out.push(n.parentNode)
}
}
else {
$.each(n.childNodes, function(a, b) {
textNodes(b)
})
}
}
this.each(function() {
textNodes(this)
})
return out
};
And I've got the ability to hide columns and rows in a table. When I submit a search and get the highlighted results, there would be in this case, the array length of the text nodes found would be 6, but there would only be 3 highlighted on the page. When you output the array to the console you get this:
So you get the 3 tags which I was expecting, but you see that the array is actually consisting of a [span,undefined,span,undefined,undefined,span]. Thus giving me the length of 6.
<span>
<span>
<span>
[span, undefined, span, undefined, undefined, span]
I don't know why it's not stripping out all of the undefined text nodes when I do the check for them. Here's what I've got for the function.
performTextSearch = function(currentObj){
if($.trim(currentObj.val()).length > 0){
var n = $("body").eoTextSearch($.trim(currentObj.val())),
recordTitle = "matches",
arrayRecheck = new Array(),
genericElemArray = new Array()
if(n.length == 1){
recordTitle = "match"
}
//check to see if we need to do a recount on the array length.
//if it's more than 0, then they're doing a compare and we need to strip out all of the text nodes that don't have a visible parent.
if($(".rows:checked").length > 0){
$.each(n,function(i,currElem){
if($(currElem).length != 0 && typeof currElem != 'undefined'){
if($(currElem).closest("tr").is(":visible") || $(currElem).is(":visible")){
//remove the element from the array
console.log(currElem)
arrayRecheck[i] = currElem
}
}
})
}
if(arrayRecheck.length > 0){
genericElemArray.push(arrayRecheck)
console.log(arrayRecheck)
}
else{
genericElemArray.push(n)
}
genericElemArray = genericElemArray[0]
$("#recordCount").text(genericElemArray.length + " " +recordTitle)
$(".searchResults").show()
for(var i = 0; i < genericElemArray.length; ++i){
void($(genericElemArray[i]).addClass("yellowBkgd").addClass("highLighted"))
}
}
else{
$(".highLighted").css("background","none")
}
}
If you look at the code below "//check to see if we need to do a recount on the array length. ", you'll see where I'm stripping out the text nodes based off of the display and whether or not the object is defined. I'm checking the length instead of undefined because the typeof == undefined wasn't working at all for some reason. Apparently, things are still slipping by though.
Any idea why I'm still getting undefined objects in the array?
My apologies for such a big post!
Thanks in advance
I've modified your eoTextSearch() function to remove dependencies on global variables in exchange for closures:
$.fn.extend({
// helper function
// recurses into a DOM object and calls a custom function for every descendant
eachDescendant: function (callback) {
for (var i=0, j=this.length; i<j; i++) {
callback.call(this[i]);
$.fn.eachDescendant.call(this[i].childNodes, callback);
}
return this;
},
// your text search function, revised
eoTextSearch: function () {
var text = document.createTextNode("test").textContent
? "textContent" : "innerText";
// the "matches" function uses an out param instead of a return value
var matches = function (pat, outArray) {
var isRe = typeof pat.test == "function";
return function() {
if (this.nodeType != 3) return; // ...text nodes only
if (isRe && pat.test(this[text]) || this[text].indexOf(pat) > -1) {
outArray.push(this.parentNode);
}
}
};
// this is the function that will *actually* become eoTextSearch()
return function (stringOrPattern) {
var result = $(); // start with an empty jQuery object
this.eachDescendant( matches(stringOrPattern, result) );
return result;
}
}() // <- instant calling is important here
});
And then you can do something like this:
$("body").eoTextSearch("foo").filter(function () {
return $(this).closest("tr").is(":visible");
});
To remove unwanted elements from the search result. No "recounting the array length" necessary. Or you use each() directly and decide within what to do.
I cannot entirely get my head around your code, but the most likely issue is that you are removing items from the array, but not shrinking the array afterwards. Simply removing items will return you "undefined", and will not collapse the array.
I would suggest that you do one of the following:
Copy the array to a new array, but only copying those items that are not undefined
Only use those array items that are not undefined.
I hope this is something of a help.
Found the answer in another post.
Remove empty elements from an array in Javascript
Ended up using the answer's second option and it worked alright.
Let say I have a mark up like this
<html id="test">
<body>
Some text node.
<div class="cool"><span class="try">This is another text node.</span></div>
Yet another test node.
</body>
</html>
my js code
function countText(node){
var counter = 0;
if(node.nodeType === 3){
counter+=node.nodeValue.length;
countText(node);
}
else{}
}
Now if I want to count the text nodes
console.log("count text : " + countText(document.getElementById("test"));
this should return me the count but its not working and moreover what should I put in else condition.
I never used nodeType so kind of having problem using it . Any help will be appreciated.
There are a couple of things wrong in your code:
Your HTML is malformed.
You are appending text to your counter instead of increasing it.
You never loop over the children of the a node, you always pass the same node to the recursive call.
You don't do anything if a node is not a text node.
This will work:
function countText(node){
var counter = 0;
if(node.nodeType === 3){
counter++;
}
else if(node.nodeType === 1) { // if it is an element node,
var children = node.childNodes; // examine the children
for(var i = children.length; i--; ) {
counter += countText(children[i]);
}
}
return counter;
}
alert(countText(document.body));
DEMO
Which number corresponds to which node type can be found here.
Update:
If you want to count the words, you have to split each text node into words first. In the following I assume that words are separated by white spaces:
if(node.nodeType === 3){
counter = node.nodeValue.split(/\s+/g).length;
}
Update 2
I know you want to use a recursive function, but if you want to count the words only, then there is a much easier and more efficient way:
function countWords(node){
// gets the text of the node and all its descendants
var text = node.innerText || node.textContent
return text.split(/\s+/g).length;
}
You want something like
function countTextNodes(node) {
var n = 0;
if(node.nodeType == 3)
n = 1;
for(var i = 0; i < node.childNodes.length; ++i)
n += countTextNodes(node.childNodes[i]);
return n;
}
This can be compressed into more compact code, but I went for legibility here.
Call this on the root in which you want to count text nodes. For example, to count text nodes throughout the entire document, you would want to call countTextNodes(document.getDocumentElement()).