Generic function to find child elements without jQuery - javascript

I'm trying to write a generic set of functions so that I can check if children of a particular element, with a particular feature exist. Unfortunately, for this task I don't have jQuery available to me.
A function call example:
has_child_with_id(element, 'obj123');
I'm trying to keep it compatible with the maximum number of browsers, and I'm aiming to have some explicitly named functions to find by name, id, class etc.
I'm still new to the javascript module pattern, but would something like this be appropriate?:
var myfunctions = (function() {
var public_interface = {
// NAMED FUNCTION
has_child_with_id: function(el, n) {
return public_interface.has_child_with_('Id', el, n);
},
// GENERIC FUNCTION
has_child_with_: function(type, el, n) {
// Option 1 (querySelectorAll)
return typeof el['querySelectorAll'] === 'function' && el['querySelectorAll']('['+type+'="'+n+'"]').length > 0
// Option 2 (get a bunch of elements, doesn't work on forms)
|| typeof el['getElementsBy'+type] === 'function' && el['getElementsBy'+type](n).length > 0
// Option 3 (get a single element)
|| typeof el['getElementBy'+type] === 'function' && typeof el['getElementBy'+type](n) !== 'undefined'
// Option 4 (Manually loop through elements)
|| (function(children, n) {
for (var i=0;i<children.length;i++) {
if (children[i].hasOwnProperty(type) && children[i][type] == n)
return true;
}
})(el.getElementsByTagName('*', n));
}
};
return public_interface;
})();
alert(myfunctions.has_child_with_id(document, 'myid'));

document.querySelector actually fits the bill here nicely. It's IE8+ compatible (IE9 if you're using CSS3 selectors). Rather than searching the document, simply prefix with the element you want to search within.
Get the first element that matches a class, ID, or data attribute.
var elem = document.querySelector('#some-element');
var firstMatch = elem.querySelector('.sample-class');
Get all elements that match a class, ID, or data attribute.
var elem = document.querySelector('#some-element');
var allMatches = elem.querySelectorAll('.sample-class');
In this case, firstMatch and allMatches are the ones you're looking for.
You could also search by ID, data attribute, or another other valid CSS selector:
elem.querySelectorAll('[data-something]'); // Matches by data attribute
elem.querySelectorAll('input[type="checkbox"]'); // Returns all checkboxes
querySelectorAll will return an array of nodes that you can loop through.

Related

How can I make this function recursive/run until the requirements are not met?

