1/* 2 * jQuery MultiSelect UI Widget 1.14pre 3 * Depends: 4 * - jQuery 1.4.2+ 5 * - jQuery UI 1.8 widget factory 6 * 7 * Optional: 8 * - jQuery UI effects 9 * - jQuery UI position utility 10 * 11 * Dual licensed under the MIT and GPL licenses: 12 * http://www.opensource.org/licenses/mit-license.php 13 * http://www.gnu.org/licenses/gpl.html 14 */ 15(function($, undefined) { 16 17 var multiselectID = 0; 18 var $doc = $(document); 19 20 $.widget("ech.multiselect", { 21 22 // default options 23 options: { 24 header: true, 25 height: 175, 26 minWidth: 225, 27 menuLeftMargin: 0, 28 classes: '', 29 checkAllText: 'Check all', 30 uncheckAllText: 'Uncheck all', 31 noneSelectedText: 'Select options', 32 selectedText: '# selected', 33 selectedList: 0, 34 show: null, 35 hide: null, 36 autoOpen: false, 37 multiple: true, 38 position: {}, 39 appendTo: "body" 40 }, 41 42 _create: function() { 43 var el = this.element.hide(); 44 var o = this.options; 45 46 this.speed = $.fx.speeds._default; // default speed for effects 47 this._isOpen = false; // assume no 48 49 // create a unique namespace for events that the widget 50 // factory cannot unbind automatically. Use eventNamespace if on 51 // jQuery UI 1.9+, and otherwise fallback to a custom string. 52 this._namespaceID = this.eventNamespace || ('multiselect' + multiselectID); 53 54 var wrapper = (this.wrapper = $('<div>')) 55 .addClass('ui-multiselect-wrapper') 56 .insertAfter(el), 57 58 button = (this.button = $('<button type="button"><span class="ui-icon ui-icon-triangle-1-s"></span></button>')) 59 .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all') 60 .addClass(o.classes) 61 .attr({ 'title':el.attr('title'), 'aria-haspopup':true, 'tabIndex':el.attr('tabIndex') }) 62 .appendTo(wrapper), 63 64 buttonlabel = (this.buttonlabel = $('<span class="ui-multiselect-text" />')) 65 .html(o.noneSelectedText) 66 .appendTo(button), 67 68 menu = (this.menu = $('<div />')) 69 .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all') 70 .addClass(o.classes) 71 .appendTo($(o.appendTo)), 72 73 header = (this.header = $('<div />')) 74 .addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix') 75 .appendTo(menu), 76 77 headerLinkContainer = (this.headerLinkContainer = $('<ul />')) 78 .addClass('ui-helper-reset') 79 .html(function() { 80 if(o.header === true) { 81 return '<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>' + o.checkAllText + '</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>' + o.uncheckAllText + '</span></a></li>'; 82 } else if(typeof o.header === "string") { 83 return '<li>' + o.header + '</li>'; 84 } else { 85 return ''; 86 } 87 }) 88 .append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>') 89 .appendTo(header), 90 91 checkboxContainer = (this.checkboxContainer = $('<ul />')) 92 .addClass('ui-multiselect-checkboxes ui-helper-reset') 93 .appendTo(menu); 94 95 // perform event bindings 96 this._bindEvents(); 97 98 // build menu 99 this.refresh(true); 100 101 // some addl. logic for single selects 102 if(!o.multiple) { 103 menu.addClass('ui-multiselect-single'); 104 } 105 106 // bump unique ID 107 multiselectID++; 108 }, 109 110 _init: function() { 111 if(this.options.header === false) { 112 this.header.hide(); 113 } 114 if(!this.options.multiple) { 115 this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide(); 116 } 117 if(this.options.autoOpen) { 118 this.open(); 119 } 120 if(this.element.is(':disabled')) { 121 this.disable(); 122 } 123 }, 124 125 refresh: function(init) { 126 var el = this.element; 127 var o = this.options; 128 var menu = this.menu; 129 var checkboxContainer = this.checkboxContainer; 130 var optgroups = []; 131 var html = ""; 132 var id = el.attr('id') || multiselectID++; // unique ID for the label & option tags 133 134 // build items 135 el.find('option').each(function(i) { 136 var $this = $(this); 137 var parent = this.parentNode; 138 var description = this.innerHTML; 139 var title = this.title; 140 var value = this.value; 141 var inputID = 'ui-multiselect-' + (this.id || id + '-option-' + i); 142 var isDisabled = this.disabled; 143 var isSelected = this.selected; 144 var labelClasses = [ 'ui-corner-all' ]; 145 var liClasses = 'ui-menu-item ' + (isDisabled ? 'ui-multiselect-disabled ' : ' ') + this.className; 146 var optLabel; 147 148 // is this an optgroup? 149 if(parent.tagName === 'OPTGROUP') { 150 optLabel = parent.getAttribute('label'); 151 152 // has this optgroup been added already? 153 if($.inArray(optLabel, optgroups) === -1) { 154 html += '<li class="ui-multiselect-optgroup-label ' + parent.className + '"><a href="#">
154' + optLabel + '</a></li>'; 155 optgroups.push(optLabel); 156 } 157 } 158 159 if(isDisabled) { 160 labelClasses.push('ui-state-disabled'); 161 } 162 163 // browsers automatically select the first option 164 // by default with single selects 165 if(isSelected && !o.multiple) { 166 labelClasses.push('ui-state-active'); 167 } 168 169 html += '<li class="' + liClasses + '">'; 170 171 // create the label 172 html += '<label for="' + inputID + '" title="' + title + '" class="' + labelClasses.join(' ') + '">'; 173 html += '<input id="' + inputID + '" name="multiselect_' + id + '" type="' + (o.multiple ? "checkbox" : "radio") + '" value="' + value + '" title="' + title + '"'; 174 175 // pre-selected? 176 if(isSelected) { 177 html += ' checked="checked"'; 178 html += ' aria-selected="true"'; 179 } 180 181 // disabled? 182 if(isDisabled) { 183 html += ' disabled="disabled"'; 184 html += ' aria-disabled="true"'; 185 } 186 187 // add the title and close everything off 188 html += ' /><span>' + description + '</span></label></li>'; 189 }); 190 191 // insert into the DOM 192 checkboxContainer.html(html); 193 194 // cache some moar useful elements 195 this.labels = menu.find('label'); 196 this.inputs = this.labels.children('input'); 197 198 // set widths 199 this._setButtonWidth(); 200 this._setMenuWidth(); 201 202 // remember default value 203 this.button[0].defaultValue = this.update(); 204 205 // broadcast refresh event; useful for widgets 206 if(!init) { 207 this._trigger('refresh'); 208 } 209 }, 210 211 // updates the button text. call refresh() to rebuild 212 update: function() { 213 var o = this.options; 214 var $inputs = this.inputs; 215 var $checked = $inputs.filter(':checked'); 216 var numChecked = $checked.length; 217 var value; 218 219 if(numChecked === 0) { 220 value = o.noneSelectedText; 221 } else { 222 if($.isFunction(o.selectedText)) { 223 value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get()); 224 } else if(/\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList) { 225 value = $checked.map(function() { return $(this).next().html(); }).get().join(', '); 226 } else { 227 value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length); 228 } 229 } 230 231 this._setButtonValue(value); 232 233 return value; 234 }, 235 236 // this exists as a separate method so that the developer 237 // can easily override it. 238 _setButtonValue: function(value) { 239 this.buttonlabel.text(value); 240 }, 241 242 // binds events 243 _bindEvents: function() { 244 var self = this; 245 var button = this.button; 246 247 function clickHandler() { 248 self[ self._isOpen ? 'close' : 'open' ](); 249 return false; 250 } 251 252 // webkit doesn't like it when you click on the span :( 253 button 254 .find('span') 255 .bind('click.multiselect', clickHandler); 256 257 // button events 258 button.bind({ 259 click: clickHandler, 260 keypress: function(e) { 261 switch(e.which) { 262 case 27: // esc 263 case 38: // up 264 case 37: // left 265 self.close(); 266 break; 267 case 39: // right 268 case 40: // down 269 self.open(); 270 break; 271 } 272 }, 273 mouseenter: function() { 274 if(!button.hasClass('ui-state-disabled')) { 275 $(this).addClass('ui-state-hover'); 276 } 277 }, 278 mouseleave: function() { 279 $(this).removeClass('ui-state-hover'); 280 }, 281 focus: function() { 282 if(!button.hasClass('ui-state-disabled')) { 283 $(this).addClass('ui-state-focus'); 284 } 285 }, 286 blur: function() { 287 $(this).removeClass('ui-state-focus'); 288 } 289 }); 290 291 // header links 292 this.header.delegate('a', 'click.multiselect', function(e) { 293 // close link 294 if($(this).hasClass('ui-multiselect-close')) { 295 self.close(); 296 297 // check all / uncheck all 298 } else { 299 self[$(this).hasClass('ui-multiselect-all') ? 'checkAll' : 'uncheckAll'](); 300 } 301 302 e.preventDefault(); 303 }); 304 305 // optgroup label toggle support 306 this.menu.delegate('li.ui-multiselect-optgroup-label a', 'click.multiselect', function(e) { 307 e.preventDefault(); 308 309 var $this = $(this);
310 var $inputs = $this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)'); 311 var nodes = $inputs.get(); 312 var label = $this.parent().text(); 313 314 // trigger event and bail if the return is false 315 if(self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false) { 316 return; 317 } 318 319 // toggle inputs 320 self._toggleChecked( 321 $inputs.filter(':checked').length !== $inputs.length, 322 $inputs 323 ); 324 325 self._trigger('optgrouptoggle', e, { 326 inputs: nodes, 327 label: label, 328 checked: nodes[0].checked 329 }); 330 }) 331 .delegate('label', 'mouseenter.multiselect', function() { 332 if(!$(this).hasClass('ui-state-disabled')) { 333 self.labels.removeClass('ui-state-hover'); 334 $(this).addClass('ui-state-hover').find('input').focus(); 335 } 336 }) 337 .delegate('label', 'keydown.multiselect', function(e) { 338 339 switch(e.which) { 340 case 9: // tab 341 case 27: // esc 342 self.close(); 343 break; 344 case 38: // up 345 case 40: // down 346 case 37: // left 347 case 39: // right 348 self._traverse(e.which, this); 349 break; 350 case 13: // enter 351 $(this).find('input')[0].click(); 352 break; 353 default: 354 // skip e.preventDefault() 355 return true; 356 } 357 358 e.preventDefault(); 359 }) 360 .delegate('input[type="checkbox"], input[type="radio"]', 'click.multiselect', function(e) { 361 var $this = $(this); 362 var val = this.value; 363 var checked = this.checked; 364 var tags = self.element.find('option'); 365 366 // bail if this input is disabled or the event is cancelled 367 if(this.disabled || self._trigger('click', e, { value: val, text: this.title, checked: checked }) === false) { 368 e.preventDefault(); 369 return; 370 } 371 372 // make sure the input has focus. otherwise, the esc key 373 // won't close the menu after clicking an item. 374 $this.focus(); 375 376 // toggle aria state 377 $this.attr('aria-selected', checked); 378 379 // change state on the original option tags 380 tags.each(function() { 381 if(this.value === val) { 382 this.selected = checked; 383 } else if(!self.options.multiple) { 384 this.selected = false; 385 } 386 }); 387 388 // some additional single select-specific logic 389 if(!self.options.multiple) { 390 self.labels.removeClass('ui-state-active'); 391 $this.closest('label').toggleClass('ui-state-active', checked); 392 393 // close menu 394 self.close(); 395 } 396 397 // fire change on the select box 398 self.element.trigger("change"); 399 400 // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827 401 // http://bugs.jquery.com/ticket/3827 402 setTimeout($.proxy(self.update, self), 10); 403 }); 404 405 // close each widget when clicking on any other element/anywhere else on the page 406 $doc.bind('mousedown.' + this._namespaceID, function(event) { 407 var target = event.target; 408 409 if(self._isOpen 410 && target !== self.button[0] 411 && target !== self.menu[0] 412 && !$.contains(self.menu[0], target) 413 && !$.contains(self.button[0], target) 414 ) { 415 self.close(); 416 } 417 }); 418 419 // deal with form resets. the problem here is that buttons aren't 420 // restored to their defaultValue prop on form reset, and the reset 421 // handler fires before the form is actually reset. delaying it a bit 422 // gives the form inputs time to clear. 423 $(this.element[0].form).bind('reset.multiselect', function() { 424 setTimeout($.proxy(self.refresh, self), 10); 425 }); 426 }, 427 428 // set button width 429 _setButtonWidth: function() { 430 var width = this.element.outerWidth(); 431 var o = this.options; 432 433 if(/\d/.test(o.minWidth) && width < o.minWidth) { 434 width = o.minWidth; 435 } 436 437 // set widths 438 this.button.outerWidth(width); 439 }, 440 441 // set menu width 442 _setMenuWidth: function() { 443 var m = this.menu; 444 m.outerWidth(this.options.menuWidth || this.button.outerWidth()); 445 }, 446 447 // move up or down within the menu 448 _traverse: function(which, start) { 449 var $start = $(start); 450 var moveToLast = which === 38 || which === 37; 451 452 // select the first li that isn't an optgroup label / disabled 453 var $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)').first(); 454 455 // if at the first/last element 456 if(!$next.length) { 457 var $container = this.menu.find('ul').last(); 458 459 // move to the first/last 460 this.menu.find('label')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover'); 461 462 // set scroll position 463 $container.scrollTop(moveToLast ? $container.height() : 0); 464 465 } else { 466 $next.find('label').trigger('mouseover'); 467 } 468 }, 469 470 // This is an internal function to toggle the checked property and 471 // other related attributes of a checkbox. 472 // 473 // The context of this function should be a checkbox; do not proxy it. 474 _toggleState: function(prop, flag) { 475 return function() { 476 if(!this.disabled) { 477 this[ prop ] = flag; 478 } 479 480 if(flag) { 481 this.setAttribute('aria-selected', true); 482 } else { 483 this.removeAttribute('aria-selected'); 484 } 485 }; 486 }, 487 488 _toggleChecked: function(flag, group) {
489 var $inputs = (group && group.length) ? group : this.inputs; 490 var self = this; 491 492 // toggle state on inputs 493 $inputs.each(this._toggleState('checked', flag)); 494 495 // give the first input focus 496 $inputs.eq(0).focus(); 497 498 // update button text 499 this.update(); 500 501 // gather an array of the values that actually changed 502 var values = $inputs.map(function() { 503 return this.value; 504 }).get(); 505 506 // toggle state on original option tags 507 this.element 508 .find('option') 509 .each(function() { 510 if(!this.disabled && $.inArray(this.value, values) > -1) { 511 self._toggleState('selected', flag).call(this); 512 } 513 }); 514 515 // trigger the change event on the select 516 if($inputs.length) { 517 this.element.trigger("change"); 518 } 519 }, 520 521 _toggleDisabled: function(flag) { 522 this.button.attr({ 'disabled':flag, 'aria-disabled':flag })[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled'); 523 524 var inputs = this.menu.find('input'); 525 var key = "ech-multiselect-disabled"; 526 527 if(flag) { 528 // remember which elements this widget disabled (not pre-disabled) 529 // elements, so that they can be restored if the widget is re-enabled. 530 inputs = inputs.filter(':enabled').data(key, true) 531 } else { 532 inputs = inputs.filter(function() { 533 return $.data(this, key) === true; 534 }).removeData(key); 535 } 536 537 inputs 538 .attr({ 'disabled':flag, 'arial-disabled':flag }) 539 .parent()[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled'); 540 541 this.element.attr({ 542 'disabled':flag, 543 'aria-disabled':flag 544 }); 545 }, 546 547 // open the menu 548 open: function(e) { 549 var self = this; 550 var button = this.button; 551 var menu = this.menu; 552 var speed = this.speed; 553 var o = this.options; 554 var args = []; 555 556 // bail if the multiselectopen event returns false, this widget is disabled, or is already open 557 if(this._trigger('beforeopen') === false || button.hasClass('ui-state-disabled') || this._isOpen) { 558 return; 559 } 560 561 var $container = menu.find('ul').last(); 562 var effect = o.show; 563 564 // figure out opening effects/speeds 565 if($.isArray(o.show)) { 566 effect = o.show[0]; 567 speed = o.show[1] || self.speed; 568 } 569 570 // if there's an effect, assume jQuery UI is in use 571 // build the arguments to pass to show() 572 if(effect) { 573 args = [ effect, speed ]; 574 } 575 576 // set the scroll of the checkbox container 577 $container.scrollTop(0).height(o.height); 578 579 // positon 580 this.position(); 581 582 // show the menu, maybe with a speed/effect combo 583 $.fn.show.apply(menu, args); 584 585 // select the first not disabled option 586 // triggering both mouseover and mouseover because 1.4.2+ has a bug where triggering mouseover 587 // will actually trigger mouseenter. the mouseenter trigger is there for when it's eventually fixed 588 this.labels.filter(':not(.ui-state-disabled)').eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus'); 589 590 button.addClass('ui-state-active'); 591 this._isOpen = true; 592 this._trigger('open'); 593 }, 594 595 // close the menu 596 close: function() { 597 if(this._trigger('beforeclose') === false) { 598 return; 599 } 600 601 var o = this.options; 602 var effect = o.hide; 603 var speed = this.speed; 604 var args = []; 605 606 // figure out opening effects/speeds 607 if($.isArray(o.hide)) { 608 effect = o.hide[0]; 609 speed = o.hide[1] || this.speed; 610 } 611 612 if(effect) { 613 args = [ effect, speed ]; 614 } 615 616 $.fn.hide.apply(this.menu, args); 617 this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave'); 618 this._isOpen = false; 619 this._trigger('close'); 620 }, 621 622 enable: function() { 623 this._toggleDisabled(false); 624 }, 625 626 disable: function() { 627 this._toggleDisabled(true); 628 }, 629 630 checkAll: function(e) { 631 this._toggleChecked(true); 632 this._trigger('checkAll'); 633 }, 634 635 uncheckAll: function() { 636 this._toggleChecked(false); 637 this._trigger('uncheckAll'); 638 }, 639 640 getChecked: function() { 641 return this.menu.find('input').filter(':checked'); 642 }, 643 644 destroy: function() { 645 // remove classes + data 646 $.Widget.prototype.destroy.call(this); 647 648 // unbind events 649 $doc.unbind(this._namespaceID); 650 651 this.button.remove(); 652 this.menu.remove(); 653 this.element.show(); 654 655 return this; 656 }, 657 658 isOpen: function() { 659 return this._isOpen; 660 }, 661 662 widget: function() { 663 return this.menu; 664 }, 665 666 getButton: function() { 667 return this.button; 668 }, 669 670 position: function() { 671 var o = this.options; 672 673 // use the position utility if it exists and options are specifified 674 if($.ui.position && !$.isEmptyObject(o.position)) { 675 o.position.of = o.position.of || this.button; 676 677 this.menu 678 .show() 679 .position(o.position) 680 .hide(); 681 682 // otherwise fallback to custom positioning 683 } else { 684 var pos = this.button.offset(); 685 686 this.menu.css({ 687 top: pos.top + this.button.outerHeight(), 688 left: pos.left + o.menuLeftMargin 689 }); 690 } 691 }, 692 693 // react to option changes after initialization 694 _setOption: function(key, value) { 695 var menu = this.menu; 696 697 switch(key) { 698 case 'header': 699 menu.find('div.ui-multiselect-header')[value ? 'show' : 'hide'](); 700 break; 701 case 'checkAllText': 702 menu.find('a.ui-multiselect-all span').eq(-1).text(value); 703 break; 704 case 'uncheckAllText': 705 menu.find('a.ui-multiselect-none span').eq(-1).text(value); 706 break; 707 case 'height': 708 menu.find('ul').last().height(parseInt(value, 10)); 709 break; 710 case 'minWidth': 711 this.options[key] = parseInt(value, 10); 712 this._setButtonWidth(); 713 this._setMenuWidth(); 714 break; 715 case 'selectedText': 716 case 'selectedList': 717 case 'noneSelectedText': 718 this.options[key] = value; // these all needs to update immediately for the update() call 719 this.update(); 720 break; 721 case 'classes': 722 menu.add(this.button).removeClass(this.options.classes).addClass(value); 723 break; 724 case 'multiple': 725 menu.toggleClass('ui-multiselect-single', !value); 726 this.options.multiple = value; 727 this.element[0].multiple = value; 728 this.refresh(); 729 break; 730 case 'position': 731 this.position(); 732 } 733 734 $.Widget.prototype._setOption.apply(this, arguments); 735 } 736 }); 737 738})(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.