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 var multiValues = ""; 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 MyValues:function(){ 212 return multiValues; 213 }, 214 // updates the button text. call refresh() to rebuild 215 update: function() { 216 var o = this.options; 217 var $inputs = this.inputs; 218 var $checked = $inputs.filter(':checked'); 219 var numChecked = $checked.length; 220 var value; 221 222 if(numChecked === 0) { 223 value = o.noneSelectedText; 224 } else { 225 if($.isFunction(o.selectedText)) { 226 value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get()); 227 } else if(/\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList) { 228 value = $checked.map(function() { return $(this).next().html(); }).get().join(', '); 229 } else { 230 value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length); 231 } 232 multiValues = $checked.map(function () { return $(this).val(); }).get().join(', '); 233 } 234 235 this._setButtonValue(value); 236 237 return value; 238 }, 239 240 // this exists as a separate method so that the developer 241 // can easily override it. 242 _setButtonValue: function(value) { 243 this.buttonlabel.text(value); 244 }, 245 246 // binds events 247 _bindEvents: function() { 248 var self = this; 249 var button = this.button; 250 251 function clickHandler() { 252 self[ self._isOpen ? 'close' : 'open' ](); 253 return false; 254 } 255 256 // webkit doesn't like it when you click on the span :( 257 button 258 .find('span') 259 .bind('click.multiselect', clickHandler); 260 261 // button events 262 button.bind({ 263 click: clickHandler, 264 keypress: function(e) { 265 switch(e.which) { 266 case 27: // esc 267 case 38: // up 268 case 37: // left 269 self.close(); 270 break; 271 case 39: // right 272 case 40: // down 273 self.open(); 274 break; 275 } 276 }, 277 mouseenter: function() { 278 if(!button.hasClass('ui-state-disabled')) { 279 $(this).addClass('ui-state-hover'); 280 } 281 }, 282 mouseleave: function() { 283 $(this).removeClass('ui-state-hover'); 284 }, 285 focus: function() { 286 if(!button.hasClass('ui-state-disabled')) { 287 $(this).addClass('ui-state-focus'); 288 } 289 }, 290 blur: function() { 291 $(this).removeClass('ui-state-focus'); 292 } 293 }); 294 295 // header links 296 this.header.delegate('a', 'click.multiselect', function(e) { 297 // close link 298 if($(this).hasClass('ui-multiselect-close')) { 299 self.close(); 300 301 // check all / uncheck all 302 } else { 303 self[$(this).hasClass('ui-multiselect-all') ? 'checkAll' : 'uncheckAll'](); 304 } 305 306 e.preventDefault(); 307 }); 308 309 // optgroup label toggle support 310 this.menu.delegate('li.ui-multiselect-optgroup-label a', 'click.multiselect', function(e) { 311 e.preventDefault(); 312 313 var $this = $(this);
314 var $inputs = $this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)'); 315 var nodes = $inputs.get(); 316 var label = $this.parent().text(); 317 318 // trigger event and bail if the return is false 319 if(self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false) { 320 return; 321 } 322 323 // toggle inputs 324 self._toggleChecked( 325 $inputs.filter(':checked').length !== $inputs.length, 326 $inputs 327 ); 328 329 self._trigger('optgrouptoggle', e, { 330 inputs: nodes, 331 label: label, 332 checked: nodes[0].checked 333 }); 334 }) 335 .delegate('label', 'mouseenter.multiselect', function() { 336 if(!$(this).hasClass('ui-state-disabled')) { 337 self.labels.removeClass('ui-state-hover'); 338 $(this).addClass('ui-state-hover').find('input').focus(); 339 } 340 }) 341 .delegate('label', 'keydown.multiselect', function(e) { 342 e.preventDefault(); 343 344 switch(e.which) { 345 case 9: // tab 346 case 27: // esc 347 self.close(); 348 break; 349 case 38: // up 350 case 40: // down 351 case 37: // left 352 case 39: // right 353 self._traverse(e.which, this); 354 break; 355 case 13: // enter 356 $(this).find('input')[0].click(); 357 break; 358 } 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.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 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.