Clean global search and replace using JavaScript? - javascript

I am coding a little bookmarket to convert all the devises in the current page to another. I heavily relies on regexp, and I use Jquery to easy the work.
For now, I do that like a big fat pig, replacing all the body :
$("body").children().each(function(){
var $this = $(this);
var h = $.html().replace(/eyes_hurting_regexp/g, "my_super_result");
$this.html(h);
});
It works fine on static page, but if js events are involves, it's an Apocalypse.
The only way I can think of is to go trough all the node, check if it contains text only, then replace the text. On heavy HTML markup, I'm worried about the perfs.
Any idea out here ?

Unfortunately, going through each text node, step-by-step, is the only reliable way to do this. This has worked for me in the past: (demo)
function findAndReplace(searchText, replacement, searchNode) {
if (!searchText || typeof replacement === 'undefined') {
// Throw error here if you want...
return;
}
var regex = typeof searchText === 'string' ?
new RegExp(searchText, 'g') : searchText,
childNodes = (searchNode || document.body).childNodes,
cnLength = childNodes.length,
excludes = 'html,head,style,title,link,meta,script,object,iframe';
while (cnLength--) {
var currentNode = childNodes[cnLength];
if (currentNode.nodeType === 1 &&
(excludes + ',').indexOf(currentNode.nodeName.toLowerCase() + ',') === -1) {
arguments.callee(searchText, replacement, currentNode);
}
if (currentNode.nodeType !== 3 || !regex.test(currentNode.data) ) {
continue;
}
var parent = currentNode.parentNode,
frag = (function(){
var html = currentNode.data.replace(regex, replacement),
wrap = document.createElement('div'),
frag = document.createDocumentFragment();
wrap.innerHTML = html;
while (wrap.firstChild) {
frag.appendChild(wrap.firstChild);
}
return frag;
})();
parent.insertBefore(frag, currentNode);
parent.removeChild(currentNode);
}
}

I modified the script for my own needs and put the new version here in case somebody would need the new features :
Can handle a replace callback function.
External node blacklist.
Some comments so the code won't hurt someone else eyes :-)
function findAndReplace(searchText, replacement, callback, searchNode, blacklist) {
var regex = typeof searchText === 'string' ? new RegExp(searchText, 'g') : searchText,
childNodes = (searchNode || document.body).childNodes,
cnLength = childNodes.length,
excludes = blacklist || {'html' : '',
'head' : '',
'style' : '',
'title' : '',
'link' : '',
'meta' : '',
'script' : '',
'object' : '',
'iframe' : ''};
while (cnLength--)
{
var currentNode = childNodes[cnLength];
// see http://www.sutekidane.net/memo/objet-node-nodetype.html for constant ref
// recursive call if the node is of type "ELEMENT" and not blacklisted
if (currentNode.nodeType === Node.ELEMENT_NODE &&
!(currentNode.nodeName.toLowerCase() in excludes)) {
arguments.callee(searchText, replacement, callback, currentNode, excludes);
}
// skip to next iteration if the data is not a text node or a text that matches nothing
if (currentNode.nodeType !== Node.TEXT_NODE || !regex.test(currentNode.data) ) {
continue;
}
// generate the new value
var parent = currentNode.parentNode;
var new_node = (callback
|| (function(text_node, pattern, repl) {
text_node.data = text_node.data.replace(pattern, repl);
return text_node;
}))
(currentNode, regex, replacement);
parent.insertBefore(new_node, currentNode);
parent.removeChild(currentNode);
}
}
Example of callback function :
findAndReplace(/foo/gi, "bar", function(text_node, pattern, repl){
var wrap = document.createElement('span');
var txt = document.createTextNode(text_node.data.replace(pattern, repl));
wrap.appendChild(txt);
return wrap;
});
Thanks again, J-P, for this very helpful piece of code.

Related

Injecting the functionality of one function into another larger function

