Skip to content

Instantly share code, notes, and snippets.

@b1ff
Created May 15, 2012 15:57
Show Gist options
  • Save b1ff/2702859 to your computer and use it in GitHub Desktop.
Save b1ff/2702859 to your computer and use it in GitHub Desktop.
ui.multiselect
/*
* 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.5,
isAjaxEnabled: 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.availableContainer = $('<div class="available"></div>').appendTo(this.container);
this.selectedContainer = $('<div class="selected"></div>').appendTo(this.container);
this.selectedActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><input type="text" class="search-selected empty ui-widget-content ui-corner-all"/><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());
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() + 13, 1));
this.availableList.height(Math.max(this.element.height() - this.availableActions.height() + 13, 1));
if (!this.options.animated) {
this.options.show = 'show';
this.options.hide = 'hide';
}
if (this.options.isAjaxEnabled) {
this.element.change(function (e, isCustom, isMultiselect) {
// this optimization causes improper state of selected items in moment of change event raising, additional investigation needed
//if (!isMultiselect)
that._populateLists(that.element.find('option'));
});
}
// 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);
},
stop: function (event, ui) {
// NOTE: Added for enable triggering change event of select if drag&drop was used
if (that.options.isAjaxEnabled)
that.element.trigger('change');
}
});
}
// set up livesearch
if (this.options.searchable) {
this._registerSearchEvents(this.availableContainer.find('input.search'));
this._registerSearchEvents(this.selectedContainer.find('input.search-selected'));
} else {
$('.search').hide();
$('.search-selected').hide();
}
// batch actions
this.container.find(".remove-all").click(function () {
var options = that.element.find('option:selected');
if (that.selectedList.children('li:hidden').length > 1) {
that.selectedList.children('li').each(function (i) {
if ($(this).is(":visible")) $(options[i - 1]).removeAttr('selected');
});
that._populateLists(that.element.find('option'));
}
else {
that._populateLists(that.element.find('option').removeAttr('selected'));
}
// NOTE: Added for enable triggering change event of select if remove all button was clicked
if (that.options.isAjaxEnabled)
$(that.element).trigger('change');
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]).not(":disabled"))
$(options[i - 1]).attr('selected', 'selected');
});
} else {
that.availableList.children('li').each(function (i) {
var opt = $(options[i - 1]);
if (opt.is(":enabled"))
opt.attr('selected', 'selected');
});
}
that._populateLists(that.element.find('option'));
// NOTE: Added for enable triggering change event of select if add all button was clicked
if (that.options.isAjaxEnabled)
$(that.element).trigger('change');
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, i).appendTo(this.selected ? that.selectedList : that.availableList).show();
if (this.selected) {
that.count += 1;
that._registerRemoveEvents(item.find('a.action'));
}
else {
that._registerAddEvents(item.find('a.action'));
}
that._registerDoubleClickEvents(item);
that._registerHoverEvents(item);
return item[0];
}));
// update count
this._updateCount();
that._filter.apply(this.availableContainer.find('input.search'), [that.availableList]);
that._filter.apply(this.selectedContainer.find('input.search-selected'), [that.selectedList]);
},
_updateCount: function () {
this.selectedContainer.find('span.count').text(this.count + " " + $.ui.multiselect.locale.itemsCount);
},
_getOptionNode: function (option, index) {
var sortableClasses = 'ui-icon-arrowthick-2-n-s ui-icon',
unsortableClasses = 'ui-helper-hidden',
spanClasses, actionSpanClasses;
if (option.selected) {
spanClasses = this.options.sortable ? sortableClasses : unsortableClasses;
actionSpanClasses = 'ui-icon-minus';
}
else {
spanClasses = 'ui-helper-hidden';
actionSpanClasses = 'ui-icon-plus';
}
var childSpan = '<span class="' + spanClasses + '"/>',
nodeHtml = '<li class="ui-state-default ui-element ' + option.className + '" title="'
+ option.innerHTML
+ '">'
+ childSpan //'<span class="ui-icon"/>'
+ option.innerHTML
+ '<a href="javascript:void(0)" class="action"><span class="ui-corner-all ui-icon '
+ actionSpanClasses
+ '"/></a></li>';
var node = $(nodeHtml).hide();
node.data('optionLink', $(option));
node.data('idx', index);
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 () {
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();
that.element.trigger('change', [false, true]);
//that.element.change();
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();
that.element.trigger('change', [false, true]);
//that.element.change();
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 () {
if ($(this).hasClass('search-selected')) {
that._filter.apply(this, [that.selectedList]);
return;
}
that._filter.apply(this, [that.availableList]);
});
}
});
$.extend($.ui.multiselect, {
locale: {
addAll: '+ all',
removeAll: '- all',
itemsCount: 'selected'
}
});
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment