How to make my css selector engine more flexible? - javascript

I created a custom css selector engine function for my custom javascript library like so,
var catchEl = function(el) { // Catching elements by identifying the first character of a string
var firstChar = el[0],
actualNode = el.substring(1, el.length),
elements,
tempElems = [];
if (!document.querySelectorAll) {
try{
if(firstChar === "#") {//So, we can look for ids
tempElems.push(document.getElementById(actualNode));
} else if(firstChar === ".") {//or classes
elements = document.getElementsByClassName(actualNode);
for(i=0;i<elements.length;i++) tempElems.push(elements[i]);
} else {//or tags
elements = document.getElementsByTagName(el);
for(i=0;i<elements.length;i++) tempElems.push(elements[i]);
}
} catch(e) {};
} else {//but before everything we must check if the best function is available
try{
elements = document.querySelectorAll(el);
for(i=0;i<elements.length;i++) tempElems.push(elements[i]);
} catch(e) {};
}
return tempElems;
}
This function returns an array of elements. However, I turned my head around and tried to make it more flexible so that it can also return the window, document or this object, but was unsuccessful. Whenever I try to push the window object into the tempElems array, the array is still empty.
So, I want to know how to make this function return an array of elements when a string is passed through it or return the respective objects(window, document or this) as desired.
Note: I don't want to work with jQuery. So, please don't post any answers regarding jQuery.

Related

Is there a way to test a css-selector query to an unappended element?

I have this code:
Element.prototype.queryTest = function(strQuery) {
var _r;
if (this.parentElement == null) {
_r = Array.prototype.slice.call(document.querySelectorAll(strQuery)).indexOf(this);
} else {
_r = Array.prototype.slice.call(this.parentElement.querySelectorAll(strQuery)).indexOf(this);
}
return !!(_r+1);
}
I am searching for some way to test a query to an unappended element.
I want to change the first code to make this work:
var t = document.createElement("span");
t.classList.add("asdfg");
console.log(t.queryTest("span.adsfg"));
If there is a way to detect if the element isn't appended I could create a new temporary unappended one and append the target one to the temporary one to test the css-selector query.
Is there a way to detect if the element hasn't been appended jet? Could the target element be accessible even after freeing the temporary parent one? I have tested it on Chrome and it is accessible but I don't know if that is the case for firefox.
I know I can use document.querySelectorAll("*") to get a list of nodes but... isn't too CPU-demmanding the process to turn this NodeList to an Array? This is why I prefer not to use that way.
Thanks in advance.
There is already a native Element.prototype.matches method which does that:
const el = document.createElement('span');
el.classList.add('test');
console.log(el.matches('span.test'));
Note that to check if a node is connected or not, there is the Node.prototype.isConnected getter.
I did it.
Element.prototype.querySelectorTest = function(strQuery) {
var _r;
if (this.parentElement != null) {
_r = Array.prototype.indexOf.call(this.parentElement.querySelectorAll(strQuery),this);
} else if (this == document.documentElement) {
_r = ((document.querySelector(strQuery) == this)-1);
} else {
_r = ((this == document.createElement("i").appendChild(this).parentElement.querySelector(strQuery))-1);
}
return !!(_r+1);
}
I changed the way it check the nodeList.
I renamed the function to a more proper name.
If the target element is the root one there's no need to make a querySelectorAll.
If you append the unappended element to a temporary one to test the child you don't loose the reference (variable value in case there is one).
This is not my native language so please consider that.

How to compare if an HTML element exists in the node array?

selectedContentWrap: HTML nodes.
htmlVarTag: is an string.
How do I check if the HTML element exists in the nodes?
The htmlVarTag is a string and don't understand how to convert it so it check again if there is a tag like that so that if there is I can remove it?
here is output of my nodes that is stored in selectedContentWrap
var checkingElement = $scope.checkIfHTMLinside(selectedContentWrap,htmlVarTag );
$scope.checkIfHTMLinside = function(selectedContentWrap,htmlVarTag){
var node = htmlVarTag.parentNode;
while (node != null) {
if (node == selectedContentWrap) {
return true;
}
node = node.parentNode;
}
return false;
}
Well if you could paste the content of selectedContentWrap I would be able to test this code, but I think this would work
// Code goes here
var checkIfHTMLinside = function(selectedContentWrap,htmlVarTag){
for (item of selectedContentWrap) {
if (item.nodeName.toLowerCase() == htmlVarTag.toLowerCase()){
return true;
}
}
return false;
}
Simplest is use angular.element which is a subset of jQuery compatible methods
$scope.checkIfHTMLinside = function(selectedContentWrap,htmlVarTag){
// use filter() on array and return filtered array length as boolean
return selectedContentWrap.filter(function(str){
// return length of tag collection found as boolean
return angular.element('<div>').append(str).find(htmlVarTag).length
}).length;
});
Still not 100% clear if objective is only to look for a specific tag or any tags (ie differentiate from text only)
Or as casually mentioned to actually remove the tag
If you want to remove the tag it's not clear if you simply want to unwrap it or remove it's content also ... both easily achieved using angular.element
Try using: node.innerHTML and checking against that
is it me or post a question on stackoverflow and 20min after test testing I figure it.,...
the answer is that in the selectedContentWrap I already got list of nodes, all I need to do i compare , so a simple if for loop will fit.
To compare the names I just need to use .nodeName as that works cross browser ( correct me if I am wrong)
Some dev say that "dictionary of tag names and anonymous closures instead" - but couldn't find anything. If anyone has this library could you please post it to the question?
here is my code.
var node = selectedContentWrap;
console.log('node that is selectedwrapper', selectedContentWrap)
for (var i = 0; i < selectedContentWrap.length; i++) {
console.log('tag name is ',selectedContentWrap[i].nodeName);
var temptagname = selectedContentWrap[i].nodeName; // for debugging
if(selectedContentWrap[i].nodeName == 'B' ){
console.log('contains element B');
}
}

