I am attempting to bind a dblclick event to divs in a nodeList which I am iterating through.
Here is the code:
var elems = document.getElementsByClassName("click");
currentLocation = elems[0].id;
for (var i=0; i<elems.length; i++){
$(elems[i]).dblclick(function() {
if((elems[i].id) != currentLocation){
badAnswer = true;
alert(badAnswer);
}
});
}
currentLocation is a global variable set to the first element id of the node list. badAnswer is also a global boolean set to false. If a element is double clicked that matches an element other than the currentLocation global, badAnswer is set to true.
Currently I receive a undefined error, which I tried to remedy by creating a local variable inside of the event handler. This didn't seem to work either and badAnswer is always true on double click as the elementID is always equal to the ID value of the last element.
Is there a better way to do this?
Yes, there is a much better way:
var currentLocation = $(".click")[0].id;
$(".click").on("dblclick", function() {
if (this.id != currentLocation) {
badAnswer = true;
}
});
$('.click').dblClick(function() {
if(this.id != currentLocation) {
...
}
});
You're already using jQuery so you might as well use the selectors...much easier!
Related
I need function to click on a range of check boxes. I do however not always know what i is. I tried a write a forEach loop, but it does not work:
This for loop works:
function check Boxes() {
for (let i = 0; i < 249; i++) {
document.getElementsByClassName("inventoryCbox")[i].click();
}
}
and this is the non-working for loop. I think that maybe my syntax is wrong.
checkBoxes();
var boxes = document.getElementsByClassName("inventoryCbox");
function checkBoxes(node) {
node.forEach(function(boxes) {
boxes.click()
});
}
This will check all the checkboxes with the class 'inventoryCbox':
document.querySelectorAll(".inventoryCbox").forEach(node => node.click())
node in your checkBoxes function is undefined, because you're not passing anything into the function. Also, your code has you calling checkBoxes before you assign anything to boxes. You probably meant to use boxes directly:
// This *before* `checkboxes`
var boxes = document.getElementsByClassName("inventoryCbox");
checkBoxes();
function checkBoxes() { // <== No parameter
boxes.forEach(function(box) {
//^^^^^ ^^^
box.click()
// ^^^
});
}
But that still has a problem: The HTMLCollection returned by getElementsByClassName doesn't have forEach reliably cross-browser. (The NodeList returned by querySelectorAll has it on modern browsers, but not HTMLCollection.)
You can add it if you like:
if (typeof HTMLCollection !== "undefined" &&
HTMLCollection.prototype &&
!HTMLCollection.prototype.forEach) {
// Yes, direct assignment is fine here, no need for `Object.defineProperty`
HTMLCollection.prototype.forEach = Array.prototype.forEach;
}
Then the updated code above would work.
Or stick with your existing loop, or use Array.prototype.forEach directly:
function checkBoxes() { // <== No parameter
Array.prototype.forEach.call(boxes, function(box) {
box.click()
});
}
My answer here goes into details for adding not just forEach but iterability to HTMLCollection (and NodeList in environments that haven't implemented iterability for NodeList yet).
If you want to click on all elements selected by some class, you can use this example
var els = document.querySelectorAll('.inventoryCbox');
for (i = 0; i < els.length; ++i) {
els[i].click();
};
I am using jQuery to clone elements, then I save a reference to an element within that clone. And much later remove the clone. Here is a basic example:
HTML
<div> <span></span> </div>
Script
var i, $clone, $span,
$saved = $('span'),
$orig = $('div');
for (i = 0; i < 100; i++) {
$clone = $orig.clone().appendTo('body');
$span = $clone.find('span');
$saved = $saved.add($span);
$clone.remove();
}
console.log( 'leaking = ', $saved.length);
The console log outputs a length of 101.
I need to clean up the $saved jQuery object and remove references to elements no longer attached to the DOM. So I wrote this basic function to clean it all up.
var cleanUpLeaks = function ($el) {
var el, remove,
index = $el.length - 1;
while (index >= 0) {
el = $el[index];
remove = true;
while (el) {
el = el.parentNode;
if (el && el.nodeName === 'HTML') {
remove = false;
break;
}
}
if (remove) {
$el.splice(index, 1);
}
index--;
}
return $el;
};
console.log( 'cleaned up = ', cleanUpLeaks( $saved ).length );
This time the console outputs 1.
So now my questions are:
How could I have prevented the memory leak in the first place?
And if that isn't possible, should I be using .splice() in the cleanUpLeaks function to remove the reference? Or would it be better to set that reference to null as is recommended? Because when I do set it to null, $saved remains at a length of 101.
Demo: http://jsfiddle.net/Mottie/6q2hjazg/
To elaborate, I save a reference to the span in $saved. There are other functions that use this value for styling and such. This is a very basic example; and no, I do not immediately remove the clone after appending it to the body, it was done here to show how the memory leak is occurring.
The better solution here is to stop saving dynamic DOM elements in a persistent jQuery variable. If your page is regularly removing content from the DOM, then saving these in a persistent jQuery object just sets you up for having to deal with memory leaks, rather than changing the design to a design that does not have to save references to DOM elements at all.
If instead, you just tag interesting elements with a particular class name that is not used elsewhere in the document, you can generate the desired list of elements at any time with a simple jQuery selector query and you will have no issues at all with leaks because you aren't ever retaining DOM references in persistent variables.
One possible solution is that you take a leaf out of AngularJS's book and monkey-patch jQuery to fire an event when an element is removed. Then you can add a handler for that event and restore the state of $saved to what it was before you added the $span.
First, monkey patch jQuery (taken from AngularJS source):
// All nodes removed from the DOM via various jQuery APIs like .remove()
// are passed through jQuery.cleanData. Monkey-patch this method to fire
// the $destroy event on all removed nodes.
var originalCleanData = jQuery.cleanData;
var skipDestroyOnNextJQueryCleanData;
jQuery.cleanData = function (elems) {
var events;
if (!skipDestroyOnNextJQueryCleanData) {
for (var i = 0, elem;
(elem = elems[i]) != null; i++) {
events = jQuery._data(elem, "events");
if (events && events.$destroy) {
jQuery(elem).triggerHandler('$destroy');
}
}
} else {
skipDestroyOnNextJQueryCleanData = false;
}
originalCleanData(elems);
};
Next, add in your $destroy event handler and restore the captured original state of $saved.
var i, $clone, $span,
$saved = $('span'),
$orig = $('div');
for (i = 0; i < 100; i++) {
(function ($originalSaved) {
$clone = $orig.clone().appendTo('body');
$span = $clone.find('span');
$clone.on('$destroy', function () {
$saved = $originalSaved;
$originalSaved = null;
});
$saved = $saved.add($span);
$clone.remove();
})($saved);
}
console.log('original length = ', $saved.length); // => 1
Here is a jsFiddle with this working. In my testing in Chrome, this doesn't introduce additional leaks.
I'm trying the following, which doesn't work, but am wondering if there is something similar?
Basically, I'm trying to set the same attribute on multiple elements:
document.querySelectorAll("#id1, #id2, #id3").setAttribute('onclick','return false;');
I'm using vanilla Javascript, no library.
querySelectorAll() will return an array-like node list that you need to loop through:
var elems = document.querySelectorAll("#id1, #id2, #id3");
for(var i = 0; i < elems.length; i ++){
elems[i].setAttribute('onclick', 'return false');
}
JSFiddle
There are several approaches for this, the most basic one is to iterate the set and add click handlers (In your case, you want an addEventHandler not an attribute!)
var elements = document.querySelectorAll("#id1, #id2, #id3");
[].forEach.call(elements, function addClickHandler(el) {
el.addEventListener('click', function() { return false; });
};
Perhaps a better approach if you have many elements is to set one event handler on the document, and see if it matches a query selector:
var elements = [].slice.call(document.querySelectorAll("#id1, #id2, #id3")); //Turn into an array
document.addEventHandler('click', function(e) {
if (elements.indexOf(e.target) !== -1) { //Element in list was clicked!
return false;
}
};
I've been trying to understand why whenever value of the array I click, it always add the class "foo".
Example: I clicked on London (cities[1], right?) and it added the class foo.
var cities = [
document.getElementById('Paris'),
document.getElementById('London'),
document.getElementById('Berlin')
];
for (var i = 0; i < cities.length; i++) {
cities[i].onclick = test;
function test(){
if(cities[i] === cities[0]) {
el.classList.add("foo");
}
}
}
EDIT: my original answer was incorrect, this updated one is right. addEventListener returns nothing. Instead, you should use some kind of wrapper to add and remove your listeners, again so that you don't waste resources on listeners that you aren't using:
function on (element, eventName, callback) {
element.addEventListener(eventName, callback);
return function unregister () {
element.removeEventListener(callback);
}
}
function test (event) {
if (event.currentTarget===cities[0]) {
event.target.classList.add('foo');
}
}
var listenerRemovers = cities.map(function (city) {
return on(city, 'click', test);
});
Now you can remove any of these listeners by calling the corresponding function in your listenerRemovers array:
listenerRemovers.forEach(function (unRegisterFunc) { unRegisterFunc(); });
ORIGINAL WRONG ANSWER:
For what it's worth, you're probably better off using .map in a case like this, since best practice is to keep a reference to the event listeners so you can cancel them if needed.
function test (event) {
if (event.currentTarget===cities[0]) {
event.target.classList.add('foo');
}
}
var listenerHandlers = cities.map(function (city) {
return city.addEventListener('click', test);
});
This is happening because you are setting the event functions inside a loop. Each function is using the same value of i.
Try to use this instead of trying to cities[i] inside the function.
function test(){
if(this === cities[0]) {
el.classList.add("foo");
}
}
The easiest approach to achieve this functionality is to use jQuery, here is the idea:
In html tags, give those cities a common class, e.g. class="city"
$('.city').click(function(){$('.city').addClass('foo')});
jQuery saves you more time and coding efforts.
The problem is you are trying to assign a function to a DOM attribute. You are not registering a listener but modifying the DOM. If you wish to do it this way, you must assign the onclick as cities[i].onclick = 'test()'
Also, you should move the function test outside of the for loop to look like the following. The problem is the function test is being declared many times, each with a different 'i' value.
for (var i = 0; i < cities.length; i++) {
cities[i].onclick = 'test(this)';
}
function test(el){
if(cities[i] === cities[0]) {
el.classList.add("foo");
}
}
I have a problem with the following JavaScript function. I have two UL and I need that when the user clicks on a LI, this element transfers to the other UL.
I've managed to move them onClick from on list to the other, the problem appears when I try to move again a LI that was previously in the other UL, when that happens it just doesn't work...
function testList() {
usersA = document.getElementById("users-a");
usersB = document.getElementById("users-b");
for (var i=0; i < usersA.getElementsByTagName("li").length; i++) {
usersA.getElementsByTagName("li")[i].onclick = function() {
transfer = this.cloneNode(true);
usersB.appendChild(transfer);
usersA.removeChild(this);
return false;
}
}
for (var i=0; i < usersB.getElementsByTagName("li").length; i++) {
usersB.getElementsByTagName("li")[i].onclick = function() {
transfer = this.cloneNode(true);
usersA.appendChild(transfer);
usersB.removeChild(this);
return false;
}
}
}
I know that my logic sucks but it's all I could come up with. Any ideas why it works the first time I transfer a LI but when I try to move back to its original UL it doesn't work?
You're not "moving" elements, you're creating a copy and deleting the original. Although this seems like a "move" from the user's point of view the new elements that you create do not have click handlers assigned. From MDN: "Cloning a node copies all of its attributes and their values but does not copy event listeners."
According to MDN, .appendChild() will remove the child from its current parent so you shouldn't need the two-step clone/remove that you are currently doing. I haven't tested it, but perhaps using just .appendChild() it will keep the handlers? If so you need to remove that handler and assign a new one to allow for which list it now belongs to.
Or, rewrite your handlers so that they check which list is the current parent (have a look at .parentNode) and move to the other list as appropriate.
Bearing in mind that click events "bubble up" starting from the target/source element and up through the parent heirarchy you're probably better off setting your click handlers on the parent <ul> elements and then testing which <li> was clicked. That way you don't have to worry about setting new click handlers on new child <li> elements.
function testList() {
var usersA = document.getElementById("users-a"),
usersB = document.getElementById("users-b");
usersA.onclick = function(e) {
// allow for the IE and non-IE way of handling the event object
if (!e) e = window.event;
var el = e.target || e.srcElement;
if (el.tagName === "li") {
usersB.appendChild(el);
}
}
usersB.onclick = function(e) {
// allow for the IE and non-IE way of handling the event object
if (!e) e = window.event;
var el = e.target || e.srcElement;
if (el.tagName === "li") {
usersA.appendChild(el);
}
}
}
Also, if you're using .getElementsByTagName() call it once in the loop initialisation and assign the result to a variable, then use the variable - don't keep calling the function again and again to test the length, or to access individual elements inside your loop:
for (var i=0, lis = usersA.getElementsByTagName("li"); i < lis.length; i++) {
lis[i].onclick = function() {
// etc
}
}
The problem is that even after you've moved an element from list A to list B, it still keeps its old onclick handler, which still says "remove me from list A and add me to list B". You need to change its onclick handler to say "remove me from list B and add me to list A". Here's one way to fix that:
var usersA = document.getElementById("users-a");
var usersB = document.getElementById("users-b");
var onclickA;
var onclickB = function() {
usersA.appendChild(this);
this.onclick = onclickA;
return false;
};
onclickA = function() {
usersB.appendChild(this);
this.onclick = onclickB;
return false;
};
for (var i=0; i < usersA.getElementsByTagName("li").length; i++)
usersA.getElementsByTagName("li")[i].onclick = onclickA;
for (var i=0; i < usersB.getElementsByTagName("li").length; i++)
usersB.getElementsByTagName("li")[i].onclick = onclickB;
(I also got rid of the cloneNode stuff, for exactly the reasons that nnnnnn gives.)