padolsey / findandreplacedomtext Goto Github PK
View Code? Open in Web Editor NEW:mag: Find and replace DOM text
:mag: Find and replace DOM text
Noticed the function is not matching over newlines. Here is the fiddle
http://jsfiddle.net/nk34mbs3/3/
The main idea of this library is to split a match into portions when it crosses element boundaries.
Note: I will refer to only the
input
andoutput
as a shorthand for calling the following function:function wrap(input) { var el = document.createElement('div'); el.innerHTML = input; findAndReplaceDOMText(el, { find: /\w+/g, wrap: 'w' }); var output = el.innerHTML; return output; }
As expected, the following code splits the matched word b1b2
among two <w>
elements:
input: a b1<i>b2 c</i> <i>d e</i> output: <w>a</w> <w>b1</w><i><w>b2</w> <w>c</w></i> <i><w>d</w> <w>e</w></i>
However, if the entire match corresponds to properly-nested HTML, then it is possible to just use a single element to wrap the match. So instead of the following:
input: a b1<i>b2</i> <i>c d</i> output: <w>a</w> <w>b1</w><i><w>b2</w></i> <i><w>c</w> <w>d</w></i>
the two portions b1
and b2
could be combined as such:
output: <w>a</w> <w>b1<i>b2</i></w> <i><w>c</w> <w>d</w></i>
I think it could be useful to add this behavior in a new option called wrapCombineMode
.
wrapCombineMode | behavior |
---|---|
separate |
Original behavior. Each leaf (portion) of a match gets wrapped. |
combine |
If the entire match encompasses balanced HTML, it is wrapped. Otherwise, fall back to separate . |
split |
Force the entire match to be wrapped as a unit. (If the entire match encompasses balanced HTML, it is wrapped. Otherwise, split up elements that cross the edges of the match.) |
Here are some test cases (spaced out for legibility):
Basic balanced
input: <i>aa</i>a a<i>aa</i> a<i>a</i>a separate: <i><w>aa</w></i><w>a</w> <w>a</w><i><w>aa</w></i> <w>a</w><i><w>a</w></i><w>a</w> combine: <w><i>aa</i>a</w> <w>a<i>aa</i></w> <w>a<i>a</i>a</w> split: <w><i>aa</i>a</w> <w>a<i>aa</i></w> <w>a<i>a</i>a</w>
Complex balanced
input: a<i><i>a</i></i>a <i>a<i>a</i></i>a <i>a</i>a<i>a</i> separate: <w>a</w><i><i><w>a</w></i></i><w>a</w> <i><w>a</w><i><w>a</w></i></i><w>a</w> <i><w>a</w></i><w>a</w><i><w>a</w></i> combine: <w>a<i><i>a</i></i>a</w> <w><i>a<i>a</i></i>a</w> <w><i>a</i>a<i>a</i></w> split: <w>a<i><i>a</i></i>a</w> <w><i>a<i>a</i></i>a</w> <w><i>a</i>a<i>a</i></w>
Unbalanced
input: aa<i>a a</i>a<i>a a</i>aa separate: <w>aa</w><i><w>a</w> <w>a</w></i><w>a</w><i><w>a</w> <w>a</w></i><w>aa</w> combine: <w>aa</w><i><w>a</w> <w>a</w></i><w>a</w><i><w>a</w> <w>a</w></i><w>aa</w> split: <w>aa<i>a</i></w> <w><i>a</i>a<i>a</i></w> <w><i>a</i>aa</w>
I was trying to replace the selected text in the page. If I get the selected text range using below code:
if (window.getSelection) {
t = window.getSelection();
} else if (document.getSelection) {
t = document.getSelection();
} else if (document.selection) {
t = document.selection.createRange().text;
}
var range = t.getRangeAt(0).cloneRange();
findAndReplaceDOMText(range, $('body')[0], 'chucknorris');
Above code may be wrong but can we do something like this using findAndReplaceDOMText.
Hi,
I need to emulate a regular expression with a negative lookahead, which is, AFAIK, not supported in JS. This can be achieved with capture groups, but, when using the wrap
attribute, I have no access to them.
Example: in the expression /(This)(\s+)(document)/
I need to wrap the document
part with an element, but only if it's preceded by This
.
I can't find a way to achieve this with the current API, any suggestions?
After a recent API addition (the element filter, see #11) I was considering a slight refactor of the way findAndReplaceDOMText
is called. It's getting quite busy/lengthy at the moment and it may benefit from something like:
findAndReplaceDOMText(elementToSearch, {
match: /foo/,
replace: theReplacement,
capture: optionalCaptureGroupNumber,
filter: optionalElementFilterFunction
});
(i.e. an intuitive configuration object as the second arg)
Of course, the old way would still work but would be deprecated.
This would make it more readable and much easier to add new features without adding complexity to the already-long argument list.
I am planning this change for 0.4.0. Thoughts are welcome.
What is the license of this project? Can we use the code freely?
Does this support finding characters in other languages like Russian or Chinese that don't use the our characters?
hi
Is it possible to get this behavior with findAndReplaceDOMText?
I want to add a class to the tag that contains the text
example:
if I have
<body>
<div>
<p>yoshi</p>
</div>
<p>
<li>
<p>yoshi</p>
</li>
</p>
</body>
I need
<body>
<div>
<p CLASS_ADDED>yoshi</p>
</div>
<p>
<li>
<p CLASS_ADDED>yoshi</p>
</li>
</p>
</body>
The goal is to hide the nodes that contain variations of yoshi
text..
So in this particular case, CLASS_ADDED would be class="hide_me"
I've tested but
findAndReplaceDOMText(container, {
find: = RegExp("yoshi", 'gi'),
....??????....
})
This issue has been observed in IE8. It may or may not be present in later versions of IE.
Improperly nested and/or un-closed tags in HTML markup can corrupt the DOM tree in Internet Explorer 8. While trying to parse and fix the markup, IE messes it up by creating duplicate nodes at multiple locations in the document. This leads to incorrect parent references with the parentNode property.
The current implementation of findAndReplaceDOMText uses the parentNode property to traverse up a document tree, and the results in IE can be unpredictable with improperly formed HTML documents.
The Fiddle http://fiddle.jshell.net/2fxRF/show/ demonstrates the issue. The box with a red border contains an un-closed DIV, while the box with a blue border contains well formed HTML. In the red box, the first replacement of the text "the" is correct but the replacement after the un-closed DIV is not correct.
Example: https://jsfiddle.net/zzby5zb6/1/
Actually, this isn't technically incorrect, as the replace
function has no way of knowing if the intention was to put the !!!
inside of the span
element or outside of it.
As far as I understand, this is limited by the current design. I understand your desire to stay true to the original regexp/replace syntax, but to avoid such scenarios, maybe it would make sense to consider using capture group delimiters? Being able to express something like {$1!!!}{$2}
would solve the issue.
I use this to replace things, but I would like to know if there was a way to not let it replace things in certain elements, like textarea's etc. Thanks!
@padolsey :
I've got this tricky task using Javascript to find every nodes in the DOM with the following criterias:
I've found the following plugin for the task, it works great with the first and second criteria, but I haven't got any luck working for the last criteria.
Any help would be appreciated :)
My attempt so far: http://codepen.io/herihehe/pen/YPwxar
var container = document.getElementById('container');
var ignores = 'ignoreme|ignoreme2|ignoremeN';
var re = new RegExp("\(ignoreme|ignoreme2|ignoremeN)|\\w","g");
findAndReplaceDOMText(container, {
find: re,
replace: function(portion, match) {
return match[1] || '*';
}
});
Is there a way to perform an exact match?
I require the ability to find/replace throughout the whole document.
findAndReplaceDOMText(document, {find: text, wrap: wrapElem});
Funnily enough, if text is equal to Hello World
and I have a comment in my Javascript like /** Contains "Hello World" **/
, this is also matched and replaced, causing issues when getting a list of all the newly wrapped elements.
I don't know the best way to fix this as there are different (all legitimate) ways of commenting in Javascript from /* */
to //
. I'm not sure if there are any more. Given a list of comment types (which could easily be modified in the future), perhaps we could ignore any where they exist around the match?
I get this error Uncaught TypeError: Cannot read property 'global' of undefined (on line 214)
My code
var replace = function(search, replacement) {
findAndReplaceDOMText(document.body, {
preset:'prose',
find: search,
replace: function(m) {
var el = document.createElement('span');
el.style.backgroundSize = '100% 100%';
el.style.fontFamily = 'Helvetica, Sans-Serif';
el.style.fontSize = '120%';
el.style.lineHeight = '0';
el.style.backgroundImage = 'url("data:image/svg+xml;charset=UTF-8, ' + replacement + '")';
el.innerHTML = '    ';
return el;
}
})
};
...
var emojify = function() {
for (i=0; i<=Object.keys(emojis).length; i++) {
replace(Object.keys(emojis)[i], emojis[Object.keys(emojis)[i]]);
}
};
emojify();
var observer = new MutationObserver(
function(){
if (search() > 0) {
emojify();
}
}
);
var config = { subtree: true, childList: true };
observer.observe(document.body, config);
I could submit a PR for this (including within your JSDoc comments) if you like but I tend to find people like to do their own wording anyways.
And a few nits while I'm at it unless you'd like a PR (I've already made these updates in a JSLinted version if you want that):
toString
, and isArray
can be removed as they are not in use (though if you did use the latter, you might rely on Array.isArray
).escapeRegExp
doesn't need to include /
(only when you manually use one in a literal does it need to be escaped). And if you want this function as a general purpose function (e.g., including when used to escape just a part of a regular expression), it should also escape -
and ,
, but in your case it is not needed.I have a dictionary function dict() which append result after input string
function dict (match){return match +dictionary[match] }
I want to edit text of elements like
document.body.innerHTML = document.body.innerHTML.replace(/something/g, dict());
does, but without messing up tags' name and properties like 'src'.
I am confused with 'portion' and 'match' in your method 'replace'.
Could you give me a simple demo? Thank you.
Hi James,
I was wondering, is it possible to see what strings in the DOM matched the find: RegExp?
I am using the following options and would like to know which strings where matched.
var searchConfig = {
find: new RegExp(pool.searchText, 'g'),
wrap: wType || 'span',
wrapClass: wClass || 'filter-number',
};
var finder = findAndReplaceDOMText(domElement, searchConfig);
Since the matches are popped finder.matches is empty after execution.
Is there a way to seperate find and wrap function calls to extract the matches in beween?
If not I could implement some 'persistentMatches: true' flag that would write the matches into this.persistentMatches or so?
Keep up the awsome work.
Thank you
Christian
Great script!!
Question, when matching regex's, each of the matches gets called multiple times?
findAndReplaceDOMText($("body")[0], {
preset: 'prose',
find: /(bananas are cool)|(i prefer oranges)|(super foo bar)/gi,
replace: function(portion, match) {
//called on every match if it doesn't match?
return match + " i totally agree";
}
});
Hi,
I've noticed that indexInMatch is just returning the negative of indexInNode. Is that the expected behavior? I'm having trouble understanding how to use this to locate the portion within the match.
I added it to a chrome extension so I can test it on any page. I used this code to test:
var test = findAndReplaceDOMText(document.getElementsByTagName("p"), {
find: /\w+/g,
wrap: 'b'
});
console.log(test);
but no matter what I try, it returns no matches:
https://i.gyazo.com/f9dfc2024e7edc98bd938409f632a81b.png
What I need to do is select a bunch of tags with JQuery, search for a regular expression and replace them with a pattern. But I can't get it to work at all.
If I use a regular expression with word boundaries, and attempt to match it over a set of list elements, e.g. /\bitem\b/
over
<ul>
<li>item</li>
<li>item</li>
<li>item</li>
<ul>
it won't match the text in the li elements because getAggregateText will concatenate them all together as itemitemitem
I'm trying your code to do a search and replace on some html where I need to retain the events. I don't have access server side (and won't) so this would be ideal but for some reason the replace is off:
See this jsfiddle:
https://jsfiddle.net/py1t60L4/1/
Note: The hyperlink after the search/replace runs has transposed backwards a little bit.
I got as far as finding out that getPortionReplacementNode has a matchIndex value but this never gets passed so its undefined. I tried bubbling this through but then it's always 0 and didn't make any difference.
Maybe you can figure it out?
I know that the prose
option allows this for block level elements. But as it mentions:
<em>
, <span>
, etc.)Is there any way to exclude matches that have textual-inline element borders like <span>
?
Hello,
Could this script be used to add counter to identical elements, for example
Hello, nice bike, hello.
Would result in
<span id="find_1">Hello</span>, nice bike, <span id="find_2">hello</span>.
I am currently using split that lets me piece back the string together,and add these html ids with conters.
When use this lib for keywords highlight in Google search, I usually got this error:
Uncaught TypeError: Cannot call method 'insertBefore' of null findAndReplaceDOMText.js:239
Any idea?
I called the function like this:
findAndReplaceDOMText(/keyword/g, document.body, function(fill, matchIndex) {
var el = document.createElement('span');
el.style.backgroundColor = 'red';
el.innerHTML = fill;
return el;
});
I noticed that selection will not work across element borders.
For example:
<div id=="test">
<h1>This is a test</h1>
<p>Grumpy wizards make toxic brew for the evil Queen and Jack.</p>
</div>
findAndReplaceDOMText(document.getElementById('test'), {
find: /test Grumpy wizards/,
wrap: 'em'
});
Won't work. Are there ways around this?
Hi James. Firstly, thanks a lot for this code!
All the examples I've seen have demonstrated how to wrap text with an element node.
I wanted to know if you had any ideas for adapting this code to provide an API method to actually replace the text in the DOM?
For example: I have the following DOM tree:
<div>
badw<strong>o<em>rd</em></strong>d
</div>
I want to replace the word 'badwordd' with 'bad word' in the above DOM tree.
I've had a go at doing this myself by providing a handler function for the replacementNode argument, but the issue here is that the replacementNode function is called in the incorrect order.
It seems the recursive logic for traversing the tree does not match the order of the word you are searching for.
For example:
findAndReplaceDOMText(new RegExp(oldWord), this.element[0], function(fill){
console.log(fill);
return document.createTextNode(fill);
});
/* will output:
badw
d
o
rd
*/
The order in which the replacementNode handler is called makes it a bit tricky for me to replace the 'fill' value with something else.
If you have any ideas on how to do this elegantly, it would be most appreciated.
Thanks,
Rich
This is a simple example. Find all instances of 4 consecutive digits, but do not match '2000'. There might be a way to do this with regex alone? But more complex examples would probably be a problem.
find: /\d\d\d\d/g,
exclude: /2000/g,
Hi James,
when i use a regex of /\btrump\b/gi in my chrome extension that looks for the exact word in a document body it works on most sites. But sometimes like on a google search result page it doesn't work on the result titles. This happens when the word i'm looking for is the first or last word in a sentence?! So I changed the regex to /(trump|donald)/gi instead of /\b(trump|donald)\b/gi. But now, off course, also trumpet and mcDonald will be matched. I hope you can help me out!
Hi
Please, look at my test case
https://jsfiddle.net/weirrrdy/xLcfnrpk/
I'd like to match the exact word "Highlighted", that's why there is a special metacharacter "\b" wrapping the regex expression. Semantically it doesn't necessary to put '\b' in this case but I'm using it in my application and it's required for the test case.
The expected behaviour here would be selecting both 'highlighted' words, however only the second phrase has matched.
The problem is the getAggregateText function has combined text of first two paragraphs into the following text: "HighlightedPhrase". Therefore the current regexp expression has skipped the word "Highlighted".
I would really appreciate if you fix this.
Avoiding child elements, such as <style>
and <script>
, to be replaced inside the target elements is necessary; otherwise, it would simply keep them from executing. For example,
<p id="tester">
<style scoped>span { color: red; }</style>
<span>Hello,</span>
people in red.
</p>
<script>
findAndReplaceDOMText(
/red/g,
document.getElementById('tester'),
'em'
);
</script>
Would outputs,
<p id="tester">
<style scoped>span { color: <em>red</em>; }</style>
<span>Hello,</span>
people in <em>red</em>.
</p>
Tried editing the function, but just not able to make it right. Hoping this could be solved.
Am trying to detect specific words in an external text area, and highlight them as they're found.
In said text area, words on separate lines are only separated by a <br />
tag
For example, when far
looks at the previous two lines, it's essentially running my regex on
as they're found.In said text area
Normally I'd detect a word break \b
and call it a day, but as an example, if I'm trying to detect google
I don't want it flagging on http://google.com
Perhaps I should cut my losses and not worry about how often similar use cases would occur, but figured it was worth checking if there's a simple solution I'm overlooking
https://jsfiddle.net/devdost/zoyqptzd/
edits
When doing a replacement - and using some HTML as the replacement text - the text appears to be replaced with entities, instead of actual html.
Is this by design? Is there a workaround?
Trying to use version 0.40 to match text elements within a div
var imageLinkSpan = document.createElement('span');
imageLinkSpan.className = 'imageLink';
findAndReplaceDOMText(document.getElementById("answerField"), {
find: /\(see\simage\s[^)]*\)/,
wrap: imageLinkSpan
});
Searching within the following div...
<div id="answerField">
Tufted astrocytes. Globose tau positive neurofibrillary tangles in neurons. Thorn astrocytes (contain tau along the length of their processes, often binucleated). Gross pathology - midbrain atrophy.
<span class="imageLink" onclick="displayImage("images/PSP-pathology_1.png");">
(see image 1)
</span>
(see image 2)
</div>
(the onclick handler is added later, by my own code)
The goal being to wrap each text fragment of type (see image n) with a span of class imageLink. Instead, only the first match is made. The issue doesn't appear to be due to the RegExp, as replacing it with /\w+/ just matches the first word, not all words. I had the same problem with 0.20 and just tried the new version today, with the same results.
Great tool, and thanks for all the work so far!
When using replace
with optional capture groups findAndReplaceDOMText seems to replace empty capture group values with undefined
in HTML (instead of empty string). Example:
findAndReplaceDOMText(context, {
find: /(This)(\s+)?(document),
replace: "$1$2$3",
});
ran against Thisdocument
(in HTML) will result in Thisundefineddocument
Hi there,
I want to use your script (tried via npm, and the bower repo) for my application but if I include it I get the following error:
Uncaught TypeError: Cannot set property 'findAndReplaceDOMText' of undefined
Which is caused by this line (22) of code:
root.findAndReplaceDOMText = factory();
In my case it looks like root is not defined, cause if I change the code to
window.findAndReplaceDOMText = factory();
the error dissapears and the script works. Do you have any idea what is causing this? Couldn't find anyone facing a similiar issue via Google.
Thanks for your help.
Not even sure what the problem is...
Had a big problem with my actual use of findAndReplaceDOMText :
I let people difined patterns, wich are either regexp or simple string, and i manipulate the found matches afterwards with your library (wich is awesome by the way).
Weirdly, with strings containing urls like 0.0.0.0:5000/accounts/250/projects/60 it blocked everything, like if the JS was in an infinite loop.
I just ended up with a hack for the moment :
filterElements: (element) ->
if element.id==""
return true
else if $("#"+element.id+":contains('http')").length
return false
else
return true
but if you had any idea, would be great.
I guess it as something to be with the slashes, because it also blocks with "qwerty/qwerty/qwerty/qwerty", but not "/qwerty/qwerty/qwerty/" or qwerty/qwerty/qwerty
Hey James, this is an awesome library. However, I want to be able to add attributes like title
to my span
tags I wrap with. Is that already possible?
NPM is becoming the de-facto standard repository of JS modules, both browser and server. So it would be nice to have this published there.
Please provide a license so I know whether I'm allowed to use this code in another project :)
Hey,
First of all, Its a very useful JS and really thank you for the same...
I am trying to replace words with links and it works... But i don't see the actual link and end up seeing the HTML code...
obj = document.getElementsByTagName("body")[0];
for (i in array) {
findAndReplaceDOMText(obj, {
find: new RegExp("\\s(?!</a>)(" + i + "\\b)", "gi"),
replace: ' <a href="'+array[i]+'" target="_blank">'+i+'</a>'
});
}
The word gets replaced but can't see the link... so if lets say i try to replace google, what i see is:
<a href ="http://www.google.com/" target="_blank"> google </a>
What i want to see is the actual link...
Hello,
I would like to execute javascript after the element is highlighted, and it looks like js that comes right after findAndReplaceDOMText does not yet find the added element. Any tips? I am trying to add a qtip on the highlighted element.
Hey there,
It would be cool to have classname support added to set a custom class on the wrapping tag.
This could be achieved quite simple:
L38. function findAndReplaceDOMText(regex, node, replacementNode, replacementClass, captureGroup, elFilter) {
[...]
L41. var replaceFn = _genReplacer(replacementNode, replacementClass);
[...]
L208. function _genReplacer(nodeName, nodeClass) {
[...]
L216+. if (nodeClass) stencilNode.className = nodeClass;
I've added it myself, but thought that maybe others could profit from this enhancement as well.
Thanx for a fast and accurate text replacement function!
I'm using /Subscribe/gi
as my regex. What I'm trying to do is highlight the word "Subscribe" by wrapping it in a span
element. I was getting matches to "Unsubscribe" though, so I adjusted my regex to include [^n]
. Now it reads /[^n]Subscribe/gi
. This solved my problem for a moment. Unfortunately, this now matches "Subscribe" when it's at the beginning of a text node because the previous text node butts up against it. [^n]
matches whatever that character happens to be at the end of the previous node.
Here's my code:
findAndReplaceDOMText(document.getElementsByTagName('body')[0], {
find: /[^n]Subscribe/gi,
wrap: 'span',
wrapClass: "text-error"
});
Here's a working example of what I'm talking about on Codepen: http://codepen.io/jimmykup/pen/ggLdja
You can see that it breaks my table in the left example (compare it to the table on the right). Is there something I could be doing with findAndReplaceDOMText or a way to better structure my RegEx to avoid instances like this? Ideally, if it could just ignore anything that matches [^] when wrapping it with an element that would work, but I don't know if that can be done.
It appears as though fARDT will only replace text that exists within the contents of an element as opposed to any/all text that exists within the entire document.
Is there an option that will replace text that exists within the attributes of an element?
Example:
How to replace the text within the 'title' attribute of all 'img' tags. So can be changed to
.
Repro
<textarea>lorem ipsum</textarea>
to the demo contentsExpect
Actual
Thanks for the awesome library! :)
From the docs, `$`` should represent everything to the left of the match. I was hoping that meant "to the left of the 1st captured group", but it seems that I misunderstood. Running this:
findAndReplaceDOMText(document.body, {
preset: 'prose',
find: /Specific Words \((\d+)/g,
replace: '$`0',
});
On a document containing:
Specific Words (123)
Winds up as:
Specific Words (123)0
When I would expect:
Specific Words (0)
Any advice?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.