I would like to get all descendant text nodes of an element, as a jQuery collection. What is the best way to do that?
jQuery doesn't have a convenient function for this. You need to combine contents(), which will give just child nodes but includes text nodes, with find(), which gives all descendant elements but no text nodes. Here's what I've come up with:
var getTextNodesIn = function(el) {
return $(el).find(":not(iframe)").addBack().contents().filter(function() {
return this.nodeType == 3;
});
};
getTextNodesIn(el);
Note: If you're using jQuery 1.7 or earlier, the code above will not work. To fix this, replace addBack() with andSelf(). andSelf() is deprecated in favour of addBack() from 1.8 onwards.
This is somewhat inefficient compared to pure DOM methods and has to include an ugly workaround for jQuery's overloading of its contents() function (thanks to #rabidsnail in the comments for pointing that out), so here is non-jQuery solution using a simple recursive function. The includeWhitespaceNodes parameter controls whether or not whitespace text nodes are included in the output (in jQuery they are automatically filtered out).
Update: Fixed bug when includeWhitespaceNodes is falsy.
function getTextNodesIn(node, includeWhitespaceNodes) {
var textNodes = [], nonWhitespaceMatcher = /\S/;
function getTextNodes(node) {
if (node.nodeType == 3) {
if (includeWhitespaceNodes || nonWhitespaceMatcher.test(node.nodeValue)) {
textNodes.push(node);
}
} else {
for (var i = 0, len = node.childNodes.length; i < len; ++i) {
getTextNodes(node.childNodes[i]);
}
}
}
getTextNodes(node);
return textNodes;
}
getTextNodesIn(el);
Jauco posted a good solution in a comment, so I'm copying it here:
$(elem)
.contents()
.filter(function() {
return this.nodeType === 3; //Node.TEXT_NODE
});
$('body').find('*').contents().filter(function () { return this.nodeType === 3; });
jQuery.contents() can be used with jQuery.filter to find all child text nodes. With a little twist, you can find grandchildren text nodes as well. No recursion required:
$(function() {
var $textNodes = $("#test, #test *").contents().filter(function() {
return this.nodeType === Node.TEXT_NODE;
});
/*
* for testing
*/
$textNodes.each(function() {
console.log(this);
});
});
div { margin-left: 1em; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="test">
child text 1<br>
child text 2
<div>
grandchild text 1
<div>grand-grandchild text 1</div>
grandchild text 2
</div>
child text 3<br>
child text 4
</div>
jsFiddle
I was getting a lot of empty text nodes with the accepted filter function. If you're only interested in selecting text nodes that contain non-whitespace, try adding a nodeValue conditional to your filter function, like a simple $.trim(this.nodevalue) !== '':
$('element')
.contents()
.filter(function(){
return this.nodeType === 3 && $.trim(this.nodeValue) !== '';
});
http://jsfiddle.net/ptp6m97v/
Or to avoid strange situations where the content looks like whitespace, but is not (e.g. the soft hyphen character, newlines \n, tabs, etc.), you can try using a Regular Expression. For example, \S will match any non-whitespace characters:
$('element')
.contents()
.filter(function(){
return this.nodeType === 3 && /\S/.test(this.nodeValue);
});
If you can make the assumption that all children are either Element Nodes or Text Nodes, then this is one solution.
To get all child text nodes as a jquery collection:
$('selector').clone().children().remove().end().contents();
To get a copy of the original element with non-text children removed:
$('selector').clone().children().remove().end();
For some reason contents() didn't work for me, so if it didn't work for you, here's a solution I made, I created jQuery.fn.descendants with the option to include text nodes or not
Usage
Get all descendants including text nodes and element nodes
jQuery('body').descendants('all');
Get all descendants returning only text nodes
jQuery('body').descendants(true);
Get all descendants returning only element nodes
jQuery('body').descendants();
Coffeescript Original:
jQuery.fn.descendants = ( textNodes ) ->
# if textNodes is 'all' then textNodes and elementNodes are allowed
# if textNodes if true then only textNodes will be returned
# if textNodes is not provided as an argument then only element nodes
# will be returned
allowedTypes = if textNodes is 'all' then [1,3] else if textNodes then [3] else [1]
# nodes we find
nodes = []
dig = (node) ->
# loop through children
for child in node.childNodes
# push child to collection if has allowed type
nodes.push(child) if child.nodeType in allowedTypes
# dig through child if has children
dig child if child.childNodes.length
# loop and dig through nodes in the current
# jQuery object
dig node for node in this
# wrap with jQuery
return jQuery(nodes)
Drop In Javascript Version
var __indexOf=[].indexOf||function(e){for(var t=0,n=this.length;t<n;t++){if(t in this&&this[t]===e)return t}return-1}; /* indexOf polyfill ends here*/ jQuery.fn.descendants=function(e){var t,n,r,i,s,o;t=e==="all"?[1,3]:e?[3]:[1];i=[];n=function(e){var r,s,o,u,a,f;u=e.childNodes;f=[];for(s=0,o=u.length;s<o;s++){r=u[s];if(a=r.nodeType,__indexOf.call(t,a)>=0){i.push(r)}if(r.childNodes.length){f.push(n(r))}else{f.push(void 0)}}return f};for(s=0,o=this.length;s<o;s++){r=this[s];n(r)}return jQuery(i)}
Unminified Javascript version: http://pastebin.com/cX3jMfuD
This is cross browser, a small Array.indexOf polyfill is included in the code.
Can also be done like this:
var textContents = $(document.getElementById("ElementId").childNodes).filter(function(){
return this.nodeType == 3;
});
The above code filters the textNodes from direct children child nodes of a given element.
if you want to strip all tags, then try this
function:
String.prototype.stripTags=function(){
var rtag=/<.*?[^>]>/g;
return this.replace(rtag,'');
}
usage:
var newText=$('selector').html().stripTags();
For me, plain old .contents() appeared to work to return the text nodes, just have to be careful with your selectors so that you know they will be text nodes.
For example, this wrapped all the text content of the TDs in my table with pre tags and had no problems.
jQuery("#resultTable td").content().wrap("<pre/>")
I had the same problem and solved it with:
Code:
$.fn.nextNode = function(){
var contents = $(this).parent().contents();
return contents.get(contents.index(this)+1);
}
Usage:
$('#my_id').nextNode();
Is like next() but also returns the text nodes.
This gets the job done regardless of the tag names. Select your parent.
It gives an array of strings with no duplications for parents and their children.
$('parent')
.find(":not(iframe)")
.addBack()
.contents()
.filter(function() {return this.nodeType == 3;})
//.map((i,v) => $(v).text()) // uncomment if you want strings
Related
How can I find DIV with certain text? For example:
<div>
SomeText, text continues.
</div>
Trying to use something like this:
var text = document.querySelector('div[SomeText*]').innerTEXT;
alert(text);
But ofcourse it will not work. How can I do it?
OP's question is about plain JavaScript and not jQuery.
Although there are plenty of answers and I like #Pawan Nogariya answer, please check this alternative out.
You can use XPATH in JavaScript. More info on the MDN article here.
The document.evaluate() method evaluates an XPATH query/expression. So you can pass XPATH expressions there, traverse into the HTML document and locate the desired element.
In XPATH you can select an element, by the text node like the following, whch gets the div that has the following text node.
//div[text()="Hello World"]
To get an element that contains some text use the following:
//div[contains(., 'Hello')]
The contains() method in XPATH takes a node as first parameter and the text to search for as second parameter.
Check this plunk here, this is an example use of XPATH in JavaScript
Here is a code snippet:
var headings = document.evaluate("//h1[contains(., 'Hello')]", document, null, XPathResult.ANY_TYPE, null );
var thisHeading = headings.iterateNext();
console.log(thisHeading); // Prints the html element in console
console.log(thisHeading.textContent); // prints the text content in console
thisHeading.innerHTML += "<br />Modified contents";
As you can see, I can grab the HTML element and modify it as I like.
You could use this pretty simple solution:
Array.from(document.querySelectorAll('div'))
.find(el => el.textContent === 'SomeText, text continues.');
The Array.from will convert the NodeList to an array (there are multiple methods to do this like the spread operator or slice)
The result now being an array allows for using the Array.find method, you can then put in any predicate. You could also check the textContent with a regex or whatever you like.
Note that Array.from and Array.find are ES2015 features. Te be compatible with older browsers like IE10 without a transpiler:
Array.prototype.slice.call(document.querySelectorAll('div'))
.filter(function (el) {
return el.textContent === 'SomeText, text continues.'
})[0];
Since you have asked it in javascript so you can have something like this
function contains(selector, text) {
var elements = document.querySelectorAll(selector);
return Array.prototype.filter.call(elements, function(element){
return RegExp(text).test(element.textContent);
});
}
And then call it like this
contains('div', 'sometext'); // find "div" that contain "sometext"
contains('div', /^sometext/); // find "div" that start with "sometext"
contains('div', /sometext$/i); // find "div" that end with "sometext", case-insensitive
This solution does the following:
Uses the ES6 spread operator to convert the NodeList of all divs to an array.
Provides output if the div contains the query string, not just if it exactly equals the query string (which happens for some of the other answers). e.g. It should provide output not just for 'SomeText' but also for 'SomeText, text continues'.
Outputs the entire div contents, not just the query string. e.g. For 'SomeText, text continues' it should output that whole string, not just 'SomeText'.
Allows for multiple divs to contain the string, not just a single div.
[...document.querySelectorAll('div')] // get all the divs in an array
.map(div => div.innerHTML) // get their contents
.filter(txt => txt.includes('SomeText')) // keep only those containing the query
.forEach(txt => console.log(txt)); // output the entire contents of those
<div>SomeText, text continues.</div>
<div>Not in this div.</div>
<div>Here is more SomeText.</div>
Coming across this in 2021, I found using XPATH too complicated (need to learn something else) for something that should be rather simple.
Came up with this:
function querySelectorIncludesText (selector, text){
return Array.from(document.querySelectorAll(selector))
.find(el => el.textContent.includes(text));
}
Usage:
querySelectorIncludesText('button', 'Send')
Note that I decided to use includes and not a strict comparison, because that's what I really needed, feel free to adapt.
You might need those polyfills if you want to support all browsers:
/**
* String.prototype.includes() polyfill
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill
* #see https://vanillajstoolkit.com/polyfills/stringincludes/
*/
if (!String.prototype.includes) {
String.prototype.includes = function (search, start) {
'use strict';
if (search instanceof RegExp) {
throw TypeError('first argument must not be a RegExp');
}
if (start === undefined) {
start = 0;
}
return this.indexOf(search, start) !== -1;
};
}
You best see if you have a parent element of the div you are querying. If so get the parent element and perform an element.querySelectorAll("div"). Once you get the nodeList apply a filter on it over the innerText property. Assume that a parent element of the div that we are querying has an id of container. You can normally access container directly from the id but let's do it the proper way.
var conty = document.getElementById("container"),
divs = conty.querySelectorAll("div"),
myDiv = [...divs].filter(e => e.innerText == "SomeText");
So that's it.
If you don't want to use jquery or something like that then you can try this:
function findByText(rootElement, text){
var filter = {
acceptNode: function(node){
// look for nodes that are text_nodes and include the following string.
if(node.nodeType === document.TEXT_NODE && node.nodeValue.includes(text)){
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
}
}
var nodes = [];
var walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, filter, false);
while(walker.nextNode()){
//give me the element containing the node
nodes.push(walker.currentNode.parentNode);
}
return nodes;
}
//call it like
var nodes = findByText(document.body,'SomeText');
//then do what you will with nodes[];
for(var i = 0; i < nodes.length; i++){
//do something with nodes[i]
}
Once you have the nodes in an array that contain the text you can do something with them. Like alert each one or print to console. One caveat is that this may not necessarily grab divs per se, this will grab the parent of the textnode that has the text you are looking for.
Google has this as a top result for For those who need to find a node with certain text.
By way of update, a nodelist is now iterable in modern browsers without having to convert it to an array.
The solution can use forEach like so.
var elList = document.querySelectorAll(".some .selector");
elList.forEach(function(el) {
if (el.innerHTML.indexOf("needle") !== -1) {
// Do what you like with el
// The needle is case sensitive
}
});
This worked for me to do a find/replace text inside a nodelist when a normal selector could not choose just one node so I had to filter each node one by one to check it for the needle.
Use XPath and document.evaluate(), and make sure to use text() and not . for the contains() argument, or else you will have the entire HTML, or outermost div element matched.
var headings = document.evaluate("//h1[contains(text(), 'Hello')]", document, null, XPathResult.ANY_TYPE, null );
or ignore leading and trailing whitespace
var headings = document.evaluate("//h1[contains(normalize-space(text()), 'Hello')]", document, null, XPathResult.ANY_TYPE, null );
or match all tag types (div, h1, p, etc.)
var headings = document.evaluate("//*[contains(text(), 'Hello')]", document, null, XPathResult.ANY_TYPE, null );
Then iterate
let thisHeading;
while(thisHeading = headings.iterateNext()){
// thisHeading contains matched node
}
Here's the XPath approach but with a minimum of XPath jargon.
Regular selection based on element attribute values (for comparison):
// for matching <element class="foo bar baz">...</element> by 'bar'
var things = document.querySelectorAll('[class*="bar"]');
for (var i = 0; i < things.length; i++) {
things[i].style.outline = '1px solid red';
}
XPath selection based on text within element.
// for matching <element>foo bar baz</element> by 'bar'
var things = document.evaluate('//*[contains(text(),"bar")]',document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
for (var i = 0; i < things.snapshotLength; i++) {
things.snapshotItem(i).style.outline = '1px solid red';
}
And here's with case-insensitivity since text is more volatile:
// for matching <element>foo bar baz</element> by 'bar' case-insensitively
var things = document.evaluate('//*[contains(translate(text(),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"bar")]',document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
for (var i = 0; i < things.snapshotLength; i++) {
things.snapshotItem(i).style.outline = '1px solid red';
}
There are lots of great solutions here already. However, to provide a more streamlined solution and one more in keeping with the idea of a querySelector behavior and syntax, I opted for a solution that extends Object with a couple prototype functions. Both of these functions use regular expressions for matching text, however, a string can be provided as a loose search parameter.
Simply implement the following functions:
// find all elements with inner text matching a given regular expression
// args:
// selector: string query selector to use for identifying elements on which we
// should check innerText
// regex: A regular expression for matching innerText; if a string is provided,
// a case-insensitive search is performed for any element containing the string.
Object.prototype.queryInnerTextAll = function(selector, regex) {
if (typeof(regex) === 'string') regex = new RegExp(regex, 'i');
const elements = [...this.querySelectorAll(selector)];
const rtn = elements.filter((e)=>{
return e.innerText.match(regex);
});
return rtn.length === 0 ? null : rtn
}
// find the first element with inner text matching a given regular expression
// args:
// selector: string query selector to use for identifying elements on which we
// should check innerText
// regex: A regular expression for matching innerText; if a string is provided,
// a case-insensitive search is performed for any element containing the string.
Object.prototype.queryInnerText = function(selector, text){
return this.queryInnerTextAll(selector, text)[0];
}
With these functions implemented, you can now make calls as follows:
document.queryInnerTextAll('div.link', 'go');
This would find all divs containing the link class with the word go in the innerText (eg. Go Left or GO down or go right or It's Good)
document.queryInnerText('div.link', 'go');
This would work exactly as the example above except it would return only the first matching element.
document.queryInnerTextAll('a', /^Next$/);
Find all links with the exact text Next (case-sensitive). This will exclude links that contain the word Next along with other text.
document.queryInnerText('a', /next/i);
Find the first link that contains the word next, regardless of case (eg. Next Page or Go to next)
e = document.querySelector('#page');
e.queryInnerText('button', /Continue/);
This performs a search within a container element for a button containing the text, Continue (case-sensitive). (eg. Continue or Continue to Next but not continue)
I had similar problem.
Function that return all element which include text from arg.
This works for me:
function getElementsByText(document, str, tag = '*') {
return [...document.querySelectorAll(tag)]
.filter(
el => (el.text && el.text.includes(str))
|| (el.children.length === 0 && el.outerText && el.outerText.includes(str)))
}
Since there are no limits to the length of text in a data attribute, use data attributes! And then you can use regular css selectors to select your element(s) like the OP wants.
for (const element of document.querySelectorAll("*")) {
element.dataset.myInnerText = element.innerText;
}
document.querySelector("*[data-my-inner-text='Different text.']").style.color="blue";
<div>SomeText, text continues.</div>
<div>Different text.</div>
Ideally you do the data attribute setting part on document load and narrow down the querySelectorAll selector a bit for performance.
I was looking for a way to do something similar using a Regex, and decided to build something of my own that I wanted to share if others are looking for a similar solution.
function getElementsByTextContent(tag, regex) {
const results = Array.from(document.querySelectorAll(tag))
.reduce((acc, el) => {
if (el.textContent && el.textContent.match(regex) !== null) {
acc.push(el);
}
return acc;
}, []);
return results;
}
How do I find every word on a page beginning with http:// and wrap tags around it?
Can I use something like regex perhaps?
I disagree heavily that jQuery can be much use in finding a solution here. Granted you have to get down and dirty with some of the textNode element attributes but putting the DOM back together again after you split your matched node can be made a wee bit easier using the jQuery library.
The following code is documented inline to explain the action taken. I've written it as a jQuery plugin in case you just want to take this and move it around elsewhere. This way you can scope which elements you want to convert URLs for or you can simply use the $("body") selector.
(function($) {
$.fn.anchorTextUrls = function() {
// Test a text node's contents for URLs and split and rebuild it with an achor
var testAndTag = function(el) {
// Test for URLs along whitespace and punctuation boundaries (don't look too hard or you will be consumed)
var m = el.nodeValue.match(/(https?:\/\/.*?)[.!?;,]?(\s+|"|$)/);
// If we've found a valid URL, m[1] contains the URL
if (m) {
// Clone the text node to hold the "tail end" of the split node
var tail = $(el).clone()[0];
// Substring the nodeValue attribute of the text nodes based on the match boundaries
el.nodeValue = el.nodeValue.substring(0, el.nodeValue.indexOf(m[1]));
tail.nodeValue = tail.nodeValue.substring(tail.nodeValue.indexOf(m[1]) + m[1].length);
// Rebuild the DOM inserting the new anchor element between the split text nodes
$(el).after(tail).after($("<a></a>").attr("href", m[1]).html(m[1]));
// Recurse on the new tail node to check for more URLs
testAndTag(tail);
}
// Behave like a function
return false;
}
// For each element selected by jQuery
this.each(function() {
// Select all descendant nodes of the element and pick out only text nodes
var textNodes = $(this).add("*", this).contents().filter(function() {
return this.nodeType == 3
});
// Take action on each text node
$.each(textNodes, function(i, el) {
testAndTag(el);
});
});
}
}(jQuery));
$("body").anchorTextUrls(); //Sample call
Please keep in mind that given the way I wrote this to populate the textNodes array, the method will find ALL descendant text nodes, not just immediate children text nodes. If you want it to replace URLs only amongst the text within a specific selector, remove the .add("*", this) call that adds all the descendants of the selected element.
Here's a fiddle example.
This is one of those few things that jQuery doesn't directly help you with much. You basically have to walk through the DOM tree and examine the text nodes (nodeType === 3); if you find a text node containing the target text you want to wrap ("http://.....", whatever rules you want to apply), you then split the text node (using splitText) into three parts (the part before the string, the part that is the string, and the part following the string), then put the a element around the second of those.
That sounds a bit complicated, but it isn't really all that bad. It's just a recursive descent walker function (for working through the DOM), a regex match to find the things you want to replace, and then a couple of calls to splitText, createElement, insertBefore, appendChild.
Here's an example that searches for a fixed string; just add your regex matching for "http://":
walk(document.body, "foo");
function walk(node, targetString) {
var child;
switch (node.nodeType) {
case 1: // Element
for (child = node.firstChild;
child;
child = child.nextSibling) {
walk(child, targetString);
}
break;
case 3: // Text node
handleText(node, targetString);
break;
}
}
function handleText(node, targetString) {
var start, targetNode, followingNode, wrapper;
// Does the text contain our target string?
// (This would be a regex test in your http://... case)
start = node.nodeValue.indexOf(targetString);
if (start >= 0) {
// Split at the beginning of the match
targetNode = node.splitText(start);
// Split at the end of the match
followingNode = targetNode.splitText(targetString.length);
// Wrap the target in an element; in this case, we'll
// use a `span` with a class, but you'd use an `a`.
// First we create the wrapper and insert it in front
// of the target text.
wrapper = document.createElement('span');
wrapper.className = "wrapper";
targetNode.parentNode.insertBefore(wrapper, targetNode);
// Now we move the target text inside it
wrapper.appendChild(targetNode);
// Clean up any empty nodes (in case the target text
// was at the beginning or end of a text ndoe)
if (node.nodeValue.length == 0) {
node.parentNode.removeChild(node);
}
if (followingNode.nodeValue.length == 0) {
followingNode.parentNode.removeChild(followingNode);
}
}
}
Live example
Update: The above didn't handle it if there were multiple matches in the same text node (doh!). And oh what the heck, I did a regexp match — you will have to adjust the regexp, and probably do some post-processing on each match, because what's here is too simplistic. But it's a start:
// The regexp should have a capture group that
// will be the href. In our case below, we just
// make it the whole thing, but that's up to you.
// THIS REGEXP IS ALMOST CERTAINLY TOO SIMPLISTIC
// AND WILL NEED ADJUSTING (for instance: what if
// the link appears at the end of a sentence and
// it shouldn't include the ending puncutation?).
walk(document.body, /(http:\/\/[^ ]+)/i);
function walk(node, targetRe) {
var child;
switch (node.nodeType) {
case 1: // Element
for (child = node.firstChild;
child;
child = child.nextSibling) {
walk(child, targetRe);
}
break;
case 3: // Text node
handleText(node, targetRe);
break;
}
}
function handleText(node, targetRe) {
var match, targetNode, followingNode, wrapper;
// Does the text contain our target string?
// (This would be a regex test in your http://... case)
match = targetRe.exec(node.nodeValue);
if (match) {
// Split at the beginning of the match
targetNode = node.splitText(match.index);
// Split at the end of the match.
// match[0] is the full text that was matched.
followingNode = targetNode.splitText(match[0].length);
// Wrap the target in an `a` element.
// First we create the wrapper and insert it in front
// of the target text. We use the first capture group
// as the `href`.
wrapper = document.createElement('a');
wrapper.href = match[1];
targetNode.parentNode.insertBefore(wrapper, targetNode);
// Now we move the target text inside it
wrapper.appendChild(targetNode);
// Clean up any empty nodes (in case the target text
// was at the beginning or end of a text ndoe)
if (node.nodeValue.length == 0) {
node.parentNode.removeChild(node);
}
if (followingNode.nodeValue.length == 0) {
followingNode.parentNode.removeChild(followingNode);
}
// Continue with the next match in the node, if any
match = followingNode
? targetRe.exec(followingNode.nodeValue)
: null;
}
}
Live example
I am not practically but you can try it
$('a([href^="http://"])').each( function(){
//perform your task
})
I have some simple HTML which I need to strip simple formatting.
A nice house was found in <b>Toronto</b>.
I need to remove the bold, but leave the sentence intact.
How is this possible in jQuery?
$('b').contents().unwrap();
This selects all <b> elements, then uses .contents() to target the text content of the <b>, then .unwrap() to remove its parent <b> element.
For the greatest performance, always go native:
var b = document.getElementsByTagName('b');
while(b.length) {
var parent = b[ 0 ].parentNode;
while( b[ 0 ].firstChild ) {
parent.insertBefore( b[ 0 ].firstChild, b[ 0 ] );
}
parent.removeChild( b[ 0 ] );
}
This will be much faster than any jQuery solution provided here.
You can also use .replaceWith(), like this:
$("b").replaceWith(function() { return $(this).contents(); });
Or if you know it's just a string:
$("b").replaceWith(function() { return this.innerHTML; });
This can make a big difference if you're unwrapping a lot of elements since either approach above is significantly faster than the cost of .unwrap().
The simplest way to remove inner html elements and return only text would the JQuery .text() function.
Example:
var text = $('<p>A nice house was found in <b>Toronto</b></p>');
alert( text.html() );
//Outputs A nice house was found in <b>Toronto</b>
alert( text.text() );
////Outputs A nice house was found in Toronto
jsFiddle Demo
Behold, for the simplest answer is mind blowing:
outerHTML is supported down to Internet Explorer 4 !
Here is to do it with javascript even without jQuery
element.outerHTML = element.innerHTML
with jQuery
var element = $('b')[0];
element.outerHTML = element.innerHTML;
or without jQuery
var element = document.querySelector('b');
element.outerHTML = element.innerHTML
If you want it as a function:
function unwrap(selector) {
var nodelist = document.querySelectorAll(selector);
Array.prototype.forEach.call(nodelist, function(item,i){
item.outerHTML = item.innerHTML; // or item.innerText if you want to remove all inner html tags
})
}
unwrap('b')
This should work in all major browser including old IE.
If you get NoModificationAllowedError or DOMException, it means the element has no parent. Usually you get this when you're trying this answer by creating a new node from javascript console without putting it as other element's child. But don't worry and remember that any element in the document is at least has one parent (<html></html> element)
NOTE:
this rewrite the innerHTML, so if you have a variable referencing to the inner element, it will not point to the same element.
If you need to keep the reference of some of the inner elements in your coding, you can use the jQuery (top answer) which move the elements to the expected position without rewriting the element.
How about this?
$("b").insertAdjacentHTML("afterend",$("b").innerHTML);
$("b").parentNode.removeChild($("b"));
The first line copies the HTML contents of the b tag to the location directly after the b tag, and then the second line removes the b tag from the DOM, leaving only its copied contents.
I normally wrap this into a function to make it easier to use:
function removeElementTags(element) {
element.insertAdjacentHTML("afterend",element.innerHTML);
element.parentNode.removeChild(element);
}
All of the code is actually pure Javascript, the only JQuery being used is that to select the element to target (the b tag in the first example). The function is just pure JS :D
Also look at:
https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
// For MSIE:
el.removeNode(false);
// Old js, w/o loops, using DocumentFragment:
function replaceWithContents (el) {
if (el.parentElement) {
if (el.childNodes.length) {
var range = document.createRange();
range.selectNodeContents(el);
el.parentNode.replaceChild(range.extractContents(), el);
} else {
el.parentNode.removeChild(el);
}
}
}
// Modern es:
const replaceWithContents = (el) => {
el.replaceWith(...el.childNodes);
};
// or just:
el.replaceWith(...el.childNodes);
// Today (2018) destructuring assignment works a little slower
// Modern es, using DocumentFragment.
// It may be faster than using ...rest
const replaceWithContents = (el) => {
if (el.parentElement) {
if (el.childNodes.length) {
const range = document.createRange();
range.selectNodeContents(el);
el.replaceWith(range.extractContents());
} else {
el.remove();
}
}
};
Another native solution (in coffee):
el = document.getElementsByTagName 'b'
docFrag = document.createDocumentFragment()
docFrag.appendChild el.firstChild while el.childNodes.length
el.parentNode.replaceChild docFrag, el
I don't know if it's faster than user113716's solution, but it might be easier to understand for some.
I wanted a directive to highlight text in an element based on a search string.
Most of the available solutions use a filter instead of a directive and are used like this:
<div ng-html-bind-unsafe="This is the contents for this div | highlight:highlightText"></div>
Here is an example
I would rather use a directive than a filter because I don't like the idea of having to put the content of an element in the ng-html-bind attribute. I feel like an element's contents should be inside of it.
Anyway, I wrote I directive for this but was wondering if there is a better way to write it. I feel like it is not the most efficient of methods. Here is the fiddle. Notice that text within the <code> element is not highlighted. This is due to .contents() only returning the direct children and text nodes of the element. This behaviour is fine unless there is a very simple way to recurse through the contents of each child element.
Thanks in advance.
For traversing through the contents of each child elements, you can use recursion. Put the code for add highlighters and remove highlighterd into a function and call these functions for each child element.
.contents() returns a Jquery object. Convert it to an angular element if node.nodeType === 1 and call contents() on it again.
/*Function to add Highlighters*/
scope.addHighlight = function (elm, value) {
angular.forEach(elm.contents(), function (node) {
if (node.nodeType === 3 && scope.needle.test(node.nodeValue)) {
node = angular.element(node);
node.after(node[0].nodeValue.replace(scope.needle, '<span class="highlight">$1</span>')).remove();
} else if (node.nodeType === 1) {
node = angular.element(node);
if (node.contents().length > 0) scope.addHighlight(node, value);
}
});
}
/*Function to remove current Highlighters*/
scope.removeHighlight = function (elm, value) {
angular.forEach(elm.contents(), function (node) {
nodetype = node.nodeType;
node = angular.element(node);
if (node[0].nodeName === 'SPAN' && node.hasClass('highlight')) {
node.after(node.html()).remove();
elm[0].normalize();
}
if (node.children().length > 0 && nodetype === 1) scope.removeHighlight(node, value);
});
}
Here is the updated fiddle.
Could you please look at this jsFiddle example, and tell me why the number '11' is alerted rather than '5' (the number of <li> elements)?
From jsFiddle:
HTML
<ul id="list">
<li>milk</li>
<li>butter</li>
<li>eggs</li>
<li>orange juice</li>
<li>bananas</li>
</ul>
JavaScript
var list = document.getElementById('list');
var list_items = list.childNodes;
alert(list_items.length);
The childNodes, depending on the browser used, will return the text nodes, as well as the tags that are children of the parent node. So technically, the whitespace in between the <li> tags will also be counted among the childNodes.
To avoid processing them, you may check that nodeType != 3. Here is a list of node types.
var list = document.getElementById('list');
var list_items = list.childNodes;
var li_items = [];
for (var i=0; i<list_items.length; i++) {
console.log(list_items[i].nodeType);
// Add all the <li> nodes to an array, skip the text nodes
if (list_items[i].nodeType != 3) {
li_items.push(list_items[i]);
}
}
You have text nodes there.
You can skip them while iterating with...
for (var i = 0, length = list_items.length; i < length; i++) {
if (list_items[i].nodeType != 1) {
continue;
}
// Any code here that accesses list_items[i] will sure to be an element.
}
jsFiddle.
Alternatively, you could do it in a more functional way...
list_items = Array.prototype.filter.call(list_items, function(element) {
return element.nodeType == 1;
});
jsFiddle.
You must use convert it to a proper array to use the filter() method. childNodes property returns a NodeList object.
As others have pointed out, the childNode count inclues the text nodes, generated by the whitespace between the <li> elements.
<ul id="list"><li>milk</li><li>butter</li><li>eggs</li><li>orange juice</li><li>bananas</li></ul>
That will give you 5 childNodes because it omits the whitespace.
Text nodes are included in the child nodes count. To get the proper value, you'd need to strip out text nodes, or make sure they are not in your code. Any white space between code is considered a space and a text node, so your count is the total number of text nodes.
I cobbled together a solution for this that I like. (I got the idea from this blog post.)
1) First I get the number of child elements nodes by using:
nodeObject.childElementCount;
2) Then I wrote a function that will return any child element node by index number. I did this by using firstElementChild and nextElementSibling in a for loop.
function getElement(x, parentNode){
var item = parentNode.firstElementChild
for (i=0;i<x;i++){
item = item.nextElementSibling;
}
return item;
}
This returns the child element I need for anything I want to pull from it. It skips the problem with childNodes retuning all the different nodes that are not helpful when trying to parse just the elements. I am sure someone more experienced than me could clean this up. But I found this so helpful that I had to post it.
Use obj.children instead.
var list = document.getElementById('list');
var list_items = list.children;
alert(list_items.length);
The difference between this children and childNodes, is that childNodes contain all nodes, including text nodes and comment nodes, while children only contain element nodes.
from w3schools.