As of now, I can only hardcode the same snippet after every if statement, which only needs to have the argument that getAdjacentCells(id) takes changed. I haven't been able to find a way to repeat this part. I think this can be done recursively, but I don't know how to do it.
Edit: I initially typed that isCellEmpty got an array of objects: [{topLeft: null}, {topCenter: "cell-1-2"}, {topRight: "cell-1-3"}, {middleLeft: null}, {middleRight: "cell-2-3"}], when in reality it is a single object: {topLeft: null, topCenter: "cell-1-2", topRight: "cell-1-3", middleLeft: null, middleRight: "cell-2-3"}
// Gets an object that looks like this: {topLeft: null, topCenter: "cell-1-2", topRight: "cell-1-3", middleLeft: null, middleRight: "cell-2-3"}
function isCellEmpty(adjacentCells) {
Object.values(adjacentCells).forEach(id => {
// Checks that the ids in stored in the object values do not equal null
if (id !== null) {
board[getBoardPosition(id)].opened = true;
// getAdjacentCells() will return either an array of objects similar to the one the function takes as an argument or an integer
// if getAdjacentCells(id) returns a number, add a div to the HTML element with that id
if (typeof (getAdjacentCells(id)) === "number") {
// Removes all other divs, this prevents repetition
$("#" + id).empty();
// Appends an empty div
$("#" + id).append("<div></div>");
// HERE'S WHERE IT STARTS: If getAdjacentCells(id) returns an object, do the same as above with every id in it
} else if (typeof (getAdjacentCells(id)) === "object") {
Object.values(getAdjacentCells(id)).forEach(id2 => {
if (id2 !== null) {
board[getBoardPosition(id2)].opened = true;
if (typeof (getAdjacentCells(id2)) === "number") {
$("#" + id2).empty();
$("#" + id2).append("<div></div>");
// HERE IT REPEATS:
} else if (typeof (getAdjacentCells(id2)) === "object") {
...
}
}
})
}
}
});
}
You can make the recursive call with the value you get from getAdjacentCells. Make sure however to call getAdjacentCells only once for the same id. Now it is quite inefficient as you repeat the same call.
See also some other suggestions in the code.
function isCellEmpty(adjacentCells) {
// I would move this check here, although not necessary if you prefer it in the loop.
if (typeof adjacentCells === "number") {
$("#" + id).empty().append("<div>"); // You can chain jQuery...
return;
}
for (let id of adjacentCells) { // Just use a for..of loop
if (id === null) continue; // keep IF-ELSE nesting flat.
let cell = board[getBoardPosition(id)];
if (cell.opened) continue; // Add this to avoid circling around
cell.opened = true;
isCellEmpty(getAdjacentCells(id)); // recursive call
}
}
Object.values
You write in the comments of your code that:
getAdjacentCells() will return either an array of objects similar to the one the function takes as an argument or an integer
However, your comments below this answer seem to suggest that this is not (always) the case. It maybe a plain object which would explain why you used Object.values to iterate it. If this is the case I would urge to change getAdjacentCells so that it really does return an array. Or if that is not possible then use Object.values like you already did:
function isCellEmpty(adjacentCells) {
// I would move this check here, although not necessary if you prefer it in the loop.
if (typeof adjacentCells === "number") {
$("#" + id).empty().append("<div>"); // You can chain jQuery...
return;
}
for (let id of Object.values(adjacentCells)) { // Just use a for..of loop
if (id === null) continue; // keep IF-ELSE nesting flat.
let cell = board[getBoardPosition(id)];
if (cell.opened) continue; // Add this to avoid circling around
cell.opened = true;
isCellEmpty(getAdjacentCells(id)); // recursive call
}
}
Recursion should work just fine here: at its most basic you'd call your own method with id2. However, assuming getAdjacentCells may return cells that you've already visited, you'll end up recursing infinitely unless you're able to track which IDs you've already visited, and pass that in.
function setCellState(id, visited) {
if(id === null) {
return;
}
if(visited === undefined) {
visited = new Set();
}
if(visited.has(id)) {
return;
}
visited.add(id);
board[getBoardPosition(id)].opened = true;
// getAdjacentCells() will return either an array of objects similar to the one the function takes as an argument or an integer
let adjacentCells = getAdjacentCells(id);
// if getAdjacentCells(id) returns a number, add a div to the HTML element with that id
if (typeof (adjacentCells) === "number") {
// Removes all other divs, this prevents repetition
$("#" + id).empty()
// Appends an empty div
.append("<div></div>");
} else if (typeof (adjacentCells) === "object") {
Object.values(adjacentCells).forEach(id2 => setCellState(id2, visited));
}
I took the liberty of changing the method name to be more representative of what the method actually does. I also changed it to start with a single cell's ID, since that simplifies the recursion and that allows the comment around the behavior of getAdjacentCells to provide better context.

Elegant way of checking if one of the parentNodes has a certain class

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>

Creating a custom jquery selector (not filter/pseudo selector)

I wanted to create a custom selector similar to Basic CSS Selectors, not like those Pseudo selectors which starts with a colon (created using $.expr[':']). This is to incorporate the document.elementFromPoint method to jquery.
I wrote a code for that as follows:
$._find = $.find;
$.find = function(query, context, extra, seed ){
var start,end,expr,_ref,top,left;
if(typeof query === 'string'){
start = query.indexOf("(");
end = query.indexOf(")",start);
if(start !== -1 && end !== -1){
expr = "["+query.slice(start+1,end)+"]";
_ref = $.parseJSON(expr);top=_ref[0];left=_ref[1];
console.log(document.elementFromPoint(top,left));
return $([document.elementFromPoint(top,left)]);
}
}
return $._find.apply(null,[query, context, extra, seed]);
};
It works for the normal queries. but when a code $('(10,20)') is executed, an empty jquery result is returned.The problem is happening at the line return $([document.elementFromPoint(top,left)]); as the previous line happens to give a valid DOM Element.
Any comment is welcome.
PS: I'm not looking for a Pseudo-Selector,Thanks in advance

How to filter elements returned by QuerySelectorAll

I'm working on a javascript library, and I use this function to match elements:
$ = function (a)
{
var x;
if (typeof a !== "string" || typeof a === "undefined"){ return a;}
//Pick the quickest method for each kind of selector
if(a.match(/^#([\w\-]+$)/))
{
return document.getElementById(a.split('#')[1]);
}
else if(a.match(/^([\w\-]+)$/))
{
x = document.getElementsByTagName(a);
}
else
{
x = document.querySelectorAll(a);
}
//Return the single object if applicable
return (x.length === 1) ? x[0] : x;
};
There are occasions where I would want to filter the result of this function, like pick out a div span, or a #id div or some other fairly simple selector.
How can I filter these results? Can I create a document fragment, and use the querySelectorAll method on that fragment, or do I have to resort to manual string manipulation?
I only care about modern browsers and IE8+.
If you want to look at the rest of my library, it's here: https://github.com/timw4mail/kis-js
Edit:
To clarify, I want to be able to do something like $_(selector).children(other_selector) and return the children elements matching that selector.
Edit:
So here's my potential solution to the simplest selectors:
tag_reg = /^([\w\-]+)$/;
id_reg = /#([\w\-]+$)/;
class_reg = /\.([\w\-]+)$/;
function _sel_filter(filter, curr_sel)
{
var i,
len = curr_sel.length,
matches = [];
if(typeof filter !== "string")
{
return filter;
}
//Filter by tag
if(filter.match(tag_reg))
{
for(i=0;i<len;i++)
{
if(curr_sell[i].tagName.toLowerCase() == filter.toLowerCase())
{
matches.push(curr_sel[i]);
}
}
}
else if(filter.match(class_reg))
{
for(i=0;i<len;i++)
{
if(curr_sel[i].classList.contains(filter))
{
matches.push(curr_sel[i]);
}
}
}
else if(filter.match(id_reg))
{
return document.getElementById(filter);
}
else
{
console.log(filter+" is not a valid filter");
}
return (matches.length === 1) ? matches[0] : matches;
}
It takes a tag like div, an id, or a class selector, and returns the matching elements with the curr_sel argument.
I don't want to have to resort to a full selector engine, so is there a better way?
I don't think I get the question right. Why would you want to "filter" the result of querySelectorAll() which infact, is some kind of a filter itself. If you query for div span or even better #id div, those results are already filtered, no ?
However, you can apply Array.prototype.filter to the static result of querySelectorAll like follows:
var filter = Array.prototype.filter,
result = document.querySelectorAll('div'),
filtered = filter.call( result, function( node ) {
return !!node.querySelectorAll('span').length;
});
That code would first use querySelectorAll() to query for all <div> nodes within the document. Afterwards it'll filter for <div> nodes which contain at least one <span>. That code doesn't make much sense and is just for demonstrative purposes (just in case some SO member wants to create a donk comment)
update
You can also filter with Element.compareDocumentPosition. I'll also tell if Elements are disconnected, following, preceding, or contained. See MDC .compareDocumentPosition()
Note: NodeList is not a genuine array, that is to say it doesn't have
the array methods like slice, some, map etc. To convert it into an
array, try Array.from(nodeList).
ref: https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll
for example:
let highlightedItems = Array.from(userList.querySelectorAll(".highlighted"));
highlightedItems.filter((item) => {
//...
})
Most concise way in 2019 is with spread syntax ... plus an array literal [...], which work great with iterable objects like the NodeList returned by querySelectorAll:
[...document.querySelectorAll(".myClass")].filter(el=>{/*your code here*/})
Some browsers that support qsa also support a non-standard matchesSelector method, like:
element.webkitMatchesSelector('.someSelector')
...that will return a boolean representing whether element matched the selector provided. So you could iterate the collection, and apply that method, retaining positive results.
In browsers that don't have a matchesSelector, you'd probably need to build your own selector based method similar to the selector engine you're building.

removing all the DOM Elements with a specific className

How I can remove all DOM Elements with specific classname or element width ID's that start with a specific pattern. like (without any framework!)
id="selectbox1"
id="selectbox2"
id="selectbox3"
id="selectbox4"
Thanks
You'd have to use getElementsByTagName(*) iterate over the entire collection, check the .className property with a regex /\bYourClasName\b/ (className can have more than one class, seperated by a space) and then also check the element's .id property with another regex: /^IDStartsWithThis/ finally on any matches you would have to call element.parentNode.removeChild(element);
(On my way to work and in a rush, if you need more code I can supply it once I get there around 630 est)
Edit: here's the code:
usage: removeElemIf(idStartsWith,containsClass). you can pass null, only the id (second param is undefined), blanks (blanks are ignored, both parameters are trimmed first). Case is insensitive for both parameters.
function removeElemIf(theID, theClass) { /* class => full match, id => startswith */
checkID = !(theID === undefined || theID === null || (theID = theID.replace(/^\s*|\s*$/g, '')).length == 0);
checkClass = !(theClass === undefined || theClass === null || (theClass = theClass.replace(/^\s*|\s*$/g, '')).length == 0);
if (!(checkID || checkClass)) return;
var oBody = document.getElementsByTagName('body')[0]; // only search the body
var oElems = oBody.getElementsByTagName('*'); // get all the elements within body
for (i = oElems.length - 1; i >= 0; i--) { // loop through backwards in case of delete
el = oElems[i]; // set current element
found = false; // reset flag
if (checkID) { /* check the ID for starting with "theID", originally used indexOf but its case sensitive*/
re = new RegExp('^'+theID,'i');
if (el.id.match(re)) found = true;
}
if (!found && checkClass) { /* only test class if the id doesn't match,
save the regex in instances where the
class names are long or many*/
re = new RegExp('\\b' + theClass + '\\b', 'i');
if (el.className.match(re)) found = true;
}
if (found) el.parentNode.removeChild(el); /* if found, remove from parent */
}
}
Traverse through the dom tree and compare against the className property of each element found. Yes it's tedious, but that's how it's done. You can either traverse in a recursive fashion or an iterative one. The first is the easiest to write, but the second has much better performance.
see this SO thread:
jQuery matching pattern
and check out getElementsByTagName() function

Categories