I need to pick up list items from an list, and then perform operations like adding event handlers on them. I can think of two ways of doing this.
HTML:
<ul id="list">
<li id="listItem-0"> first item </li>
<li id="listItem-1"> second item </li>
<li id="listItem-2"> third item </li>
</ul>
Using the IDs-
for(i=0;i<3;i++)
{
document.getElementById("listItem-"+i).addEventListener("click",foo,false);
}
Using childNodes property-
for(i=0;i<3;i++)
{
document.getElementById("list").childNodes[i]
.addEventListener("click",foo,false);
}
The reason i'm using the first approach is that in the function foo, if I want the index at which the item is located in the list, i can do it by splitting the id -
function foo()
{
tempArr = this.id.split('-');
index = tempArr[tempArr.length-1]; // the last element in the array
}
I can't think of a way of doing it by using the second method, that is, without using the id naming scheme.
The questions:
How do I get the index using the second method or any better method
Are there some very bad affects of following the first method ?
You can avoid adding event handlers to each list item by adding a single event handler to the containing element (the unordered list) and leveraging the concept of event bubbling. In this single event handler, you can use properties of the event object to determine what was clicked.
It appears that you are wanting to map data in an array to list items. While parsing out an array index out of the list item id would work, another approach would be to store a "key" value on the list item as an expando, and use javascript object properties to do a lookup for your data.
Partial Example:
<li key="myKey">
//oData is a object (eg. var oData = {};) that has been populated with properties
//whose value is the desired data (eg. oData["myKey"] = 123;)
alert(oData[event.srcElement.key]); // alerts 123
As far as bad effects of the first technique you showed, one bad effect is that with many list items you end up with many event handlers being defined, which at some point would have an impact on performance.
Also note that you may be unintentionally creating a global var in your loops by omitting the var keyword for "i".
If you opt to use jQuery it comes as simple as:
$('ul#list li').click(function () {
var i = this.id.split('-').pop();
alert( i );
});
Maybe something similar to:
var lis = document.getElementById("list").getElementsByTagName("li");
for (var i = 0, li; li = lis[i]; ++i) {
li.addEventListener("click", (function(pos) {
return function() {
alert(pos);
};
})(i), false);
}
Or, with some inspiration from J cs answer and custom data attributes:
var lis = document.getElementById("list").getElementsByTagName("li");
for (var i = 0, li; li = lis[i]; ++i) {
li.setAttribute("data-index", i); // Or whatever value you want...
li.addEventListener("click", function() {
alert(this.getAttribute("data-index"));
}, false);
}
As stated before, if you want to retrieve the list of li, you should use getElementsByTagName on your ul, because childNodes might retrieve some text nodes as well as li nodes.
Now if what you need is to use the index in the event handler, you might be better directly using a closure in order to reuse the loop variable inside the event handler.
var lis = document.getElementById("list").getElementsByTagName("li");
for( var i = 0, l = lis.length; i < l; ++i )
{
(function(){
// As a new variable will be created for each loop,
// you can use it in your event handler
var li = lis[i];
li.addEventListener("click", function()
{
li.className = "clicked";
}, false);
})();
}
But you may consider event delegation for this purpose. You only have to attach an event handler to the parent element and find the clicked element using the target attribute.
document.getElementById("list").addEventListener("click", function(event)
{
var li = event.target;
if( li.nodeName.toLowerCase() == "li" )
{
...
}
}, false);
Related
when I remove this with id it works but when I tried with className it didn't work
const remov= document.createElement('a');
remov.classList='clsx';
remov.textContent='X';
remov.setAttribute("id", "Div1");
remov.onclick = function (){
document.getElementById('Div1').remove(); }
this is my code im trying to remove the element that i've created with id 'Div1' emphasized text
const lista = document.getElementById('list');
eventlisteners();
function eventlisteners(){
document.querySelector('#form').addEventListener('submit', yrlist);
}
//function
function yrlist(e){
e.preventDefault();
var textare =document.getElementById('textar').value;
const li= document.createElement('li');
li.textContent=textare;
li.classList='clsli';
li.setAttribute("id", "Div1");
const remov= document.createElement('a');
remov.classList='clsx';
remov.textContent='X';
remov.setAttribute("id", "Div1");
remov.onclick = function (){
document.getElementById('Div1').remove();
}
li.appendChild(remov);
lista.appendChild(li);
}
Note that getElementById returns single element while getElementsByClassName returns list of elements. So you need to iterate through this list. And remove each element.
For example you can do this using loop:
const elements = document.getElementsByClassName("clsx");
for(var x=0; x < elements.length; x++) {
elements[x].remove();
}
remov.classList = 'clasx'; is just wrong. The classList member is actually a DOMTokenList, not a string. As such, it has functions of it's own. Rather than trying to set this member, you need to use the add function it contains.
I.e
remov.classList.add('clsx');
It seems that clicking on the element will have the effect of removing it. This is functionality you've given it at creation time. There's a better way to go about this. Rather than trying to find the element, (which must be identifiable in some manner) why not just have the element remove itself? If you connect the action and the event using AddEventListener, rather than by overwriting a member of the element yourself, a neat thing happens - the function that gets called behaves as though it was a part of the element that triggered it, and so the this keyword refers to itself. It's a huge help, but for the uninitiated, can be a real headache.
const removeMe = document.createElement('a');
removeMe.classList.add('clsx');
removeMe.textContent = 'X';
removeMe.addEventListener('click', function(evt){ this.remove(); }, false);
li.appendChild(removeMe);
EDIT: I don't want to skip index 1. I want to skip the current (clicked on) element. Also, see below for more of the code as requested. You'll see that I have a class CatListItem and five instances of that class in an array allCatListItems.
Here's some context for the question: I have a list of cats. When I click on a cat's name (list item), I want that cat's picture and other info to be appended to the page (got that down). When a cat is clicked, I also want any other cat that is being displayed to be hidden (that way there is only one cat on the screen at a time).
I'm trying to accomplish this with a for loop, but obviously if it iterates over every item in the array, then when I click an item, the cat being clicked will be hidden as well.
I want to skip the current item in the array and only run the code on the other items. Using continue, I know I can skip a specific item (item 1 in the below example). This will run my code on every item in the array except that at index one. But how can I make that continue dynamic? Meaning... how can I hide all of the cats, except the one being currently clicked?
Here's the loop that skips index 1:
CatListItem.prototype.hideCats = function() {
allCatListItems.forEach(function(cat) {
cat.a.addEventListener('click', function() {
for (var i = 0; i < allCatListItems.length; i++) {
if (i === 1) {
continue;
}
allCatListItems[i].img.className = 'hide';
};
});
});
}
var allCatListItems = [
catListItem1 = new CatListItem('El', 'images/el.jpg', 'el'),
catListItem2 = new CatListItem('Widdle Baby', 'images/widdle-baby.jpg', 'widdle-baby'),
catListItem3 = new CatListItem('Mama', 'images/mama.jpg', 'mama'),
catListItem4 = new CatListItem('Legion', 'images/legion.jpg', 'legion'),
catListItem5 = new CatListItem('Boy', 'images/boy.jpg', 'boy'),
];
EDIT: Here's a fiddle.JSFIDDLE Click the names to see the functionality without the hideCats function. Then uncomment where it says to to see my issue.
I'm starting to think maybe a for loop isn't the best option?
In that case compare the event.target(its the element clicked)
EDIT: allCatListItems[i] needs it's .a property attached to it in the if statement (this is what contains the anchor element). This is because the event listener is grabbing an anchor tag, so e.target will be returning an anchor tag as well. The if statement will never return as true if you aren't comparing the same type of element.
cat.a.addEventListener('click', function(e) {
for (var i = 0; i < allCatListItems.length; i++) {
if (allCatListItems[i].a === e.target) {
continue;
}
allCatListItems[i].img.className += ' hide';
}
});
Here is a jsfiddle, it doesn't use the same element names, but it should be doing what you want. https://jsfiddle.net/5qb4rwzc/
$('li').on('click', function() {
var index = $(this).index();
var items = document.getElementsByTagName('li');
for(var i = 0; i < items.length; i++) {
if(i === index) continue;
items[i].style = "display:none;";
}
});
Its really depend on how you call the function "hideCat". Realizing that each time that function is called, more eventListeners are add to every cat item. Each time a cat is clicked, more than one event fired. Perhaps you should re-structure how to attach eventListeners to each cat item.
I want to save a DOM subtree (everything under the div called "block diagram") then paint it on a div called "bus_diagram". Saving the childNodes property doesn't seem to capture all of the elements for some reason.
here's the javascript I'm using. On calling the function "dostuff()" everything under "block_diagram" should go to "bus_diagram"
var SAVED_BLOCK_DOM = null;
function save_block() {
SAVED_BLOCK_DOM = document.getElementById("block_diagram").childNodes;
}
function refresh_block() {
for (var i = 0; i < SAVED_BLOCK_DOM.length; i++) {
document.getElementById("bus_diagram").appendChild(SAVED_BLOCK_DOM[i]);
}
}
function dostuff() {
save_block();
refresh_block();
}
Here's a JSFiddle: http://jsfiddle.net/BFp5s/3/
.childNodes is a live collection of nodes and as you start doing .appendChild() on the nodes (which moves the elements to a different place in the DOM), the live collection changes while you are iterating it, causing you to miss nodes. So, when the index of your for loop is 0, you do .appendChild() on the 0th element of the list. That removes that element from the live list. You then increment your index to 1, but the next item to process is now in the 0th spot in the list causing you to process every other item.
You can either make a copy of the live list into an array (so it won't change while iterating it) or change the way you iterate the list.
For example, you can change save_block() to this:
function save_block() {
SAVED_BLOCK_DOM = Array.prototype.slice.call(document.getElementById("block_diagram").childNodes, 0);
}
This makes SAVED_BLOCK_DOM into a normal array so it won't change while you iterate it.
jsFiddle Demo: http://jsfiddle.net/jfriend00/R8c94/
Or, if you want/need to support IE6/7/8 support which won't work with the above copy mechanism, you can just copy the nodeList manually:
function save_block() {
SAVED_BLOCK_DOM = [];
var list = document.getElementById("block_diagram").childNodes;
for (var i = 0; i < list.length; i++) {
SAVED_BLOCK_DOM.push(list[i]);
}
}
If you don't need SAVED_BLOCK_DOM to continue to hold the list of nodes and want to support IE8, you can change the way you iterate like this:
function refresh_block() {
while (SAVED_BLOCK_DOM.length) {
document.getElementById("bus_diagram").appendChild(SAVED_BLOCK_DOM[0]);
}
}
jsFiddle demo: http://jsfiddle.net/jfriend00/PK7Tg/
I'm trying to add a event listener for clicking and I want to know the position of the node that was clicked
function evl(etna) {
document.addEventListener("click", function (el) {
alert("You clicked on " + 'the name of element that was clicked or his array code');
}, false);
};
where etna is:
document.getElementsByTagName("*");
function evl(etna){
document.addEventListener("click",function (el) {
var clickedElement = el.target || el.srcElement;
alert("Link detected a click. Cancellable: "+clickedElement.name);
for(var i = 0; i < etna.length; i++) {
if(etna[i] === clickedElement) {
//i is a position of an element in etna
break;
}
}
},false);
};
You can use this which will point to a clicked element. As to Phil H IE 8 does not work that way. But anyway, there should be used .target or .srcElement. And maybe it will be better to get its id. Name attribute is not valid for divs, spans, etc.
But also you are attaching an event to a document. And this will point to a document.
Instead of that you should use el.target || el.srcElement where .target/.srcElement is a pointer to a node where click actually happened.
Also, I do not think you can get index of an element in array (actually, node list) returned by document.getElementsByTagName("*") (well, you can get that list and iterate through it in loop and check each element if it is eaqual to this). Plus, I have no idea why it could be needed.
Add a loop and set the event listener differently for each item in the etna array:
function evl(etna){
for(var i=0; i < etna.length; ++i) {
(function() {
var thisNode = etna[i];
var localval = i;
etna[i].addEventListener("click",function (el) {
alert("Link detected a click. Cancellable: "+ thisNode.id + " which is id " + localval );
},false);
})();
}
}
Working jsfiddle: http://jsfiddle.net/5xDjE/
The function that is immediately called is merely to force the scoping of thisNode and localval, otherwise all the elements get references to the same variable (javascript scoping is interesting).
I would advise against using the index (scoped via localval) because it requires retaining the original array of nodes. Since nodes change over time and javascript does reference counting for you, you want to avoid these kinds of long arrays of nodes.
Note that this doesn't always have element that was clicked, in IE8 and below this points to the global window object.
I'm trying to re-sort the child elements of the tag input by comparing
their category attribute to the category order in the Javascript
variable category_sort_order. Then I need to remove divs whose category attribute
does not appear in category_sort_order.
The expected result should be:
any
product1
product2
download
The code:
<div id="input">
<div category="download">download</div>
<div category="video">video1</div>
<div category="video">video2</div>
<div category="product">product1</div>
<div category="any">any</div>
<div category="product">product2</div>
</div>
<script type="text/javascript">
var category_sort_order = ['any', 'product', 'download'];
</script>
I really don't even know where to begin with this task but if you could please provide any assistance whatsoever I would be extremely grateful.
I wrote a jQuery plugin to do this kind of thing that can be easily adapted for your use case.
The original plugin is here
Here's a revamp for you question
(function($) {
$.fn.reOrder = function(array) {
return this.each(function() {
if (array) {
for(var i=0; i < array.length; i++)
array[i] = $('div[category="' + array[i] + '"]');
$(this).empty();
for(var i=0; i < array.length; i++)
$(this).append(array[i]);
}
});
}
})(jQuery);
and use like so
var category_sort_order = ['any', 'product', 'download'];
$('#input').reOrder(category_sort_order);
This happens to get the right order for the products this time as product1 appears before product2 in the original list, but it could be changed easily to sort categories first before putting into the array and appending to the DOM. Also, if using this for a number of elements, it could be improved by appending all elements in the array in one go instead of iterating over the array and appending one at a time. This would probably be a good case for DocumentFragments.
Just note,
Since there is jQuery 1.3.2 sorting is simple without any plugin like:
$('#input div').sort(CustomSort).appendTo('#input');
function CustomSort( a ,b ){
//your custom sort function returning -1 or 1
//where a , b are $('#input div') elements
}
This will sort all div that are childs of element with id="input" .
Here is how to do it. I used this SO question as a reference.
I tested this code and it works properly for your example:
$(document).ready(function() {
var categories = new Array();
var content = new Array();
//Get Divs
$('#input > [category]').each(function(i) {
//Add to local array
categories[i] = $(this).attr('category');
content[i] = $(this).html();
});
$('#input').empty();
//Sort Divs
var category_sort_order = ['any', 'product', 'download'];
for(i = 0; i < category_sort_order.length; i++) {
//Grab all divs in this category and add them back to the form
for(j = 0; j < categories.length; j++) {
if(categories[j] == category_sort_order[i]) {
$('#input').append('<div category="' +
category_sort_order[i] + '">'
+ content[j] + '</div>');
}
};
}
});
How it works
First of all, this code requires the JQuery library. If you're not currently using it, I highly recommend it.
The code starts by getting all the child divs of the input div that contain a category attribute. Then it saves their html content and their category to two separate arrays (but in the same location.
Next it clears out all the divs under the input div.
Finally, it goes through your categories in the order you specify in the array and appends the matching child divs in the correct order.
The For loop section
#eyelidlessness does a good job of explaining for loops, but I'll also take a whack at it. in the context of this code.
The first line:
for(i = 0; i < category_sort_order.length; i++) {
Means that the code which follows (everything within the curly brackets { code }) will be repeated a number of times. Though the format looks archaic (and sorta is) it says:
Create a number variable called i and set it equal to zero
If that variable is less than the number of items in the category_sort_order array, then do whats in the brackets
When the brackets finish, add one to the variable i (i++ means add one)
Then it repeats step two and three until i is finally bigger than the number of categories in that array.
A.K.A whatever is in the brackets will be run once for every category.
Moving on... for each category, another loop is called. This one:
for(j = 0; j < categories.length; j++) {
loops through all of the categories of the divs that we just deleted from the screen.
Within this loop, the if statement checks if any of the divs from the screen match the current category. If so, they are appending, if not the loop continues searching till it goes through every div.
Appending (or prepending) the DOM nodes again will actually sort them in the order you want.
Using jQuery, you just have to select them in the order you want and append (or prepend) them to their container again.
$(['any', 'product', 'video'])
.map(function(index, category)
{
return $('[category='+category+']');
})
.prependTo('#input');
Sorry, missed that you wanted to remove nodes not in your category list. Here is the corrected version:
// Create a jQuery from our array of category names,
// it won't be usable in the DOM but still some
// jQuery methods can be used
var divs = $(['any', 'product', 'video'])
// Replace each category name in our array by the
// actual DOM nodes selected using the attribute selector
// syntax of jQuery.
.map(function(index, category)
{
// Here we need to do .get() to return an array of DOM nodes
return $('[category='+category+']').get();
});
// Remove everything in #input and replace them by our DOM nodes.
$('#input').empty().append(divs);
// The trick here is that DOM nodes are selected
// in the order we want them in the end.
// So when we append them again to the document,
// they will be appended in the order we want.
I thought this was a really interesting problem, here is an easy, but not incredibly performant sorting solution that I came up with.
You can view the test page on jsbin here: http://jsbin.com/ocuta
function compare(x, y, context){
if($.inArray(x, context) > $.inArray(y, context)) return 1;
}
function dom_sort(selector, order_list) {
$items = $(selector);
var dirty = false;
for(var i = 0; i < ($items.length - 1); i++) {
if (compare($items.eq(i).attr('category'), $items.eq(i+1).attr('category'), order_list)) {
dirty = true;
$items.eq(i).before($items.eq(i+1).remove());
}
}
if (dirty) setTimeout(function(){ dom_sort(selector, order_list); }, 0);
};
dom_sort('#input div[category]', category_sort_order);
Note that the setTimeout might not be necessary, but it just feels safer. Your call.
You could probably clean up some performance by storing a reference to the parent and just getting children each time, instead of re-running the selector. I was going for simplicity though. You have to call the selector each time, because the order changes in a sort, and I'm not storing a reference to the parent anywhere.
It's seems fairly direct to use the sort method for this one:
var category_sort_order = ['any', 'product', 'download'];
// select your categories
$('#input > div')
// filter the selection down to wanted items
.filter(function(){
// get the categories index in the sort order list ("weight")
var w = $.inArray( $(this).attr('category'), category_sort_order );
// in the sort order list?
if ( w > -1 ) {
// this item should be sorted, we'll store it's sorting index, and keep it
$( this ).data( 'sortindex', w );
return true;
}
else {
// remove the item from the DOM and the selection
$( this ).remove();
return false;
}
})
// sort the remainder of the items
.sort(function(a, b){
// use the previously defined values to compare who goes first
return $( a ).data( 'sortindex' ) -
$( b ).data( 'sortindex' );
})
// reappend the selection into it's parent node to "apply" it
.appendTo( '#input' );
If you happen to be using an old version of jQuery (1.2) that doesn't have the sort method, you can add it with this:
jQuery.fn.sort = Array.prototype.sort;