343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
/*
|
|
* jQuery UI Multiselect
|
|
*
|
|
* Authors:
|
|
* Michael Aufreiter (quasipartikel.at)
|
|
* Yanick Rochon (yanick.rochon[at]gmail[dot]com)
|
|
*
|
|
* Dual licensed under the MIT (MIT-LICENSE.txt)
|
|
* and GPL (GPL-LICENSE.txt) licenses.
|
|
*
|
|
* http://www.quasipartikel.at/multiselect/
|
|
*
|
|
*
|
|
* Depends:
|
|
* ui.core.js
|
|
* ui.sortable.js
|
|
*
|
|
* Optional:
|
|
* localization (http://plugins.jquery.com/project/localisation)
|
|
* scrollTo (http://plugins.jquery.com/project/ScrollTo)
|
|
*
|
|
* Todo:
|
|
* Make batch actions faster
|
|
* Implement dynamic insertion through remote calls
|
|
*/
|
|
|
|
|
|
(function($) {
|
|
|
|
$.widget("ui.multiselect", {
|
|
options: {
|
|
sortable: true,
|
|
searchable: true,
|
|
doubleClickable: true,
|
|
animated: 'fast',
|
|
show: 'slideDown',
|
|
hide: 'slideUp',
|
|
dividerLocation: 0.6,
|
|
availableFirst: false,
|
|
nodeComparator: function(node1,node2) {
|
|
var text1 = node1.text(),
|
|
text2 = node2.text();
|
|
return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1);
|
|
}
|
|
},
|
|
_create: function() {
|
|
this.element.hide();
|
|
this.id = this.element.attr("id");
|
|
this.container = $('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element);
|
|
this.count = 0; // number of currently selected options
|
|
this.selectedContainer = $('<div class="selected"></div>').appendTo(this.container);
|
|
this.availableContainer = $('<div class="available"></div>')[this.options.availableFirst?'prependTo': 'appendTo'](this.container);
|
|
this.selectedActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><span class="count">0 '+$.ui.multiselect.locale.itemsCount+'</span><a href="#" class="remove-all">'+$.ui.multiselect.locale.removeAll+'</a></div>').appendTo(this.selectedContainer);
|
|
this.availableActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><input type="text" class="search empty ui-widget-content ui-corner-all"/><a href="#" class="add-all">'+$.ui.multiselect.locale.addAll+'</a></div>').appendTo(this.availableContainer);
|
|
this.selectedList = $('<ul class="selected connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer);
|
|
this.availableList = $('<ul class="available connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer);
|
|
|
|
var that = this;
|
|
|
|
// set dimensions
|
|
this.container.width(this.element.width()+1);
|
|
this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation));
|
|
this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation)));
|
|
|
|
// fix list height to match <option> depending on their individual header's heights
|
|
this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1));
|
|
this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1));
|
|
|
|
if ( !this.options.animated ) {
|
|
this.options.show = 'show';
|
|
this.options.hide = 'hide';
|
|
}
|
|
|
|
// init lists
|
|
this._populateLists(this.element.find('option'));
|
|
|
|
// make selection sortable
|
|
if (this.options.sortable) {
|
|
this.selectedList.sortable({
|
|
placeholder: 'ui-state-highlight',
|
|
axis: 'y',
|
|
update: function(event, ui) {
|
|
// apply the new sort order to the original selectbox
|
|
that.selectedList.find('li').each(function() {
|
|
if ($(this).data('optionLink'))
|
|
$(this).data('optionLink').remove().appendTo(that.element);
|
|
});
|
|
},
|
|
receive: function(event, ui) {
|
|
ui.item.data('optionLink').attr('selected', true);
|
|
// increment count
|
|
that.count += 1;
|
|
that._updateCount();
|
|
// workaround, because there's no way to reference
|
|
// the new element, see http://dev.jqueryui.com/ticket/4303
|
|
that.selectedList.children('.ui-draggable').each(function() {
|
|
$(this).removeClass('ui-draggable');
|
|
$(this).data('optionLink', ui.item.data('optionLink'));
|
|
$(this).data('idx', ui.item.data('idx'));
|
|
that._applyItemState($(this), true);
|
|
});
|
|
|
|
// workaround according to http://dev.jqueryui.com/ticket/4088
|
|
setTimeout(function() { ui.item.remove(); }, 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
// set up livesearch
|
|
if (this.options.searchable) {
|
|
this._registerSearchEvents(this.availableContainer.find('input.search'));
|
|
} else {
|
|
$('.search').hide();
|
|
}
|
|
|
|
// batch actions
|
|
this.container.find(".remove-all").click(function() {
|
|
that._populateLists(that.element.find('option').removeAttr('selected'));
|
|
return false;
|
|
});
|
|
|
|
this.container.find(".add-all").click(function() {
|
|
var options = that.element.find('option').not(":selected");
|
|
if (that.availableList.children('li:hidden').length > 1) {
|
|
that.availableList.children('li').each(function(i) {
|
|
if ($(this).is(":visible")) $(options[i-1]).attr('selected', 'selected');
|
|
});
|
|
} else {
|
|
options.attr('selected', 'selected');
|
|
}
|
|
that._populateLists(that.element.find('option'));
|
|
return false;
|
|
});
|
|
},
|
|
destroy: function() {
|
|
this.element.show();
|
|
this.container.remove();
|
|
|
|
$.Widget.prototype.destroy.apply(this, arguments);
|
|
},
|
|
_populateLists: function(options) {
|
|
this.selectedList.children('.ui-element').remove();
|
|
this.availableList.children('.ui-element').remove();
|
|
this.count = 0;
|
|
|
|
var that = this;
|
|
var items = $(options.map(function(i) {
|
|
var item = that._getOptionNode(this).appendTo(this.selected ? that.selectedList : that.availableList).show();
|
|
|
|
if (this.selected) that.count += 1;
|
|
that._applyItemState(item, this.selected);
|
|
item.data('idx', i);
|
|
return item[0];
|
|
}));
|
|
|
|
// update count
|
|
this._updateCount();
|
|
that._filter.apply(this.availableContainer.find('input.search'), [that.availableList]);
|
|
},
|
|
_updateCount: function() {
|
|
this.element.trigger('change');
|
|
this.selectedContainer.find('span.count').text(this.count+" "+$.ui.multiselect.locale.itemsCount);
|
|
},
|
|
_getOptionNode: function(option) {
|
|
option = $(option);
|
|
var node = $('<li class="ui-state-default ui-element" title="'+option.text()+'"><span class="ui-icon"/>'+option.text()+'<a href="#" class="action"><span class="ui-corner-all ui-icon"/></a></li>').hide();
|
|
node.data('optionLink', option);
|
|
return node;
|
|
},
|
|
// clones an item with associated data
|
|
// didn't find a smarter away around this
|
|
_cloneWithData: function(clonee) {
|
|
var clone = clonee.clone(false,false);
|
|
clone.data('optionLink', clonee.data('optionLink'));
|
|
clone.data('idx', clonee.data('idx'));
|
|
return clone;
|
|
},
|
|
_setSelected: function(item, selected) {
|
|
item.data('optionLink').attr('selected', selected);
|
|
|
|
if (selected) {
|
|
var selectedItem = this._cloneWithData(item);
|
|
item[this.options.hide](this.options.animated, function() { $(this).remove(); });
|
|
selectedItem.appendTo(this.selectedList).hide()[this.options.show](this.options.animated);
|
|
|
|
this._applyItemState(selectedItem, true);
|
|
return selectedItem;
|
|
} else {
|
|
|
|
// look for successor based on initial option index
|
|
var items = this.availableList.find('li'), comparator = this.options.nodeComparator;
|
|
var succ = null, i = item.data('idx'), direction = comparator(item, $(items[i]));
|
|
|
|
// TODO: test needed for dynamic list populating
|
|
if ( direction ) {
|
|
while (i>=0 && i<items.length) {
|
|
direction > 0 ? i++ : i--;
|
|
if ( direction != comparator(item, $(items[i])) ) {
|
|
// going up, go back one item down, otherwise leave as is
|
|
succ = items[direction > 0 ? i : i+1];
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
succ = items[i];
|
|
}
|
|
|
|
var availableItem = this._cloneWithData(item);
|
|
succ ? availableItem.insertBefore($(succ)) : availableItem.appendTo(this.availableList);
|
|
item[this.options.hide](this.options.animated, function() { $(this).remove(); });
|
|
availableItem.hide()[this.options.show](this.options.animated);
|
|
|
|
this._applyItemState(availableItem, false);
|
|
return availableItem;
|
|
}
|
|
},
|
|
_applyItemState: function(item, selected) {
|
|
if (selected) {
|
|
if (this.options.sortable)
|
|
item.children('span').addClass('ui-icon-arrowthick-2-n-s').removeClass('ui-helper-hidden').addClass('ui-icon');
|
|
else
|
|
item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon');
|
|
item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus');
|
|
this._registerRemoveEvents(item.find('a.action'));
|
|
|
|
} else {
|
|
item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon');
|
|
item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus');
|
|
this._registerAddEvents(item.find('a.action'));
|
|
}
|
|
|
|
this._registerDoubleClickEvents(item);
|
|
this._registerHoverEvents(item);
|
|
},
|
|
// taken from John Resig's liveUpdate script
|
|
_filter: function(list) {
|
|
var input = $(this);
|
|
var rows = list.children('li'),
|
|
cache = rows.map(function(){
|
|
|
|
return $(this).text().toLowerCase();
|
|
});
|
|
|
|
var term = $.trim(input.val().toLowerCase()), scores = [];
|
|
|
|
if (!term) {
|
|
rows.show();
|
|
} else {
|
|
rows.hide();
|
|
|
|
cache.each(function(i) {
|
|
if (this.indexOf(term)>-1) { scores.push(i); }
|
|
});
|
|
|
|
$.each(scores, function() {
|
|
$(rows[this]).show();
|
|
});
|
|
}
|
|
},
|
|
_registerDoubleClickEvents: function(elements) {
|
|
if (!this.options.doubleClickable) return;
|
|
elements.dblclick(function(ev) {
|
|
if ($(ev.target).closest('.action').length === 0) {
|
|
// This may be triggered with rapid clicks on actions as well. In that
|
|
// case don't trigger an additional click.
|
|
elements.find('a.action').click();
|
|
}
|
|
});
|
|
},
|
|
_registerHoverEvents: function(elements) {
|
|
elements.removeClass('ui-state-hover');
|
|
elements.mouseover(function() {
|
|
$(this).addClass('ui-state-hover');
|
|
});
|
|
elements.mouseout(function() {
|
|
$(this).removeClass('ui-state-hover');
|
|
});
|
|
},
|
|
_registerAddEvents: function(elements) {
|
|
var that = this;
|
|
elements.click(function() {
|
|
var item = that._setSelected($(this).parent(), true);
|
|
that.count += 1;
|
|
that._updateCount();
|
|
return false;
|
|
});
|
|
|
|
// make draggable
|
|
if (this.options.sortable) {
|
|
elements.each(function() {
|
|
$(this).parent().draggable({
|
|
connectToSortable: that.selectedList,
|
|
helper: function() {
|
|
var selectedItem = that._cloneWithData($(this)).width($(this).width() - 50);
|
|
selectedItem.width($(this).width());
|
|
return selectedItem;
|
|
},
|
|
appendTo: that.container,
|
|
containment: that.container,
|
|
revert: 'invalid'
|
|
});
|
|
});
|
|
}
|
|
},
|
|
_registerRemoveEvents: function(elements) {
|
|
var that = this;
|
|
elements.click(function() {
|
|
that._setSelected($(this).parent(), false);
|
|
that.count -= 1;
|
|
that._updateCount();
|
|
return false;
|
|
});
|
|
},
|
|
_registerSearchEvents: function(input) {
|
|
var that = this;
|
|
|
|
input.focus(function() {
|
|
$(this).addClass('ui-state-active');
|
|
})
|
|
.blur(function() {
|
|
$(this).removeClass('ui-state-active');
|
|
})
|
|
.keypress(function(e) {
|
|
if (e.keyCode == 13)
|
|
return false;
|
|
})
|
|
.keyup(function() {
|
|
that._filter.apply(this, [that.availableList]);
|
|
});
|
|
}
|
|
});
|
|
|
|
$.extend($.ui.multiselect, {
|
|
locale: {
|
|
addAll:'Add all',
|
|
removeAll:'Remove all',
|
|
itemsCount:'items selected'
|
|
}
|
|
});
|
|
|
|
|
|
})(jQuery);
|