jQuery replace strings in many classes - javascript

I'm trying to use jQuery to replace all occurrences of a particular string that occurs in a certain class. There are multiple classes of this type on the page.
So far I have the following code:
var el = $('div.myclass');
if(el != null && el.html() != null )
{
el.html(el.html().replace(/this/ig, "that"));
}
This doesn't work if there is more than one div with class myclass. If there is more than one div then the second div is replaced with the contents of the first! It is as if jQuery performs the replacement on the first div and then replaces all classes of myclass with the result.
Anyone know how I should be doing this? I'm thinking some kind of loop over all instances of mychass divs - but my JS is a bit weak.

I think what you are looking for is something like this:
$('div.myclass').each(function(){
var content = $(this).html();
content = content.replace(/this/ig,'that');
$(this).html(content);
});
(not tested)

slightly different of previous answer:
$('div.myclass').each(function(i, el) {
if($(el).html() != "" ) {
$(el).html($(el).html().replace(/this/ig, "that"));
}
});
should work

If the contents of your .myclass elements are purely textual, you can get away with this. But if they contain other elements your regex processing might change attribute values by mistake. Don't process HTML with regex.
Also by writing to the innerHTML/html(), you would lose any non-serialisable data in any child element, such as form field values, event handlers and other JS references.
function isTextNode(){
return this.nodeType===3; // Node.TEXT_NODE
}
$('div.myclass, div.myclass *').each(function () {
$(this).contents().filter(isTextNode).each(function() {
this.data= this.data.replace(/this/g, 'that');
});
});

Related

jQuery if data attribute add class

I'm really new to JS and jQuery, but I try adding a class "hide" to an element if it has a data attribute
with a specific string. So if "class" hat a data-rating attribute of "0.0", the class "hide" should be added. It doesn't work and I don't know why.
$(document).ready(function() {
if ($(".class").data('rating') === ('0.0')){
$(".class").addClass('hide');
}
});
jQuery recognises data-rating="0.0" as numeric, so when you call $(".class").data('rating') you get the number 0. Hence, strictly comparing it to any string will fail.
Additionally, your code will not behave as expected if there is more than one element with the given class.
$(".class").each(elem=>{
const $elem = $(elem);
if( $elem.data('rating') === 0) {
$elem.addClass('hide');
}
});
Or, without jQuery (and therefore immeasurably faster)...
document.querySelectorAll(".class").forEach(elem=>{
if( parseFloat(elem.getAttribute("data-rating")) === 0) {
elem.classList.add("hide");
}
});
Rapid shift back to jQuery, you could also do this:
$(".class[data-rating='0.0']").addClass('hide');
... as a one-liner.

Stripping useless elements from the DOM

