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