How to replace only text using JavaScript? [duplicate] - javascript

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.

Related

querySelectorAll doesn't capture all elements

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>

Replace text with link with chrome extension

I am trying to replace text on a webpage with links. When I try this it just replaces the text with the tag and not a link. For example this code will replace "river" with:
asdf
This is what I have so far:
function handleText(textNode)
{
var v = textNode.nodeValue;
v = v.replace(/\briver\b/g, 'asdf');
textNode.nodeValue = v;
}
If all you wanted to do was change the text to other plain text, then you could change the contents of the text nodes directly. However, you are wanting to add an <a> element. For each <a> element you want to add, you are effectively wanting to add a child element. Text nodes can not have children. Thus, to do this you have to actually replace the text node with a more complicated structure. In doing so, you will want to make as little impact on the DOM as possible, in order to not disturb other scripts which rely on the current structure of the DOM. The simplest way to make little impact is to replace the text node with a <span> which contains the new text nodes (the text will split around the new <a>) and any new <a> elements.
The code below should do what you desire. It replaces the textNode with a <span> containing the new text nodes and the created <a> elements. It only makes the replacement when one or more <a> elements need to be inserted.
function handleTextNode(textNode) {
if(textNode.nodeName !== '#text'
|| textNode.parentNode.nodeName === 'SCRIPT'
|| textNode.parentNode.nodeName === 'STYLE'
) {
//Don't do anything except on text nodes, which are not children
// of <script> or <style>.
return;
}
let origText = textNode.textContent;
let newHtml=origText.replace(/\briver\b/g,'asdf');
//Only change the DOM if we actually made a replacement in the text.
//Compare the strings, as it should be faster than a second RegExp operation and
// lets us use the RegExp in only one place for maintainability.
if( newHtml !== origText) {
let newSpan = document.createElement('span');
newSpan.innerHTML = newHtml;
textNode.parentNode.replaceChild(newSpan,textNode);
}
}
//Testing: Walk the DOM of the <body> handling all non-empty text nodes
function processDocument() {
//Create the TreeWalker
let treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT,{
acceptNode: function(node) {
if(node.textContent.length === 0) {
//Alternately, could filter out the <script> and <style> text nodes here.
return NodeFilter.FILTER_SKIP; //Skip empty text nodes
} //else
return NodeFilter.FILTER_ACCEPT;
}
}, false );
//Make a list of the text nodes prior to modifying the DOM. Once the DOM is
// modified the TreeWalker will become invalid (i.e. the TreeWalker will stop
// traversing the DOM after the first modification).
let nodeList=[];
while(treeWalker.nextNode()){
nodeList.push(treeWalker.currentNode);
}
//Iterate over all text nodes, calling handleTextNode on each node in the list.
nodeList.forEach(function(el){
handleTextNode(el);
});
}
document.getElementById('clickTo').addEventListener('click',processDocument,false);
<input type="button" id="clickTo" value="Click to process"/>
<div id="testDiv">This text should change to a link -->river<--.</div>
The TreeWalker code was taken from my answer here.

How to change innerHTML of childNodes in case some childnodes without tags?

That is my example of problem
<div onclick="this.childNodes(0).innerHTML='0';">
1<b>2</b>3<b>4</b>5
</div>
as you see, two childnodes ("2" and "4") are tagged, others are simple text.
The question is how to change innerHTML of tagged and untagged nodes (texts) in sertain div container without touching other nodes/texts?
Essentially, you'll use the data(text) property for text nodes (nodeType 3) and innerHTML otherwise (fiddle):
<div onclick="this.childNodes[0][this.childNodes[0].nodeType === 3 ? 'data' : 'innerHTML'] = '0'">
1<b>2</b>3<b>4</b>5
</div>​
[edit] I'm getting really tired of everyone offering libraries as solutions when all that's required is a simple explanation of a basic concept, e.g.: text-nodes and element nodes have differing content properties, i.e.: data and innerHTML.
I wrote a lib called Linguigi. It would be as easy as
new Linguigi(element).eachText(function(text) {
if(this.parentNode.tagName === 'B') {
return "BACON";
}
});
which turns the text of all text nodes inside b-tags to "BACON". You get the original content as "text" parameter and could transform that.
http://jsfiddle.net/Kz2jX/
BTW: You should get rid of the inline event handling (onclick attribute)
You can cycle through each of the nodes recursively, checking their nodeType property in turn and updating the nodeValue property with '0' if the node is a text node (indicated by nodeType == 3).
Assuming you have this HTML:
<div onclick="doReplace(this)">
1<b>2</b>3<b>4</b>5
</div>
You can then write a simple replace function that calls itself recursively, like so:
window.doReplace = function (rootNode) {
var children = rootNode.childNodes;
for(var i = 0; i < children.length; i++) {
var aChild = children[i];
if(aChild.nodeType == 3) {
aChild.nodeValue = '0';
}
else {
doReplace(aChild);
}
}
}
A working fiddle can be found here: http://jsfiddle.net/p9YCn/1/

