ID Ends With in pure Javascript - javascript

I am working in a Javascript library that brings in jQuery for one thing: an "ends with" selector. It looks like this:
$('[id$=foo]')
It will find the elements in which the id ends with "foo".
I am looking to do this without jQuery (straight JavaScript). How might you go about this? I'd also like it to be as efficient as reasonably possible.

Use querySelectorAll, not available in all browsers (like IE 5/6/7/8) though. It basically works like jQuery:
http://jsfiddle.net/BBaFa/2/
console.log(document.querySelectorAll("[id$=foo]"));

You will need to iterate over all elements on the page and then use string functions to test it. The only optimizations I can think of is changing the starting point - i.e. not document.body but some other element where you know your element will be a child of - or you could use document.getElementsByTagName() to get an element list if you know the tag name of the elements.
However, your task would be much easier if you could use some 3rd-party-javascript, e.g. Sizzle (4k minified, the same selector engine jQuery uses).

So, using everything that was said, I put together this code. Assuming my elements are all inputs, then the following code is probably the best I am going to get?
String.prototype.endsWith = function(suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
function getInputsThatEndWith(text) {
var result = new Array();
var inputs = document.getElementsByTagName("input");
for(var i=0; i < inputs.length; i++) {
if(inputs[i].id.endsWith(text))
result.push(inputs[i]);
}
return result;
}
I put it on JsFiddle: http://jsfiddle.net/MF29n/1/

#ThiefMaster touched on how you can do the check, but here's the actual code:
function idEndsWith(str)
{
if (document.querySelectorAll)
{
return document.querySelectorAll('[id$="'+str+'"]');
}
else
{
var all,
elements = [],
i,
len,
regex;
all = document.getElementsByTagName('*');
len = all.length;
regex = new RegExp(str+'$');
for (i = 0; i < len; i++)
{
if (regex.test(all[i].id))
{
elements.push(all[i]);
}
}
return elements;
}
}
This can be enhanced in a number of ways. It currently iterates through the entire dom, but would be more efficient if it had a context:
function idEndsWith(str, context)
{
if (!context)
{
context = document;
}
...CODE... //replace all occurrences of "document" with "context"
}
There is no validation/escaping on the str variable in this function, the assumption is that it'll only receive a string of chars.

Suggested changes to your answer:
RegExp.quote = function(str) {
return str.replace(/([.?*+^$[\]\\(){}-])/g, "\\$1");
}; // from https://stackoverflow.com/questions/494035/#494122
String.prototype.endsWith = function(suffix) {
return !!this.match(new RegExp(RegExp.quote(suffix) + '$'));
};
function getInputsThatEndWith(text) {
var results = [],
inputs = document.getElementsByTagName("input"),
numInputs = inputs.length,
input;
for(var i=0; i < numInputs; i++) {
var input = inputs[i];
if(input.id.endsWith(text)) results.push(input);
}
return results;
}
http://jsfiddle.net/mattball/yJjDV/
Implementing String.endsWith using a regex instead of indexOf() is mostly a matter of preference, but I figured it was worth including for variety. If you aren't concerned about escaping special characters in the suffix, you can remove the RegExp.quote() bit, and just use
new RegExp(suffix + '$').

If you know the type of DOM elements you are targeting,
then get a list of references to them using getElementsByTagName , and then iterate over them.
You can use this optimization to fasten the iterations:
ignore the elements not having id.
target the nearest known parent of elements you want to seek, lets say your element is inside a div with id='myContainer', then you can get a restricted subset using
document.getElementById('myContainer').getElementsByTagName('*') , and then iterate over them.

Related

Create a function to get selectors like jquery does with pure javascript

I am tired of including jquery in simple projects but I am so used to using it and I am trying to break free form my dependency on it. I am trying to create a function that will give the same feel of getting selectors like classes and tags. Example: $('selector').innerHTML = ".something";. I have just been looping through them one by one like so:
var classElements = document.querySelectorAll('.something');
for (var i = classElements.length - 1; i >= 0; i--) {
classElements[i].innerHTML = "This Is A Div";
}
But I wanted to create a function where I could just loop through a selector without having to write out a for loop for everything that I want to find. So I could just write it our like above $('.something').innerHTML = "something";
So far this is what I have but it will only get the first of each selector and won't get all of them. Needless to say I am very stuck and the more I read on the subject the more confused I get. I was wondering if someone could point me in the right direction where my thinking is flawed or explain how jquery goes about doing this. Here is my code:
window.getElements = function(selector) {
var selectors = document.querySelectorAll(selector);
for (var i = selectors.length - 1; i >= 0; i--) {
var elements = selectors[i];
}
return elements;
};
getElements(".something").innerHTML = "something";
Here is a fiddle Fiddle
Here is how you would do it. I have done what you have asked which is allow you to use all the native functionality rather than coin wrappers around it. jQuery returns its own api which acts on the selectors. What I have done is create a selector which allows you to act on each element it finds
window.getElements = function(selector,cb) {
var selectors = document.querySelectorAll(selector);
[].forEach.call(selectors, cb);
};
getElements(".something", function(el){el.innerHTML = "ha"});
getElements("#one", function(el){el.style.background = "red" });
It takes the dom list that is found, converts it into an array and then calls your passed function where you pass your native code
Here is a fiddle
https://jsfiddle.net/y52f4wh8/5/
Jquery works differently:
window.jquery = function(selector,cb) {
var selectors = document.querySelectorAll(selector);
function command(cb) {
[].forEach.call(selectors, cb);
};
// Here we return our own api that uses the command function to act on
// our selected list.
return {
html: function(str){
command(function(el){
el.innerHTML=str;
});
},
bg: function(color){
command(function(el){
el.style.background = color;
});
}
}
};
// The jquery way!
jquery(".something").html("ha");
getElements(".something").innerHTML = "something";
this does not work because that your function gets and returns all of the elements, which is a NodeList, but it does not automatically apply methods to every element in the collection as jQuery does. In order to do that, you would have to convert the elements to an array and actually call the function on each element using a loop or some other function.
EDIT: To be clear, you cannot just call NodeList.innerHTML = '' on a NodeList because innerHTML is applied to one element. jQuery internally takes care of BOTH of the collecting of elements, and the applying of methods for you.
EDIT #2: After examining your function more carefully, I have realized there are other issues, but what I wrote above is still the basis from which you want to spring.
You could use something like this for getting elements?:
function getElements(elements) {
return [...querySelectorAll(elements)]
}
But applying functions on nodes is going to be more selective on a case-by-case basis since many of them are applied differently.
The selector part of JQuery is called Sizzle. It has all the functionality that you need but does not come with the other parts of JQuery.
If you would like to find out more about the javascript behind it, I recommend to take a look at the sourcefiles of Sizzle.
jQuery is essentially a wrapper object for NodeList which adds more functionality to DOM operations. If you want to create your own wrapper object which defines functions for bulk versions of all the Element API, you are free to do so, but then you might as well use jQuery or some derivative.
If you want an extremely lightweight proxy object for doing bulk operations on DOM elements, there is the Proxy object in ES6 which can make this very easy to do, but has no IE support.
const $ = function(selector) {
const nodeList = document.querySelectorAll(selector);
return new Proxy(nodeList, {
set: function(target, property, value) {
for (let i = 0; i < target.length; i++) {
target[i][property] = value;
}
},
get: function(target, property) {
return target[0] && target[0][property];
}
});
};
console.log($('p').innerHTML);
$('p').innerHTML = 'Bulk assignement!';
<p>A B C</p>
<p>1 2 3</p>
<p>Do Re Mi</p>
An approach which would be best avoided is to define a setter for innerHTML on NodeList.
Object.defineProperty(NodeList.prototype, 'innerHTML', {
set(text) {
[...this].forEach(elt => elt.innerHTML = text);
}
});
const $ = selector => document.querySelectorAll(selector);
$('.foo').innerHTML = "it works";
<div class="foo"></div>
<div class="foo"></div>

Replacing content of html document

I am trying to replace a word in an html document with selected word using javascript.
JavaScript
var node=document.body;
var childs=node.childNodes;
var n=childs.length,i=0;
while (i < n) {
node=childs[i];
if (node.nodeType == 3) {
if (node.textContent) {
node.nodeValue=node.nodeValue.replace("injected","hai");
}
}
i++;
}
but string is not getting replaced...pls help
Add document.body=node; at the end. When you set node to equal body you are copying the value, not editing it by reference.
I'm not sure why you're trying to work with the text node directly. console.log on nodeValue shows that the textContent of displayed tags is neither retrieved nor set in your code.
This works great. Live demo here (click).
<p>something to be replaced.</p>
and the js:
var childs = document.body.childNodes;
var len = childs.length;
for (var i=0; i<len; ++i) {
var node=childs[i];
if (node.nodeName === 'P') {
node.textContent = node.textContent.replace("to be replaced","was replaced");
}
}
There is a much simpler method using the String replace method. For example, you can convert the body of the page into a string and use regular expressions to replace the word. This means that you can avoid having to traverse the entire DOM and node lists, which is unnecessarily slow for your task.
document.getElementByTagName("body")[0].innerHTML.replace("injected","hai")

How can I loop through ALL DOM elements on a page?

I'm trying to loop over ALL elements on a page, so I want to check every element that exists on this page for a special class.
So, how do I say that I want to check EVERY element?
You can pass a * to getElementsByTagName() so that it will return all elements in a page:
var all = document.getElementsByTagName("*");
for (var i=0, max=all.length; i < max; i++) {
// Do something with the element here
}
Note that you could use querySelectorAll(), if it's available (IE9+, CSS in IE8), to just find elements with a particular class.
if (document.querySelectorAll)
var clsElements = document.querySelectorAll(".mySpeshalClass");
else
// loop through all elements instead
This would certainly speed up matters for modern browsers.
Browsers now support foreach on NodeList. This means you can directly loop the elements instead of writing your own for loop.
document.querySelectorAll('*').forEach(function(node) {
// Do whatever you want with the node object.
});
Performance note - Do your best to scope what you're looking for by using a specific selector. A universal selector can return lots of nodes depending on the complexity of the page. Also, consider using document.body.querySelectorAll instead of document.querySelectorAll when you don’t care about <head> children.
Was looking for same. Well, not exactly. I only wanted to list all DOM Nodes.
var currentNode,
ni = document.createNodeIterator(document.documentElement, NodeFilter.SHOW_ELEMENT);
while(currentNode = ni.nextNode()) {
console.log(currentNode.nodeName);
}
To get elements with a specific class, we can use filter function.
var currentNode,
ni = document.createNodeIterator(
document.documentElement,
NodeFilter.SHOW_ELEMENT,
function(node){
return node.classList.contains('toggleable') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
);
while(currentNode = ni.nextNode()) {
console.log(currentNode.nodeName);
}
Found solution on
MDN
As always the best solution is to use recursion:
loop(document);
function loop(node){
// do some thing with the node here
var nodes = node.childNodes;
for (var i = 0; i <nodes.length; i++){
if(!nodes[i]){
continue;
}
if(nodes[i].childNodes.length > 0){
loop(nodes[i]);
}
}
}
Unlike other suggestions, this solution does not require you to create an array for all the nodes, so its more light on the memory. More importantly, it finds more results. I am not sure what those results are, but when testing on chrome it finds about 50% more nodes compared to document.getElementsByTagName("*");
Here is another example on how you can loop through a document or an element:
function getNodeList(elem){
var l=new Array(elem),c=1,ret=new Array();
//This first loop will loop until the count var is stable//
for(var r=0;r<c;r++){
//This loop will loop thru the child element list//
for(var z=0;z<l[r].childNodes.length;z++){
//Push the element to the return array.
ret.push(l[r].childNodes[z]);
if(l[r].childNodes[z].childNodes[0]){
l.push(l[r].childNodes[z]);c++;
}//IF
}//FOR
}//FOR
return ret;
}
For those who are using Jquery
$("*").each(function(i,e){console.log(i+' '+e)});
Andy E. gave a good answer.
I would add, if you feel to select all the childs in some special selector (this need happened to me recently), you can apply the method "getElementsByTagName()" on any DOM object you want.
For an example, I needed to just parse "visual" part of the web page, so I just made this
var visualDomElts = document.body.getElementsByTagName('*');
This will never take in consideration the head part.
from this link
javascript reference
<html>
<head>
<title>A Simple Page</title>
<script language="JavaScript">
<!--
function findhead1()
{
var tag, tags;
// or you can use var allElem=document.all; and loop on it
tags = "The tags in the page are:"
for(i = 0; i < document.all.length; i++)
{
tag = document.all(i).tagName;
tags = tags + "\r" + tag;
}
document.write(tags);
}
// -->
</script>
</head>
<body onload="findhead1()">
<h1>Heading One</h1>
</body>
</html>
UPDATE:EDIT
since my last answer i found better simpler solution
function search(tableEvent)
{
clearResults()
document.getElementById('loading').style.display = 'block';
var params = 'formAction=SearchStocks';
var elemArray = document.mainForm.elements;
for (var i = 0; i < elemArray.length;i++)
{
var element = elemArray[i];
var elementName= element.name;
if(elementName=='formAction')
continue;
params += '&' + elementName+'='+ encodeURIComponent(element.value);
}
params += '&tableEvent=' + tableEvent;
createXmlHttpObject();
sendRequestPost(http_request,'Controller',false,params);
prepareUpdateTableContents();//function js to handle the response out of scope for this question
}
Getting all elements using var all = document.getElementsByTagName("*"); for (var i=0, max=all.length; i < max; i++); is ok if you need to check every element but will result in checking or looping repeating elements or text.
Below is a recursion implementation that checks or loop each element of all DOM elements only once and append:
(Credits to #George Reith for his recursion answer here: Map HTML to JSON)
function mapDOMCheck(html_string, json) {
treeObject = {}
dom = new jsdom.JSDOM(html_string) // use jsdom because DOMParser does not provide client-side Window for element access
document = dom.window.document
element = document.querySelector('html')
// Recurse and loop through DOM elements only once
function treeHTML(element, object) {
var nodeList = element.childNodes;
if (nodeList != null) {
if (nodeList.length) {
object[element.nodeName] = []; // IMPT: empty [] array for parent node to push non-text recursivable elements (see below)
for (var i = 0; i < nodeList.length; i++) {
console.log("nodeName", nodeList[i].nodeName);
if (nodeList[i].nodeType == 3) { // if child node is **final base-case** text node
console.log("nodeValue", nodeList[i].nodeValue);
} else { // else
object[element.nodeName].push({}); // push {} into empty [] array where {} for recursivable elements
treeHTML(nodeList[i], object[element.nodeName][object[element.nodeName].length - 1]);
}
}
}
}
}
treeHTML(element, treeObject);
}
Use *
var allElem = document.getElementsByTagName("*");
for (var i = 0; i < allElem.length; i++) {
// Do something with all element here
}
i think this is really quick
document.querySelectorAll('body,body *').forEach(function(e) {
You can try with
document.getElementsByClassName('special_class');

JavaScript - Efficiently find all elements containing one of a large set of strings

I have a set of strings and I need to find all all of the occurrences in an HTML document. Where the string occurs is important because I need to handle each case differently:
String is all or part of an attribute. e.g., the string is foo: <input value="foo"> -> Add class ATTR to the element.
String is the full text of an element. e.g., <button>foo</button> -> Add class TEXT to the element.
String is inline in the text of an element. e.g., <p>I love foo</p> -> Wrap the text in a span tag with class TEXT.
Also, I need to match the longest string first. e.g., if I have foo and foobar, then <p>I love foobar</p> should become <p>I love <span class="TEXT">foobar</span></p>, not <p>I love <span class="TEXT">foo</span>bar</p>.
The inline text is easy enough: Sort the strings descending by length and find and replace each in document.body.innerHTML with <span class="TEXT">$1</span>, although I'm not sure if that is the most efficient way to go.
For the attributes, I can do something like this:
sortedStrings.each(function(it) {
document.body.innerHTML.replace(new RegExp('(\S+?)="[^"]*'+escapeRegExChars(it)+'[^"]*"','g'),function(s,attr) {
$('[+attr+'*='+it+']').addClass('ATTR');
});
});
Again, that seems inefficient.
Lastly, for the full text elements, a depth first search of the document that compares the innerHTML to each string will work, but for a large number of strings, it seems very inefficient.
Any answer that offers performance improvements gets an upvote :)
EDIT: I went with a modification on Bob's answer. delim is an optional delimiter around the string (to differentiate it from normal text), and keys is the list of strings.
function dfs(iterator,scope) {
scope = scope || document.body;
$(scope).children().each(function() {
return dfs(iterator,this);
});
return iterator.call(scope);
}
var escapeChars = /['\/.*+?|()[\]{}\\]/g;
function safe(text) {
return text.replace(escapeChars, '\\$1');
}
function eachKey(iterator) {
var key, lit, i, len, exp;
for(i = 0, len = keys.length; i < len; i++) {
key = keys[i].trim();
lit = (delim + key + delim);
exp = new RegExp(delim + '(' + safe(key) + ')' + delim,'g');
iterator(key,lit,exp);
}
}
$(function() {
keys = keys.sort(function(a,b) {
return b.length - a.length;
});
dfs(function() {
var a, attr, html, val, el = $(this);
eachKey(function(key,lit,exp) {
// check attributes
for(a in el[0].attributes) {
attr = el[0].attributes[a].nodeName;
val = el.attr(attr);
if(exp.test(val)) {
el.addClass(attrClass);
el.attr(attr,val.replace(exp,"$1"));
}
}
// check all content
html = el.html().trim();
if(html === lit) {
el.addClass(theClass);
el.html(key); // remove delims
} else if(exp.test(html)) {
// check partial content
el.html(html.replace(exp,wrapper));
}
});
});
});
Under the assumption that the traversal is the most expensive operation, this seems optimal, although improvements are still welcome.
Trying to parse HTML with regex is a mug's game. It simply can't handle even the basic strucures of HTML, never mind the quirks. There's so much wrong with your snippet already. (Doesn't detect unquoted attributes; fails for a wide variety of punctuation in it due to lack of HTML-escaping, regex-escaping or CSS-escaping(*); failure for attributes with - in; strange non-use of replace...)
So, use the DOM. Yes, that'll mean a traversal. But then so does a selector like the [attr*=] you're using already.
var needle= 'foo';
$('*').each(function() {
var tag= this.tagName.toLowerCase();
if (tag==='script' || tag==='style' || tag==='textarea' || tag==='option') return;
// Find text in attribute values
//
for (var attri= this.attributes.length; attri-->0;)
if (this.attributes[attri].value.indexOf(needle)!==-1)
$(this).addClass('ATTR');
// Find text in child text nodes
//
for (var childi= this.childNodes.length; childi-->0;) {
var child= this.childNodes[childi];
if (child.nodeType!=3) continue;
// Sole text content of parent: add class directly to parent
//
if (child.data==needle && element.childNodes.length===1) {
$(this).addClass('TEXT');
break;
}
// Else find index of each occurence in text, and wrap each in span
//
var parts= child.data.split(needle);
for (var parti= parts.length; parti-->1;) {
var span= document.createElement('span');
span.className= 'TEXT';
var ix= child.data.length-parts[parti].length;
var trail= child.splitText(ix);
span.appendChild(child.splitText(ix-needle.length));
this.insertBefore(span, trail);
}
}
});
(The reverse-loops are necessary as this is a destructive iteration of content.)
(*: escape doesn't do any of those things. It's more like URL-encoding, but it's not really that either. It's almost always the wrong thing; avoid.)
There is really no good way to do this. Your last requirement makes you have to traverse the entire dom.
for the first 2 requirements i would select all elements by tag name, and interate over them inserting the stuff as needed.
only performance improvement i can think of is to do this on the server side at all costs, this may even mean an extra post to have your faster server do the work, otherwise this can be really slow on say, IE6

JS: How do we work with classes in CSS?

How do we manipulate the class of DOM elements with javascript? Is there a getElementsByClassName function?
Standard way is
error_message.className = 'error-message';
But you'll find these functions can simplify things a lot:
function hasClass(ele,cls) {
return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
}
//chekcs if selected element has class "cls", works for elements with multiple classes
function addClass(ele,cls) {
if (!this.hasClass(ele,cls)) ele.className += " "+cls;
}
//adds new class to element
function removeClass(ele,cls) {
if (hasClass(ele,cls)) {
var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
ele.className=ele.className.replace(reg,' ');
}
}
//removes class from element
Usage in a stackoverflow greasemonkey script to show all questions on page, regardless if they're ignored or not:
var childNodes=document.getElementById("questions").childNodes; //array of all questions
for (var i=1; i<childNodes.length; i+=2) //iterates through all questions on page.
{
removeClass(childNodes[i],"tagged-ignored-hidden");
addClass(childNodes[i],"user_defined_class");
}
(Don't worry if the for loop looks weird in that it skips every other element; the specifics of Stackoverflow's DOM layout with extra nodes between questions aren't important here.)
As to document.getElementsByClassName, it returns an array of DOM elements with the specific class (as you would suspect). BUT:
Safari 3.1 has native
getElmentsByClassName support, and
upcoming Firefox 3 and Opera 9.5 will
have it too. It only leaves out,
you’ve guessed it, Internet Explorer.
source
You can change a class in plain-old JavaScript using something like:
document.getElementById('myElement').className = 'myClass';
Or, if you're using JQuery, you can just use the "Class" functions.
Addressing the added details to the question about 'getElementsByClassName' and your comment:
It would probably be safest (and easiest) to use your favourite JavaScript library for this.
JQuery example:
$(".myClassName").each(function() {
//do what you want with the current element $(this)
});
Hope that helps.
Many JavaScript implementations do have a getElementsByClassName method built in. But if they don’t, you can implement it for yourself:
if (typeof Element.prototype.getElementsByClassName == "undefined") {
Element.prototype.getElementsByClassName = function(className) {
var elems = document.getElementsByTagName("*"),
matches = [];
for (var i=0, n=elems.length; i<n; ++i) {
if (elems[i].hasAttribute("class")) {
var classNames = elems[i].getAttribute("class").split(/\s+/);
for (var j=0,m=classNames.length; j<m; ++j) {
if (classNames[j] == className) {
matches.push(elems[i]);
break;
}
}
}
}
return new NodeList(matches);
};
}

Categories