querySelectorAll doesn't capture all elements - javascript

I am trying to scan and manipulate DOM of a webpage the following Code:
var elements = document.querySelectorAll('*');
for (var i = 0; i < elements.length; i++) {
if (!elements[i].firstElementChild) {
if (elements[i].innerHTML != "" ){
elements[i].innerHTML = "abc_"+ elements[i].innerHTML+"_123";
}
}
}
While it works well on many pages, it is not picking up all the elements on a specific page that is my real target. On that page, it captures and edit strings of few elements, but not all.
I have also tried using getElementsByTagName()
The elements that are not captured have an XPath such as:
/html/body/div[4]/div[2]/div/div[2]/div/div/div/div/div[1]/div/div[2]/nav/div/div[1]/div/span/div/text()[1]
I also noticed "flex" written in front of these elements.
I also tried the script by Douglas Crockford, but, this also is unable to catch the elements described above.
The script by Douglas is published at
https://www.javascriptcookbook.com/article/traversing-dom-subtrees-with-a-recursive-walk-the-dom-function/
function walkTheDOM(node, func) {
func(node);
node = node.firstChild;
while (node) {
walkTheDOM(node, func);
node = node.nextSibling;
}
}
// Example usage: Process all Text nodes on the page
walkTheDOM(document.body, function (node) {
if (node.nodeType === 3) { // Is it a Text node?
var text = node.data.trim();
if (text.length > 0) { // Does it have non white-space text content?
// process text
}
}
});
Any idea what am I doing wrong?
Here is a screenshot of inspect element:
[]

In your snippet, you are not selecting all the nodes, since document.querySelectorAll(*) does not select the text-nodes, but only elements.
Besides, you are explicitly ignoring the text-nodes, because you specify .firstElementChild. A text-node is not an element. An element in the DOM is a "tag" like <div> for example. It has the nodeType: 1 a text-node has nodeType: 3.
So, if you'd process for example:
OuterTextNode<div>InnerTextNode</div>
the div would be the first element and Inner- and OuterTextNode are text-nodes. Both, the query selector and the .firstElementChild would only select the element (div) here.
It should work with the DOM-tree-walking code:
const blackList = ['script']; // here you could add some node names that you want to ignore
function walkTheDOM(node, func) {
func(node);
node = node.firstChild;
while (node) {
if (!blackList.includes(node.nodeName.toLowerCase())) {
walkTheDOM(node, func);
}
node = node.nextSibling;
}
}
walkTheDOM(document.body, function(node) {
if (node.nodeType === 3) {
var text = node.data.trim();
if (text.length > 0) {
console.log(text);
console.log(`replaced: PREFIX_${text}_POSTFIX`);
}
}
});
.as-console-wrapper {
top: 0;
max-height: 100% !important;
}
<div>
All
<span>In span</span> Some more text
<div>
<div>
Some nested text
<div>Sibling</div>
<span>
Another
Another
<span>
Deep
<span>
<span>
<span>
<span>
<span>Deeper</span>
</span>
</span>
</span>
</span>
</span>
</span>
</div>
<!-- Some comment !-->
<script>
// some script
const foo = 'foo';
</script>
</div>
</div>

Related

How to replace only text using JavaScript? [duplicate]

