Created
May 15, 2012 15:57
-
-
Save b1ff/2702859 to your computer and use it in GitHub Desktop.
ui.multiselect
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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