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