diff --git a/plugins/ui.multiselect.css b/plugins/ui.multiselect.css
new file mode 100644
index 000000000..8ab18ef2c
--- /dev/null
+++ b/plugins/ui.multiselect.css
@@ -0,0 +1,30 @@
+/* Multiselect
+----------------------------------*/
+
+.ui-multiselect { border: solid 1px; font-size: 0.8em; }
+.ui-multiselect ul { -moz-user-select: none; }
+.ui-multiselect li { margin: 0; padding: 0; cursor: default; line-height: 20px; height: 20px; font-size: 11px; list-style: none; }
+.ui-multiselect li a { color: #999; text-decoration: none; padding: 0; display: block; float: left; cursor: pointer;}
+.ui-multiselect li.ui-draggable-dragging { padding-left: 10px; }
+
+.ui-multiselect div.selected { position: relative; padding: 0; margin: 0; border: 0; float:left; }
+.ui-multiselect ul.selected { position: relative; padding: 0; overflow: auto; overflow-x: hidden; background: #fff; margin: 0; list-style: none; border: 0; position: relative; width: 100%; }
+.ui-multiselect ul.selected li { }
+
+.ui-multiselect div.available { position: relative; padding: 0; margin: 0; border: 0; float:left; border-left: 1px solid; }
+.ui-multiselect ul.available { position: relative; padding: 0; overflow: auto; overflow-x: hidden; background: #fff; margin: 0; list-style: none; border: 0; width: 100%; }
+.ui-multiselect ul.available li { padding-left: 10px; }
+
+.ui-multiselect .ui-state-default { border: none; margin-bottom: 1px; position: relative; padding-left: 20px;}
+.ui-multiselect .ui-state-hover { border: none; }
+.ui-multiselect .ui-widget-header {border: none; font-size: 11px; margin-bottom: 1px;}
+
+.ui-multiselect .add-all { float: right; padding: 7px;}
+.ui-multiselect .remove-all { float: right; padding: 7px;}
+.ui-multiselect .search { float: left; padding: 4px;}
+.ui-multiselect .count { float: left; padding: 7px;}
+
+.ui-multiselect li span.ui-icon-arrowthick-2-n-s { position: absolute; left: 2px; }
+.ui-multiselect li a.action { position: absolute; right: 2px; top: 2px; }
+
+.ui-multiselect input.search { height: 14px; padding: 1px; opacity: 0.5; margin: 4px; width: 100px; }
\ No newline at end of file
diff --git a/plugins/ui.multiselect.js b/plugins/ui.multiselect.js
new file mode 100644
index 000000000..fcbea81a3
--- /dev/null
+++ b/plugins/ui.multiselect.js
@@ -0,0 +1,314 @@
+/*
+ * 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", {
+ _init: function() {
+ this.element.hide();
+ this.id = this.element.attr("id");
+ this.container = $('
').insertAfter(this.element);
+ this.count = 0; // number of currently selected options
+ this.selectedContainer = $('
').appendTo(this.container);
+ this.availableContainer = $('
').appendTo(this.container);
+ this.selectedActions = $('').appendTo(this.selectedContainer);
+ this.availableActions = $('').appendTo(this.availableContainer);
+ this.selectedList = $('').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer);
+ this.availableList = $('').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 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) {
+ $("ul.selected").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
+ $(".remove-all").click(function() {
+ that._populateLists(that.element.find('option').removeAttr('selected'));
+ return false;
+ });
+ $(".add-all").click(function() {
+ that._populateLists(that.element.find('option').attr('selected', 'selected'));
+ 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();
+ },
+ _updateCount: function() {
+ this.selectedContainer.find('span.count').text(this.count+" "+$.ui.multiselect.locale.itemsCount);
+ },
+ _getOptionNode: function(option) {
+ option = $(option);
+ var node = $(' '+option.text()+' ').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();
+ 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 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._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();
+ });
+ }
+ },
+ _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
+ .each(function() {
+ $(this).parent().draggable({
+ connectToSortable: 'ul.selected',
+ helper: function() {
+ var selectedItem = that._cloneWithData($(this)).width($(this).width() - 50);
+ selectedItem.width($(this).width());
+ return selectedItem;
+ },
+ appendTo: '.ui-multiselect',
+ containment: '.ui-multiselect',
+ 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, {
+ defaults: {
+ sortable: true,
+ searchable: true,
+ animated: 'fast',
+ show: 'slideDown',
+ hide: 'slideUp',
+ dividerLocation: 0.6,
+ nodeComparator: function(node1,node2) {
+ var text1 = node1.text(),
+ text2 = node2.text();
+ return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1);
+ }
+ },
+ locale: {
+ addAll:'Add all',
+ removeAll:'Remove all',
+ itemsCount:'items selected'
+ }
+});
+
+})(jQuery);