How can I write a javascript/jquery function that replaces text in the html document without affecting the markup, only the text content?
For instance if I want to replace the word "style" with "no style" here:
<tr>
<td style="width:300px">This TD has style</td>
<td style="width:300px">This TD has <span class="style100">style</span> too</td>
</tr>
I don't want the replacement to affect the markup, just the text content that is visible to the user.
You will have to look for the text nodes on your document, I use a recursive function like this:
function replaceText(oldText, newText, node){
node = node || document.body; // base node
var childs = node.childNodes, i = 0;
while(node = childs[i]){
if (node.nodeType == 3){ // text node found, do the replacement
if (node.textContent) {
node.textContent = node.textContent.replace(oldText, newText);
} else { // support to IE
node.nodeValue = node.nodeValue.replace(oldText, newText);
}
} else { // not a text mode, look forward
replaceText(oldText, newText, node);
}
i++;
}
}
If you do it in that way, your markup and event handlers will remain intact.
Edit: Changed code to support IE, since the textnodes on IE don't have a textContent property, in IE you should use the nodeValue property and it also doesn't implements the Node interface.
Check an example here.
Use the :contains selector to find elements with matching text and then replace their text.
$(":contains(style)").each(function() {
for (node in this.childNodes) {
if (node.nodeType == 3) { // text node
node.textContent = node.textContent.replace("style", "no style");
}
}
});
Unfortunately you can't use text() for this as it strips out HTML from all descendant nodes, not just child nodes and the replacement won't work as expected.

Why is my code not affecting entire content?

