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.
Related
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 adding emoticons to user input with:
function emoticons(html){
for(var emoticon in emotes){
for(var i = 0; i < emotes[emoticon].length; i++){
// Escape bad characters like )
var r = RegExp.escape(emotes[emoticon][i]);
// Set the regex up to replace all matches
r_escaped = new RegExp(r, "g");
// Replace the emote with the image
html = html.replace(r_escaped,"<img src=\""+icon_folder+"/face-"+emoticon+".png\" class=\"emoticonimg\" />");
}
}
return html;
}
The problem is sometimes the user input is in <code>xxx</code> blocks. Is there a way to get the emoticons function to ignore everything inside the code blocks if they exists. As they won't always exist?
Thanks
In order to do this easily, I'd work only with text nodes (not serialised HTML) and skip code elements.
You tagged it jquery, so there is some jQuery convenience code to ease cross browser issues with utility functions. It is very easily modified to work without jQuery, however.
var searchText = function(parentNode, regex, callback, skipElements) {
skipElements = skipElements || ['script', 'style'];
var node = parentNode.firstChild;
do {
if (node.nodeType == 1) {
var tag = node.tagName.toLowerCase();
if (~$.inArray(tag, skipElements)) {
continue;
}
searchText.call(this, node, regex, callback);
} else if (node.nodeType == 3) {
while (true) {
// Does this node have a match? If not, break and return.
if (!regex.test(node.data)) {
break;
}
node.data.replace(regex, function(match) {
var args = $.makeArray(arguments),
offset = args[args.length - 2],
newTextNode = node.splitText(offset);
callback.apply(window, [node].concat(args));
newTextNode.data = newTextNode.data.substr(match.length);
node = newTextNode;
});
}
}
} while (node = node.nextSibling);
};
searchText($('body')[0], /:\)/, function(node, match) {
var img = $('<img />')[0];
img.src = 'http://www.gravatar.com/avatar/80200e1488ab252197b7f0f51ae230ef?s=32&d=identicon&r=PG';
img.alt = match;
node.parentNode.insertBefore(img, node.nextSibling);
}, ['code']);
jsFiddle.
I wrote this function recently, it should do what you hope to achieve.
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
}
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.
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.