I want to strip all elements within the DOM that contains useless spaces or are empty (I'm working with an outdated back-end system that generates some messy HTML and these elements can sometimes add unwanted spaces in the design). Using the filter function I'm able to test for specific cases but for some reason it doesn't seem to work with empty elements so I tried to test to see if any elements length is < 0 and then remove it. Why doesn't this work, and is there a way to do it with the filter function? I've tried
("<br>", "", " ");
but it doesn't seem to work.
stripEmpties();
function stripEmpties() {
var domChildren = $("*").children();
if (domChildren.length <= 0) {
$(this).remove();
} else {
domChildren.filter(function () {
return $.trim(this.innerHTML) === ("<br>", " ");
}).remove();
}
}
Actually after playing with it I now see that only the last match is even used in the filter so it looks like I can't use this list format I've come up with. So I guess another question would be is there a way to get similar functionality while checking multiple cases with the filter function?
It's not as easy as it sounds and it certainly is not efficient if the document is large. For example this will remove all br, img (tags with no innerHtml) etc tags aswell:
$(function () {
$("*").filter(function () {
return ($(this).text().trim() === "");
}).remove();
});
So maybe you should use a better selector than "*", for example "div".

Select tags that starts with "x-" in jQuery

How can I select nodes that begin with a "x-" tag name, here is an hierarchy DOM tree example:
<div>
<x-tab>
<div></div>
<div>
<x-map></x-map>
</div>
</x-tab>
</div>
<x-footer></x-footer>
jQuery does not allow me to query $('x-*'), is there any way that I could achieve this?
The below is just working fine. Though I am not sure about performance as I am using regex.
$('body *').filter(function(){
return /^x-/i.test(this.nodeName);
}).each(function(){
console.log(this.nodeName);
});
Working fiddle
PS: In above sample, I am considering body tag as parent element.
UPDATE :
After checking Mohamed Meligy's post, It seems regex is faster than string manipulation in this condition. and It could become more faster (or same) if we use find. Something like this:
$('body').find('*').filter(function(){
return /^x-/i.test(this.nodeName);
}).each(function(){
console.log(this.nodeName);
});
jsperf test
UPDATE 2:
If you want to search in document then you can do the below which is fastest:
$(Array.prototype.slice.call(document.all)).filter(function () {
return /^x-/i.test(this.nodeName);
}).each(function(){
console.log(this.nodeName);
});
jsperf test
There is no native way to do this, it has worst performance, so, just do it yourself.
Example:
var results = $("div").find("*").filter(function(){
return /^x\-/i.test(this.nodeName);
});
Full example:
http://jsfiddle.net/6b8YY/3/
Notes: (Updated, see comments)
If you are wondering why I use this way for checking tag name, see:
JavaScript: case-insensitive search
and see comments as well.
Also, if you are wondering about the find method instead of adding to selector, since selectors are matched from right not from left, it may be better to separate the selector. I could also do this:
$("*", $("div")). Preferably though instead of just div add an ID or something to it so that parent match is quick.
In the comments you'll find a proof that it's not faster. This applies to very simple documents though I believe, where the cost of creating a jQuery object is higher than the cost of searching all DOM elements. In realistic page sizes though this will not be the case.
Update:
I also really like Teifi's answer. You can do it in one place and then reuse it everywhere. For example, let me mix my way with his:
// In some shared libraries location:
$.extend($.expr[':'], {
x : function(e) {
return /^x\-/i.test(this.nodeName);
}
});
// Then you can use it like:
$(function(){
// One way
var results = $("div").find(":x");
// But even nicer, you can mix with other selectors
// Say you want to get <a> tags directly inside x-* tags inside <section>
var anchors = $("section :x > a");
// Another example to show the power, say using a class name with it:
var highlightedResults = $(":x.highlight");
// Note I made the CSS class right most to be matched first for speed
});
It's the same performance hit, but more convenient API.
It might not be efficient, but consider it as a last option if you do not get any answer.
Try adding a custom attribute to these tags. What i mean is when you add a tag for eg. <x-tag>, add a custom attribute with it and assign it the same value as the tag, so the html looks like <x-tag CustAttr="x-tag">.
Now to get tags starting with x-, you can use the following jQuery code:
$("[CustAttr^=x-]")
and you will get all the tags that start with x-
custom jquery selector
jQuery(function($) {
$.extend($.expr[':'], {
X : function(e) {
return /^x-/i.test(e.tagName);
}
});
});
than, use $(":X") or $("*:X") to select your nodes.
Although this does not answer the question directly it could provide a solution, by "defining" the tags in the selector you can get all of that type?
$('x-tab, x-map, x-footer')
Workaround: if you want this thing more than once, it might be a lot more efficient to add a class based on the tag - which you only do once at the beginning, and then you filter for the tag the trivial way.
What I mean is,
function addTagMarks() {
// call when the document is ready, or when you have new tags
var prefix = "tag--"; // choose a prefix that avoids collision
var newbies = $("*").not("[class^='"+prefix+"']"); // skip what's done already
newbies.each(function() {
var tagName = $(this).prop("tagName").toLowerCase();
$(this).addClass(prefix + tagName);
});
}
After this, you can do a $("[class^='tag--x-']") or the same thing with querySelectorAll and it will be reasonably fast.
See if this works!
function getXNodes() {
var regex = /x-/, i = 0, totalnodes = [];
while (i !== document.all.length) {
if (regex.test(document.all[i].nodeName)) {
totalnodes.push(document.all[i]);
}
i++;
}
return totalnodes;
}
Demo Fiddle
var i=0;
for(i=0; i< document.all.length; i++){
if(document.all[i].nodeName.toLowerCase().indexOf('x-') !== -1){
$(document.all[i].nodeName.toLowerCase()).addClass('test');
}
}
Try this
var test = $('[x-]');
if(test)
alert('eureka!');
Basically jQuery selector works like CSS selector.
Read jQuery selector API here.

Regex for visible text, not HTML

If i had a string:
hey user, what are you doing?
How, with regex could I say: look for user, but not inside of < or > characters? So the match would grab the user between the <a></a> but not the one inside of the href
I'd like this to work for any tag, so it wont matter what tags.
== Update ==
Why i can't use .text() or innerText is because this is being used to highlight results much like the native cmd/ctrl+f functionality in browsers and I dont want to lose formatting. For example, if i search for strong here:
Some <strong>strong</strong> text.
If i use .text() itll return "Some strong text" and then I'll wrap strong with a <span> which has a class for styling, but now when I go back and try to insert this into the DOM it'll be missing the <strong> tags.
If you plan to replace the HTML using html() again then you will loose all event handlers that might be bound to inner elements and their data (as I said in my comment).
Whenever you set the content of an element as HTML string, you are creating new elements.
It might be better to recursively apply this function to every text node only. Something like:
$.fn.highlight = function(word) {
var pattern = new RegExp(word, 'g'),
repl = '<span class="high">' + word + '</span>';
this.each(function() {
$(this).contents().each(function() {
if(this.nodeType === 3 && pattern.test(this.nodeValue)) {
$(this).replaceWith(this.nodeValue.replace(pattern, repl));
}
else if(!$(this).hasClass('high')) {
$(this).highlight(word);
}
});
});
return this;
};
DEMO
It could very well be that this is not very efficient though.
To emulate Ctrl-F (which I assume is what you're doing), you can use window.find for Firefox, Chrome, and Safari and TextRange.findText for IE.
You should use a feature detect to choose which method you use:
function highlightText(str) {
if (window.find)
window.find(str);
else if (window.TextRange && window.TextRange.prototype.findText) {
var bodyRange = document.body.createTextRange();
bodyRange.findText(str);
bodyRange.select();
}
}
Then, after you the text is selected, you can style the selection with CSS using the ::selection selector.
Edit: To search within a certain DOM object, you could use a roundabout method: use window.find and see whether the selection is in a certain element. (Perhaps say s = window.getSelection().anchorNode and compare s.parentNode == obj, s.parentNode.parentNode == obj, etc.). If it's not in the correct element, repeat the process. IE is a lot easier: instead of document.body.createTextRange(), you can use obj.createTextRange().
$("body > *").each(function (index, element) {
var parts = $(element).text().split("needle");
if (parts.length > 1)
$(element).html(parts.join('<span class="highlight">needle</span>'));
});
jsbin demo
at this point it's evolving to be more and more like Felix's, so I think he's got the winner
original:
If you're doing this in javascript, you already have a handy parsed version of the web page in the DOM.
// gives "user"
alert(document.getElementById('user').innerHTML);
or with jQuery you can do lots of nice shortcuts:
alert($('#user').html()); // same as above
$("a").each(function (index, element) {
alert(element.innerHTML); // shows label text of every link in page
});
I like regexes, but because tags can be nested, you will have to use a parser. I recommend http://simplehtmldom.sourceforge.net/ it is really powerful and easy to use. If you have wellformed xhtml you can also use SimpleXML from php.
edit: Didn't see the javascript tag.
Try this:
/[(<.+>)(^<)]*user[(^>)(<.*>)]/
It means:
Before the keyword, you can have as many <...> or non-<.
Samewise after it.
EDIT:
The correct one would be:
/((<.+>)|(^<))*user((^>)|(<.*>))*/
Here is what works, I tried it on your JS Bin:
var s = 'hey user, what are you doing?';
s = s.replace(/(<[^>]*)user([^<]>)/g,'$1NEVER_WRITE_THAT_ANYWHERE_ELSE$2');
s = s.replace(/user/g,'Mr Smith');
s = s.replace(/NEVER_WRITE_THAT_ANYWHERE_ELSE/g,'user');
document.body.innerHTML = s;
It may be a tiny little bit complicated, but it works!
Explanation:
You replace "user" that is in the tag (which is easy to find) with a random string of your choice that you must never use again... ever. A good use would be to replace it with its hashcode (md5, sha-1, ...)
Replace every remaining occurence of "user" with the text you want.
Replace back your unique string with "user".
this code will strip all tags from sting
var s = 'hey user, what are you doing?';
s = s.replace(/<[^<>]+>/g,'');

How can I inject a string of HTML into an element?

Using Mootools, we can inject an element into another element:
$('childID').inject($('parentID'), 'top');
The second parameter allows me to control the location and can either be 'top' or 'bottom' to inject it into a parent object or 'before' or 'after' to inject it as a sibling.
We can also set the HTML of an element from a string:
var foo = "<p>Some text</p>";
$('parentID').set('html', foo);
My problem is that I want to have the same flexibility with strings as I do with elements. I can't, for example, put a string at the top of an element using set() as this overwrites the HTML rather than appending it at a specific location. Similarly, I can't append HTML after or before a sibling element.
Is there a function that will allow me to inject strings in the same way as I inject elements?
Insert at bottom:
foo.innerHTML = foo.innerHTML + 'string';
Insert at top:
foo.innerHTML = 'string' + foo.innerHTML;
Best Solution
The inject method will look like this:
inject: function(element, location) {
var el = Elements.from(this);
if($type(el) === 'array') var el = el.reverse();
return el.inject(element, location);
}
Let's break this into parts.
1) Elements.from(this) will take whatever the method is applied to and convert it into elements:
var foo = "<p>Some text</p>";
var el = Elements.from(foo);
//el is equal to a p element.
var bar = "<div>First div</div><div>Second div</div>";
var el = Elements.from(bar);
//el is equal to an array containing 2 div elements
2) if($type(el) === 'array') checks if el is an array. If it is then it applies .reverse() to el. This is necessary to inject the elements in the correct order. Otherwise they would inject with, for example, the second div first and the first div second. Obviously if el is just a single element, we don't need to change its order.
3) Finally, we just use the original inject method to inject el into the element specified in the element parameter to the location specified in the location parameter. If el is an array of elements, they will all get injected just fine.
To be able to use this function, we have to add it as a method on string objects. To do this you have to use implement():
String.implement({
inject: function(element, location) {
var el = Elements.from(this);
if($type(el) === 'array') var el = el.reverse();
return el.inject(element, location);
}
});
This will allow you to use the inject function on any variable containing a string. Make sure you don't put this inside the domready event i.e. Before window.addEvent('domready', function() { ... });
Now the inject function itself will look like this:
var foo = "<p>Some text</p>";
foo.inject($('parentID'), 'top');
This will create the p element and inject it at the top of parentID.
Alternative Solution
If you just wish to use inject with the 'top' and 'bottom' locations, you can use this inject method instead:
inject: function(element, location) {
var html = element.get('html')
if(location === 'top') return element.set('html', this + html);
else if (location === 'bottom') return element.set('html', html + this);
}
This method will get the innerHTML of the element you need to convert and either concatenate the string with that HTML or the HTML with that string, placing the string at the top or the bottom of the element respectively. The element's innerHTML is then set to this value.
The advantage of this method is that as long as the innerHTML of the element isn't too great, this is likely to be faster as we don't need to create new elements which could be time-consuming if the string contains many top-level sibling elements. Obviously if this situation were reversed (few top-level siblings and small innerHTML), the speed advantage would also be reversed (I haven't tested the speed difference so this is just an educated guess and might be negligible).
The disadvantage, however, is that we can't easily use it with the 'after' and 'before' locations.
You're looking for appendText. Example similar to the Mootools docs:
http://mootools.net/docs/core/Element/Element#Element:appendText
HTML
<div id="myElement">partner.</div>
JavaScript
$('myElement').appendText('Howdy, ', 'top');
The second (where) argument defaults to 'bottom' but also accepts 'top', 'bottom', 'before' and 'after'.
Resulting HTML
<div id="myElement">Howdy, partner.</div>
Working example:
http://jsfiddle.net/hq5Gr/
Try this:
var foo = "<p>Some text</p>"
$('parentID').set('html', foo + $('parentID').get('html')); // prepend/top
$('parentID').set('html', $('parentID').get('html') + foo)); // append/bottom
couple of things you ought to look at that may help.
First off, slick and mootools 1.3 offer a "nicer" new Element constructor which can add and configure elements from pseudo string markup very nicely:
http://www.jsfiddle.net/dimitar/aQvpb/
new Element('div#myId.myClass.myOtherClass[title=Mouseover Title][text=Dimitar Was Here]').injectAfter(document.id("foo"));
new Element("input#someID.someClass1.someClass2[disabled=true]");
second of all, element.injectAfter(previousEl) and element.injectBefore(followingEl) can also be helpful in injecting somewhere after or before a particular node.
totally do NOT append html by rewriting old html or any events the elements have that are not delegated will be gone (new UIDs)
and you can use Slick with older versions of mootools as well although I can't find the gist for that atm, post here if you're interested. the currently nightly is fairly stable but 1.3 release is due shortly.
you can use insertAdjacentHTML()
const divInString = '<div class="todo">Stuff</div>'
const parentOfDiv = document.querySelector(".parent")
parentOfDiv.insertAdjacentHTML("beforeEnd",divInString)
// "afterEnd , "beforeBegin","afterBegin"
// I was looking for a solution for this problem as well and
// this is what solved my issue.
You want to use text nodes.
To append text to an element:
var yourTextNode = element.appendChild(document.createTextNode("some text"))
To prepend text to an element:
var yourTextNode = element.parentNode.insertBefore(document.createTextNode("some text"), element)
To change the value of the text node, you'd do yourTextNode.nodeValue = "new value here".
#shanebo's answer is close but appendText escapes HTML.
Try:
http://mootools.net/core/docs/1.5.1/Element/Element#Element:appendHTML
$('myElement').appendHTML('<div>Hello world</div>', 'top');
Fiddle: http://jsfiddle.net/m0ez1t50/1/

Categories