1/* 2 * jQuery UI Multiselect 3 * 4 * Authors: 5 * Michael Aufreiter (quasipartikel.at) 6 * Yanick Rochon (yanick.rochon[at]gmail[dot]com) 7 * 8 * Dual licensed under the MIT (MIT-LICENSE.txt) 9 * and GPL (GPL-LICENSE.txt) licenses. 10 * 11 * http://www.quasipartikel.at/multiselect/ 12 * 13 * 14 * Depends: 15 * ui.core.js 16 * ui.sortable.js 17 * 18 * Optional: 19 * localization (http://plugins.jquery.com/project/localisation) 20 * scrollTo (http://plugins.jquery.com/project/ScrollTo) 21 * 22 * Todo: 23 * Make batch actions faster 24 * Implement dynamic insertion through remote calls 25 */ 26 27 28(function($) { 29 30$.widget("ui.multiselect", { 31 _init: function() { 32 this.element.hide(); 33 this.id = this.element.attr("id"); 34 this.container = $('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element); 35 this.count = 0; // number of currently selected options 36 this.selectedContainer = $('<div class="selected"></div>').appendTo(this.container); 37 this.availableContainer = $('<div class="available"></div>').appendTo(this.container); 38 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); 39 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); 40 this.selectedList = $('<ul class="selected connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer); 41 this.availableList = $('<ul class="available connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer); 42 43 var that = this; 44 45 // set dimensions 46 this.container.width(this.element.width()+1); 47 this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation)); 48 this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation))); 49 50 // fix list height to match <option> depending on their individual header's heights 51 this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1)); 52 this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1)); 53 54 if ( !this.options.animated ) { 55 this.options.show = 'show'; 56 this.options.hide = 'hide'; 57 } 58 59 // init lists 60 this._populateLists(this.element.find('option')); 61 62 // make selection sortable 63 if (this.options.sortable) { 64 $("ul.selected").sortable({ 65 placeholder: 'ui-state-highlight', 66 connectWith: 'ul.available', 67 axis: 'y', 68 update: function(event, ui) { 69 // apply the new sort order to the original selectbox 70 that.selectedList.find('li').each(function() { 71 if ($(this).data('optionLink')) 72 $(this).data('optionLink').remove().appendTo(that.element); 73 }); 74 }, 75 receive: function(event, ui) { 76 ui.item.data('optionLink').attr('selected', true); 77 // increment count 78 that.count += 1; 79 that._updateCount(); 80 // workaround, because there's no way to reference 81 // the new element, see http://dev.jqueryui.com/ticket/4303 82 that.selectedList.children('.ui-draggable').each(function() { 83 $(this).removeClass('ui-draggable'); 84 $(this).data('optionLink', ui.item.data('optionLink')); 85 $(this).data('idx', ui.item.data('idx')); 86 that._applyItemState($(this), true); 87 }); 88 89 // workaround according to http://dev.jqueryui.com/ticket/4088 90 setTimeout(function() { ui.item.remove(); }, 1); 91 } 92 }); 93 } 94 95 // set up livesearch 96 if (this.options.searchable) { 97 this._registerSearchEvents(this.availableContainer.find('input.search')); 98 } else { 99 $('.search').hide(); 100 } 101 102 // batch actions 103 $(".remove-all").click(function() { 104 that._populateLists(that.element.find('option').removeAttr('selected')); 105 return false; 106 }); 107 $(".add-all").click(function() { 108 that._populateLists(that.element.find('option').attr('selected', 'selected')); 109 return false; 110 }); 111 }, 112 destroy: function() { 113 this.element.show(); 114 this.container.remove(); 115 116 $.widget.prototype.destroy.apply(this, arguments); 117 }, 118 _populateLists: function(options) { 119 this.selectedList.children('.ui-element').remove(); 120 this.availableList.children('.ui-element').remove(); 121 this.count = 0; 122 123 var that = this; 124 var items = $(options.map(function(i) { 125 var item = that._getOptionNode(this).appendTo(this.selected ? that.selectedList : that.availableList).show(); 126 127 if (this.selected) that.count += 1; 128 that._applyItemState(item, this.selected); 129 item.data('idx', i); 130 return item[0]; 131 })); 132 133 // update count 134 this._updateCount(); 135 }, 136 _updateCount: function() { 137 this.selectedContainer.find('span.count').text(this.count+" "+$.ui.multiselect.locale.itemsCount); 138 }, 139 _getOptionNode: function(option) { 140 option = $(option); 141 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(); 142 node.data('optionLink', option); 143 return node; 144 }, 145 // clones an item with associated data 146 // didn't find a smarter away around this 147 _cloneWithData: function(clonee) { 148 var clone = clonee.clone(); 149 clone.data('optionLink', clonee.data('optionLink')); 150 clone.data('idx', clonee.data('idx')); 151 return clone; 152 }, 153 _setSelected: function(item, selected) { 154 item.data('optionLink').attr('selected', selected); 155 156 if (selected) { 157 var selectedItem = this._cloneWithData(item); 158 item[this.options.hide](this.options.animated, function() { $(this).remove(); }); 159 selectedItem.appendTo(this.selectedList).hide()[this.options.show](this.options.animated); 160 161 this._applyItemState(selectedItem, true); 162 return selectedItem; 163 } else { 164 165 // look for successor based on initial option index 166 var items = this.availableList.find('li'), comparator = this.options.nodeComparator; 167 var succ = null, i = item.data('idx'), direction = comparator(item, $(items[i])); 168 169 // TODO: test needed for dynamic list populating 170 if ( direction ) { 171 while (i>=0 && i<items.length) { 172 direction > 0 ? i++ : i--; 173 if ( direction != comparator(item, $(items[i])) ) { 174 // going up, go back one item down, otherwise leave as is 175 succ = items[direction > 0 ? i : i+1]; 176 break; 177 } 178 } 179 } else { 180 succ = items[i]; 181 } 182 183 var availableItem = this._cloneWithData(item); 184 succ ? availableItem.insertBefore($(succ)) : availableItem.appendTo(this.availableList); 185 item[this.options.hide](this.options.animated, function() { $(this).remove(); }); 186 availableItem.hide()[this.options.show](this.options.animated); 187 188 this._applyItemState(availableItem, false); 189 return availableItem; 190 } 191 }, 192 _applyItemState: function(item, selected) { 193 if (selected) { 194 if (this.options.sortable) 195 item.children('span').addClass('ui-icon-arrowthick-2-n-s').removeClass('ui-helper-hidden').addClass('ui-icon'); 196 else 197 item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon'); 198 item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus'); 199 this._registerRemoveEvents(item.find('a.action')); 200 201 } else { 202 item.children('span').removeClass('ui-icon-arrowthick-2-n-s').addClass('ui-helper-hidden').removeClass('ui-icon'); 203 item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus'); 204 this._registerAddEvents(item.find('a.action')); 205 } 206 207 this._registerHoverEvents(item); 208 }, 209 // taken from John Resig's liveUpdate script 210 _filter: function(list) { 211 var input = $(this); 212 var rows = list.children('li'), 213 cache = rows.map(function(){ 214 215 return $(this).text().toLowerCase(); 216 }); 217 218 var term = $.trim(input.val().toLowerCase()), scores = []; 219 220 if (!term) { 221 rows.show(); 222 } else { 223 rows.hide(); 224 225 cache.each(function(i) { 226 if (this.indexOf(term)>-1) { scores.push(i); } 227 }); 228 229 $.each(scores, function() { 230 $(rows[this]).show(); 231 }); 232 } 233 }, 234 _registerHoverEvents: function(elements) { 235 elements.removeClass('ui-state-hover'); 236 elements.mouseover(function() { 237 $(this).addClass('ui-state-hover'); 238 }); 239 elements.mouseout(function() { 240 $(this).removeClass('ui-state-hover'); 241 }); 242 }, 243 _registerAddEvents: function(elements) { 244 var that = this; 245 elements.click(function() { 246 var item = that._setSelected($(this).parent(), true); 247 that.count += 1; 248 that._updateCount(); 249 return false; 250 }) 251 // make draggable 252 .each(function() { 253 $(this).parent().draggable({ 254 connectToSortable: 'ul.selected', 255 helper: function() { 256 var selectedItem = that._cloneWithData($(this)).width($(this).width() - 50); 257 selectedItem.width($(this).width()); 258 return selectedItem; 259 }, 260 appendTo: '.ui-multiselect', 261 containment: '.ui-multiselect',
262 revert: 'invalid' 263 }); 264 }); 265 }, 266 _registerRemoveEvents: function(elements) { 267 var that = this; 268 elements.click(function() { 269 that._setSelected($(this).parent(), false); 270 that.count -= 1; 271 that._updateCount(); 272 return false; 273 }); 274 }, 275 _registerSearchEvents: function(input) { 276 var that = this; 277 278 input.focus(function() { 279 $(this).addClass('ui-state-active'); 280 }) 281 .blur(function() { 282 $(this).removeClass('ui-state-active'); 283 }) 284 .keypress(function(e) { 285 if (e.keyCode == 13) 286 return false; 287 }) 288 .keyup(function() { 289 that._filter.apply(this, [that.availableList]); 290 }); 291 } 292}); 293 294$.extend($.ui.multiselect, { 295 defaults: { 296 sortable: true, 297 searchable: true, 298 animated: 'fast', 299 show: 'slideDown', 300 hide: 'slideUp', 301 dividerLocation: 0.6, 302 nodeComparator: function(node1,node2) { 303 var text1 = node1.text(), 304 text2 = node2.text(); 305 return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1); 306 } 307 }, 308 locale: { 309 addAll:'Add all', 310 removeAll:'Remove all', 311 itemsCount:'items selected' 312 } 313}); 314 315})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.