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://yanickrochon.uuuq.com/multiselect/ 12 * 13 * 14 * Depends: 15 * ui.core.js 16 * ui.draggable.js 17 * ui.droppable.js 18 * ui.sortable.js 19 * jquery.blockUI (http://github.com/malsup/blockui/) 20 * jquery.tmpl (http://andrew.hedges.name/blog/2008/09/03/introducing-jquery-simple-templates 21 * 22 * Optional: 23 * localization (http://plugins.jquery.com/project/localisation) 24 * 25 * Notes: 26 * The strings in this plugin use a templating engine to enable localization 27 * and allow flexibility in the messages. Read the documentation for more details. 28 * 29 * Todo: 30 * restore selected items on remote searchable multiselect upon page reload (same behavior as local mode) 31 * (is it worth it??) add a public function to apply the nodeComparator to all items (when using nodeComparator setter) 32 * support for option groups, disabled options, etc. 33 * speed improvements 34 * tests and optimizations 35 * - test getters/setters (including options from the defaults) 36 */ 37 38 39/******************************** 40 * Default callbacks 41 ********************************/ 42 43// expect data to be "val1=text1[\nval2=text2[\n...]]" 44var defaultDataParser = function(data) { 45 if ( typeof data == 'string' ) { 46 var pattern = /^(\s\n\r\t)*\+?$/; 47 var selected, line, lines = data.split(/\n/); 48 data = {}; 49 for (var i in lines) { 50 line = lines[i].split("="); 51 // make sure the key is not empty 52 if (!pattern.test(line[0])) { 53 selected = (line[0].lastIndexOf('+') == line.length - 1); 54 if (selected) line[0] = line.substr(0,line.length-1); 55 // if no value is specified, default to the key value 56 data[line[0]] = { 57 selected: false, 58 value: line[1] || line[0] 59 }; 60 } 61 } 62 } else { 63 this._messages($.ui.multiselect.constante.MESSAGE_ERROR, $.ui.multiselect.locale.errorDataFormat); 64 data = false; 65 } 66 return data; 67}; 68 69var defaultNodeComparator = function(node1,node2) { 70 var text1 = node1.text(), 71 text2 = node2.text(); 72 return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1); 73}; 74 75(function($) { 76 77$.widget("ui.multiselect", { 78 options: { 79 // sortable and droppable 80 sortable: 'left', 81 droppable: 'both', 82 // searchable 83 searchable: true, 84 searchDelay: 400, 85 remoteUrl: null, 86 remoteParams: {}, 87 // animated 88 animated: 'fast', 89 show: 'slideDown', 90 hide: 'slideUp', 91 // ui 92 dividerLocation: 0.6, 93 // callbacks 94 dataParser: defaultDataParser, 95 nodeComparator: defaultNodeComparator, 96 nodeInserted: null 97 }, 98 _create: function() { 99 this.element.hide(); 100 this.busy = false; // busy state
101 this.container = $('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element); 102 this.selectedContainer = $('<div class="ui-widget-content list-container selected"></div>').appendTo(this.container); 103 this.availableContainer = $('<div class="ui-widget-content list-container available"></div>').appendTo(this.container); 104 this.selectedActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><span class="count">'+$.tmpl($.ui.multiselect.locale.itemsCount,{count:0})+'</span><a href="#" class="remove-all">'+$.tmpl($.ui.multiselect.locale.removeAll)+'</a></div>').appendTo(this.selectedContainer); 105 this.availableActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><span class="busy">'+$.tmpl($.ui.multiselect.locale.busy)+'</span><input type="text" class="search ui-widget-content ui-corner-all"/><a href="#" class="add-all">'+$.tmpl($.ui.multiselect.locale.addAll)+'</a></div>').appendTo(this.availableContainer); 106 this.selectedList = $('<ul class="list selected"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer); 107 this.availableList = $('<ul class="list available"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer); 108 109 var that = this; 110 111 // initialize data cache 112 this.availableList.data('multiselect.cache', {}); 113 this.selectedList.data('multiselect.cache', {}); 114 115 if ( !this.options.animated ) { 116 this.options.show = 'show'; 117 this.options.hide = 'hide'; 118 } 119 120 // sortable / droppable / draggable 121 var dragOptions = { 122 selected: { 123 sortable: ('both' == this.options.sortable || 'left' == this.options.sortable), 124 droppable: ('both' == this.options.droppable || 'left' == this.options.droppable) 125 }, 126 available: { 127 sortable: ('both' == this.options.sortable || 'right' == this.options.sortable), 128 droppable: ('both' == this.options.droppable || 'right' == this.options.droppable) 129 } 130 }; 131 this._prepareLists('selected', 'available', dragOptions); 132 this._prepareLists('available', 'selected', dragOptions); 133 134 // set up livesearch 135 this._registerSearchEvents(this.availableContainer.find('input.search'), true); 136 // make sure that we're not busy yet 137 this._setBusy(false); 138 139 // batch actions 140 this.container.find(".remove-all").bind('click.multiselect', function() { that.selectNone(); return false; }); 141 this.container.find(".add-all").bind('click.multiselect', function() { that.selectAll(); return false; }); 142 143 // set dimensions 144 this.container.width(this.element.width()+1); 145 this._refreshDividerLocation(); 146 // set max width of search input dynamically 147 this.availableActions.find('input').width(Math.max(this.availableActions.width() - this.availableActions.find('a.add-all').width() - 30, 20)); 148 // fix list height to match <option> depending on their individual header's heights 149 this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1)); 150 this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1)); 151 152 // init lists 153 this._populateLists(this.element.find('option')); 154 }, 155 156 /************************************** 157 * Public 158 **************************************/ 159 160 destroy: function() { 161 this.container.remove(); 162 this.element.show(); 163 164 $.widget.prototype.destroy.apply(this, arguments); 165 }, 166 isBusy: function() { 167 return !!this.busy; 168 }, 169 isSelected: function(item) { 170 if (this.enabled()) { 171 return !!this._findItem(item, this.selectedList); 172 } else { 173 return null; 174 } 175 }, 176 // get all selected values in an array 177 selectedValues: function() { 178 return $.map( this.element.find('option[selected]'), function(item,i) { return $(item).val(); }); 179 }, 180 // get/set enable state 181 enabled: function(state, msg) { 182 if (undefined !== state) { 183 if (state) { 184 this.container.unblock(); 185 this.element.removeAttr('disabled'); 186 } else { 187 this.container.block({message:msg||null,overlayCSS:{backgroundColor:'#fff',opacity:0.4,cursor:'default'}}); 188 this.element.attr('disabled', true); 189 } 190 } 191 return !this.element.attr('disabled'); 192 }, 193 selectAll: function() { 194 if (this.enabled()) { 195 this._batchSelect(this.availableList.children('li.ui-element:visible'), true); 196 } 197 }, 198 selectNone: function() { 199 if (this.enabled()) { 200 this._batchSelect(this.selectedList.children('li.ui-element:visible'), false); 201 } 202 }, 203 select: function(text) { 204 if (this.enabled()) { 205 var available = this._findItem(text, this.availableList); 206 if ( available ) { 207 this._setSelected(available, true); 208 } 209 } 210 }, 211 deselect: function(text) { 212 if (this.enabled()) { 213 var selected = this._findItem(text, this.selectedList); 214 if ( selected ) { 215 this._setSelected(selected, false); 216 } 217 } 218 }, 219 search: function(query) { 220 if (!this.busy && this.enabled() && this.options.searchable) {
221 var input = this.availableActions.children('input:first'); 222 input.val(query); 223 input.trigger('keydown.multiselect'); 224 } 225 }, 226 // insert new <option> and _populate 227 // @return int the number of options added 228 addOptions: function(data) { 229 if (this.enabled()) { 230 this._setBusy(true); 231 232 // format data 233 var elements = []; 234 if (data = this.options.dataParser.call(this, data)) { 235 for (var key in data) { 236 // check if the option does not exist already 237 if (this.element.find('option[value="'+key+'"]').size()==0) { 238 elements.push( $('<option value="'+key+'"/>').text(data[key].value).appendTo(this.element)[0] ); 239 } 240 } 241 } 242 243 if (elements.length>0) { 244 this._populateLists($(elements)); 245 } 246 247 this._filter(this.availableList.children('li.ui-element')); 248 249 this._setBusy(false); 250 return elements.length; 251 } else { 252 return false; 253 } 254 }, 255 256 /************************************** 257 * Private 258 **************************************/ 259 260 _setData: function(key, value) { 261 switch (key) { 262 // special treatement must be done for theses values when changed 263 case 'dividerLocation': 264 this.options.dividerLocation = value; 265 this._refreshDividerLocation(); 266 break; 267 case 'searchable': 268 this.options.searchable = value; 269 this._registerSearchEvents(this.availableContainer.find('input.search'), false); 270 break; 271 272 case 'droppable': 273 case 'sortable': 274 // readonly options 275 this._messages( 276 $.ui.multiselect.constants.MESSAGE_WARNING, 277 $.ui.multiselect.locale.errorReadonly, 278 {option: key} 279 ); 280 default: 281 // default behavior 282 this.options[key] = value; 283 break; 284 } 285 }, 286 _ui: function(type) { 287 var uiObject = {sender: this.element}; 288 switch (type) { 289 // events: messages 290 case 'message': 291 uiObject.type = arguments[1]; 292 uiObject.message = arguments[2]; 293 break; 294 295 // events: selected, deselected 296 case 'selection': 297 uiObject.option = arguments[1]; 298 break; 299 } 300 return uiObject; 301 }, 302 _messages: function(type, msg, params) { 303 this._trigger('messages', null, this._ui('message', type, $.tmpl(msg, params))); 304 }, 305 306 _refreshDividerLocation: function() { 307 this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation)); 308 this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation))); 309 }, 310 _prepareLists: function(side, otherSide, opts) { 311 var that = this; 312 var itemSelected = ('selected' == side); 313 var list = this[side+'List']; 314 var otherList = this[otherSide+'List']; 315 var listDragHelper = opts[otherSide].sortable ? _dragHelper : 'clone'; 316 317 list 318 .data('multiselect.sortable', opts[side].sortable ) 319 .data('multiselect.droppable', opts[side].droppable ) 320 .data('multiselect.draggable', !opts[side].sortable && (opts[otherSide].sortable || opts[otherSide].droppable) ); 321 322 if (opts[side].sortable) { 323 list.sortable({ 324 appendTo: this.container, 325 connectWith: otherList, 326 containment: this.container, 327 helper: listDragHelper, 328 items: 'li.ui-element', 329 revert: !(opts[otherSide].sortable || opts[otherSide].droppable), 330 receive: function(event, ui) { 331 // DEBUG 332 //that._messages(0, "Receive : " + ui.item.data('multiselect.optionLink') + ":" + ui.item.parent()[0].className + " = " + itemSelected); 333 334 // we received an element from a sortable to another sortable... 335 if (opts[otherSide].sortable) { 336 var optionLink = ui.item.data('multiselect.optionLink'); 337 338 that._applyItemState(ui.item.hide(), itemSelected); 339 340 // if the cache already contain an element, remove it 341 if (otherList.data('multiselect.cache')[optionLink.val()]) { 342 delete otherList.data('multiselect.cache')[optionLink.val()]; 343 } 344 345 ui.item.hide(); 346 that._setSelected(ui.item, itemSelected, true); 347 } else { 348 // the other is droppable only, so merely select the element... 349 setTimeout(function() { 350 that._setSelected(ui.item, itemSelected); 351 }, 10); 352 } 353 }, 354 stop: function(event, ui) { 355 // DEBUG 356 //that._messages(0, "Stop : " + (ui.item.parent()[0] == otherList[0])); 357 that._moveOptionNode(ui.item); 358 } 359 }); 360 } 361 362 // cannot be droppable if both lists are sortable, it breaks the receive function 363 if (!(opts[side].sortable && opts[otherSide].sortable) 364 && (opts[side].droppable || opts[otherSide].sortable || opts[otherSide].droppable)) { 365 //alert( side + " is droppable "); 366 list.droppable({ 367 accept: '.ui-multiselect li.ui-element', 368 hoverClass: 'ui-state-highlight', 369 revert: !(opts[otherSide].sortable || opts[otherSide].droppable), 370 greedy: true, 371 drop: function(event, ui) { 372 // DEBUG 373 //that._messages(0, "drop " + side + " = " + ui.draggable.data('multiselect.optionLink') + ":" + ui.draggable.parent()[0].className); 374 375 //alert( "drop " + itemSelected ); 376 // if no optionLink is defined, it was dragged in 377 if (!ui.draggable.data('multiselect.optionLink')) { 378 var optionLink = ui.helper.data('multiselect.optionLink'); 379 ui.draggable.data('multiselect.optionLink', optionLink); 380 381 // if the cache already contain an element, remove it 382 if (list.data('multiselect.cache')[optionLink.val()]) { 383 delete list.data('multiselect.cache')[optionLink.val()]; 384 } 385 list.data('multiselect.cache')[optionLink.val()] = ui.draggable; 386 387 that._applyItemState(ui.draggable, itemSelected); 388 389 // received an item from a sortable to a droppable 390 } else if (!opts[side].sortable) { 391 setTimeout(function() { 392 ui.draggable.hide(); 393 that._setSelected(ui.draggable, itemSelected); 394 }, 10); 395 } 396 397 } 398 }); 399 } 400 }, 401 _populateLists: function(options) { 402 this._setBusy(true); 403 404 var that = this; 405 // do this async so the browser actually display the waiting message 406 setTimeout(function() { 407 $(options.each(function(i) { 408 var list = (this.selected ? that.selectedList : that.availableList); 409 var item = that._getOptionNode(this).show(); 410 that._applyItemState(item, this.selected); 411 item.data('multiselect.idx', i); 412 413 // cache 414 list.data('multiselect.cache')[item.data('multiselect.optionLink').val()] = item; 415 416 that._insertToList(item, list); 417 })); 418 419 // update count 420 that._setBusy(false); 421 that._updateCount(); 422 }, 1); 423 }, 424 _insertToList: function(node, list) { 425 var that = this; 426 this._setBusy(true); 427 // the browsers don't like batch node insertion... 428 var _addNodeRetry = 0; 429 var _addNode = function() { 430 var succ = (that.options.nodeComparator ? that._getSuccessorNode(node, list) : null); 431 try { 432 if (succ) { 433 node.insertBefore(succ); 434 } else { 435 list.append(node); 436 }
437 if (list === that.selectedList) that._moveOptionNode(node); 438 439 // callback after node insertion 440 if ('function' == typeof that.options.nodeInserted) that.options.nodeInserted(node); 441 that._setBusy(false); 442 } catch (e) { 443 // if this problem did not occur too many times already 444 if ( _addNodeRetry++ < 10 ) { 445 // try again later (let the browser cool down first) 446 setTimeout(function() { _addNode(); }, 1); 447 } else { 448 that._messages( 449 $.ui.multiselect.constants.MESSAGE_EXCEPTION, 450 $.ui.multiselect.locale.errorInsertNode, 451 {key:node.data('multiselect.optionLink').val(), value:node.text()} 452 ); 453 that._setBusy(false); 454 } 455 } 456 }; 457 _addNode(); 458 }, 459 _updateCount: function() { 460 var that = this; 461 // defer until system is not busy 462 if (this.busy) setTimeout(function() { that._updateCount(); }, 100); 463 // count only visible <li> (less .ui-helper-hidden*) 464 var count = this.selectedList.children('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder):visible').size(); 465 var total = this.availableList.children('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder,.shadowed)').size() + count; 466 this.selectedContainer.find('span.count') 467 .text($.tmpl($.ui.multiselect.locale.itemsCount, {count:count})) 468 .attr('title', $.tmpl($.ui.multiselect.locale.itemsTotal, {count:total})); 469 }, 470 _getOptionNode: function(option) { 471 option = $(option); 472 var node = $('<li class="ui-state-default ui-element"><span class="ui-icon"/>'+option.text()+'<a href="#" class="ui-state-default action"><span class="ui-corner-all ui-icon"/></a></li>').hide(); 473 node.data('multiselect.optionLink', option); 474 return node; 475 }, 476 _moveOptionNode: function(item) { 477 // call this async to let the item be placed correctly 478 setTimeout( function() { 479 var optionLink = item.data('multiselect.optionLink'); 480 if (optionLink) { 481 var prevItem = item.prev('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder):visible'); 482 var prevOptionLink = prevItem.size() ? prevItem.data('multiselect.optionLink') : null; 483 484 if (prevOptionLink) { 485 optionLink.insertAfter(prevOptionLink); 486 } else { 487 optionLink.prependTo(optionLink.parent()); 488 } 489 } 490 }, 100); 491 }, 492 // used by select and deselect, etc. 493 _findItem: function(text, list) { 494 var found = null; 495 list.children('li.ui-element:visible').each(function(i,el) { 496 el = $(el); 497 if (el.text().toLowerCase() === text.toLowerCase()) { 498 found = el; 499 } 500 }); 501 if (found && found.size()) { 502 return found; 503 } else { 504 return false; 505 } 506 }, 507 // clones an item with 508 // didn't find a smarter away around this (michael) 509 // now using cache to speed up the process (yr) 510 _cloneWithData: function(clonee, cacheName, insertItem) { 511 var that = this; 512 var id = clonee.data('multiselect.optionLink').val(); 513 var selected = ('selected' == cacheName); 514 var list = (selected ? this.selectedList : this.availableList); 515 var clone = list.data('multiselect.cache')[id]; 516 517 if (!clone) { 518 clone = clonee.clone().hide(); 519 this._applyItemState(clone, selected); 520 // update cache 521 list.data('multiselect.cache')[id] = clone; 522 // update <option> and idx 523 clone.data('multiselect.optionLink', clonee.data('multiselect.optionLink')); 524 // need this here because idx is needed in _getSuccessorNode 525 clone.data('multiselect.idx', clonee.data('multiselect.idx')); 526 527 // insert the node into it's list 528 if (insertItem) { 529 this._insertToList(clone, list); 530 } 531 } else { 532 // update idx 533 clone.data('multiselect.idx', clonee.data('multiselect.idx')); 534 } 535 return clone; 536 }, 537 _batchSelect: function(elements, state) { 538 this._setBusy(true); 539 540 var that = this; 541 // do this async so the browser actually display the waiting message 542 setTimeout(function() { 543 var _backup = { 544 animated: that.options.animated, 545 hide: that.options.hide, 546 show: that.options.show 547 }; 548 549 that.options.animated = null; 550 that.options.hide = 'hide'; 551 that.options.show = 'show'; 552 553 elements.each(function(i,element) { 554 that._setSelected($(element), state); 555 }); 556 557 // filter available items 558 if (!state) that._filter(that.availableList.find('li.ui-element')); 559 560 // restore 561 $.extend(that.options, _backup); 562 563 that._updateCount(); 564 that._setBusy(false); 565 }, 10); 566 }, 567 // find the best successor the given item in the specified list 568 // TODO implement a faster sorting algorithm (and remove the idx depen
568dancy) 569 _getSuccessorNode: function(item, list) { 570 // look for successor based on initial option index 571 var items = list.find('li.ui-element'), comparator = this.options.nodeComparator; 572 var itemsSize = items.size(); 573 574 // no successor, list is null 575 if (items.size() == 0) return null; 576 577 var succ, i = Math.min(item.data('multiselect.idx'),itemsSize-1), direction = comparator(item, $(items[i])); 578 579 if ( direction ) { 580 // quick checks 581 if (0>direction && 0>=i) { 582 succ = items[0]; 583 } else if (0<direction && itemsSize-1<=i) { 584 i++; 585 succ = null; 586 } else { 587 while (i>=0 && i<items.length) { 588 direction > 0 ? i++ : i--; 589 if (i<0) { 590 succ = item[0] 591 } 592 if ( direction != comparator(item, $(items[i])) ) { 593 // going up, go back one item down, otherwise leave as is 594 succ = items[direction > 0 ? i : i+1]; 595 break; 596 } 597 } 598 } 599 } else { 600 succ = items[i]; 601 } 602 // update idx 603 item.data('multiselect.idx', i); 604 605 return succ; 606 }, 607 // @param DOMElement item is the item to set 608 // @param bool selected true|false (state) 609 // @param bool noclone (optional) true only if item should not be cloned on the other list 610 _setSelected: function(item, selected, noclone) { 611 var that = this, otherItem; 612 var optionLink = item.data('multiselect.optionLink'); 613 614 if (selected) { 615 // already selected 616 if (optionLink.attr('selected')) return; 617 optionLink.attr('selected','selected'); 618 619 if (noclone) { 620 otherItem = item; 621 } else { 622 // retrieve associatd or cloned item 623 otherItem = this._cloneWithData(item, 'selected', true).hide(); 624 item.addClass('shadowed')[this.options.hide](this.options.animated, function() { that._updateCount(); }); 625 } 626 otherItem[this.options.show](this.options.animated); 627 } else { 628 // already deselected 629 if (!optionLink.attr('selected')) return; 630 optionLink.removeAttr('selected'); 631 632 if (noclone) { 633 otherItem = item; 634 } else { 635 // retrieve associated or clone the item 636 otherItem = this._cloneWithData(item, 'available', true).hide().removeClass('shadowed'); 637 item[this.options.hide](this.options.animated, function() { that._updateCount() }); 638 } 639 if (!otherItem.is('.filtered')) otherItem[this.options.show](this.options.animated); 640 } 641 642 if (!this.busy) { 643 if (this.options.animated) { 644 // pulse 645 //otherItem.effect("pulsate", { times: 1, mode: 'show' }, 400); // pulsate twice??? 646 otherItem.fadeTo('fast', 0.3, function() { $(this).fadeTo('fast', 1); }); 647 } 648 } 649 650 // fire selection event 651 this._trigger(selected ? 'selected' : 'deselected', null, this._ui('selection', optionLink)); 652 653 return otherItem; 654 }, 655 _setBusy: function(state) { 656 var input = this.availableActions.children('input.search'); 657 var busy = this.availableActions.children('.busy'); 658 659 this.busy = Math.max(state ? ++this.busy : --this.busy, 0); 660 661 this.container.find("a.remove-all, a.add-all")[this.busy ? 'hide' : 'show'](); 662 if (state && (1 == this.busy)) { 663 if (this.options.searchable) { 664 // backup input state 665 input.data('multiselect.hadFocus', input.data('multiselect.hasFocus')); 666 // webkit needs to blur before hiding or it won't fire focus again in the else block 667 input.blur().hide(); 668 } 669 busy.show(); 670 } else if(!this.busy) { 671 if (this.options.searchable) { 672 input.show(); 673 if (input.data('multiselect.hadFocus')) input.focus(); 674 } 675 busy.hide(); 676 } 677 678 // DEBUG 679 //this._messages(0, "Busy state changed to : " + this.busy); 680 }, 681 _applyItemState: function(item, selected) { 682 if (selected) { 683 item.children('span').addClass('ui-helper-hidden').removeClass('ui-icon'); 684 item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus'); 685 this._registerRemoveEvents(item.find('a.action')); 686 687 } else { 688 item.children('span').addClass('ui-helper-hidden').removeClass('ui-icon'); 689 item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus'); 690 this._registerAddEvents(item.find('a.action')); 691 } 692 693 this._registerHoverEvents(item); 694 695 return item; 696 }, 697 // apply filter and return elements 698 _filter: function(elements) { 699 var input = this.availableActions.children('input.search'); 700 var term = $.trim( input.val().toLowerCase() ); 701 702 if ( !term ) { 703 elements.removeClass('filtered'); 704 } else { 705 elements.each(function(i,element) { 706 element = $(element); 707 element[(element.text().toLowerCase().indexOf(term)>=0 ? 'remove' : 'add')+'Class']('filtered'); 708 }); 709 } 710 711 return elements.not('.filtered, .shadowed').show().end().filter('.filtered, .shadowed').hide().end(); 712 }, 713 _registerHoverEvents: function(elements) { 714 elements 715 .unbind('mouseover.multiselect').bind('mouseover.multiselect', function() { 716 $(this).find('a').andSelf().addClass('ui-state-hover'); 717 }) 718 .unbind('mouseout.multiselect').bind('mouseout.multiselect', function() { 719 $(this).find('a').andSelf().removeClass('ui-state-hover'); 720 }) 721 .find('a').andSelf().removeClass('ui-state-hover') 722 ; 723 }, 724 _registerAddEvents: function(elements) { 725 var that = this; 726 elements.unbind('click.multiselect').bind('click.multiselect', function() { 727 // ignore if busy... 728 if (!this.busy) { 729 that._setSelected($(this).parent(), true); 730 } 731 return false;
732 }); 733 if (this.availableList.data('multiselect.draggable')) { 734 // make draggable 735 elements.each(function() { 736 $(this).parent().draggable({ 737 connectToSortable: that.selectedList, 738 helper: _dragHelper, 739 appendTo: that.container, 740 containment: that.container, 741 revert: 'invalid' 742 }); 743 }); 744 // refresh the selected list or the draggable will not connect to it first hand 745 if (this.selectedList.data('multiselect.sortable')) { 746 this.selectedList.sortable('refresh'); 747 } 748 } 749 }, 750 _registerRemoveEvents: function(elements) { 751 var that = this; 752 elements.unbind('click.multiselect').bind('click.multiselect', function() { 753 // ignore if busy... 754 if (!that.busy) { 755 that._setSelected($(this).parent(), false); 756 } 757 return false; 758 }); 759 if (this.selectedList.data('multiselect.draggable')) { 760 // make draggable 761 elements.each(function() { 762 $(this).parent().draggable({ 763 connectToSortable: that.availableList, 764 helper: _dragHelper, 765 appendTo: that.container, 766 containment: that.container, 767 revert: 'invalid' 768 }); 769 }); 770 // refresh the selected list or the draggable will not connect to it first hand 771 if (this.availableList.data('multiselect.sortable')) { 772 this.availableList.sortable('refresh'); 773 } 774 } 775 }, 776 _registerSearchEvents: function(input, searchNow) { 777 var that = this; 778 var previousValue = input.val(), timer; 779 780 var _searchNow = function(forceUpdate) { 781 if (that.busy) return; 782 783 var value = input.val(); 784 if ((value != previousValue) || (forceUpdate)) { 785 that._setBusy(true); 786 787 if (that.options.remoteUrl) { 788 var params = $.extend({}, that.options.remoteParams); 789 try { 790 $.ajax({ 791 url: that.options.remoteUrl, 792 data: $.extend(params, {q:escape(value)}), 793 success: function(data) { 794 that.addOptions(data); 795 that._setBusy(false); 796 }, 797 error: function(request,status,e) { 798 that._messages( 799 $.ui.multiselect.constants.MESSAGE_ERROR, 800 $.ui.multiselect.locale.errorRequest, 801 {status:status} 802 ); 803 that._setBusy(false); 804 } 805 }); 806 } catch (e) { 807 that._messages($.ui.multiselect.constants.MESSAGE_EXCEPTION, e.message); // error message template ?? 808 that._setBusy(false); 809 } 810 } else { 811 that._filter(that.availableList.children('li.ui-element')); 812 that._setBusy(false); 813 } 814 815 previousValue = value; 816 } 817 }; 818 819 // reset any events... if any 820 input.unbind('focus.multiselect blur.multiselect keydown.multiselect keypress.multiselect'); 821 if (this.options.searchable) { 822 input 823 .bind('focus.multiselect', function() { 824 $(this).addClass('ui-state-active').data('multiselect.hasFocus', true); 825 }) 826 .bind('blur.multiselect', function() { 827 $(this).removeClass('ui-state-active').data('multiselect.hasFocus', false); 828 }) 829 .bind('keydown.multiselect keypress.multiselect', function(e) { 830 if (timer) clearTimeout(timer); 831 switch (e.which) { 832 case 13: // enter 833 _searchNow(true); 834 return false; 835 836 default: 837 timer = setTimeout(function() { _searchNow(); }, Math.max(that.options.searchDelay,1)); 838 } 839 }) 840 .show(); 841 } else { 842 input.val('').hide(); 843 this._filter(that.availableList.find('li.ui-element')) 844 } 845 // initiate search filter (delayed) 846 var _initSearch = function() { 847 if (that.busy) { 848 setTimeout(function() { _initSearch(); }, 100); 849 } 850 _searchNow(true); 851 }; 852 853 if (searchNow) _initSearch(); 854 } 855}); 856// END ui.multiselect 857 858 859/******************************** 860 * Internal functions 861 ********************************/ 862 863var _dragHelper = function(event, ui) { 864 var item = $(event.target); 865 var clone = item.clone().width(item.width()); 866 clone 867 .data('multiselect.optionLink', item.data('multiselect.optionLink')) 868 .data('multiselect.list', item.parent() ) 869 // node ui cleanup 870 .find('a').remove() 871 ; 872 return clone; 873}; 874 875 876 877/**************************** 878 * Settings 879 ****************************/ 880 881$.extend($.ui.multiselect, { 882 getter: 'selectedValues enabled isBusy', 883 locale: { 884 addAll:'Add all', 885 removeAll:'Remove all', 886 itemsCount:'#{count} items selected', 887 itemsTotal:'#{count} items total', 888 busy:'please wait...', 889 errorDataFormat:"Cannot add options, unknown data format", 890 errorInsertNode:"There was a problem trying to add the item:\n\n\t[#{key}] => #{value}\n\nThe operation was aborted.", 891 errorReadonly:"The option #{option} is readonly", 892 errorRequest:"Sorry! There seemed to be a problem with the remote call. (Type: #{status})" 893 }, 894 constants: { 895 MESSAGE_WARNING: 0, 896 MESSAGE_EXCEPTION: 1, 897 MESSAGE_ERROR: 2 898 } 899}); 900 901})(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.