Fraking functioning functions, Functionman!
I have one big function that searches a DOM for one parameter and replaces it with another:
function findAndReplace(searchText, replacement, searchNode) {
if (!searchText || typeof replacement === 'undefined') {
// Throw error here if you want...
return;
}
var regex = typeof searchText === 'string' ?
new RegExp(searchText, 'g') : searchText,
childNodes = (searchNode || document.body).childNodes,
cnLength = childNodes.length,
excludes = 'html,head,style,title,link,meta,script,object,iframe';
while (cnLength--) {
var currentNode = childNodes[cnLength];
if (currentNode.nodeType === 1 &&
(excludes + ',').indexOf(currentNode.nodeName.toLowerCase() + ',') === -1) {
arguments.callee(searchText, replacement, currentNode);
}
if (currentNode.nodeType !== 3 || !regex.test(currentNode.data)) {
continue;
}
var parent = currentNode.parentNode,
frag = (function () {
var html = currentNode.data.replace(regex, replacement),
wrap = document.createElement('div'),
frag = document.createDocumentFragment();
wrap.innerHTML = html;
while (wrap.firstChild) {
frag.appendChild(wrap.firstChild);
}
return frag;
})();
parent.insertBefore(frag, currentNode);
parent.removeChild(currentNode);
}
}
Then I have another little function that takes a dollar amount and creates another number:
function = oppCostCalc(dollarAmount) {
var wage = 7.25;
var costNum = Number(dollarAmount.replace(/[^0-9\.]+/g,""));
var oppCost = Math.round(costNum/wage);
return oppCost;
}
What I'm trying to do is create one big function that searches a page for dollarAmount then replaces all occurrences with oppCost.
Can I just squeeze oppCostCalc into findAndReplace somehow? One of the main problems is that find and replace has both an input and output parameter and I need it to have only the input and use it to find the output. I've read some stuff about wrapping the two in another function but that seems a step backwards.
I'm not necessarily looking for a "here's your script" kind of answer just a solid push in the right direction. I'll come back and edit this post once I figure it out
Thank you for your time.

GWT/CSS - styling part of a label

