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