move up and down elements in a multiple select not working - javascript

I have created an angularjs app with multiple select upon which I am having up and down button , within which when I click up and down button corresponding movement of items should be done within the multiple select, I have a sample stuff which has been done with normal javascript which does the similar thing correctly like as shown in this fiddle, but when I tried to implement the same stuff in AngularJS its not working properly
Can anyone please tell me some solution for this
My code is as given below
JSFiddle
html
<div ng-app='myApp' ng-controller="ArrayController">
<select id="select" size="9" ng-model="persons" ng-options="item as item.name for item in peoples | orderBy:'name'" multiple></select>
<br/>
<button ng-click="moveUp()">Up</button>
<br/>
<button ng-click="moveDown()">Down</button>
<br/>
</div>
script
var app = angular.module('myApp', []);
app.controller('ArrayController', function ($scope) {
$scope.peoples = [{
name: 'Jacob'
}, {
name: 'Sunny'
}, {
name: 'Lenu'
}, {
name: 'Mathew'
}, {
name: 'Ferix'
}, {
name: 'Kitex'
}];
$scope.moveUp = function () {
var select = document.getElementById("select");
var i1=0, i2=1;
while (i2 < select.options.length) {
swapIf(select,i1++,i2++);
}
};
$scope.moveDown = function () {
var select = document.getElementById("select");
var i1=select.options.length-1, i2=i1-1;
while (i1 > 0) {
swapIf(select,i1--,i2--);
}
};
var swapVar = '';
function swapIf(sel,i1,i2) {
if ( ! select[i1].selected && select[i2].selected) {
swapVar = select[i2].text;
select[i2].text = select[i1].text;
select[i1].text = swapVar;
swapVar = select[i2].value;
select[i2].value = select[i1].value;
select[i1].value = swapVar;
select[i1].selected = true;
select[i2].selected = false;
}
}
});

persons will return an array of of whatever items are selected in the list. One solution is to create a for loop that gets the indexOf of each item in the persons array. splice that items out of the peoples array, increment/decrement the index, and splice it back in to the peoples array.
Here is a new moveUp() function that can move up multiple selected items:
$scope.moveUp = function () {
for(var i = 0; i < $scope.persons.length; i++) {
var idx = $scope.peoples.indexOf($scope.persons[i])
console.log(idx);
if (idx > 0) {
var itemToMove = $scope.peoples.splice(idx, 1)
console.log(itemToMove[0])
$scope.peoples.splice(idx-1, 0, itemToMove[0]);
}
}
};
Here is the updated moveDown() function:
$scope.moveDown = function () {
for(var i = 0; i < $scope.persons.length; i++) {
var idx = $scope.peoples.indexOf($scope.persons[i])
console.log(idx);
if (idx < $scope.peoples.length) {
var itemToMove = $scope.peoples.splice(idx, 1)
console.log(itemToMove[0])
$scope.peoples.splice(idx+2, 0, itemToMove[0]);
}
}
};
Here is the Working Demo (Not working so well, just kept for reference - see below)
This solution also maintains the separation between the View and the Controller. The controller has the job of manipulating the data, the view displays that data. This way we can avoid any uncomely entanglement. DOM manipulations from within the controller are incredibly difficult to test.
EDIT after some tinkering:
So my previous solution worked in some cases but would perform oddly with different select combinations. After some digging, I found it necessary to add track by:
<select id="select" size="9" ng-model="persons" ng-options="item as item.name for item in peoples track by item.name" multiple>
It seems the select would return the persons object with arbitrary selection orders and this was messing things up, especially after you clicked a few times, it seemed to get confused about where things were.
Additionally, I had to clone and reverse the persons array when moving items down because when adding track by item.name it returns the items in order, but if you try to iterate through the array, moving each one down, you are potentially impacting the location of other items in the array (further producing unpredictable behavior). So we need to start from the bottom and work our way up when moving multiple items down.
Here is a solution in which I seem to have eliminated any unpredictable behavior when making multiple arbitrary selections:
Working Demo
EDIT:
One bug I have found is that weird things happen when you move multiple selected items all the way up or down, and then try to move it that direction one more time. Any further movement without reselecting produces unpredictable results.
EDIT:
The unpredictable behavior mentioned in the previous edit was because the functions were seeing that, although the first item was in it's final position, the second, third, fourth, etc. items were not in an end position, and thus it tried to move them which led to crazy re-ordering of items that were already pushed all the one to the top or bottom. In order to solve this I set a var that would track the position of the previously moved item. If the current item was found to be in the adjacent position it would simply leave it there and move on.
The final functions look something like this:
$scope.moveUp = function () {
var prevIdx = -1;
var person = $scope.persons.concat();
console.log($scope.persons);
for(var i = 0; i < $scope.persons.length; i++) {
var idx = $scope.peoples.indexOf($scope.persons[i])
console.log(idx);
if (idx-1 === prevIdx) {
prevIdx = idx
} else if (idx > 0) {
var itemToMove = $scope.peoples.splice(idx, 1)
console.log(itemToMove[0])
$scope.peoples.splice(idx-1, 0, itemToMove[0]);
}
}
};
(Hopefully) Final Demo
EDIT:
I enjoyed this problem and wanted to have a better solution in case there were duplicate list items. This was fairly easy to solve by giving each object in the array a unique id key, and then changing track by item.name to track by item.id, and all works as before.
Working Demo for Duplicates