I have written the following JS for my chrome project which allows you to bold random letters in a word or sentence.
The problem is that whenever there is a hyperlink in a paragraph the code only bolds random letters up until that point and the rest of the text is unaffected.
let all = document.querySelectorAll("*");
all.forEach(a => a.childNodes.forEach(b => makeRandomBold(b)));
function makeRandomBold(node) {
if (node.nodeType !== 3) {
return;
}
let text = node.textContent;
node.textContent = "";
text.split('').forEach(s => {
if (s !== " " && Math.random() > .49) {
let strong = document.createElement("strong");
strong.textContent = s;
node.parentNode.insertBefore(strong, node);
} else {
node.parentNode.insertBefore(document.createTextNode(s), node);
}
I have tried to change from the universal selector tag, to individually selecting HTML elements such as p a span. Which did not work.
Might finding random intervals and putting them in a single strong tag work?
What might be causing this?
Here you have one way to do it.
I added comments to the code:
// The selector * will pull everything: html, head, style, script, body, etc.
// Let's indicate that we only want elements inside the body that aren't scripts,
// styles, img, or any element that wont hold a string.
let allElements = document.querySelectorAll("body > *:not(style):not(script):not(img)");
// Now, lets iterate:
allElements.forEach(elem => {
traverseDOMToBold(elem);
});
// We want to visit all the futher children first,
// then we can change the content
function traverseDOMToBold(elem){
if (elem.hasChildNodes()){
[...elem.children].forEach(child => {
traverseDOMToBold(child);
});
}
boldInnerHTML(elem);
}
// I like to try to create function that have a Single Responsability if possible.
function boldInnerHTML(elem){
if (elem.innerHTML.length > 0){
let currentString = elem.innerHTML;
let newString = `<strong>${currentString}</strong>`;
elem.innerHTML = currentString.replace(currentString, newString)
}
}
<h1>Title</h1>
<p>Paragraph</p>
<div>Div</div>
<span id="parent">
Parent Span
<span id="child">
Child Span
</span>
</span>
<div>
1.
<div>
2.
<div>
3.
</div>
</div>
</div>
<ul>
<li>A.</li>
<li>B.</li>
<li>
<ul>C
<li>C1.</li>
<li>C2.</li>
<li>C3.</li>
</ul>
</li>
</ul>

How to change color of specific text inside html element pre?

I want a code snippet that will search for a keyword text inside HTML element - pre -> code and replace the color of all occurrences of those keywords.
For example, If my text inside pre contains a text - SET, I want to replace it with red color.
I have tried a few codes but it just prints "externalHtml" in red color.
Also, what will be the efficient way to write this code. I may have around 10 to 15 of those keywords and I want to change all of those to just one color. There is no other group of keywords or colors.
var keyword = document.getElementsByClassName('language-sas');
var externalHtml = '<span style="color:red">'+keyword[0]+'</span>'
keyword[0].innerHTML = keyword[0].innerHTML.replace('set',externalHtml );
/*
code.html(code.html().replace(/set/, ' <
span style = "color: red" > $ & < /span>'
));
}
*/
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<pre class="language-sas1">
<code class=" language-sas">
<span class="token keyword">data</span> keep_vars <span class="token punctuation">;</span>
<span class="t">set</span> sashelp<span class="token punctuation">.</span>citimon<span class="token punctuation">;</span>
</div></div></pre>
</body>
</html>
Any thoughts or suggestions will be really helpful.
Whatever you have written is mostly correct. You forgot to write .innerHTML in second line of your JS code.
var externalHtml = '<span style="color:red">'+keyword[0].innerHTML+'</span>'
I avoid using innerHTML when ever possible.
I've made something similar before for a relatable question:
https://stackoverflow.com/a/31852811/1519836
You could adapt it to your problem like this:
// This function will allow you to process DOM Trees
// and run a defined function on each Node.
const textWalker = function (node, callback) {
const nodes = [node];
while (nodes.length > 0) {
node = nodes.shift();
const children = [...node.childNodes];
for (let i = 0; i < children.length; i++) {
var child = children[i];
if (child.nodeType === HTMLElement.TEXT_NODE)
callback(child);
else
nodes.push(child);
}
}
};
// This will run a callback for each text node matching a given regexp pattern
// The callback function can return an array of elements and strings.
// The callback return value will be constructed in DOM
const textSplit = function (source, regexp, callback) {
textWalker(source, function (node) {
let text = node.nodeValue;
if (!regexp.test(text)) {
return;
}
const out = callback(regexp, text);
if (!(out instanceof Array)) {
node.nodeValue = ""+out;
}
else {
let nnode = out.shift();
if (nnode instanceof HTMLElement || nnode.nodeType === HTMLElement.TEXT_NODE) {
// Doesn't work with text nodes yet: node.insertAdjacentElement("afterend", nnode);
node.parentNode.insertBefore(nnode, node);
node.parentNode.removeChild(node);
node = nnode;
}
else {
node.textContent = ""+nnode;
}
while (out.length > 0) {
nnode = out.shift();
if (!(nnode instanceof HTMLElement) && nnode.nodeType !== HTMLElement.TEXT_NODE) {
nnode = document.createTextNode(""+nnode);
}
// Doesn't work with text nodes yet: node.insertAdjacentElement("afterend", nnode);
node.parentNode.insertBefore(nnode, node);
node.parentNode.insertBefore(node, nnode);
node = nnode;
}
}
});
};
// put this function call into your onload function:
const template = document.createElement("span");
template.style.color = "red";
// You could obviously simplify any of this how ever you want.
textSplit(document.querySelector("pre"), /(set)/g, function(regexp, text) {
const out = [];
text.split(regexp).forEach((text, i) => {
if (i % 2 === 0) {
out.push(text);
return;
};
const node = template.cloneNode(true);
node.textContent = text;
out.push(node);
});
return out;
});
<pre class="language-sas1">
<code class=" language-sas">
<span class="token keyword">data</span> keep_vars <span class="token punctuation">;</span>
<span class="t">set</span> sashelp<span class="token punctuation">.</span>citimon<span class="token punctuation">;</span>
</div></div></pre>
Maybe one day I extend this past one text node barrier.
For now you can only use it for self contained text nodes.
Alternatively you could also execute color calls on selected text through the contentEditable stuff.
I'd recommend using an editor for that tho.

jQuery closest (inside and outside DOM tree )

Given an element and any selector, I need to find the closest element which matches it, not matter if it's inside the element or outside of it.
Currently jQuery doesn't provide such traversing functionality, but there is a need. Here is the scenario:
A list of many items where the <button> element reside inside <a>
<ul>
<li>
<a>
<button>click me</button>
<img src="..." />
</a>
</li>
<li>
<a>
<button>click me</button>
<img src="..." />
</a>
</li>
...
</ul>
Or the <button> element might reside outside of the <a> element
<ul>
<li>
<a>
<img src="..." />
</a>
<button>click me</button>
</li>
<li>
<a>
<img src="..." />
</a>
<button>click me</button>
</li>
...
</ul>
The very very basic code would look like this:
$('a').closest1('button'); // where `closest1` is a new custom function
// or
$('a').select('> button') // where `select` can parse any selector relative to the object, so it would also know this:
$('a').select('~ button') // where the button is a sibling to the element
the known element is <a> and anything else can change. I want to locate the nearest <button> element for a given <a> element, no matter if that button is inside or outside of <a>'s DOM tree.
It would be very logical that native jQuery function "closest" would do as the name suggests and find the closest, but it only searches upwards as you all know. (it should have been named differently IMO).
Does anyone know any custom traversing function which does the above?
Thanks. (i'm asking you people because someone must have written this for sure but I was unlucky to find a lead on the internet)
Here is another attempt using the idea I mentioned in comment:
$(this).parents(':has(button):first').find('button').css({
"border": '3px solid red'
});
JSFiddle: http://jsfiddle.net/TrueBlueAussie/z3vwk1ko/40/
It basically looks for the first ancestor that contains both the elements (clicked and target), then finds the target.
Performance:
With regard to speed, this is used at human interaction speeds, i.e. a few times per second maximum, so being a "slow selector" is irrelevant if it solves the problem, in a reasonably obvious way, with minimal code. You would have to click 100s of times per second to notice any different compared to a fast selector :)
None of the built-in selectors allow searching up and down the tree. I did create a custom findThis extension that allows you to do things like $elementClicked.('li:has(this) button') which would allow you to do something similar.
// Add findThis method to jQuery (with a custom :this check)
jQuery.fn.findThis = function (selector) {
// If we have a :this selector
if (selector.indexOf(':this') > 0) {
var ret = $();
for (var i = 0; i < this.length; i++) {
var el = this[i];
var id = el.id;
// If not id already, put in a temp (unique) id
el.id = 'id' + new Date().getTime();
var selector2 = selector.replace(':this', '#' + el.id);
ret = ret.add(jQuery(selector2, document));
// restore any original id
el.id = id;
}
ret.selector = selector;
return ret;
}
// do a normal find instead
return this.find(selector);
}
// Test case
$(function () {
$('a').click(function () {
$(this).findThis('li:has(:this) button').css({
"border": '3px solid red'
});
});
});
JSFiddle: http://jsfiddle.net/TrueBlueAussie/z3vwk1ko/38/
note: Click the images/links to test.
A while ago I wanted to do the same for a completely DOM-based text editor, and needed to find the previous (ARR LEFT) and next (ARR RIGHT) text nodes, both up and down the tree. Based on this code I have made an adaptation suiting this question. Be warned, it's quite performance-heavy, but it is adapted to any scenario. There are two functions findPrevElementNode and findNextElementNode which both return an object with properties:
match - returns the closest matching node for the search or FALSE if none is found
iterations - returns the number of iterations done to find the node. This allows you to check whether the previous node is closer than the next or vice-versa.
The parameters are as follows:
//#param {HTMLElement} referenceNode - The node from which to start the search
//#param {function} truthTest - A function that returns true for the given element
//#param {HTMLElement} [limitNode=document.body] - The limit up to which to search to
var domUtils = {
findPrevElementNode: function(referenceNode, truthTest, limitNode) {
var element = 1,
iterations = 0,
limit = limitNode || document.body,
node = referenceNode;
while (!truthTest(node) && node !== limit) {
if (node.previousSibling) {
node = node.previousSibling;
iterations++;
if (node.lastChild) {
while (node.lastChild) {
node = node.lastChild;
iterations++;
}
}
} else {
if (node.parentNode) {
node = node.parentNode;
iterations++;
} else {
return false;
}
}
}
return {match: node === limit ? false : node, iterations: iterations};
},
findNextElementNode: function(referenceNode, truthTest, limitNode) {
var element = 1,
iterations = 0,
limit = limitNode || document.body,
node = referenceNode;
while (!truthTest(node) && node !== limit) {
if (node.nextSibling) {
node = node.nextSibling;
iterations++;
if (node.firstChild) {
while (node.firstChild) {
node = node.firstChild;
iterations++;
}
}
} else {
if (node.parentNode) {
node = node.parentNode;
iterations++;
} else {
return false;
}
}
}
return {match: node === limit ? false : node, iterations: iterations};
}
};
In your case, you could do:
var a = domUtils.findNextElementNode(
document.getElementsByTagName('a')[0], // known element
function(node) { return (node.nodeName === 'BUTTON'); }
);
var b = domUtils.findPrevElementNode(
document.getElementsByTagName('a')[0], // known element
function(node) { return (node.nodeName === 'BUTTON'); }
);
var result = a.match ? (b.match ? (a.iterations < b.iterations ? a.match :
(a.iterations === b.iterations ? fnToHandleEqualDistance() : b.match)) : a.match) :
(b.match ? b.match : false);
See it in action.
DEMO PAGE / GIST
I have solved working by logic, so I would first look inside the elements, then their siblings, and last, if there are still unfound items, I would do a recursive search on the parents.
JS CODE:
jQuery.fn.findClosest = function (selector) {
// If we have a :this selector
var output = $(),
down = this.find(selector),
siblings,
recSearch,
foundCount = 0;
if(down.length) {
output = output.add(down);
foundCount += down.length;
}
// if all elements were found, return at this point
if( foundCount == this.length )
return output;
siblings = this.siblings(selector);
if( siblings.length) {
output = output.add(siblings);
foundCount += siblings.length;
}
// this is the expensive search path if there are still unfound elements
if(foundCount < this.length){
recSearch = rec(this.parent().parent());
if( recSearch )
output = output.add(recSearch);
}
function rec(elm){
var result = elm.find(selector);
if( result.length )
return result;
else
rec(elm.parent());
}
return output;
};
// Test case
var buttons = $('a').findClosest('button');
console.log(buttons);
buttons.click(function(){
this.style.outline = "1px solid red";
})
I think using sibling selector (~) or child selector (>) will solve your purpose(What ever your case is!!).

Javascript replace an element with a string

How can you change this
<div id='myDiv'><p>This div has <span>other elements</span> in it.</p></div>
into this
<div id='myDiv'>This div has other elements in it.</div>
hopefully using something like this
var ele = document.getElementById('myDiv');
while(ele.firstChild) {
replaceFunction(ele.firstChild, ele.firstChild.innerHTML);
}
function replaceFunction(element, text) {
// CODE TO REPLACE ELEMENT WITH TEXT
}
You can use innerText and textContent if you want to remove all descendant nodes, but leave the text:
// Microsoft
ele.innerText = ele.innerText;
// Others
ele.textContent = ele.textContent;
If you only want to flatten certain ones, you can define:
function foldIntoParent(element) {
while(ele.firstChild) {
ele.parentNode.insertBefore(ele.firstChild, ele);
}
ele.parentNode.removeChild(ele);
}
should pull all the children out of ele into its parent, and remove ele.
So if ele is the span above, it will do what you want. To find and fold all the element children of #myDiv do this:
function foldAllElementChildren(ele) {
for (var child = ele.firstChild, next; child; child = next) {
next = child.nextSibling;
if (child.nodeType === 1 /* ELEMENT */) {
foldIntoParent(child);
}
}
}
foldAllElementChildren(document.getElementById('myDiv'));
If you're not opposed to using jQuery, stripping the HTML from an element and leaving only the text is as simple as:
$(element).html($(element).text());
You can just take the innerText
console.log(ele.innerText)

Categories

Resources