Is there a way to style part of the text in a label - change color, boldness, size, etc?
Use HTML widget instead of Label. Then:
HTML label = new HTML();
label.setHtml("Brown <span class=\"brown\">fox</span>");
I was a little bored, and I thought I might be able to offer something useful, so, that said, I offer this:
function elemStyle(el, needle, settings) {
// if there's no 'el' or 'needle' arguments, we quit here
if (!el || !needle) {
return false;
}
else {
// if 'el' has a nodeType of 1, then it's an element node, and we can use that,
// otherwise we assume it's the id of an element, and search for that
el = el.nodeType == 1 ? el : document.getElementById(el);
// if we have a 'settings' argument and it's an object we use that,
// otherwise we create, and use, an empty object
settings = settings && typeof settings === 'object' ? settings : {};
// defining the defaults
var defaults = {
'class': 'presentation',
'elementType': 'span'
},
// get the text from the 'el':
haystack = el.textContent || el.innerText;
// iterate over the (non-prototypal) properties of the defaults
for (var prop in defaults) {
if (defaults.hasOwnProperty(prop)) {
// if the 'settings' object has that property set
// we use that, otherwise we assign the default value:
settings[prop] = settings[prop] || defaults[prop];
}
}
// defining the opening, and closing, tags (since we're using HTML
// as a string:
var open = '<' + settings.elementType + ' class="' + settings.class + '">',
close = '</' + settings.elementType + '>';
// if 'needle' is an array (which is also an object in JavaScript)
// *and* it has a length of 2 (a start, and stop, point):
if (typeof needle === 'object' && needle.length === 2) {
var start = needle[0],
stop = needle[1];
el.innerHTML = haystack.substring(0, start) + open + haystack.substring(start, stop) + close + haystack.substring(stop);
}
// otherwise if it's a string we use regular expressions:
else if (typeof needle === 'string') {
var reg = new RegExp('(' + needle + ')');
el.innerHTML = haystack.replace(reg, open + '$1' + close);
}
}
}
The above is called like so:
// a node-reference, and a string:
elemStyle(document.getElementsByTagName('label')[0], 'Input');​
JS Fiddle demo.
// a node-reference, and a start-stop array:
elemStyle(document.getElementsByTagName('label')[0], [6, 8]);​
JS Fiddle demo.
// an id (as a string), and a string to find, with settings:
elemStyle('label1', 'Input', {
'elementType' : 'em'
});​
JS Fiddle demo.
This could definitely do with some error-catching (for example if an array is passed into the function that's less, or more, than two-elements nothing happens, and no error is returned to the user/developer; also if the el variable is neither a node-reference or an id, things go wrong: Uncaught TypeError: Cannot read property 'textContent' of null).
Having said that, I felt dirty, so I added in a simple error-check, and reporting, if the el doesn't resolve to an actual node in the document:
el = el.nodeType == 1 ? el : document.getElementById(el);
// if 'el' is null, and therefore has no value we report the error to the console
// and then quit
if (el === null) {
console.log("You need to pass in either an 'id' or a node-reference, using 'document.getElementById(\"elemID\")' or 'document.getElementsByTagName(\"elemTag\")[0].");
return false;
}
References:
document.getElementById().
JavaScript regular expressions.
Node.nodeType.
string.replace().
String.substring().
typeof variable.

Ways of comparing Text Content between HTML Elements

As the title says, I am looking for a way of comparing the Text content of an HTML Element with another HTML Elements's Text content and only if they are identical, alert a message. Any thoughts? Greatly appreciate it!
(Posted with code): For example, I can't equalize the remItem's content with headElms[u]'s content.
else if (obj.type == 'checkbox' && obj.checked == false) {
var subPal = document.getElementById('submissionPanel');
var remItem = obj.parentNode.parentNode.childNodes[1].textContent;
alert("You have disselected "+remItem);
for (var j=0; j < checkSum.length; j++) {
if (remItem == checkSum[j]) {
alert("System found a match: "+checkSum[j]+" and deleted it!");
checkSum.splice(j,1);
} else {
//alert("There were no matches in the search!");
}
}
alert("Next are...");
alert("This is the checkSum: "+checkSum);
alert("Worked!!!");
var headElms = subPal.getElementsByTagName('h3');
alert("We found "+headElms.length+" elements!");
for (var u=0; u < headElms.length; u++){
alert("YES!!");
if (remItem == headElms[u].textContent) {
alert("System found a matching element "+headElms[u].textContent+" and deleted it!");
}
else {
alert("NO!!");
alert("This didn't work!");
}
}
}
var a = document.getElementById('a');
var b = document.getElementById('b');
var tc_a = a ? a.textContent || a.innerText : NaN;
var tc_b = b ? b.textContent || b.innerText : NaN;
if( tc_a === tc_b )
alert( 'equal' );
Using NaN to ensure a false result if one or both elements don't exist.
If you don't like the verbosity of it, or you need to do this more than once, create a function that hides away most of the work.
function equalText(id1, id2) {
var a = document.getElementById(id1);
var b = document.getElementById(id2);
return (a ? a.textContent || a.innerText : NaN) ===
(b ? b.textContent || b.innerText : NaN);
}
Then invoke it...
if( equalText('a','b') )
alert( 'equal' );
To address your updated question, there isn't enough info to be certain of the result, but here are some potential problems...
obj.parentNode.parentNode.childNodes[1] ...may give different element in different browsers
"System found a matching element ... and deleted it!" ...if you're deleting elements, you need to account for it in your u index because when you remove it from the DOM, it will be removed from the NodeList you're iterating. So you'd need to decrement u when removing an element, or just iterate in reverse.
.textContent isn't supported in older versions of IE
Whitespace will be taken into consideration in the comparison. So if there are different leading and trailing spaces, it won't be considered a match.
If you're a jQuery user....
var a = $('#element1').text(),
b = $('#element2').text();
if (a === b) {
alert('equal!');
}
The triple equals is preferred.
To compare two specific elements the following should work:
<div id="e1">Element 1</div>
<div id="e2">Element 2</div>
$(document).ready(function(){
var $e1 = $('#e1'),
$e2 = $('#e2'),
e1text = $e1.text(),
e2text = $e2.text();
if(e1text == e2text) {
alert("The same!!!");
}
});
I will highly recommend using jQuery for this kind of comparison. jQuery is a javascript library that allows you to draw values from between HTML elements.
var x = $('tag1').text();
var y = $('tag2').text();
continue js here
if(x===y){
//do something
}
for a quick intro to jQuery...
First, download the file from jQuery.com and save it into a js file in your js folder.
Then link to the file. I do it this way:
Of course, I assume that you're not doing inline js scripting...it is always recommended too.
A simple getText function is:
var getText = (function() {
var div = document.createElement('div');
if (typeof div.textContent == 'string') {
return function(el) {
return el.textContent;
}
} else if (typeof div.innerText == 'string') {
return function(el) {
return el.innerText;
}
}
}());
To compare the content of two elements:
if (getText(a) == getText(b)) {
// the content is the same
}

Regexp to wrap each word on HTML page

Is it possible to wrap each word on HTML page with span element?
I'm trying something like
/(\s*(?:<\/?\w+[^>]*>)|(\b\w+\b))/g
but results far from what I need.
Thanks in advance!
Well, I don't ask for the reason, you could do it like this:
function getChilds( nodes ) {
var len = nodes.length;
while( len-- ) {
if( nodes[len].childNodes && nodes[len].childNodes.length ) {
getChilds( nodes[len].childNodes );
}
var content = nodes[len].textContent || nodes[len].text;
if( nodes[len].nodeType === 3 ) {
var parent = nodes[len].parentNode,
newstr = content.split(/\s+/).forEach(function( word ) {
var s = document.createElement('span');
s.textContent = word + ' ';
parent.appendChild(s);
});
parent.removeChild( nodes[len] );
}
};
}
getChilds( document.body.childNodes );
Even tho I have to admit I didn't test the code yet. That was just the first thing which came to my mind. Might be buggy or screw up completely, but for that case I know the gentle and kind stackoverflow community will kick my ass and downvote like hell :-p
You're going to have to get down to the "Text" nodes to make this happen. Without making it specific to a tag, you really to to traverse every element on the page, wrap it, and re-append it.
With that said, try something like what a garble post makes use of (less making fitlers for words with 4+ characters and mixing the letters up).
To get all words between span tags from current page, you can use:
var spans = document.body.getElementsByTagName('span');
if (spans)
{
for (var i in spans)
{
if (spans[i].innerHTML && !/[^\w*]/.test(spans[i].innerHTML))
{
alert(spans[i].innerHTML);
}
}
}
else
{
alert('span tags not found');
}
You should probably start off by getting all the text nodes in the document, and working with their contents instead of on the HTML as a plain string. It really depends on the language you're working with, but you could usually use a simple XPath like //text() to do that.
In JavaScript, that would be document.evaluate('//text()', document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null), than iterating over the results and working with each text node separately.
See demo
Here's how I did it, may need some tweaking...
var wrapWords = function(el) {
var skipTags = { style: true, script: true, iframe: true, a: true },
child, tag;
for (var i = el.childNodes.length - 1; i >= 0; i--) {
child = el.childNodes[i];
if (child.nodeType == 1) {
tag = child.nodeName.toLowerCase();
if (!(tag in skipTags)) { wrapWords(child); }
} else if (child.nodeType == 3 && /\w+/.test(child.textContent)) {
var si, spanWrap;
while ((si = child.textContent.indexOf(' ')) >= 0) {
if (child != null && si == 0) {
child.splitText(1);
child = child.nextSibling;
} else if (child != null) {
child.splitText(si);
spanWrap = document.createElement("span");
spanWrap.innerHTML = child.textContent;
child.parentNode.replaceChild(spanWrap, child);
child = spanWrap.nextSibling;
}
}
if (child != null) {
spanWrap = document.createElement("span");
spanWrap.innerHTML = child.textContent;
child.parentNode.replaceChild(spanWrap, child);
}
}
}
};
wrapWords(document.body);
See demo

prototype.js highlight words. DOM traversing correctly and efficiently

I want to highlight a specific word in my HTML page after the page is loaded. I don't want to use the dumb:
document.innerHTML = document.innerHTML.replace(.....);
I want to traverse every DOM node, find out the ones that contain text and modify the innerHTML of only those individual nodes. Here's what I came up with:
function highlightSearchTerms(sword) {
$$('body').map(Element.extend).first().descendants().each(function (el) {
if (el.nodeType == Node.ELEMENT_NODE && el.tagName != 'TD') {
//$A(el.childNodes).each(function (onlyChild) {
//if (onlyChild.nodeType == Node.TEXT_NODE) {
//console.log(onlyChild);
el.innerHTML = el.innerHTML.replace(new RegExp('('+sword+')', 'gi'), '<span class="highlight">$1</span>');
//}
//});
}
});
//document.body.innerHTML.replace(new RegExp('('+sword+')', 'gi'), '<span class="highlight">$1</span>');
}
It works as it is right now, but it's VERY inefficient and is hardly better than the single line above as it may do a replacement several times over the same text. (Hmmm..., or not?)
If you uncomment the commented stuff and change el.innerHTML.replace to onlyChild.textContent.replace it would work almost like it needs to, but modifying textContent doesn't create a new span as an element, but rather adds the HTML content as text.
My question/request is to find a way that it highlights the words in the document traversing elements one by one.
This works quick and clean:
function highlightSearchTerms(sword) {
$$('body').map(Element.extend).first().descendants().each(function (el) {
if (el.nodeType == Node.ELEMENT_NODE && el.tagName != 'TEXTAREA' && el.tagName != 'INPUT' && el.tagName != 'SCRIPT') {
$A(el.childNodes).each(function (onlyChild) {
var pos = onlyChild.textContent.indexOf(sword);
if (onlyChild.nodeType == Node.TEXT_NODE && pos >= 0) {
//console.log(onlyChild);
var spannode = document.createElement('span');
spannode.className = 'highlight';
var middlebit = onlyChild.splitText(pos);
var endbit = middlebit.splitText(sword.length);
var middleclone = middlebit.cloneNode(true);
spannode.appendChild(middleclone);
middlebit.parentNode.replaceChild(spannode, middlebit);
//onlyChild. = el.innerHTML.replace(new RegExp('('+sword+')', 'gi'), '<span class="highlight">$1</span>');
}
});
}
});
}
But I've trouble understanding how exactly it works. This seems to be the magic line:
middlebit.parentNode.replaceChild(spannode, middlebit);
I converted one from jQuery to PrototypeJS some time ago :
Element.addMethods({
highlight: function(element, term, className) {
function innerHighlight(element, term, className) {
className = className || 'highlight';
term = (term || '').toUpperCase();
var skip = 0;
if ($(element).nodeType == 3) {
var pos = element.data.toUpperCase().indexOf(term);
if (pos >= 0) {
var middlebit = element.splitText(pos),
endbit = middlebit.splitText(term.length),
middleclone = middlebit.cloneNode(true),
spannode = document.createElement('span');
spannode.className = 'highlight';
spannode.appendChild(middleclone);
middlebit.parentNode.replaceChild(spannode, middlebit);
skip = 1;
}
}
else if (element.nodeType == 1 && element.childNodes && !/(script|style)/i.test(element.tagName)) {
for (var i = 0; i < element.childNodes.length; ++i)
i += innerHighlight(element.childNodes[i], term);
}
return skip;
}
innerHighlight(element, term, className);
return element;
},
removeHighlight: function(element, term, className) {
className = className || 'highlight';
$(element).select("span."+className).each(function(e) {
e.parentNode.replaceChild(e.firstChild, e);
});
return element;
}
});
You can use it on every element like this:
$("someElementId").highlight("foo", "bar");
, and use the className of your choice. You can also remove the highlights.
if you're using the prototype version posted by Fabien, make sure to add the className
as argument to the call of innerHighlight:
i += innerHighlight(element.childNodes[i], term)
needs to be
i += innerHighlight(element.childNodes[i], term, className)
if you care about custom classNames for your highlights.
Grab $(document.body) and do a search/replace and wrap a span around the term, then swap the entire $(document.body) in one go. Treat it as a big string, forget about the DOM. This way you only have to update the DOM once. It should be very quick.
I have found a script that will do what you want (it seems pretty fast), it is not specific to any library so you may want to modify it:
http://www.nsftools.com/misc/SearchAndHighlight.htm
The method you provided above (although commented out) will have problems with replacing items that might be inside a an html element. ie ` a search and replace might "highlight" "thing" when that would not be what you want.
here is a Jquery based highlight script:
http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
It dosent look to hard to convert to prototype.

Categories

Resources