Sorting Divs in jQuery by Custom Sort Order - javascript

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;

Related

JavaScript: How to skip over current item in array during a for loop? (continue?)

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.

Remove an item from datalist

I have a working Ajax call and function that populates a datalist. The datalist is used to select from a finite list an addition to an UL element on the page. Once I add one from the datalist to the actual list, I want to remove that option from the datalist. The brute force method I'm using is to clear the datalist and repopulate it.
function populate_datalist_from_array(list_id, list_str)
{
clearChildren( list_id );
var arr = eval ( list_str );
for (var i = 0; i < arr.length; i++) {
var opt = document.createElement('option');
opt.innerHTML = arr[i];
opt.value = arr[i];
document.getElementById(list_id).appendChild(opt);
}
}
function clearChildren( parent_id ) {
var childArray = document.getElementById( parent_id ).children;
if ( childArray.length > 0 ) {
document.getElementById( parent_id ).removeChild( childArray[ 0 ] );
clearChildren( parent_id );
}
}
I've verified that the list_str object is correct. i.e., it contains only the options not already in the current list. But after calling populate_datalist_from_array with that new list, the datalist in the dropdown doesn't change. Is this because the browser has essentially compiled all of the values that were there (like it were a normal, browser-based autocomplete) and doesn't 'forget' the values that I want removed?
Teemu's JsFiddle works fine. However, it's normally better to avoid recursion, and multiple DOM queries when not required.
Here is an edit that only requires a single DOM query, and is iterative. (Note decrement before index because this is a zero based list)
clearChildren = function (parent_id) {
var parent = document.getElementById(parent_id);
var childArray = parent.children;
var cL = childArray.length;
while(cL > 0) {
cL--;
parent.removeChild(childArray[cL]);
}
};
(In JSFiddle on MacBookPro I saved 10 ms - from 15 ms total - on a list of 500 elements, but could be more dramatic with larger DOM's on mobile).
If list_str really is OK, your code works. You can check it in action at jsFiddle.
The most general reason for a behaviour you've described, is that your code refreshes the page. If you remove type="button" from the "Change options" button in the linked fiddle, you'll get an error (due to the fiddle itself). In your page you probably have something similar invoking populate_datalist_from_array(). Notice, that also hitting Enter on an active text input will do submit/refresh.

How can I keep a jQuery DOM element reference when splicing/sorting an array?

I have an array of objects. One of the properties of these objects is a jQuery reference to a DOM element that may or may not actually be attached to the DOM at any given time;
For example:
Array = [{
name : 'whatever 1',
element : $('<div id="item_1" class="item"><img src="" /></div>')
},
{
name : 'whatever 2',
element : $('<div id="item_2" class="item"><img src="" /></div>')
}];
When this array is untouched I can detach and append these elements to the DOM without any troubles as well as use standard jQuery methods upon the elements.
For example:
Array[0].element.find('img');
...Will work fine.
However if I sort or splice this array, I lose the references.
I understand the reason why this is happening but what I would like to know is if there is anyway around this so that this element can continually be changed, attached, detached, modified while sorting or splicing the overall array itself?
Thanks in advance.
EDIT:
Here is a code sample of my rearrange function:
rearrangeItems : function(){
var self = this;
var offset = 0;
// get number of items that are less than insertindex
for(var i = 0; i < self.cache.selecteditems.length; i++) {
if(self.cache.selecteditems[i] < self.cache.rearrangepos){
offset++;
}
}
//subtract the offset from the intended insertion index
var rearrangeindex = self.cache.rearrangepos - offset;
var removedItems = [];
//sort the selected element indexes into ascending order
self.cache.selecteditems.sort(function (a, b) {
if (a < b) return -1;
else if (b < a) return 1;
return 0;
});
//remove the selected array elemens from the overall array and push them into the temporary array
for(var i = 0; i < self.cache.selecteditems.length; i++) {
var index = self.cache.selecteditems[i];
removedItems.push(self.cache.items.splice(index - removedItems.length, 1)[0]);
}
//Add the selected array elements back into the main array at the correct insertion point
self.cache.items.splice.apply(self.cache.items, [rearrangeindex, 0].concat(removedItems));
}
When calling this function all array elements are reordered exactly as intended.
Before reordering I can do the following:
self.cache.items[index].html.find('img');
Afterwards however, it will result in an empty object (the html property is the equivalent of the element property in my example above).
I would work with the ID, cause you have one. Don't know if this is the cleanest solution but it will work.
Calling your example like this:
$('#' + Array[0].element.attr('id')).find('img');
Hope this works for you.
Sadly this was down to my own stupidity. In my code I was referencing the element incorrectly.
I was actually doing the following:
self.cache.items[index].html.find('#image_' + index);
After reordering the elements I was intentionally resetting indexes afterwards, therefore when calling this after a sort/reorder the element was incorrect.
by switching to a class selector everything was fixed.
self.cache.items[index].html.find('.image_item');
How embarrassing! My apologies to all.

Displaying a Random div on Click

I am using the Colorbox Lightbox script to call a hidden div on a page. It works great but there is a catch. I have 15 hidden divs. When a link is clicked I would like a new lightbox to show each time without repeating until all have been shown. I do not know how to do this.
Here is my code to call the lightbox:
$(".colorbox").colorbox({
inline:true
});
Here is the HTML of the hidden divs
<div class="hide">
<div id="lightbox1">
<!-- Content -->
</div>
<div id="lightbox2">
<!-- Content -->
</div>
<!-- etc -->
</div>
How would I call each div at random until all have been shown then start over?
Also is there a way that once divs 1 - 15 have been shown to then show one last div (id="last-div") before restarting?
Note: All divs would be called on a click and I am using jQuery 1.8.2.
I do not know where to start, I have seen scripts using Math.random() but I do not understand enough to make that work.
UPDATE
I have tried Ian's answer but the lightbox is not showing (but I can see in the console log that the script is working)
Originally he has this in his script:
$(selector).show();
which I changed to this:
$(selector).colorbox({inline:true });
What do I need to do to call the lightbox?
Note: No errors are thrown.
So my idea was similar to Eric's, but I wanted to make it work "completely". So instead of storing references to all the divs in an array, I just decided to store an array of ints representing each div. The way I eventually select them with jQuery is "#lightbox + i", so if you don't have this exact structure (where the divs have an id like "lightbox" and an int - from 1 to the last count), then you can use .eq() or nth-child. It won't be the exact same results, but it will have the same random effect, just done in a different way. I found a function that "randomizes" an array - I'm guessing like what Eric's Shuffle does. But here's where I got it from - How to randomize (shuffle) a JavaScript array? . I had to modify it to return a new array instead of modify the one passed to the function. Also, I kept everything in the document.ready scope, instead of the global scope, so things are passed/returned a lot. It worked fine before when I had all and randomed declared globally and didn't pass them around, I just thought this would be "better" since they weren't global.
Here's the fiddle:
http://jsfiddle.net/6qYCL/1/
And here's the Javascript:
$(document).ready(function () {
var all,
randomed;
all = generateAll();
randomed = generateRandomed(all);
$("#generator").on("click", function (evt) {
evt.preventDefault();
randomed = doNext(all, randomed);
});
});
function generateAll() {
// Generates the array of "all" divs to work on
var a = [];
var divs = $(".hide > div.lightbox");
for (var i = 1; i <= divs.length; i++) {
a.push(i);
}
console.log("List of divs available to toggle: " + a);
return a;
}
function generateRandomed(all) {
// Randomizes the original array
randomed = fisherYates(all);
console.log("Setting randomized array: " + randomed);
return randomed;
}
function doNext(all, randomed) {
$(".lightbox, #last-div").hide();
if (randomed.length < 1) {
console.log("All lightboxes toggled, showing last, then starting over");
$("#last-div").show();
randomed = generateRandomed(all);
} else {
var next = randomed.shift();
var selector = "#lightbox" + next;
console.log("Showing " + selector);
$(selector).show();
console.log("What's left: " + randomed);
}
return randomed;
}
// Randomizes an array and returns the new one (doesn't modify original)
function fisherYates ( myArray ) {
var return_arr = myArray.slice(0);
var i = return_arr.length;
if ( i == 0 ) return false;
while ( --i ) {
var j = Math.floor( Math.random() * ( i + 1 ) );
var tempi = return_arr[i];
var tempj = return_arr[j];
return_arr[i] = tempj;
return_arr[j] = tempi;
}
return return_arr;
}
It accounts for getting to the end of the list and display #new-div like you mentioned, then starting the process over. If you look in your browser's console, you can "watch" what's happening during initialization and when clicking the link.
I think this is close to what you were looking for. I'm not sure which is a better solution - storing references to the elements or just an array of ints to loop through and eventually find. I know there are many variations on how to do this - when/how to store the counting stuff, when/how to randomize the array or retrieve a random value (and how to keep track of which has been used), where to store all references, and plenty more. I hope this at least helps!
Create an array of all of them, then shuffle that array, then pull the next one each time you get a click. When you run out, you can repopulate the array if necessary.
Something like this (using the Shuffle method from this source):
Fiddle
var array = $(".hide div").toArray(); // Add all divs within the hide div to an array
var randomArray = Shuffle(array); // Shuffle the array
$("a").click(function() {
if (randomArray.length > 0)
alert(randomArray.shift().innerHTML); // Show contents of div, as an example
else
alert("None left!");
return false;
});
The solution below works by passing an array of elements to a function. As each div is displayed it is taken out of the array. Then from the divs left in the array the next one is picked at random.
// call this function on page load
function begin( ) {
var arr = $(".hide div").toArray();
// further elements can be added to arr i.e. last-div
showDivs( arr.length, arr );
}
// begin displaying divs
function showDivs( numberOfDivs, divArray ) {
var i, lastDiv;
function nextDiv( ) {
// depending on number of slides left get random number
i = randomInt( numberOfDivs );
if( lastDiv ) { $(lastDiv).hide() };
$( divArray[ i ] ).fadeIn( 3000 );
// now that this div has been displayed
// remove from array and cache
lastDiv = divArray.splice( i, 1 );
numberOfDivs--;
// no more divs to display
if( numberOfDivs == 0 ) { return };
setTimeout( nextDiv, 4000);
}
setTimeout( nextDiv, 1000);
}
// calculate next random index
function randomInt( divsLeft ) {
var i = Math.random() * divsLeft;
return Math.round( i );
}
Fiddle here

select an array of elements and use them

Using this syntax:
var position = array($('#ipadmenu > section').attr('data-order'));
I cannot get my code to work. I have never used arrays before so im kind of lost on how to use them. (especially in jquery).
How would I make an array of all section elements and associate the value of data-order to that list. Example:
first section - data-order:1
second section - data-order:2
etc and then use that info afterwards.
Thank you!
Since .attr just gets one attribute -- the first one found by the jQuery selector -- you need to build your array element by element. One way to do that is .each (you can also use .data to extract data attributes):
var position = new Array;
$('#ipadmenu > section').each(function() {
position.push($(this).data('order'));
});
alert(position[0]); // alerts "1"
This will be an indexed array, not an associative array. To build one of those (which in JavaScript is technically an object, not any kind of array) just change the inner part of your .each loop:
var position = {};
$('#ipadmenu > section').each(function(i) {
position["section"+i] = $(this).data('order');
});
The resulting object position can now be accessed like:
alert(position['section1']); // alerts "1"
A different approach involves using jQuery.map, but since that only works on arrays, not jQuery objects, you need to use jQuery.makeArray to convert your selection into a true array first:
var position = $.map($.makeArray($('#ipadmenu > section')), function() {
return $(this).data('order');
} ); // position is now an indexed array
This approach is technically shorter than using .each, but I find it less clear.
Javascript:
var orders = [];
$('#ipadmenu > section').each(function() {
orders.push($(this).data('order'))
});
HTML:
<div id="ipadmenu">
<section data-order="1">1</section>
<section data-order="2">2</section>
</div>
You will want to do something like this:
// Get the elements and put them in an array
var position = $('#ipadmenu section').toArray();
console.log(position);
// Loop through the array
for (var i = 0; i < position.length; i++){
// Display the attribute value for each one
console.log("Section " + i + ": " + $(position[i]).attr('data-order'));
}
Working example here: http://jsfiddle.net/U6n8E/3/

Categories

Resources