123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- /*
- * 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';
- }
- this.useProp = !!$.fn.prop;
- // 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')[ this.useProp ? 'prop' : '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])[ that.useProp ? 'prop' : 'attr' ]('selected', true);
- });
- } else {
- options[ that.useProp ? 'prop' : 'attr' ]('selected', true);
- }
- 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')[ this.useProp ? 'prop' : '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);
|