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