I am using Drupal's Webform module which writes all the ids and classes automatically, so I can't add an Id to the place I want to add it. And Drupal doesn't put one in for me. Grrr.
I have no problem when I use my code with just a simple getElementById('myid'), but the element I need is a blank, no id, no class "a" that's inside a "legend" (which has a class but no id) that's inside a fieldset with an id.
I tried this code and several variations but it didn't work:
document.getElementById('webform-created-id-here').getElementsByTagName('legend').getElementsByTagName('a');
I feel like I'm not understanding how to properly access that part of the DOM. Can anyone offer help or suggestions?
Thank you!
getElementsByTagName return a nodelist with the elements found, so you must select the index of the element
Example for the first element
var test = document.getElementById('webform-created-id-here').getElementsByTagName('legend')[0].getElementsByTagName('a')[0];
DEMO VIEW
Recurse through the DOM yourself. References to the children of any element are stored in the childNodes attribute which exist for every node.
function recurseDOM (el,test) {
if (test(el)) {
return el;
}
else {
var children = el.childNodes;
var result;
for (var i=0; i<children.length; i+) {
result = recurseDOM(children[i],test);
if (result) {
return result;
}
}
}
return null;
}
This is only one possible implementation that I wrote in the 2 minutes it took me to type out this answer. So it can probably use some improvements. But you get the idea.
Use it like this:
recurseDOM(document.body,function(el){
if (el == something_something) { return 1}
return 0
});
You can write a similar function to test parents:
function testParents (el, test) {
var parent = el.parentNode;
if (test(parent)) {
return 1;
}
else {
if (parent != document.body) {
return testParents(parent,test);
}
}
return 0;
}
So you can write something like:
recurseDOM(document.body,function(el){
if (el.tagName == 'a' && testParents(el,function(p){
if (p.tagName == 'legend' && testParents(p,function(pp){
if (pp.id == 'webform-created-id-here') {
return 1;
}
return 0;
})) {
return 1;
}
return 0;
})) {
return 1;
}
return 0;
});
Alternatively, you can start the recursion form the getElementById instead of document.body.
Related
I'm just trying to convert jQuery method to VanillaJS as below.
jQuery:
elements.parent(".item").removeClass("item");
Vanilla :
var parent = elements.parentNode;
parent.querySelector(".item").classList.remove('item');
But the Vanilla script is not working as same as jQuery. Someone please suggest any better solution.
Given that elements is plural, I'm assuming it's a collection, so use .forEach(). (This needs to be patched in legacy browsers unless elements is an actual Array)
elements.forEach(function(el) {
el.parentNode.classList.remove("item");
});
Note that you do not need to filter down to just the .item elements. If it doesn't have the class, it'll be a no-op.
Also note that .querySelector only searches for descendants. If you did need to filter on the class for other reasons, you'd use .matches.
elements.forEach(function(el) {
var par = el.parentNode;
if (par.matches(".item")) {
// Do work with the `.item` element
par.classList.remove("item");
}
});
If there could be multiple ancestors with the .item class, then traverse those ancestors in a nested loop.
elements.forEach(function(el) {
var par = el.parentNode;
do {
if (par.matches(".item")) {
// Do work with the `.item` element
par.classList.remove("item");
}
} while((par = par.parentNode));
});
You could make a helper function if you're doing this frequently.
function ancestors(el, filter) {
var res = [];
var par = el.parentNode;
do {
if (!filter || par.matches(filter)) {
res.push(par);
}
} while((par = par.parentNode));
return res;
}
So then it's like this.
elements.forEach(function(el) {
ancestors(el, ".item")
.forEach(function(par) { par.classList.remove(".item"); });
});
I am working with angular and I am trying to create a "select all" button.
I have a list of items, each item has a toggle and what I am doing is, on change (everytime the toggle changes from true (selected) to false (not selected), I run a function to create an array with all the IDs of the selected elements.
This works almost perfectly, the problem is that I am facing some issues with the indexfOf method to check if the ID is already in the array.
var isInArray;
isInArray = function(arr, id) {
console.log("index of ", arr.indexOf(id));
return arr.indexOf(id);
};
scope.evtSelectAll = function() {
return angular.forEach(scope.listToDisplay, function(element) {
element.copyTo = true;
return scope.selectFromList(element.iID, element.copyTo);
});
};
scope.selectFromList = function(id, copy) {
if (copy === true && isInArray(scope.selected, id) === -1) {
scope.selected.push(id);
} else {
scope.selected.pop(id);
}
console.log("scope.selected - ", scope.selected);
if (scope.selected.length > 0) {
console.log("Emitted event: can proceed!");
scope.$emit('enough-elements');
} else {
console.log("Emitted event: can not proceed!");
scope.$emit('not-enough-elements');
}
return scope.result = scope.selected;
};
the problem I've got is when the array (scope.selected) has multiple IDs.
Let's say, for example, that my scope.selected looks like this:
scope.selected = [2,3,4,7]
if I click on select all, nothing gets added (and this is correct)
Now, let's say I untick 4 and 7 for example, and my scope.selected now looks like this:
scope.selected = [2,3]
If I now click on select all, my result is the following: [2,4,7].
I lose the 3
I think this is due to the fact that my array doesn't have one single item?
thanks for any help. Here's also a quick codepen to explain the problem. If you check the console and play with the toggles you should be able to see straight away what I am referring to.
Thanks in advance
Thanks to Matthias and Christian Bonato for their suggestions.
At the end, I solved using both of their suggestions and the final result seems to work as expected.
Here's a codepen with the final version: http://codepen.io/NickHG/pen/KNXPBb
Basically, I changed
scope.selected.pop(id);
with
$scope.selected.splice( isInArray($scope.selected, id),1);
and in the selectAll event function, I always empty scope.selected[] before adding elements to the array
$scope.evtSelectAll = function() {
$scope.selected = []
angular.forEach($scope.list, function(element) {
element.copyTo = true;
return $scope.selectFromList(element.id, element.copyTo);
});
};
thank you for your help!
I think mostly your code contains a logical error. You are using the function selectFromList to de-select (when done individually) and for the select all (which you don't want to use to de-select).
As someone pointed out in a for some reason now deleted answer, the pop.() function shouldn't be called with any arguments (it is only for removing the last element), you should use splice like this:
$scope.selected.splice( isInArray($scope.selected, id),1);
Unless you really need the emitted functionality to run on a select all, you can try if this is the answer for you:
var isInArray;
isInArray = function(arr, id) {
console.log("index of ", arr.indexOf(id));
return arr.indexOf(id);
};
scope.evtSelectAll = function() {
return angular.forEach(scope.listToDisplay, function(element) {
element.copyTo = true;
if (isInArray($scope.selected, element.id) === -1) {
$scope.selected.push(element.id);
}
});
};
scope.selectFromList = function(id, copy) {
if (copy === true && isInArray(scope.selected, id) === -1) {
scope.selected.push(id);
} else {
$scope.selected.splice(isInArray($scope.selected, id), 1);
}
console.log("scope.selected - ", scope.selected);
if (scope.selected.length > 0) {
console.log("Emitted event: can proceed!");
scope.$emit('enough-elements');
} else {
console.log("Emitted event: can not proceed!");
scope.$emit('not-enough-elements');
}
return scope.result = scope.selected;
};
Now the select all only adds to scope.selected if it doesn't find the id in the scope.selected list.
My Collection has following structure
{
_id:1,
parent_id:0
}
{
_id:2,
parent_id:1
}
{
_id:3,
parent_id:1
}
{
_id:4,
parent_id:3
}
Like
a > a1 > a1-1,a1-2,a1-3 > a11
One parent has many children and the childrens can have many childrens infinite loop
but when user clicks on a I want to delete all its children and it childrens too
I tried following function
var deleteChildCards = function(id){
var count=userCards.find({parent_id: id}).count();
if(count > 0){
userCards.find({parent_id: id}).forEach(function (card) {
deleteChildCards(card._id);
});
}
else{
return userCards.remove({_id: id});
}
}
If I pass id to the function it must remove all it's childs,This is not removing all docs what is wrong in this function.
Is there any other way I can write this function?
In your example, the else portion of the if-else will never get run. Try the following adjustment:
var deleteChildCards = function(id) {
userCards.find({parent_id: id}).forEach(function(card) {
deleteChildCards(card._id);
userCards.remove(card._id);
});
}
You can completely eliminate the if-else in this example since the forEach won't iterate if there are no userCards found.
From my understanding, you did if else by mistake.
Try
var deleteChildCards = function(id){
var count=userCards.find({parent_id: id}).count();
if(count > 0){
userCards.find({parent_id: id}).fetch().forEach(function (card) {
deleteChildCards(card._id);
});
}
userCards.remove({_id: id});
}
Otherwise, it will only remove the cards at the lowest level of the hierarchy.
Also, you need to do .fetch() after find(), if you want to do forEach
I'm trying to create a function using vanilla JS that will:
Create a new DOM element
Assign it a Class Name
Place it in the DOM either appending to an existing div or inserting it specifically into the DOM if required using "insertBefore()"
I have come up with the somewhat inelegant solution below:
function createDomElem(elem, className, parent, refElement, type) {
var a = document.createElement(elem);
if (type == "append") {
document.querySelector(parent).appendChild(a);
} else if (type == "insert") {
document.querySelector(parent).parentNode.insertBefore(a, refElement)
}
a.className = className;
};
My problems with this solution are
Too many arguments to be passed
If not passing "insert" then you don't require refElement and to avoid "type" being mistaken for "refElement" you'd have to pass "refElement" as "null" and then define type as "append"
So my question is where can I streamline this function to become more useful within my program?
I'm also dreaming of the ability to be able to push child divs into the newly created div right within this function, defining how many child divs you would want and then using a for loop to append or insert these. Would this be better placed in a new function though?
I would split the code into two parts, as they have to separate concerns. I use something similar to the following for creating DOM elements:
var DomFactory = (function (document) {
var api = {
element: function (name, attributes) {
var el = document.createElement(name);
if (attributes) {
for (var key in attributes) {
if (attributes.hasOwnProperty(key)) {
el.setAttribute(key, attributes[key]);
}
}
}
return el;
},
div: function (attributes) {
return api.element('div', attributes);
}
};
return api;
}(window.document));
Usage:
var div = DomFactory.div({ 'class': 'hero' });
var table = DomFactory.element('table', { 'class': 'table table-bordered' });
Then for positioning, you could have a generalised position function:
function attach(source, target, position) {
switch (position) {
case 'before': {
target.parentNode.insertBefore(source, target);
break;
}
case 'after': {
if (target.nextSibling) {
target.parentNode.insertBefore(source, target.nextSibling);
} else {
target.parentNode.appendChild(source);
}
}
}
}
Usage:
attach(table, div, 'before');
I have a simple if statement as such:
if ($('html').hasClass('m320')) {
// do stuff
}
This works as expected. However, I want to add more classes to the if statement to check if any of the classes are present in the <html> tag. I need it so it's not all of them but just the presence of at least one class but it can be more.
My use case is that I have classes (e.g. m320, m768) added for various viewport widths so I only want to execute certain Jquery if it's a specific width (class).
Here is what i have tried so far:
1.
if ($('html').hasClass('m320', 'm768')) {
// do stuff
}
2.
if ($('html').hasClass('m320')) || ($('html').hasClass('m768')) {
// do stuff
}
3.
if ($('html').hasClass(['m320', 'm768'])) {
// do stuff
}
None of these seem to work though. Not sure what I am doing wrong but most likely my syntax or structure.
You could use is() instead of hasClass():
if ($('html').is('.m320, .m768')) { ... }
You just had some messed up parentheses in your 2nd attempt.
var $html = $("html");
if ($html.hasClass('m320') || $html.hasClass('m768')) {
// do stuff
}
For fun, I wrote a little jQuery add-on method that will check for any one of multiple class names:
$.fn.hasAnyClass = function() {
for (var i = 0; i < arguments.length; i++) {
if (this.hasClass(arguments[i])) {
return true;
}
}
return false;
}
Then, in your example, you could use this:
if ($('html').hasAnyClass('m320', 'm768')) {
// do stuff
}
You can pass as many class names as you want.
Here's an enhanced version that also lets you pass multiple class names separated by a space:
$.fn.hasAnyClass = function() {
for (var i = 0; i < arguments.length; i++) {
var classes = arguments[i].split(" ");
for (var j = 0; j < classes.length; j++) {
if (this.hasClass(classes[j])) {
return true;
}
}
}
return false;
}
if ($('html').hasAnyClass('m320 m768')) {
// do stuff
}
Working demo: http://jsfiddle.net/jfriend00/uvtSA/
This may be another solution:
if ($('html').attr('class').match(/m320|m768/)) {
// do stuff
}
according to jsperf.com it's quite fast, too.
For anyone wondering about some of the different performance aspects with all of these different options, I've created a jsperf case here: jsperf
In short, using element.hasClass('class') is the fastest.
Next best bet is using elem.hasClass('classA') || elem.hasClass('classB'). A note on this one: order matters! If the class 'classA' is more likely to be found, list it first! OR condition statements return as soon as one of them is met.
The worst performance by far was using element.is('.class').
Also listed in the jsperf is CyberMonk's function, and Kolja's solution.
Here is a slight variation on answer offered by jfriend00:
$.fn.hasAnyClass = function() {
var classes = arguments[0].split(" ");
for (var i = 0; i < classes.length; i++) {
if (this.hasClass(classes[i])) {
return true;
}
}
return false;
}
Allows use of same syntax as .addClass() and .removeClass(). e.g., .hasAnyClass('m320 m768')
Needs bulletproofing, of course, as it assumes at least one argument.
var classes = $('html')[0].className;
if (classes.indexOf('m320') != -1 || classes.indexOf('m768') != -1) {
//do something
}
The hasClass method will accept an array of class names as an argument, you can do something like this:
$(document).ready(function() {
function filterFilesList() {
var rows = $('.file-row');
var checked = $("#filterControls :checkbox:checked");
if (checked.length) {
var criteriaCollection = [];
checked.each(function() {
criteriaCollection.push($(this).val());
});
rows.each(function() {
var row = $(this);
var rowMatch = row.hasClass(criteriaCollection);
if (rowMatch) {
row.show();
} else {
row.hide(200);
}
});
} else {
rows.each(function() {
$(this).show();
});
}
}
$("#filterControls :checkbox").click(filterFilesList);
filterFilesList();
});
This is in case you need both classes present. For either or logic just use ||
$('el').hasClass('first-class') || $('el').hasClass('second-class')
Feel free to optimize as needed
Try this:
if ($('html').hasClass('class1 class2')) {
// do stuff
}