Demo
You need to update your swapIf implementation so that it swaps the model, not options from the view:
function swapIf(sel,i1,i2) {
if ( ! select[i1].selected && select[i2].selected) {
var obj1 = $scope.peoples[i1];
var obj2 = $scope.peoples[i2];
$scope.peoples[i2] = obj1;
$scope.peoples[i1] = obj2;
select[i1].selected = true;
select[i2].selected = false;
}
}
Also, remove the orderBy in the view, and initialize the ordering in the controller using the $filter service. The reason you need to do this is because the list is re-ordered whenever the user clicks the up/down button.

Related

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.

Traverse a nested list with Javascript/jQuery and store in array

I've seen several questions about converting an array/object into a nested list, but I've found only one relevant question to my issue. I've tried a few ways of accessing children of an element, but it only breaks my code further.
I have a nested, unordered list.
<div id="sortableSitemap">
<ul class="sortable ui-sortable">
<li id="world_news_now"><div>World</div>
<ul>
<li id="the_news"><div>The News</div></li>
</ul>
</li>
<li id="sports_news_cool"><div>Sports</div></li>
</ul>
</div>
At the moment, it contains 3 items and appears like so:
-World
--The News
-Sports
There can be any number of nodes with varying depths. I'm trying to store the list into an array with some additional information.
Each node gets a numeric, sequential ID (first node is 1, second is 2) based on the order it appears, regardless of depth (i.e. World = 1, The News = 2, Sports = 3). I also want to store the ID of a node's parent (root is 0). So, the parent IDs would be: World = 0, The News = 1, Sports = 0.
The code below seems to work except when the list is like the one above. In that case, it assigns The News = 3 and its parent = 2 (Sports). For some reason, iterating through the items (children) ends up getting to the <ul> last, even if it's directly after an open <li>.
All but one jQuery solution I found ignores depth, and even then, I need the actual parent node's ID (which I currently keep in a stack based on whether I've gone down a level).
Why is this happening, and how can I modify my code to go through the list recursively?
var count = 0;
var pages = [];
var parentStack = [];
function createNewLevel(items) {
var length = items.length;
for (var i = 0; i < length; i++) {
if (items[i].tagName == 'UL') {
parentStack.push(count);
createNewLevel($(items[i]).children().get());
parentStack.pop();
} else {
++count;
pages.push({
pId: parentStack[parentStack.length - 1],
urlStr: $(items[i]).attr('id'), myId: count
});
}
}
}
createNewLevel($('#sortableSitemap ul').get());
console.log(pages);
Update: Here's a jsFiddle to show how the code does not work ("The News" should have "World" as its parent node).
I've modified your original code. This should work for all combinations of nested lists. Instead of using children().get(), I used the native JS child methods. It will traverse everything in the list, but ignore elements unless they are <li> or <ul>. Good luck.
var count = 0;
var pages = [];
var parentStack = [];
var result = {};
parentStack.push(0);
function createNewLevel(obj) {
var obj = obj || document.getElementById('sortableSitemap');
if (obj.tagName == 'LI') {
++count;
pages.push({
pId: parentStack[parentStack.length - 1],
urlStr: obj.id, myId: count
});
}
if (obj.hasChildNodes()) {
var child = obj.firstChild;
while (child) {
if (child.nodeType === 1) {
if (child.tagName == 'UL') {
parentStack.push(count);
}
createNewLevel(child);
if (child.tagName == 'UL') {
parentStack.pop();
}
}
child = child.nextSibling;
}
}
}
createNewLevel();