How to select a part of string?

How to select a part of string?
My code (or example):
<div>some text</div>
$(function(){
$('div').each(function(){
$(this).text($(this).html().replace(/text/, '<span style="color: none">$1<\/span>'));
});
});
I tried this method, but in this case is selected all context too:
$(function(){
$('div:contains("text")').css('color','red');
});
I try to get like this:
<div><span style="color: red">text</span></div>
$('div').each(function () {
$(this).html(function (i, v) {
return v.replace(/foo/g, '<span style="color: red">$&<\/span>');
});
});
What are you actually trying to do? What you're doing at the moment is taking the HTML of each matching DIV, wrapping a span around the word "text" if it appears (literally the word "text") and then setting that as the text of the element (and so you'll see the HTML markup on the page).
If you really want to do something with the actual word "text", you probably meant to use html rather than text in your first function call:
$('div').each(function(){
$(this).html($(this).html().replace(/text/, '<span style="color: none">$1<\/span>'));
// ^-- here
}
But if you're trying to wrap a span around the text of the div, you can use wrap to do that:
$('div').wrap('<span style="color: none"/>');
Like this: http://jsbin.com/ucopo3 (in that example, I've used "color: blue" rather than "color: none", but you get the idea).
$(function(){
$('div:contains("text")').each(function() {
$(this).html($(this).html().replace(/(text)/g, '<span style="color:red;">\$1</span>'));
});
});
I've updated your fiddle: http://jsfiddle.net/nMzTw/15/
The general practice of interacting with the DOM as strings of HTML using innerHTML has many serious drawbacks:
Event handlers are removed or replaced
Opens the possibility of script inject attacks
Doesn't work in XHTML
It also encourages lazy thinking. In this particular instance, you're matching against the string "text" within the HTML with the assumption that any occurrence of the string must be within a text node. This is patently not a valid assumption: the string could appear in a title or alt attribute, for example.
Use DOM methods instead. This will get round all the problems. The following will use only DOM methods to surround every match for regex in every text node that is a descendant of a <div> element:
$(function() {
var regex = /text/;
function getTextNodes(node) {
if (node.nodeType == 3) {
return [node];
} else {
var textNodes = [];
for (var n = node.firstChild; n; n = n.nextSibling) {
textNodes = textNodes.concat(getTextNodes(n));
}
return textNodes;
}
}
$('div').each(function() {
$.each(getTextNodes(this), function() {
var textNode = this, parent = this.parentNode;
var result, span, matchedTextNode, matchLength;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchLength = result[0].length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
span = document.createElement("span");
span.style.color = "red";
span.appendChild(matchedTextNode);
parent.insertBefore(span, textNode);
}
});
});
});

Building editor with DOM Range and content editable

I'm trying to build a text editor using DOM Range. Let's say I'm trying to bold selected text. I do it using the following code. However, I couldn't figure out how I would remove the bold if it's already bolded. I'm trying to accomplish this without using the execCommand function.
this.selection = window.getSelection();
this.range = this.selection.getRangeAt(0);
let textNode = document.createTextNode(this.range.toString());
let replaceElm = document.createElement('strong');
replaceElm.appendChild(textNode);
this.range.deleteContents();
this.range.insertNode(replaceElm);
this.selection.removeAllRanges();
Basically, if the selection range is enclosed in <strong> tags, I'd want to remove it.
Ok so I drafted this piece of code. It basically grabs the current selected node, gets the textual content and removes the style tags.
// Grab the currenlty selected node
// e.g. selectedNode will equal '<strong>My bolded text</strong>'
const selectedNode = getSelectedNode();
// "Clean" the selected node. By clean I mean extracting the text
// selectedNode.textContent will return "My bolded text"
/// cleandNode will be a newly created text type node [MDN link for text nodes][1]
const cleanedNode = document.createTextNode(selectedNode.textContent);
// Remove the strong tag
// Ok so now we have the node prepared.
// We simply replace the existing strong styled node with the "clean" one.
// a.k.a undoing the strong style.
selectedNode.parentNode.replaceChild(cleanedNode, selectedNode);
// This function simply gets the current selected node.
// If you were to select 'My bolded text' it will return
// the node '<strong> My bolded text</strong>'
function getSelectedNode() {
var node,selection;
if (window.getSelection) {
selection = getSelection();
node = selection.anchorNode;
}
if (!node && document.selection) {
selection = document.selection
var range = selection.getRangeAt ? selection.getRangeAt(0) : selection.createRange();
node = range.commonAncestorContainer ? range.commonAncestorContainer :
range.parentElement ? range.parentElement() : range.item(0);
}
if (node) {
return (node.nodeName == "#text" ? node.parentNode : node);
}
};
I don't know if this is a "production" ready soution but I hope it helps. This should work for simple cases. I don't know how it will react with more complex cases. With rich text editing things can get quite ugly.
Keep me posted :)

Categories

Resources