I am writing a UserScript that will remove elements from a page that contain a certain string.
If I understand jQuery's contains() function correctly, it seems like the correct tool for the job.
Unfortunately, since the page I'll be running the UserScript on does not use jQuery, I can't use :contains(). Any of you lovely people know what the native way to do this is?
http://codepen.io/coulbourne/pen/olerh
This should do in modern browsers:
function contains(selector, text) {
var elements = document.querySelectorAll(selector);
return [].filter.call(elements, function(element){
return RegExp(text).test(element.textContent);
});
}
Then use it like so:
contains('p', 'world'); // find "p" that contain "world"
contains('p', /^world/); // find "p" that start with "world"
contains('p', /world$/i); // find "p" that end with "world", case-insensitive
...
Super modern one-line approach with optional chaining operator
[...document.querySelectorAll('*')].filter(element => element.childNodes?.[0]?.nodeValue?.match('❤'));
And better way is to search in all child nodes
[...document.querySelectorAll("*")].filter(e => e.childNodes && [...e.childNodes].find(n => n.nodeValue?.match("❤")))
If you want to implement contains method exaclty as jQuery does, this is what you need to have
function contains(elem, text) {
return (elem.textContent || elem.innerText || getText(elem)).indexOf(text) > -1;
}
function getText(elem) {
var node,
ret = "",
i = 0,
nodeType = elem.nodeType;
if ( !nodeType ) {
// If no nodeType, this is expected to be an array
for ( ; (node = elem[i]); i++ ) {
// Do not traverse comment nodes
ret += getText( node );
}
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
// Use textContent for elements
// innerText usage removed for consistency of new lines (see #11153)
if ( typeof elem.textContent === "string" ) {
return elem.textContent;
} else {
// Traverse its children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
ret += getText( elem );
}
}
} else if ( nodeType === 3 || nodeType === 4 ) {
return elem.nodeValue;
}
// Do not include comment or processing instruction nodes
return ret;
};
SOURCE: Sizzle.js
The original question is from 2013
Here is an even older solution, and the fastest solution because the main workload is done by the Browser Engine NOT the JavaScript Engine
The TreeWalker API has been around for ages, IE9 was the last browser to implement it... in 2011
All those 'modern' and 'super-modern' querySelectorAll("*") need to process all nodes and do string comparisons on every node.
The TreeWalker API gives you only the #text Nodes, and then you do what you want with them.
You could also use the NodeIterator API, but TreeWalker is faster
function textNodesContaining(txt, root = document.body) {
let nodes = [],
node,
tree = document.createTreeWalker(
root,
4, // NodeFilter.SHOW_TEXT
{
node: node => RegExp(txt).test(node.data)
});
while (node = tree.nextNode()) { // only return accepted nodes
nodes.push(node);
}
return nodes;
}
Usage
textNodesContaining(/Overflow/);
textNodesContaining("Overflow").map(x=>console.log(x.parentNode.nodeName,x));
// get "Overflow" IN A parent
textNodesContaining("Overflow")
.filter(x=>x.parentNode.nodeName == 'A')
.map(x=>console.log(x));
// get "Overflow" IN A ancestor
textNodesContaining("Overflow")
.filter(x=>x.parentNode.closest('A'))
.map(x=>console.log(x.parentNode.closest('A')));
This is the modern approach
function get_nodes_containing_text(selector, text) {
const elements = [...document.querySelectorAll(selector)];
return elements.filter(
(element) =>
element.childNodes[0]
&& element.childNodes[0].nodeValue
&& RegExp(text, "u").test(element.childNodes[0].nodeValue.trim())
);
}
Well, jQuery comes equipped with a DOM traversing engine that operates a lot better than the one i'm about to show you, but it will do the trick.
var items = document.getElementsByTagName("*");
for (var i = 0; i < items.length; i++) {
if (items[i].innerHTML.indexOf("word") != -1) {
// Do your magic
}
}
Wrap it in a function if you will, but i would strongly recommend to use jQuery's implementation.
Related
I have a menu that expands and retracts on hover. The problem is the menu has many elements and to trigger my expand function I need to write something like below. My actual code includes more code and I was wondering if there would be a better way to do this.
var e = event.target
if(
e.parentNode.className.split(" ")[0] === "main-section" ||
e.parentNode.parentNode.className.split(" ")[0] === "main-section" ||
e.parentNode.parentNode.parentNode.className.split(" ")[0] === "main-section"){
//do somehtings}
In modern environments you can use the DOM's closest method:
if (e.closest(".main-section")) {
// One was found...
}
It looks at the current element to see if it matches the selector, then its parent element, then its parent, etc. to the root of the tree. It returns the element it finds, or null if it doesn't find one.
For slightly older environments, Element#closest can be polyfilled. Or if you don't like polyfilling, you can give yourself a utility function instead that uses closest if it exists, or uses matches if not:
function closest(el, selector) {
if (el.closest) {
return el.closest(selector);
}
var matches = el.matches || el.matchesSelector;
while (el) {
if (matches.call(el, selector)) {
return el;
}
el = el.parentNode;
}
return null;
}
...which you'd use like this:
if (closest(e, ".main-section")) {
// One was found...
}
Method closest() is not supported in some browsers, so I took this function for you from this answer
function findAncestor (el, sel) {
while ((el = el.parentElement) && !((el.matches || el.matchesSelector).call(el,sel)));
return el;
}
Use classList with a recursive function like so.
const start = document.getElementById("start");
function recursiveCheck(ele, className, limit = 3, current = 0){
return ele.classList.contains(className) ? true : current >= limit ? false : recursiveCheck(ele.parentNode, className, limit, current + 1);
}
console.log(
recursiveCheck(start, "test")
);
<div class="test">
<div>
<div id="start"><div>
</div>
</div>
Let's say I have the following element TEXT in HTML:
<div id="TEXT">
<p>First <strong>Line</strong></p>
<p>Seond <em>Line</em></p>
</div>
How should one extract the raw text from this element, without HTML tags, but preserving the line breaks?
I know about the following two options but neither of them seems to be perfect:
document.getElementById("TEXT").textContent
returns
First LineSecond Line
problem: ignores the line break that should be included between paragraphs
document.getElementById("TEXT").innerText
returns
First Line
Second Line
problem: is not part of W3C standard and is not guaranteed to work in all browsers
Here's a handy function for getting text contents of any element and it works well on all platforms, and yes, it preserves line breaks.
function text(e){
var t = "";
e = e.childNodes || e;
for(var i = 0;i<e.length;i++){
t+= e[i].nodeType !=1 ? e[i].nodeValue : text(e[i].childNodes);
}
return t;
}
You can check how jQuery does it. It uses sizzle js. Here is the function that you can use.
<div id="TEXT">
<p>First <strong>Line</strong></p>
<p>Seond <em>Line</em></p>
</div>
<script>
var getText = function( elem ) {
var node,
ret = "",
i = 0,
nodeType = elem.nodeType;
if ( !nodeType ) {
// If no nodeType, this is expected to be an array
while ( (node = elem[i++]) ) {
// Do not traverse comment nodes
ret += getText( node );
}
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
// Use textContent for elements
// innerText usage removed for consistency of new lines (jQuery #11153)
if ( typeof elem.textContent === "string" ) {
return elem.textContent;
} else {
// Traverse its children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
ret += getText( elem );
}
}
} else if ( nodeType === 3 || nodeType === 4 ) {
return elem.nodeValue;
}
// Do not include comment or processing instruction nodes
return ret;
};
console.log(getText(document.getElementById('TEXT')));
<script>
I have been experiencing an issue where DOM text nodes with certain characters behave strangely in IE when using the Node.normalize() function to concatenate adjacent text nodes.
I have created a Codepen example which allows you to reproduce the bug in IE11: http://codepen.io/anon/pen/BxoKH
Output in IE11: '- Example'
Output in Chrome and earlier versions of IE: 'Test - Example'
As you can see, this truncates everything prior to the minus symbol which is apparently treated as a delimiting character, apparently due to a bug in the native implementation of normalize() in Internet Explorer 11 (but not IE10, or IE8, or even IE6).
Can anyone explain why this happens, and does anyone know of other sequences of characters which cause this issue?
Edit - I have written a codepen that will test sections of Unicode characters to identify characters that cause this behavior. It appears to affect many more characters than I originally realized:
http://codepen.io/anon/pen/Bvgtb/
This tests Unicode characters from 32-1000 and prints those that fail the test (truncate data when nodes are normalized) You can modify it to test other ranges of characters, but be careful of increasing the range too much in IE or it will freeze.
I've created an IE bug report and Microsoft reports being able to reproduce it based on the code sample I provided. Vote on it if you're also experiencing this issue:
https://connect.microsoft.com/IE/feedback/details/832750/ie11-node-normalize-dom-implementation-truncates-data-when-adjacent-text-nodes-contain-a-minus-sign
The other answers here are somewhat verbose and incomplete — they do not walk the full DOM sub-tree. Here's a more comprehensive solution:
function normalize (node) {
if (!node) { return; }
if (node.nodeType == 3) {
while (node.nextSibling && node.nextSibling.nodeType == 3) {
node.nodeValue += node.nextSibling.nodeValue;
node.parentNode.removeChild(node.nextSibling);
}
} else {
normalize(node.firstChild);
}
normalize(node.nextSibling);
}
I created a workaround by simply reimplementing the normalize method in JS, but struggled with this for many hours, so I figured I'd make a SO post to help other folks out, and hopefully get more information to help satisfy my curiosity about this bug which wasted most of my day, haha.
Here is a codepen with my workaround which works in all browsers: http://codepen.io/anon/pen/ouFJa
My workaround was based on some useful normalize code I found here: https://stackoverflow.com/a/20440845/1504529 but has been tailored to this specific IE11 bug rather than the one discussed by that post:
Here's the workaround, which works in all browsers I've tested, including IE11
function isNormalizeBuggy(){
var testDiv = document.createElement('div');
testDiv.appendChild(document.createTextNode('0-'));
testDiv.appendChild(document.createTextNode('2'));
testDiv.normalize();
return testDiv.firstChild.length == 2;
}
function safeNormalize(DOMNode) {
// If the normalize function doesn't have the bug relating to minuses,
// we use the native normalize function. Otherwise we use our custom one.
if(!isNormalizeBuggy()){
el.normalize();
return;
}
function getNextNode(node, ancestor, isOpenTag) {
if (typeof isOpenTag === 'undefined') {
isOpenTag = true;
}
var next;
if (isOpenTag) {
next = node.firstChild;
}
next = next || node.nextSibling;
if (!next && node.parentNode && node.parentNode !== ancestor) {
return getNextNode(node.parentNode, ancestor, false);
}
return next;
}
var adjTextNodes = [], nodes, node = el;
while ((node = getNextNode(node, el))) {
if (node.nodeType === 3 && node.previousSibling && node.previousSibling.nodeType === 3) {
if (!nodes) {
nodes = [node.previousSibling];
}
nodes.push(node);
} else if (nodes) {
adjTextNodes.push(nodes);
nodes = null;
}
}
adjTextNodes.forEach(function (nodes) {
var first;
nodes.forEach(function (node, i) {
if (i > 0) {
first.nodeValue += node.nodeValue;
node.parentNode.removeChild(node);
} else {
first = node;
}
});
});
};
Not the exact answer, but helped in my case.
function safeNormalize(el) {
function recursiveNormalize(elem)
{
for (var i = 0; i < elem.childNodes.length; i++) {
if (elem.childNodes[i].nodeType != 3) {
recursiveNormalize(elem.childNodes[i]);
}
else {
if (elem.childNodes[i].nextSibling != null && elem.childNodes[i].nextSibling.nodeType == 3) {
elem.childNodes[i].nodeValue = elem.childNodes[i].nodeValue + elem.childNodes[i].nextSibling.nodeValue;
elem.removeChild(elem.childNodes[i].nextSibling);
i--;
}
}
}
}
recursiveNormalize(el);
}
The normalise code looks a little convoluted, the following is a bit simpler. It traverses the siblings of the node to be normalised, collecting the text nodes until it hits an element. Then it calls itself and collects that element's text nodes, and so on.
I think seprating the two functions makes for cleaner (and a lot less) code.
// textNode is a DOM text node
function collectTextNodes(textNode) {
// while there are text siblings, concatenate them into the first
while (textNode.nextSibling) {
var next = textNode.nextSibling;
if (next.nodeType == 3) {
textNode.nodeValue += next.nodeValue;
textNode.parentNode.removeChild(next);
// Stop if not a text node
} else {
return;
}
}
}
// element is a DOM element
function normalise(element) {
var node = element.firstChild;
// Traverse siblings, call normalise for elements and
// collectTextNodes for text nodes
while (node) {
if (node.nodeType == 1) {
normalise(node);
} else if (node.nodeType == 3) {
collectTextNodes(node);
}
node = node.nextSibling;
}
}
function mergeTextNode(elem) {
var node = elem.firstChild, text
while (node) {
var aaa = node.nextSibling
if (node.nodeType === 3) {
if (text) {
text.nodeValue += node.nodeValue
elem.removeChild(node)
} else {
text = node
}
} else {
text = null
}
node = aaa
}
}
Simple question, I have an element which I am grabbing via .getElementById (). How do I check if it has any children?
A couple of ways:
if (element.firstChild) {
// It has at least one
}
or the hasChildNodes() function:
if (element.hasChildNodes()) {
// It has at least one
}
or the length property of childNodes:
if (element.childNodes.length > 0) { // Or just `if (element.childNodes.length)`
// It has at least one
}
If you only want to know about child elements (as opposed to text nodes, attribute nodes, etc.) on all modern browsers (and IE8 — in fact, even IE6) you can do this: (thank you Florian!)
if (element.children.length > 0) { // Or just `if (element.children.length)`
// It has at least one element as a child
}
That relies on the children property, which wasn't defined in DOM1, DOM2, or DOM3, but which has near-universal support. (It works in IE6 and up and Chrome, Firefox, and Opera at least as far back as November 2012, when this was originally written.) If supporting older mobile devices, be sure to check for support.
If you don't need IE8 and earlier support, you can also do this:
if (element.firstElementChild) {
// It has at least one element as a child
}
That relies on firstElementChild. Like children, it wasn't defined in DOM1-3 either, but unlike children it wasn't added to IE until IE9. The same applies to childElementCount:
if (element.childElementCount !== 0) {
// It has at least one element as a child
}
If you want to stick to something defined in DOM1 (maybe you have to support really obscure browsers), you have to do more work:
var hasChildElements, child;
hasChildElements = false;
for (child = element.firstChild; child; child = child.nextSibling) {
if (child.nodeType == 1) { // 1 == Element
hasChildElements = true;
break;
}
}
All of that is part of DOM1, and nearly universally supported.
It would be easy to wrap this up in a function, e.g.:
function hasChildElement(elm) {
var child, rv;
if (elm.children) {
// Supports `children`
rv = elm.children.length !== 0;
} else {
// The hard way...
rv = false;
for (child = element.firstChild; !rv && child; child = child.nextSibling) {
if (child.nodeType == 1) { // 1 == Element
rv = true;
}
}
}
return rv;
}
As slashnick & bobince mention, hasChildNodes() will return true for whitespace (text nodes). However, I didn't want this behaviour, and this worked for me :)
element.getElementsByTagName('*').length > 0
Edit: for the same functionality, this is a better solution:
element.children.length > 0
children[] is a subset of childNodes[], containing elements only.
Compatibility
You could also do the following:
if (element.innerHTML.trim() !== '') {
// It has at least one
}
This uses the trim() method to treat empty elements which have only whitespaces (in which case hasChildNodes returns true) as being empty.
NB: The above method doesn't filter out comments. (so a comment would classify a a child)
To filter out comments as well, we could make use of the read-only Node.nodeType property where Node.COMMENT_NODE (A Comment node, such as <!-- … -->) has the constant value - 8
if (element.firstChild?.nodeType !== 8 && element.innerHTML.trim() !== '' {
// It has at least one
}
let divs = document.querySelectorAll('div');
for(element of divs) {
if (element.firstChild?.nodeType !== 8 && element.innerHTML.trim() !== '') {
console.log('has children')
} else { console.log('no children') }
}
<div><span>An element</span>
<div>some text</div>
<div> </div> <!-- whitespace -->
<div><!-- A comment --></div>
<div></div>
You can check if the element has child nodes element.hasChildNodes(). Beware in Mozilla this will return true if the is whitespace after the tag so you will need to verify the tag type.
https://developer.mozilla.org/En/DOM/Node.hasChildNodes
Try the childElementCount property:
if ( element.childElementCount !== 0 ){
alert('i have children');
} else {
alert('no kids here');
}
Late but document fragment could be a node:
function hasChild(el){
var child = el && el.firstChild;
while (child) {
if (child.nodeType === 1 || child.nodeType === 11) {
return true;
}
child = child.nextSibling;
}
return false;
}
// or
function hasChild(el){
for (var i = 0; el && el.childNodes[i]; i++) {
if (el.childNodes[i].nodeType === 1 || el.childNodes[i].nodeType === 11) {
return true;
}
}
return false;
}
See:
https://github.com/k-gun/so/blob/master/so.dom.js#L42
https://github.com/k-gun/so/blob/master/so.dom.js#L741
A reusable isEmpty( <selector> ) function.
You can also run it toward a collection of elements (see example)
const isEmpty = sel =>
![... document.querySelectorAll(sel)].some(el => el.innerHTML.trim() !== "");
console.log(
isEmpty("#one"), // false
isEmpty("#two"), // true
isEmpty(".foo"), // false
isEmpty(".bar") // true
);
<div id="one">
foo
</div>
<div id="two">
</div>
<div class="foo"></div>
<div class="foo"><p>foo</p></div>
<div class="foo"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
returns true (and exits loop) as soon one element has any kind of content beside spaces or newlines.
<script type="text/javascript">
function uwtPBSTree_NodeChecked(treeId, nodeId, bChecked)
{
//debugger;
var selectedNode = igtree_getNodeById(nodeId);
var ParentNodes = selectedNode.getChildNodes();
var length = ParentNodes.length;
if (bChecked)
{
/* if (length != 0) {
for (i = 0; i < length; i++) {
ParentNodes[i].setChecked(true);
}
}*/
}
else
{
if (length != 0)
{
for (i = 0; i < length; i++)
{
ParentNodes[i].setChecked(false);
}
}
}
}
</script>
<ignav:UltraWebTree ID="uwtPBSTree" runat="server"..........>
<ClientSideEvents NodeChecked="uwtPBSTree_NodeChecked"></ClientSideEvents>
</ignav:UltraWebTree>
Since the getElementsByTagName() function is new (DOM-1?) I wanted another more reliable method to get a reference to an element based on its tag name/id.
Edit- Without using a framework, since I need to cut down on size; so 10-20K for a framework is unacceptable. I just need the JS code that can fetch an element
getElementsByTagName is not new. It is supported since IE5, FF1 and Opera 7 according to w3schools
[edit]
Thanks for pointing this out. It was indeed supported since Opera 7.
As mentioned, getElementsByTagName is not new...
I think you're going to get about 10 references to jQuery.
Returns all the paragraph elements:
$('p').length
If 19kb is too big, and you just want to do element selection, something like sizzle works well, at about 4kb. The only thing I would note is that you're probably going to end up needing something that's in jQuery anyway.
http://sizzlejs.com/
Queries are very similar:
Sizzle("li");
19kb is a really small one-time price to pay for the power of jQuery.
If all you want to do is select elements, it may be smart to just use the sizzle selector engine and not a full blown library. I would go with the full library, but, going with a selector engine might be useful in limited circumstances.
Sizzle is the CSS selector engine that powers jQuery.
http://sizzlejs.com/
Or prototype, etc. You'll need to use one of these javascript glue libraries to achieve this. All of them will call this function if it exists, but fake it otherwise.
Here is an implementation based on the jQuery 1.12.4 implementation. It uses getElementsByTagName if available. If not, it uses querySelectorAll if available. If not, it falls back on recursively traversal. jQuery 1.12.4 supports older browsers, such as IE6, according to themselves.
function getElementsByTagName( node, tagName ) {
if (tagName == '*') {
tagName = undefined;
}
var merge = function( first, second ) {
var len = +second.length,
j = 0,
i = first.length;
while ( j < len ) {
first[ i++ ] = second[ j++ ];
}
// Support: IE<9
// Workaround casting of .length to NaN on otherwise arraylike objects (e.g., NodeLists)
if ( len !== len ) {
while ( second[ j ] !== undefined ) {
first[ i++ ] = second[ j++ ];
}
}
first.length = i;
return first;
},
nodeName = function( elem, name ) {
return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
},
elems, elem,
i = 0,
context = node,
tag = tagName,
found = typeof context.getElementsByTagName !== "undefined" ?
context.getElementsByTagName( tag || "*" ) :
typeof context.querySelectorAll !== "undefined" ?
context.querySelectorAll( tag || "*" ) :
undefined;
if ( !found ) {
for ( found = [], elems = context.childNodes || context;
( elem = elems[ i ] ) != null;
i++
) {
if ( !tag || nodeName( elem, tag ) ) {
found.push( elem );
} else {
merge( found, getElementsByTagName( elem, tag ) );
}
}
}
return found;
/* return tag === undefined || tag && nodeName( context, tag ) ?
merge( [ context ], found ) :
found;*/
}
I took the getAll() internal function of jQuery 1.12.4 and copied in the two helper functions it needs (jQuery.nodeName and jQuery.merge). I also made sure you can call it with "*" as tagName by adding a few lines in the top of the function. Finally, at the end of the function I commented out some functionality, which adds current node to result (if it matches), and simply returns the found nodes.
Be aware that the function in some cases returns an HTMLCollection, and in other circumstances returns an Array. Also beware that when "*" is passed as tagname, output differs depending on browser: The Element.prototype.getElementsByTagName does not return TextNodes, but the recursive traversal does.
Alternatively, you could use picoQuery. picoQuery is an implementation of jQuery, where you can select which methods you need in an online builder. in this case, you need no methods, as selection is part of core, and the build is only 1kb gzipped. picoQuery is written for modern browsers, but falls back to jQuery 1.12.4 for older browsers.