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