Javascript css selector for nested classes

I am creating a CSS selector for homework. I have managed to extract and get single selectors - e.g. #_id, but I cannot work out how to get a result for nested ones such as : div#_id._class [NOTE: I cannot use any libraries to do this or querySelectorAll]
The pseudo-code below is an example of what I currently have:
if (regex match for class) {
for (a in match for class) {
if (a.indexOf('.') > -1) {
var split_ = a.split(".");
var dot = split_[0];
var class_ = split_[1];
array_of_elements = document.getElementsByClassName(class_);
}
}
The problem is when the selector is nested I can't extract the whole thing using a similar method. E.g. look for an id, look for a class. Can anyone point me in the right direction?
else if (is id) {
split by ("#");
for (each result) {
if (has class ('.')) {
array_elements = document.getElementById(result_ID)
.getElementsByClassName(result_CLASS_NAME));
} else {
array_elements = (document.getElementsByTagName(result));
}
}
What you mentioned is actually called a sequence of simple selectors.
div#_id._class
It consitst of three simple selectors div, #_id, ._class
What you need to do is get elements by tag name, and then check for matches on all of the remaining simple selectors. I'll give you an idea here:
function qSelector(sequence) {
var tagName = getTag(sequence) || '*'; // 'div'
var ids = getIDs(sequence); // ['_id']
var classes = getClasses(sequence); // ['_class']
var els = document.getElementsByTagName(tagName);
return [].filter.call(els, function (el) {
for (id in ids) { if (el.id != id) return false; }
for (cls in classes) { if (el.className not contains cls) return false; }
return true;
});
}
This is more versatile than your approach and can be easily generalized to work with selectors containing spaces.
I'll leave the implementation of the get… helpers to you.

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.

Is it possible to get element's numerical index in its parent node without looping?

Normally I'm doing it this way:
for(i=0;i<elem.parentNode.length;i++) {
if (elem.parentNode[i] == elem) //.... etc.. etc...
}
function getChildIndex(node) {
return Array.prototype.indexOf.call(node.parentNode.childNodes, node);
}
This seems to work in Opera 11, Firefox 4, Chromium 10. Other browsers untested. It will throw TypeError if node has no parent (add a check for node.parentNode !== undefined if you care about that case).
Of course, Array.prototype.indexOf does still loop, just within the function call. It's impossible to do this without looping.
Note: If you want to obtain the index of a child Element, you can modify the function above by changing childNodes to children.
function getChildElementIndex(node) {
return Array.prototype.indexOf.call(node.parentNode.children, node);
}
Option #1
You can use the Array.from() method to convert an HTMLCollection of elements to an array. From there, you can use the native .indexOf() method in order to get the index:
function getElementIndex (element) {
return Array.from(element.parentNode.children).indexOf(element);
}
If you want the node index (as oppose to the element's index), then replace the children property with the childNodes property:
function getNodeIndex (element) {
return Array.from(element.parentNode.childNodes).indexOf(element);
}
Option #2
You can use the .call() method to invoke the array type's native .indexOf() method. This is how the .index() method is implemented in jQuery if you look at the source code.
function getElementIndex(element) {
return [].indexOf.call(element.parentNode.children, element);
}
Likewise, using the childNodes property in place of the children property:
function getNodeIndex (element) {
return [].indexOf.call(element.parentNode.childNodes, element);
}
Option #3
You can also use the spread operator:
function getElementIndex (element) {
return [...element.parentNode.children].indexOf(element);
}
function getNodeIndex (element) {
return [...element.parentNode.childNodes].indexOf(element);
}
You could count siblings...
The childNodes list includes text and element nodes-
function whichChild(elem){
var i= 0;
while((elem=elem.previousSibling)!=null) ++i;
return i;
}
There is no way to get the index of a node within its parent without looping in some manner, be that a for-loop, an Array method like indexOf or forEach, or something else. An index-of operation in the DOM is linear-time, not constant-time.
More generally, if list mutations are possible (and the DOM certainly supports mutation), it's generally impossible to provide an index-of operation that runs in constant time. There are two common implementation tactics: linked lists (usually doubly) and arrays. Finding an index using a linked list requires a walk. Finding an index using an array requires a scan. Some engines will cache indexes to reduce time needed to compute node.childNodes[i], but this won't help you if you're searching for a node. Not asking the question is the best policy.
I think you've got it, but:
make sure that variable "i" is declared with var
use === instead of == in the comparison
If you have a collection input elements with the same name (like <textarea name="text_field[]"…) in your form and you want to get the exact numeric index of the field that triggered an event:
function getElementIdxFromName(elem, parent) {
var elms = parent[elem.name];
var i = 0;
if (elms.length === undefined) // there is only one element with this name in the document
return 0;
while((elem!=elms[i])) i++;
return i;
}
Getting numeric id of an element from a collection of elements with the same class name:
function getElementIdxFromClass(elem, cl) {
var elems = document.getElementsByClassName(cl);
var i = 0;
if (elems.length > 0) {
while((elem!=elems[i])) i++;
return i;
}
return 0;
}
Try this:
let element = document.getElementById("your-element-id");
let indexInParent = Array.prototype.slice.call(element.parentNode.parentNode.children).indexOf(element.parentNode));

Categories