Is this as inefficient as it feels?

I have a webpage with two lists. A source list (represented by availableThings) populated by a search, and items that the user has selected (selectedThings). I want to maintain a unique list of selectedThings, so I want to remove already selected things from the list of available things. In my code snippet below, data.AvailableThings is populated from the server and has no knowledge of user-selected things. The user can select up to 3 items, ergo selectedThings.items will contain no more than 3 items. availableThings.items can potentially be a few thousand.
After availableThings.items gets populated, I feed it into ICanHaz for the HTML generation. FWIW, I'm using jQuery for drag behavior between the lists, but the question is jQuery-agnostic.
[... jQuery AJAX call snipped ...]
success: function (data) {
availableThings.items = [];
for (var thing in data.AvailableThings) {
var addToList = true;
for (var existing in selectedThings.items) {
if (existing.Id === thing.Id) {
addToList = false;
break;
}
}
if (addToList) {
availableThings.items.push(thing);
}
}
}
If n is the count of available things and m is the count of selected things, then this is O(n * m) whereas if you hashed by ID, you could turn this into O(n + m).
var existingIds = {};
for (var existing in selectedThings.items) {
existingIds[existing.Id] = existingIds;
}
availableThings.items = [];
for (var thing in data.AvailableThings) {
if (existingIds[thing.Id] !== existingIds) {
availableThings.items.push(thing);
}
}
If there is some sort of order (ordered by ID, name, or any field) to the data coming from the server, you could just do a binary search for each of the items in the selected set, and remove them if they are found. This would reduce it to O(m log n) for a dataset of n items where selection of m items is allowed. Since you've got it fixed at 3, it would essentially be O(log n).

jQuery/Javascript index into collection/map by object property?

I have the following Javascript defining an array of countries and their states...
var countryStateMap = [{"CountryCode":"CA","Name":"Canada","States":[{"StateCode":"S!","CountryCode":"CA","Name":"State 1"},{"StateCode":"S2","CountryCode":"CA","Name":"State 2"}]},"CountryCode":"US","Name":"United States","States":[{"StateCode":"S1","CountryCode":"US","Name":"State 1"}]}];
Based on what country the user selects, I need to refresh a select box's options for states from the selected Country object. I know I can index into the country collection with an int index like so...
countryStateMap[0].States
I need a way to get the Country by CountryCode property though. I know the following doesn't work but what I would like to do is something like this...
countryStateMap[CountryCode='CA'].States
Can this be achieved without completely rebuilding my collection's structure or iterating over the set each time to find the one I want?
UPDATE:
I accepted mVChr's answer because it worked and was the simplest solution even though it required a second map.
The solution we actually ended up going with was just using the country select box's index to index into the collection. This worked because our country dropdown was also being populated from our data structure. Here is how we indexed in...
countryStateMap[$('#country').attr("selectedIndex")]
If you need to do it any other way, use any of the below solutions.
One thing you could do is cache a map so you only have to do the iteration once:
var csmMap = {};
for (var i = 0, cl = countryStateMap.length; i < cl; i++) {
csmMap[countryStateMap[i].CountryCode] = i;
}
Then if countryCode = 'CA' you can find its states like:
countryStateMap[csmMap[countryCode]].States
countryStateMap.get = function(cc) {
if (countryStateMap.get._cache[cc] === void 0) {
for (var i = 0, ii = countryStateMap.length; i < ii; i++) {
if (countryStateMap[i].CountryCode === cc) {
countryStateMap.get._cache[cc] = countryStateMap[i];
break;
}
}
}
return countryStateMap.get._cache[cc];
}
countryStateMap.get._cache = {};
Now you can just call .get("CA") like so
countryStateMap.get("CA").States
If you prefer syntatic sugar you may be interested in underscore which has utility methods to make this kind of code easier to write
countryStateMap.get = _.memoize(function(cc) {
return _.filter(countryStateMap, function(val) {
val.CountryCode = cc;
})[0];
});
_.memoize , _.filter
love your local jQuery:
small little function for you:
getByCountryCode = function(code){var res={};$.each(countryStateMap, function(i,o){if(o.CountryCode==code)res=o;return false;});return res}
so do this then:
getByCountryCode("CA").States
and it returns:
[Object, Object]

Sorting Divs in jQuery by Custom Sort Order

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;

Categories

Resources