2 * jQuery UI Multiselect
5 * Michael Aufreiter (quasipartikel.at)
6 * Yanick Rochon (yanick.rochon[at]gmail[dot]com)
8 * Dual licensed under the MIT (MIT-LICENSE.txt)
9 * and GPL (GPL-LICENSE.txt) licenses.
11 * http://www.quasipartikel.at/multiselect/
19 * localization (http://plugins.jquery.com/project/localisation)
20 * scrollTo (http://plugins.jquery.com/project/ScrollTo)
23 * Make batch actions faster
24 * Implement dynamic insertion through remote calls
30 $.widget("ui.multiselect", {
34 doubleClickable: true,
39 nodeComparator: function(node1,node2) {
40 var text1 = node1.text(),
42 return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1);
47 this.id = this.element.attr("id");
48 this.container = $('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element);
49 this.count = 0; // number of currently selected options
50 this.selectedContainer = $('<div class="selected"></div>').appendTo(this.container);
51 this.availableContainer = $('<div class="available"></div>').appendTo(this.container);
52 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);
53 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);
54 this.selectedList = $('<ul class="selected connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer);
55 this.availableList = $('<ul class="available connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer);
60 this.container.width(this.element.width()+1);
61 this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation));
62 this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation)));
64 // fix list height to match <option> depending on their individual header's heights
65 this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1));
66 this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1));
68 if ( !this.options.animated ) {
69 this.options.show = 'show';
70 this.options.hide = 'hide';
74 this._populateLists(this.element.find('option'));
76 // make selection sortable
77 if (this.options.sortable) {
78 this.selectedList.sortable({
79 placeholder: 'ui-state-highlight',
81 update: function(event, ui) {
82 // apply the new sort order to the original selectbox
83 that.selectedList.find('li').each(function() {
84 if ($(this).data('optionLink'))
85 $(this).data('optionLink').remove().appendTo(that.element);
88 receive: function(event, ui) {
89 ui.item.data('optionLink').attr('selected', true);
93 // workaround, because there's no way to reference
94 // the new element, see http://dev.jqueryui.com/ticket/4303
95 that.selectedList.children('.ui-draggable').each(function() {
96 $(this).removeClass('ui-draggable');
97 $(this).data('optionLink', ui.item.data('optionLink'));
98 $(this).data('idx', ui.item.data('idx'));
99 that._applyItemState($(this), true);
102 // workaround according to http://dev.jqueryui.com/ticket/4088
103 setTimeout(function() { ui.item.remove(); }, 1);
109 if (this.options.searchable) {
110 this._registerSearchEvents(this.availableContainer.find('input.search'));
116 this.container.find(".remove-all").click(function() {
117 that._populateLists(that.element.find('option').removeAttr('selected'));
121 this.container.find(".add-all").click(function() {
122 var options = that.element.find('option').not(":selected");
123 if (that.availableList.children('li:hidden').length > 1) {
124 that.availableList.children('li').each(function(i) {
125 if ($(this).is(":visible")) $(options[i-1]).attr('selected', 'selected');
128 options.attr('selected', 'selected');
130 that._populateLists(that.element.find('option'));
134 destroy: function() {
136 this.container.remove();
138 $.Widget.prototype.destroy.apply(this, arguments);
140 _populateLists: function(options) {
141 this.selectedList.children('.ui-element').remove();
142 this.availableList.children('.ui-element').remove();
146 var items = $(options.map(function(i) {
147 var item = that._getOptionNode(this).appendTo(this.selected ? that.selectedList : that.availableList).show();
149 if (this.selected) that.count += 1;
150 that._applyItemState(item, this.selected);
157 that._filter.apply(this.availableContainer.find('input.search'), [that.availableList]);
159 _updateCount: function() {
160 this.selectedContainer.find('span.count').text(this.count+" "+$.ui.multiselect.locale.itemsCount);
162 _getOptionNode: function(option) {
164 var node = $('<li class="ui-state-default ui-element" title="' + jsAttr(option.text()) + '"><span class="ui-icon"/>' + jsText(option.text()) + '<a href="#" class="action"><span class="ui-corner-all ui-icon"/></a></li>').hide();
165 node.data('optionLink', option);
168 // clones an item with associated data
169 // didn't find a smarter away around this
170 _cloneWithData: function(clonee) {
171 var clone = clonee.clone(false,false);
172 clone.data('optionLink', clonee.data('optionLink'));
173 clone.data('idx', clonee.data('idx'));
176 _setSelected: function(item, selected) {
177 item.data('optionLink').attr('selected', selected);
180 var selectedItem = this._cloneWithData(item);
181 item[this.options.hide](this.options.animated, function() { $(this).remove(); });
182 selectedItem.appendTo(this.selectedList).hide()[this.options.show](this.options.animated);
184 this._applyItemState(selectedItem, true);
188 // look for successor based on initial option index
189 var items = this.availableList.find('li'), comparator = this.options.nodeComparator;
190 var succ = null, i = item.data('idx'), direction = comparator(item, $(items[i]));
192 // TODO: test needed for dynamic list populating
194 while (i>=0 && i<items.length) {
195 direction > 0 ? i++ : i--;
196 if ( direction != comparator(item, $(items[i])) ) {
197 // going up, go back one item down, otherwise leave as is
198 succ = items[direction > 0 ? i : i+1];
206 var availableItem = this._cloneWithData(item);
207 succ ? availableItem.insertBefore($(succ)) : availableItem.appendTo(this.availableList);
208 item[this.options.hide](this.options.animated, function() { $(this).remove(); });
209 availableItem.hide()[this.options.show](this.options.animated);
211 this._applyItemState(availableItem, false);
212 return availableItem;
215 _applyItemState: function(item, selected) {
217 if (this.options.sortable)
218 item.children('span').addClass('ui-icon-arrowthick-2-n-s').removeClass('ui-helper-hidden').addClass('ui-icon');
220 item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon');
221 item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus');
222 this._registerRemoveEvents(item.find('a.action'));
225 item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon');
226 item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus');
227 this._registerAddEvents(item.find('a.action'));
230 this._registerDoubleClickEvents(item);
231 this._registerHoverEvents(item);
233 // taken from John Resig's liveUpdate script
234 _filter: function(list) {
236 var rows = list.children('li'),
237 cache = rows.map(function(){
239 return $(this).text().toLowerCase();
242 var term = $.trim(input.val().toLowerCase()), scores = [];
249 cache.each(function(i) {
250 if (this.indexOf(term)>-1) { scores.push(i); }
253 $.each(scores, function() {
254 $(rows[this]).show();
258 _registerDoubleClickEvents: function(elements) {
259 if (!this.options.doubleClickable) return;
260 elements.dblclick(function() {
261 elements.find('a.action').click();
264 _registerHoverEvents: function(elements) {
265 elements.removeClass('ui-state-hover');
266 elements.mouseover(function() {
267 $(this).addClass('ui-state-hover');
269 elements.mouseout(function() {
270 $(this).removeClass('ui-state-hover');
273 _registerAddEvents: function(elements) {
275 elements.click(function() {
276 var item = that._setSelected($(this).parent(), true);
283 if (this.options.sortable) {
284 elements.each(function() {
285 $(this).parent().draggable({
286 connectToSortable: that.selectedList,
288 var selectedItem = that._cloneWithData($(this)).width($(this).width() - 50);
289 selectedItem.width($(this).width());
292 appendTo: that.container,
293 containment: that.container,
299 _registerRemoveEvents: function(elements) {
301 elements.click(function() {
302 that._setSelected($(this).parent(), false);
308 _registerSearchEvents: function(input) {
311 input.focus(function() {
312 $(this).addClass('ui-state-active');
315 $(this).removeClass('ui-state-active');
317 .keypress(function(e) {
322 that._filter.apply(this, [that.availableList]);
327 $.extend($.ui.multiselect, {
330 removeAll:'Remove all',
331 itemsCount:'items selected'