https://service.essbay.de/_assets/4aa87cbc97b19002293e590a434cf15d…/jquery.multiselect.js?1750255479
1/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, boss:true, undef:true, curly:true, browser:true, jquery:true */ 2/* 3 * jQuery UI MultiSelect Widget 3.0.0 4 * Copyright (c) 2012 Eric Hynds 5 * 6 * Depends: 7 * - jQuery 1.8+ (http://api.jquery.com/) 8 * - jQuery UI 1.11 widget factory (http://api.jqueryui.com/jQuery.widget/) 9 * 10 * Optional: 11 * - jQuery UI effects 12 * - jQuery UI position utility 13 * 14 * Dual licensed under the MIT and GPL licenses: 15 * http://www.opensource.org/licenses/mit-license.php 16 * http://www.gnu.org/licenses/gpl.html 17 * 18 */ 19(function($, undefined) { 20 // Counter used to prevent collisions 21 var multiselectID = 0; 22 23 // The following information can be overridden via the linkInfo option. 24 // An $.extend is used to allow just specifying a partial object in linkInfo. 25 var linkDefaults = { 26 'open': { 27 'class': 'ui-multiselect-open', 28 'icon': '<span class="ui-icon ui-icon-triangle-1-s"></span', 29 'title': 'Open', 30 'text': 'Open', 31 }, 32 'close': { 33 'class': 'ui-multiselect-close', 34 'icon': '<span class="ui-icon ui-icon-circle-close"></span>Schliessen', 35 'title': 'Close', 36 'text': 'Close', 37 }, 38 'checkAll': { 39 'class': 'ui-multiselect-all', 40 'icon': '<span class="ui-icon ui-icon-check"></span>', 41 'text': 'Check all', 42 'title': 'Check all', 43 }, 44 'uncheckAll': { 45 'class': 'ui-multiselect-none', 46 'icon': '<span class="ui-icon ui-icon-closethick"></span>', 47 'text': 'Uncheck all', 48 'title': 'Uncheck all', 49 }, 50 'flipAll': { 51 'class': 'ui-multiselect-flip', 52 'icon': '<span class="ui-icon ui-icon-arrowrefresh-1-w"></span>', 53 'text': 'Flip all', 54 'title': 'Flip all', 55 }, 56 'collapse': { 57 'icon': '<span class="ui-icon ui-icon-minusthick"></span>', 58 'title': 'Collapse', 59 }, 60 'expand': { 61 'icon': '<span class="ui-icon ui-icon-plusthick"></span>', 62 'title': 'Expand', 63 }, 64 'collapseAll': { 65 'class': 'ui-multiselect-collapseall', 66 'icon': '<span class="ui-icon ui-icon-minus"></span>', 67 'text': 'Collapse all', 68 'title': 'Collapse all', 69 }, 70 'expandAll': { 71 'class': 'ui-multiselect-expandall', 72 'icon': '<span class="ui-icon ui-icon-plus"></span>', 73 'text': 'Expand all', 74 'title': 'Expand all', 75 }, 76 }; 77 78 /** 79 * Checks an option element for data-image-src 80 * and adds that as an image tag within the widget option 81 * 82 * @param {Node} option to pull an image from 83 * @param {Node} span to insert image tag into 84 */ 85 function insertImage(option, span) { 86 var optionImageSrc = option.getAttribute('data-image-src'); 87 if (optionImageSrc) { 88 var img = document.createElement('img'); 89 img.setAttribute('src', optionImageSrc); 90 span.insertBefore(img, span.firstChild); 91 } 92 } 93 94 /** 95 * Retrieves the font size of the document 96 * Defaults to 16px 97 * @return {string} pixel string for font size 98 */ 99 function determineFontSize() { 100 if (window.getComputedStyle) { 101 return getComputedStyle(document.body).fontSize; 102 } 103 return '16px'; 104 } 105 106 /** 107 * Creates a jQuery object from the input element 108 * This can be a string selector, Node, or jQuery object 109 * @param {(object|string)} elem 110 * @return {object} jquery object for element 111 */ 112 function getjQueryFromElement(elem) { 113 if (!!elem.jquery) { 114 return elem; 115 } 116 if (!!elem.nodeType) { 117 return $(elem); 118 } 119 120 return $(elem).eq(0); 121 } 122 123 /** 124 * Converts dimensions specified in options to pixel values. 125 * Determines if specified value is a minimum, maximum or exact value. 126 * The value can be a number or a string with px, pts, ems, in, cm, mm, or % units. 127 * Number/Numeric string treated as pixel measurements 128 * - 30 129 * - '30' 130 * - '>30px' 131 * - '1.3em' 132 * - '20 pt' 133 * - '30%' 134 * @param {string} dimText Option text (or number) containing possibly < or >, number, and a unit. 135 * @param {object} $elem jQuery object (or node) to reference for % calculations. 136 * @param {boolean} isHeight T/F to change from using width in % calculations. 137 * @return {object}
137 object containing pixels and -1/1/0 indicating min/max/exact. 138 */ 139 function parse2px(dimText, $elem, isHeight) { 140 if (typeof dimText !== 'string') { 141 return {px: dimText, minimax: 0}; 142 } 143 144 var parts = dimText.match(/([<>])?=?\s*([.\d]+)\s*([eimnptx%]*)s?/i); 145 var minimax = parts[1]; 146 var value = parseFloat(parts[2]); 147 var unit = parts[3].toLowerCase(); 148 var pixels = -1; 149 switch (unit) { 150 case 'pt': 151 case 'in': 152 case 'cm': 153 case 'mm': 154 pixels = {'pt': 4.0 / 3.0, 'in': 96.0, 'cm': 96.0 / 2.54, 'mm': 96.0 / 25.4}[unit] * value; 155 break; 156 case 'em': 157 pixels = parseFloat(determineFontSize()) * value; 158 break; 159 case '%': 160 if ( !!$elem ) { 161 if (typeof $elem === 'string' || !$elem.jquery) { 162 $elem = $($elem); 163 } 164 pixels = ( !!isHeight ? $elem.parent().height() : $elem.parent().width() ) * (value / 100.0); 165 } // else returns -1 default value from above. 166 break; 167 default: 168 pixels = value; 169 } 170 // minimax: -1 => minimum value, 1 => maximum value, 0 => exact value 171 return {px: pixels, minimax: minimax == '>' ? -1 : ( minimax == '<' ? 1 : 0 )}; 172 } 173 174 $.widget('ech.multiselect', { 175 176 // default options 177 options: { 178 buttonWidth: 225, // (integer | string | 'auto' | null) Sets the min/max/exact width of the button. 179 menuWidth: null, // (integer | string | 'auto' | null) If a number is provided, sets the exact menu width. 180 menuHeight: 400, // (integer | string | 'auto' | 'size') Sets the height of the menu or determines it using native select's size setting. 181 resizableMenu: true, // (true | false) Enables the use of jQuery UI resizable if it is loaded. 182 appendTo: null, // (jQuery | DOM element | selector string) If provided, this specifies what element to append the widget to in the DOM. 183 position: {}, // (object) A jQuery UI position object that constrains how the pop-up menu is positioned. 184 zIndex: null, // (integer) Overrides the z-index set for the menu container. 185 classes: '', // (string) Classes that you can provide to be applied to the elements making up the widget. 186 header: ['checkAll', 'uncheckAll'], // (false | string | array) False, custom string or array indicating which links to show in the header & in what order. 187 linkInfo: null, // (object | null) Supply an obect of link information to use alternative icons, icon labels, or icon title text. See linkDefaults above for object structure. 188 noneSelectedText: 'Select options', // (string | null) The text to show in the button where nothing is selected. Set to null to use the native select's placeholder text. 189 selectedText: '# of # selected', // (string) A "template" that indicates how to show the count of selections in the button. The "#'s" are replaced by the selection count & option count. 190 selectedList: 0, // (integer) The actual list selections will be shown in the button when the count of selections is <= than this number. 191 selectedListSeparator: ', ', // (string) This allows customization of the list separator. Use ',<br/>' to make the button grow vertically showing 1 selection per line. 192 maxSelected: null, // (integer | null) If selected count > maxSelected, then message is displayed, and new selection is undone. 193 openEffect: null, // (array) An array containing menu opening effect information. 194 closeEffect: null, // (array) An array containing menu closing effect information. 195 autoOpen: false, // (true | false) If true, then the menu will be opening immediately after initialization. 196 htmlText: ['button', 'options'], // (array) List of 'button' &/or 'options' indicating in which parts of the widget to treat text as html. 197 wrapText: ['button', 'header', 'options'], // (array) List of 'button', 'header', &/or 'options' indicating in which parts of the widget to wrap text. 198 listbox: false, // (true | false) Omits the button and instead of a pop-up inserts the open menu directly after the native select as a list box. 199 addInputNames: true, // (true | false) If true, names are created for each option input in the multi-select. 200 disableInputsOnToggle: true, // (true | false) If true, each individual checkbox input is also disabled
200when the widget is disabled. 201 groupsSelectable: true, // (true | false) Determines if clicking on an option group heading selects all of its options. 202 groupsCollapsable: false, // (true | false) Determines if option groups can be collapsed. 203 groupColumns: false, // (true | false) Displays groups in a horizonal column layout. 204 groupColumnsWidth: false, // (integer) The width of each select item in the groupColumns. 205 }, 206 207 /** 208 * This method determines which DOM element to append the menu to. Determination process: 209 * 1. Look up the jQuery object, DOM element, or string selector provided in the options. 210 * 2. If nothing provided in options or lookup in #1 failed, then look for .ui-front or dialog. (dialog case) 211 * 3. If still do not have a valid DOM element to append to, then append to the document body. 212 * 213 * NOTE: this.element and this.document are jQuery objects per the jQuery UI widget API. 214 * @return {object} jQuery object for the DOM element to append to. 215 */ 216 _getAppendEl: function() { 217 var elem = this.options.appendTo; // jQuery object or selector, DOM element or null. 218 219 if (elem) { // NOTE: The find below handles the jQuery selector case 220 elem = getjQueryFromElement(elem); 221 } 222 if (!elem || !elem[0]) { 223 elem = this.element.closest('.ui-front, dialog'); 224 } 225 if (!elem.length) { 226 elem = $(document.body); // Position at end of body. Note that this returns a DOM element. 227 } 228 return elem; 229 }, 230 231 /** 232 * Constructs the button element for the widget 233 * Stores the result in this.$button 234 * @return{object} jQuery object for button 235 */ 236 _buildButton: function() { 237 var wrapText = this.options.wrapText || []; 238 var $button = (this.$button = $(document.createElement('button'))) 239 .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all' 240 + (wrapText.indexOf('button') > -1 ? '' : ' ui-multiselect-nowrap') 241 + (this.options.classes ? ' ' + this.options.classes : '') 242 ) 243 .attr({ 244 'type': 'button', 245 'title': this.element[0].title, 246 'tabIndex': this.element[0].tabIndex, 247 'id': this.element[0].id ? this.element[0].id + '_ms' : null, 248 }) 249 .prop('aria-haspopup', true) 250 .html(this._linkHTML('<span class="{{class}}" title="{{title}}">{{icon}}</span>', 'open')); 251 252 this.$buttonlabel = $(document.createElement('span')) 253 .html(this.options.noneSelectedText || this.element[0].placeholder) 254 .appendTo($button); 255 return $button; 256 }, 257 258 /** 259 * Constructs HTML string for menu header 260 * @return {string} 261 */ 262 _buildHeaderHtml: function() { 263 // Header controls will contain the links & ordering specified by the header option. 264 // Depending on how the options are set, this may be empty or simply plain text 265 if (!this.options.header) { 266 return ''; 267 } 268 if (typeof this.options.header === 'string') { 269 return '<li>' + this.options.header + '</li>'; 270 } 271 var headerLinksHTML = ''; 272 if (this.options.header.constructor == Array) { 273 for (var x = 0; x < this.options.header.length; x++) { 274 var linkInfoKey = this.options.header[x]; 275 if (linkInfoKey && linkInfoKey in this.linkInfo 276 && !(this.options.maxSelected && linkInfoKey === 'checkAll') 277 && ['open', 'close', 'collapse', 'expand'].indexOf(linkInfoKey) === -1) { 278 headerLinksHTML += this._linkHTML('<li><a class="{{class}}" title="{{title}}">{{icon}}<span>{{text}}</span></a></li>', linkInfoKey); 279 } 280 } 281 } 282 283 if (this.options.header.constructor == Object) { 284 var options = Object.keys(this.options.header) 285 for (var x = 0; x < options.length; x++) { 286 var displayText = options[x]; 287 var linkInfoKey = this.options.header[displayText]; 288 if (linkInfoKey && linkInfoKey in this.linkInfo 289 && !(this.options.maxSelected && linkInfoKey === 'checkAll') 290 && ['open', 'close', 'collapse', 'expand'].indexOf(linkInfoKey) === -1) { 291 headerLinksHTML += this._linkHTML('<li><a class="{{class}}" title="{{title}}">{{icon}}<span>'+displayText+'</span></a></li>', linkInfoKey); 292 } 293 } 294 } 295 return headerLinksHTML; 296 }, 297 298 /** 299 * Performs initial widget creation 300 * Widget API has already set this.element and this.options for us 301 * All inserts into the DOM are performed at the end to limit performance impact 302 * - Build header links based on options and linkInfo object 303 * - Set UI effect speeds 304 * - Sets the multiselect ID using the global counter 305 * - Creates the button, header, and menu 306 * - Binds events for the widget 307 * - Calls refresh to populate the menu 308 */ 309 _create: function() { 310 var $element = this.element; 311 var options = this.options; 312 313 // Do an extend here to address link info missing from options.linkInfo--missing info defaults to that in linkDefaults. 314 this.linkInfo = $.extend(true, {}, linkDefaults, options.linkInfo || {}); 315 316 // grab select width before hiding it 317 this._selectWidth = $element.outerWidth(); 318 $element.hide(); 319 320 // Convert null/falsely option values to empty arrays for fewer problems 321 options.htmlText = options.htmlText || []; 322 var wrapText = ( options.wrapText = options.wrapText || [] ); 323 324 // default speed for effects 325 this.speed = $.fx.speeds._default; 326 this._isOpen = false;
327 328 // Create a unique namespace for events that 329 // the widget factory cannot unbind automatically. 330 this._namespaceID = this.eventNamespace; 331 // bump unique ID after assigning it to the widget instance 332 this.multiselectID = multiselectID++; 333 334 335 this.$headerLinkContainer = $( document.createElement('ul') ) 336 .addClass('ui-helper-reset') 337 .html( this._buildHeaderHtml() 338 + ( !options.listbox 339 ? this._linkHTML('<li class="{{class}}"><a class="{{class}}" title="{{title}}">{{icon}}</a></li>', 'close') 340 : '' ) ); 341 342 // Menu header to hold controls for the menu 343 var $header = ( this.$header = $( document.createElement('div') ) ) 344 .addClass('ui-multiselect-header ui-widget-header ui-corner-all ui-helper-clearfix') 345 .append( this.$headerLinkContainer ); 346 347 // Holds the actual check boxes for inputs 348 var $checkboxes = ( this.$checkboxes = $( document.createElement('ul') ) ) 349 .addClass('ui-multiselect-checkboxes ui-helper-reset' + (wrapText.indexOf('options') > -1 ? '' : ' ui-multiselect-nowrap')); 350 351 // This is the menu container that will hold all the options added via refresh(). 352 var $menu = ( this.$menu = $( document.createElement('div') ) ) 353 .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all' 354 + ($element[0].multiple ? '' : ' ui-multiselect-single') 355 + (!options.listbox ? '' : ' ui-multiselect-listbox') 356 + (this.options.classes ? ' ' + this.options.classes : '')) 357 .append($header, $checkboxes); 358 359 if (!options.listbox) { 360 var $button = this._buildButton(); 361 $button.insertAfter($element); 362 var $appendEl = this._getAppendEl(); 363 $appendEl.append($menu); 364 // Set z-index of menu appropriately when it is not appended to a dialog and no z-index specified. 365 if ( !options.zIndex && !$appendEl.hasClass('ui-front') ) { 366 var $uiFront = this.element.closest('.ui-front, dialog'); 367 options.zIndex = Math.max( $uiFront && parseInt($uiFront.css('z-index'), 10) + 1 || 0, 368 $appendEl && parseInt($appendEl.css('z-index'), 10) + 1 || 0); 369 } 370 371 if (options.zIndex) { 372 $menu.css('z-index', options.zIndex); 373 } 374 // Use $.extend below since the "of" position property may not be able to be supplied via the option. 375 options.position = $.extend({'my': 'left top', 'at': 'left bottom', 'of': $button}, options.position || {}); 376 } else { 377 $menu.insertAfter($element); // No button 378 } 379 380 this._bindEvents(); 381 382 // build menu 383 this.refresh(true); 384 }, 385 386 /** 387 * Helper function used in _create() 388 * @param {string} linkTemplate HTML link template string 389 * @param {string} linkID key string to look up in linkInfo object. 390 * @return {object} link HTML 391 */ 392 _linkHTML: function(linkTemplate, linkID) { 393 var self = this; 394 return linkTemplate.replace(/{{(.*?)}}/ig, function(m, p1) { 395 return self.linkInfo[linkID][p1]; 396} ) 397 .replace('<span></span>', ''); 398 }, 399 400 /** 401 * https://api.jqueryui.com/jquery.widget/#method-_init 402 * Performed every time the widget is instantiated, or called with only an options object 403 * - Set visibility of header links 404 * - Auto open menu if appropriate 405 * - Set disabled status 406 */ 407 _init: function() { 408 var elSelect = this.element[0]; 409 410 if (this.options.header !== false) { 411 this.$headerLinkContainer 412 .find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip') 413 .toggle( !!elSelect.multiple ); 414 } else { 415 this.$header.hide(); 416 } 417 418 if (this.options.autoOpen && !this.options.listbox) { 419 this.open(); 420 } 421 422 if (elSelect.disabled) { 423 this.disable(); 424 } 425 }, 426 427 /** 428 * Builds an option item for the menu. (Mostly plain JS for speed.) 429 * <li> 430 * <label> 431 * <input /> checkbox or radio depending on single/multiple select 432 * <span /> option text 433 * </label> 434 * </li> 435 * @param {node} option Option from select to be added to menu 436 * @return {object} jQuery object for menu option 437 */ 438 _makeOption: function(option) { 439 var elSelect = this.element.get(0); 440 // Determine unique ID for the label & option tags 441 var id = elSelect.id || this.multiselectID; 442 var inputID = 'ui-multiselect-' + this.multiselectID + '-' + (option.id || id + '-option-' + this.inputIdCounter++); 443 // Pick up the select type from the underlying element 444 var isMultiple = elSelect.multiple; 445 var isDisabled = option.disabled; 446 var isSelected = option.selected; 447 448 var input = document.createElement('input'); 449 var inputAttribs = { 450 'type': isMultiple ? 'checkbox' : 'radio', 451 'id': inputID, 452 'title': option.title || null, 453 'value': option.value, 454 'name': this.options.addInputNames ? 'multiselect_' + id : null, 455 'checked': isSelected ? 'checked' : null, 456 'aria-selected': isSelected ? 'true' : null, 457 'disabled': isDisabled ? 'disabled' : null, 458 'aria-disabled': isDisabled ? 'true' : null, 459 }; 460 for (var name in inputAttribs) { 461 if (inputAttribs[name] !== null) { 462 input.setAttribute(name, inputAttribs[name]); 463 } 464 } 465 // Clone data attributes 466 var optionAttribs = option.attributes; 467 var len = optionAttribs.length; 468 for (var x = 0; x < len; x++) { 469 var attribute = optionAttribs[x]; 470 if ( /^data\-.+/.test(attribute.name) ) { 471 input.setAttribute(attribute.name, attribute.value); 472 } 473 } 474 475 // Option text or html 476 var span = document.createElement('span'); 477 if (this.htmlAllowedFor('options')) { 478 span.innerHTML = option.innerHTML; 479 } else { 480 span.textContent = option.textContent; 481 } 482 483 // Icon images for each item. 484 insertImage(option, span); 485 486 var label = document.createElement('label'); 487 label.setAttribute('for', inputID); 488 if (option.title) { 489 label.setAttribute('title', option.title); 490 } 491 label.className += (isDisabled ? ' ui-state-disabled' : '') 492 + (isSelected && !isMultiple ? ' ui-state-active' : '') 493 + ' ui-corner-all'; 494 label.appendChild(input); 495 label.appendChild(span); 496 497 var item = document.createElement('li'); 498 item.className = (isDisabled ? 'ui-multiselect-disabled ' : '') 499 + (this.options.groupColumns ? ' ui-multiselect-columns' : '') 500 + (option.className || ''); 501 502 if (this.options.groupColumnsWidth) { 503 item.style.width = this.options.groupColumnsWidth+'px'; 504 } 505 506 item.appendChild(label); 507 508 return item; 509 }, 510 511 /** 512 * Processes option and optgroup tags from underlying select to construct the menu's option list 513 * If groupsCollapsable option is set, adds collapse/expand buttons for each option group. 514 * This replaces the current contents of this.$checkboxes 515 * Defers to _makeOption to actually build the options 516 * Resets the input ID counter 517 */ 518 _buildOptionList: function() { 519 var self = this; 520 var list = []; 521 522 this.inputIdCounter = 0; 523 524 this.element.children().each( function() { 525 var elem = this; 526 527 if (elem.tagName.toUpperCase() === 'OPTGROUP') { 528 var options = []; 529 530 $(elem).children().each( function() { 531 options.push(self._makeOption(this)); 532 }); 533 534 // Build the list section for this optgroup, complete w/ option inputs... 535 var $collapseButton = !!self.options.groupsCollapsable 536 ? $( document.createElement('button') ) 537 .attr({'title': self.linkInfo.collapse.title}) 538 .addClass('ui-state-default ui-corner-all ui-multiselect-collapser') 539 .html(self.linkInfo.collapse.icon) 540 : null; 541 var $optGroupLabel = $( document.createElement('a') ) 542 .addClass('ui-multiselect-grouplabel' 543 + (self.options.groupsSelectable ? ' ui-multiselect-selectable' : '')) 544 .html( elem.getAttribute('label') ); 545 var $optionGroup = $( document.createElement('ul') ).append(options); 546 var $optGroupItem = $( document.createElement('li') ) 547 .addClass('ui-multiselect-optgroup' 548 + (self.options.groupColumns ? ' ui-multiselect-columns' : '') 549 + (elem.className ? ' ' + elem.className : '')) 550 .append($collapseButton, $optGroupLabel, $optionGroup); 551 552 if (self.options.groupColumnsWidth) { 553 $optGroupItem.css('width', self.options.groupColumnsWidth+'px'); 554 } 555 556 list.push($optGroupItem); 557 } else { 558 list.push(self._makeOption(elem)); 559 } 560 }); 561 562 this.$checkboxes.empty().append(list); 563 }, 564 565 /** 566 * Refreshes the widget's menu 567 * - Refresh header links if required 568 * - Rebuild option list 569 * - Update the cached values for height, width, and cached elements 570 * - If listbox option is set, shows the menu and sets menu size. 571 * @param {boolean} init If false, broadcasts a refresh event 572 */ 573 refresh: function(init) { 574 var $element = this.element; 575 576 // update header link container visibility if needed 577 if (this.options.header !== false) { 578 this.$headerLinkContainer 579 .find('.ui-multiselect-all, .ui-multiselect-none, .ui-multiselect-flip') 580 .toggle( !!$element[0].multiple ); 581 } 582 583 this._buildOptionList(); // Clear and rebuild the menu. 584 this._updateCache(); // cache some more useful elements 585 586 if (!this.options.listbox) { 587 this._setButtonWidth(); 588 this.update(true); 589 } else { 590 if (!this._isOpen) { 591 this.$menu.show(); 592 this._isOpen = true; 593 } 594 this._setMenuWidth(); 595 this._setMenuHeight(); 596 } 597 598 // broadcast refresh event; useful for widgets 599 if (!init) { 600 this._trigger('refresh'); 601 } 602 }, 603 604 /** 605 * Updates cached values used elsewhere in the widget 606 * Causes the filter to also update its cache if the filter is loaded 607 */ 608 _updateCache: function() { 609 // Invalidate cached dimensions to force recalcs. 610 this._savedButtonWidth = 0; 611 this._savedMenuWidth = 0; 612 this._savedMenuHeight = 0; 613 614 // Recreate important cached jQuery objects 615 this.$header = this.$menu.children('.ui-multiselect-header'); 616 this.$checkboxes = this.$menu.children('.ui-multiselect-checkboxes'); 617 618 // Update saved labels and inputs 619 this.$labels = this.$menu.find('label:not(.ui-multiselect-filter-label)'); 620 this.$inputs = this.$labels.children('input'); 621 622 // If the filter widget is in use, then also update its cache. 623 if ( this.element.is(':data("ech-multiselectfilter")') ) { 624 this.element.data('ech-multiselectfilter').updateCache(true); 625 } 626 }, 627 628 /** 629 * Updates the widget checkboxes' checked states 630 * from the native select options' selected states. 631 * @param {boolean} skipDisabled If true, disabled options in either are skipped. 632 */ 633 resync: function(skipDisabled) {
634 var $inputs = this.$inputs; 635 var $options = this.element.find('option'); 636 637 if ($inputs.length === $options.length) { 638 var inputValues = {}; 639 $inputs.not(!!skipDisabled ? ':disabled' : '').each( function() { 640 inputValues[this.value] = this; 641 }); 642 $options.not(!!skipDisabled ? ':disabled' : '').each( function() { 643 if (this.value in inputValues) { 644 inputValues[this.value].checked = this.selected; 645 } 646 }); 647 this._trigger('resync'); 648 this.update(); 649 } else { 650 this.refresh(); 651 } 652 }, 653 654 /** 655 * Updates the button text 656 * If selectedText option is a function, simply call it 657 * The selectedList option determines how many options to display 658 * before switching to # of # selected 659 * This does not apply in listbox mode 660 * @param {boolean} isDefault true if value is default value for the button 661 */ 662 update: function(isDefault) { 663 if (!!this.options.listbox) { 664 return; 665 } 666 var options = this.options; 667 var selectedList = options.selectedList; 668 var selectedText = options.selectedText; 669 var $inputs = this.$inputs; 670 var inputCount = $inputs.length; 671 var $checked = $inputs.filter(':checked'); 672 var numChecked = $checked.length; 673 var value; 674 675 if (numChecked) { 676 if (typeof selectedText === 'function') { 677 value = selectedText.call(this, numChecked, inputCount, $checked.get()); 678 } else if (/\d/.test(selectedList) && selectedList > 0 && numChecked <= selectedList) { 679 value = $checked.map(function() { 680 return $(this).next().text().replace(/\n$/, ''); 681}) 682 .get().join(options.selectedListSeparator); 683 } else { 684 value = selectedText.replace('#', numChecked).replace('#', inputCount); 685 } 686 } else { 687 value = options.noneSelectedText;//console.log(options); 688 } 689 690 this._setButtonValue(value, isDefault); 691 692 if ( options.wrapText.indexOf('button') === -1 ) { 693 this._setButtonWidth(true); 694 } 695 696 // Check if the menu needs to be repositioned due to button height changing from adding/removing selections. 697 if (this._isOpen && this._savedButtonHeight != this.$button.outerHeight(false)) { 698 this.position(); 699 } 700 }, 701 702 /** 703 * Sets the button text 704 * @param {string} value content to be assigned to the button 705 * @param {boolean} isDefault true if value is default value for the button 706 */ 707 _setButtonValue: function(value, isDefault) { 708 this.$buttonlabel[this.htmlAllowedFor('button') ? 'html' : 'text'](value); 709 710 if (!!isDefault) { 711 this.$button[0].defaultValue = value; 712 } 713 }, 714 715 /** 716 * Sets button events for mouse and keyboard interaction 717 * Called by _bindEvents 718 */ 719 _bindButtonEvents: function() { 720 var self = this; 721 var $button = this.$button; 722 /** 723 * @return {Boolean} always false 724 */ 725 function buttonClickHandler() { 726 self[self._isOpen ? 'close' : 'open'](); 727 return false; 728 } 729 730 $button 731 .on({ 732 click: buttonClickHandler, 733 keydown: $.proxy(self._handleButtonKeyboardNav, self), 734 mouseenter: function() { 735 if (!this.classList.contains('ui-state-disabled')) { 736 this.classList.add('ui-state-hover'); 737 } 738 }, 739 mouseleave: function() { 740 this.classList.remove('ui-state-hover'); 741 }, 742 focus: function() { 743 if (!this.classList.contains('ui-state-disabled')) { 744 this.classList.add('ui-state-focus'); 745 } 746 }, 747 blur: function() { 748 this.classList.remove('ui-state-focus'); 749 }, 750 }) 751 // webkit doesn't like it when you click on the span :( 752 .find('span') 753 .on('click.multiselect,click', buttonClickHandler); 754 }, 755 756 // Handle keyboard events for the multiselect button. 757 _handleButtonKeyboardNav: function(e) { 758 // Change selection via up/down on a closed single select. 759 if (!this._isOpen && !this.element[0].multiple && (e.which === 38 || e.which === 40) ) {
760 var $inputs = this.$inputs; 761 var index = $inputs.index( $inputs.filter(':checked') ); 762 if (e.which === 38 && index) { 763 $inputs.eq(index - 1).trigger('click'); 764 } else if (e.which === 40 && index < $inputs.length - 1) { 765 $inputs.eq(index + 1).trigger('click'); 766 } 767 return; 768 } 769 770 switch (e.which) { 771 case 27: // esc 772 case 37: // left 773 case 38: // up 774 this.close(); 775 break; 776 case 40: // down 777 case 39: // right 778 this.open(); 779 break; 780 } 781 }, 782 783 /** 784 * Bind events to the checkboxes for options and option groups 785 * Must be bound to the checkboxes container. 786 * This method scopes actions to filtered options 787 * Called by _bindEvents 788 */ 789 _bindCheckboxEvents: function() { 790 var self = this; 791 792 // optgroup label toggle support 793 self.$checkboxes.on('click.multiselect', '.ui-multiselect-grouplabel', function(e) { 794 e.preventDefault(); 795 796 if (!self.options.groupsSelectable) { 797 return false; 798 } 799 800 var $this = $(this); 801 var $inputs = $this.next('ul').children(':not(.ui-multiselect-excluded)').find('input').not(':disabled'); 802 var nodes = $inputs.get(); 803 var label = this.textContent; 804 805 // trigger before callback and bail if the return is false 806 if (self._trigger('beforeoptgrouptoggle', e, {inputs: nodes, label: label}) === false) { 807 return; 808 } 809 810 // if maxSelected is in use, cannot exceed it 811 var maxSelected = self.options.maxSelected; 812 if (maxSelected && (self.$inputs.filter(':checked').length + $inputs.length > maxSelected) ) { 813 return; 814 } 815 816 // toggle inputs 817 self._toggleChecked( 818 $inputs.filter(':checked').length !== $inputs.length, 819 $inputs 820 ); 821 822 self._trigger('optgrouptoggle', e, { 823 inputs: nodes, 824 label: label, 825 checked: nodes.length ? nodes[0].checked : null, 826 }); 827 }) 828 // collapse button 829 .on('click.multiselect', '.ui-multiselect-collapser', function(e) { 830 var $this = $(this); 831 var $parent = $this.parent(); 832 var optgroupLabel = $parent.find('.ui-multiselect-grouplabel').first().html(); 833 var linkInfo = self.linkInfo; 834 var collapsedClass = 'ui-multiselect-collapsed'; 835 var isCollapsed = $parent.hasClass(collapsedClass); 836 837 if (self._trigger('beforecollapsetoggle', e, {label: optgroupLabel, collapsed: isCollapsed}) === false) { 838 return; 839 } 840 $parent.toggleClass(collapsedClass); 841 842 $this.attr('title', isCollapsed ? linkInfo.collapse.title : linkInfo.expand.title) 843 .html(isCollapsed ? linkInfo.collapse.icon : linkInfo.expand.icon ); 844 845 if (!self.options.listbox) { 846 self._setMenuHeight(true); 847 } 848 849 self._trigger('collapsetoggle', e, {label: optgroupLabel, collapsed: !isCollapsed}); 850 }) 851 // collapse button 852 .on('mouseenter.multiselect', '.ui-multiselect-collapser', function(e) { 853 this.classList.add('ui-state-hover'); 854 }) 855 // collapse button 856 .on('mouseleave.multiselect', '.ui-multiselect-collapser', function(e) { 857 this.classList.remove('ui-state-hover'); 858 }) 859 // option label 860 .on('mouseenter.multiselect', 'label', function(e, param) { 861 if (!this.classList.contains('ui-state-disabled')) { 862 var checkboxes = self.$checkboxes[0]; 863 var scrollLeft = checkboxes.scrollLeft; 864 var scrollTop = checkboxes.scrollTop; 865 var scrollX = window.pageXOffset; 866 var scrollY = window.pageYOffset; 867 868 self.$labels.removeClass('ui-state-hover'); 869 $(this).addClass('ui-state-hover').find('input').focus(); 870 871 // Restore scroll positions if altered by setting input focus 872 if ( !param || !param.allowScroll ) { 873 checkboxes.scrollLeft = scrollLeft; 874 checkboxes.scrollTop = scrollTop; 875 window.scrollTo(scrollX, scrollY); 876 } 877 } 878 }) 879 // Keyboard navigation of the menu 880 .on('keydown.multiselect', 'label', function(e) { 881 // Don't capture function keys or 'r' 882 if (e.which === 82) { 883 return; // r 884 } 885 886 if (e.which >
886 111 && e.which < 124) { 887 return; // Function keys. 888 } 889 890 e.preventDefault(); 891 switch (e.which) { 892 case 9: // tab 893 if (e.shiftKey) { 894 self.$menu.find('.ui-state-hover').removeClass('ui-state-hover'); 895 self.$header.find('li').last().find('a').focus(); 896 } else { 897 self.close(); 898 } 899 break; 900 case 27: // esc 901 self.close(); 902 break; 903 case 38: // up 904 case 40: // down 905 case 37: // left 906 case 39: // right 907 self._traverse(e.which, this); 908 break; 909 case 13: // enter 910 case 32: // space 911 $(this).find('input')[0].click(); 912 break; 913 case 65: // Alt-A 914 if (e.altKey) { 915 self.checkAll(); 916 } 917 break; 918 case 70: // Alt-F 919 if (e.altKey) { 920 self.flipAll(); 921 } 922 break; 923 case 85: // Alt-U 924 if (e.altKey) { 925 self.uncheckAll(); 926 } 927 break; 928 } 929 }) 930 .on('click.multiselect', 'input', function(e) { 931 // Reference to this checkbox / radio input 932 var input = this; 933 var $input = $(input); 934 var val = input.value; 935 var checked = input.checked; 936 // self is cached from outer scope above 937 var $element = self.element; 938 var $tags = $element.find('option'); 939 var isMultiple = $element[0].multiple; 940 var $allInputs = self.$inputs; 941 var numChecked = $allInputs.filter(':checked').length; 942 var options = self.options; 943 var textFxn = self.htmlAllowedFor('options') ? 'html' : 'text'; 944 var optionText = $input.parent().find('span')[textFxn](); 945 var maxSelected = options.maxSelected; 946 947 // bail if this input is disabled or the event is cancelled 948 if (input.disabled || self._trigger('click', e, {value: val, text: optionText, checked: checked}) === false) { 949 e.preventDefault(); 950 return; 951 } 952 953 if (maxSelected && checked && numChecked > maxSelected) { 954 if (self._trigger('maxselected', e, {labels: self.$labels, inputs: $allInputs}) !== false) { 955 self.buttonMessage('<center><b>LIMIT OF ' + (numChecked - 1) + ' REACHED!</b></center>'); 956 } 957 input.checked = false; 958 e.preventDefault(); 959 return false; 960 } 961 962 // make sure the input has focus. otherwise, the esc key 963 // won't close the menu after clicking an item. 964 input.focus(); 965 966 // toggle aria state 967 $input.prop('aria-selected', checked); 968 969 // change state on the original option tags 970 $tags.each(function() { 971 this.selected = (this.value === val ? checked : isMultiple && this.selected); 972 }); 973 974 // some additional single select-specific logic 975 if (!isMultiple) { 976 self.$labels.removeClass('ui-state-active'); 977 $input.closest('label').toggleClass('ui-state-active', checked); 978 979 // close menu 980 self.close(); 981 } 982 983 // fire change on the select box 984 $element.trigger('change'); 985 986 // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827 987 // http://bugs.jquery.com/ticket/3827 988 setTimeout($.proxy(self.update, self), 10); 989 }); 990 }, 991 992 /** 993 * Binds keyboard and mouse events to the header 994 * Called by _bindEvents 995 */ 996 _bindHeaderEvents: function() { 997 var self = this; 998 999 // header links 1000 self.$header 1001 .on('click.multiselect', 'a', function(e) { 1002 var headerLinks = { 1003 'ui-multiselect-close': 'close', 1004 'ui-multiselect-all': 'checkAll', 1005 'ui-multiselect-none': 'uncheckAll', 1006 'ui-multiselect-flip': 'flipAll', 1007 'ui-multiselect-collapseall': 'collapseAll', 1008 'ui-multiselect-expandall': 'expandAll', 1009 }; 1010 for (hdgClass in headerLinks) { 1011 if ( this.classList.contains(hdgClass) ) { 1012 // headerLinks[hdgClass] is the click handler name 1013 self[headerLinks[hdgClass]](); 1014 e.preventDefault(); 1015 return false;
1016 } 1017 } 1018 }). 1019 on('keydown.multiselect', 'a', function(e) { 1020 switch (e.which) { 1021 case 27: 1022 self.close(); 1023 break; 1024 case 9: // tab 1025 var $target = $(e.target); 1026 if ((e.shiftKey 1027 && !$target.parent().prev().length 1028 && !self.$header.find('.ui-multiselect-filter').length) 1029 || (!$target.parent().next().length && !self.$labels.length && !e.shiftKey)) { 1030 self.close(); 1031 e.preventDefault(); 1032 } 1033 break; 1034 } 1035 }); 1036 }, 1037 1038 /** 1039 * Allows the widget to be resized if the option is set and resizable is 1040 * included in jQuery UI 1041 */ 1042 _setResizable: function() { 1043 if (!this.options.resizableMenu || !('resizable' in $.ui)) { 1044 return; 1045 } 1046 this.$menu.show(); 1047 this.$menu.resizable({ 1048 containment: 'parent', 1049 handles: 's', 1050 helper: 'ui-multiselect-resize', 1051 stop: function(e, ui) { 1052 // Force consistent width 1053 ui.size.width = ui.originalSize.width; 1054 $(this).outerWidth(ui.originalSize.width); 1055 if (this._trigger('resize', e, ui) !== false) { 1056 this.options.menuHeight = ui.size.height; 1057 } 1058 this._setMenuHeight(true); 1059 }, 1060 }); 1061 this.$menu.hide(); 1062 }, 1063 1064 /** 1065 * Binds all events used in the widget 1066 * This calls the menu, button, and header event binding methods 1067 */ 1068 _bindEvents: function() { 1069 if (!this.options.listbox) { 1070 this._bindButtonEvents(); 1071 } 1072 this._bindHeaderEvents(); 1073 this._bindCheckboxEvents(); 1074 this._setResizable(); 1075 1076 // Close each widget when clicking on any other element/anywhere else on the page, 1077 // another widget instance, or when scrolling w/ the mouse wheel outside the menu button. 1078 this.document.on('mousedown' + this._namespaceID 1079 + ' wheel' + this._namespaceID 1080 + ' mousewheel' + this._namespaceID, function(event) { 1081 var target = event.target; 1082 1083 if ( this._isOpen 1084 && (!!this.$button ? target !== this.$button[0] && !$.contains(this.$button[0], target) : true) 1085 && target !== this.$menu[0] && !$.contains(this.$menu[0], target) ) { 1086 this.close(); 1087 } 1088 }.bind(this)); 1089 1090 // deal with form resets. the problem here is that buttons aren't 1091 // restored to their defaultValue prop on form reset, and the reset 1092 // handler fires before the form is actually reset. delaying it a bit 1093 // gives the form inputs time to clear. 1094 $(this.element[0].form).on('reset' + this._namespaceID, function() { 1095 setTimeout(this.refresh.bind(this), 10); 1096 }.bind(this)); 1097 }, 1098 1099 /** 1100 * Sets and caches the width of the button 1101 * Can set a minimum value if less than calculated width of native select. 1102 * @param {boolean} recalc true if cached width needs to be re-calculated 1103 */ 1104 _setButtonWidth: function(recalc) { 1105 if (this._savedButtonWidth && !recalc) { 1106 return; 1107 } 1108 1109 // this._selectWidth set in _create() for native select element before hiding it. 1110 var width = this._selectWidth || this._getBCRWidth( this.element ); 1111 var buttonWidth = this.options.buttonWidth || ''; 1112 if (/\d/.test(buttonWidth)) { 1113 var parsed = parse2px(buttonWidth, this.element); 1114 var pixels = parsed.px; 1115 var minimax = parsed.minimax; 1116 width = minimax < 0 ? Math.max(width, pixels) : ( minimax > 0 ? Math.min(width, pixels) : pixels ); 1117 } else { // keywords 1118 buttonWidth = buttonWidth.toLowerCase(); 1119 } 1120 1121 // The button width is set to auto in the CSS, 1122 // so we only need to change it for a specific width. 1123 if (buttonWidth !== 'auto') { 1124 this.$button.outerWidth(width); 1125 } 1126 this._savedButtonWidth = width; 1127 }, 1128 1129 /** 1130 * Sets and caches the width of the menu 1131 * Will use the width in options if provided, otherwise matches the button 1132 * @param {boolean} recalc true if cached width needs to be re-calculated 1133 */ 1134 _setMenuWidth: function(recalc) { 1135 if (this._savedMenuWidth && !recalc) { 1136 return; 1137 } 1138 1139 // Note that it is assumed that the button width was set prior. 1140 var width = !!this.options.listbox ? this._selectWidth : (this._savedButtonWidth || this._getBCRWidth( this.$button )); 1141 1142 var menuWidth = this.options.menuWidth || ''; 1143 if ( /\d/.test(menuWidth) ) { 1144 var parsed = parse2px(menuWidth, this.element); 1145 var pixels = parsed.px; 1146 var minimax = parsed.minimax; 1147 width = minimax < 0 ? Math.max(width, pixels) : ( minimax > 0 ? Math.min(width, pixels) : pixels ); 1148 } else { // keywords 1149 menuWidth = menuWidth.toLowerCase(); 1150 } 1151 1152 // Note that the menu width defaults to the button width if menuWidth option is null or blank. 1153 if (menuWidth !== 'auto') { 1154 this.$menu.outerWidth(width); 1155 this._savedMenuWidth = width; 1156 return; 1157 } 1158 1159 // Auto width determination: get intrinsic / "shrink-wrapped" outer widths w/ margins by applying floats. 1160 // cbWidth includes the width of the vertical scrollbar & ui-hover-state width increase per the applied CSS. 1161 // Note that a correction is made for jQuery floating point round-off errors below. 1162 this.$menu.addClass('ui-multiselect-measure'); 1163 var headerWidth = this.$header.outerWidth(true) + this._jqWidthFix(this.$header); 1164 var cbWidth = this.$checkboxes.outerWidth(true) + this._jqWidthFix(this.$checkboxes); 1165 this.$menu.removeClass('ui-multiselect-measure'); 1166 1167 var contentWidth = Math.max(this.options.wrapText.indexOf('header') > -1 ? 0 : headerWidth, cbWidth); 1168 1169 // Use $().width() to set menu width not including padding or border. 1170 this.$menu.width(contentWidth); 1171 // Save width including padding and border (no margins) for consistency w/ normal width setting. 1172 this._savedMenuWidth = this.$menu.outerWidth(false); 1173 }, 1174 1175 /** 1176 * Sets and caches the height of the menu 1177 * Will use the height provided in the options unless using the select size 1178 * option or the option exceeds the available height for the menu 1179 * Will set a scrollbar if the options can't all be visible at once 1180 * @param {boolean} recalc true if cached value needs to be re-calculated 1181 */ 1182 _setMenuHeight: function(recalc) { 1183 var self = this; 1184 if (self._savedMenuHeight && !recalc) { 1185 return; 1186 } 1187 1188 var maxHeight = $(window).height(); 1189 var optionHeight = self.options.menuHeight || ''; 1190 var useSelectSize = false;
1191 var elSelectSize = 4; 1192 1193 if ( /\d/.test(optionHeight) ) { 1194 // Deduct height of header & border/padding to find height available for checkboxes. 1195 var $header = self.$header.filter(':visible'); 1196 var headerHeight = $header.outerHeight(true); 1197 var menuBorderPaddingHt = this.$menu.outerHeight(false) - this.$menu.height(); 1198 var cbBorderPaddingHt = this.$checkboxes.outerHeight(false) - this.$checkboxes.height(); 1199 1200 optionHeight = parse2px(optionHeight, self.element, true).px; 1201 maxHeight = Math.min(optionHeight, maxHeight) - headerHeight - menuBorderPaddingHt - cbBorderPaddingHt; 1202 } else if (optionHeight.toLowerCase() === 'size') { 1203 // Overall height based on native select 'size' attribute 1204 useSelectSize = true; 1205 // Retrieves native select's size attribute or defaults to 4 (like native select). 1206 elSelectSize = self.element[0].size || elSelectSize; 1207 } 1208 1209 var overflowSetting = 'hidden'; 1210 var itemCount = 0; 1211 var hoverAdjust = 4; // Adjustment for hover height included here. 1212 var ulHeight = hoverAdjust; 1213 var ulTop = -1; 1214 1215 // The following determines the how many items are visible per the menuHeight option. 1216 // If the visible height calculation exceeds the calculated maximum height or if the number 1217 // of item heights summed equal or exceed the native select size attribute, the loop is aborted. 1218 // If the loop is aborted, this means that the menu must be scrolled to see all the items. 1219 self.$checkboxes.find('li:not(.ui-multiselect-optgroup),a').filter(':visible').each( function() { 1220 if (ulTop < 0) { 1221 ulTop = this.offsetTop; 1222 } 1223 ulHeight = this.offsetTop + this.offsetHeight - ulTop + hoverAdjust; 1224 if (useSelectSize && ++itemCount >= elSelectSize || ulHeight > maxHeight) { 1225 overflowSetting = 'auto'; 1226 if (!useSelectSize) { 1227 ulHeight = maxHeight; 1228 } 1229 return false; 1230 } 1231 }); 1232 1233 // We actually only set the height of the checkboxes as the outer menu container is height:auto. 1234 // The _savedMenuHeight value below can be compared to optionHeight as an accuracy check. 1235 self.$checkboxes.css('overflow', overflowSetting).height(ulHeight); 1236 self._savedMenuHeight = this.$menu.outerHeight(false); 1237 }, 1238 1239 /** 1240 * Calculate accurate outerWidth(false) using getBoundingClientRect() 1241 * Note that this presumes that the element is visible in the layout. 1242 * @param {node} elem DOM node or jQuery equivalent to get width for. 1243 * @return {float} Decimal floating point value for the width. 1244 */ 1245 _getBCRWidth: function(elem) { 1246 if (!elem || !!elem.jquery && !elem[0]) { 1247 return null; 1248 } 1249 var domRect = !!elem.jquery ? elem[0].getBoundingClientRect() : elem.getBoundingClientRect(); 1250 return domRect.right - domRect.left; 1251 }, 1252 1253 /** 1254 * Calculate jQuery width correction factor to fix floating point round-off errors. 1255 * Note that this presumes that the element is visible in the layout. 1256 * @param {node} elem node or jQuery equivalent to get width for. 1257 * @return {float} Correction value for the width--typically a decimal < 1.0 1258 */ 1259 _jqWidthFix: function(elem) { 1260 if (!elem || !!elem.jquery && !elem[0]) { 1261 return null; 1262 } 1263 return !!elem.jquery 1264 ? this._getBCRWidth(elem[0]) - elem.outerWidth(false) 1265 : this._getBCRWidth(elem) - $(elem).outerWidth(false); 1266 }, 1267 1268 /** 1269 * Moves focus up or down the options list 1270 * @param {number} which key that triggered the traversal 1271 * @param {node} start element event was triggered from 1272 */ 1273 _traverse: function(which, start) { 1274 var $start = $(start); 1275 var moveToLast = which === 38 || which === 37; 1276 1277 // select the first li that isn't an optgroup label / disabled 1278 var $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(:disabled, .ui-multiselect-optgroup):visible').first(); 1279 // we might have to jump to the next/previous option group 1280 if (!$next.length) { 1281 $next = $start.parents('.ui-multiselect-optgroup')[moveToLast ? 'prev' : 'next'](); 1282 } 1283 1284 // if at the first/last element 1285 if (!$next.length) { 1286 var $container = this.$checkboxes; 1287 1288 // move to the first/last 1289 $container.find('label').filter(':visible')[moveToLast ? 'last' : 'first']().trigger('mouseover', {allowScroll: true}); 1290 1291 // set scroll position 1292 $container.scrollTop(moveToLast ? $container.height() : 0); 1293 } else { 1294 $next.find('label').filter(':visible')[moveToLast ? 'last' : 'first']().trigger('mouseover', {allowScroll: true}); 1295 } 1296 }, 1297 1298 /** 1299 * Internal function to toggle checked property and related attributes on a checkbox 1300 * The context of this function should be a checkbox; do not proxy it. 1301 * @param {string} prop Property being toggled on the checkbox 1302 * @param {string} flag Flag to set for the property 1303 * @return {function} function for toggling checked state 1304 */ 1305 _toggleState: function(prop, flag) { 1306 return function() { 1307 var state = (flag === '!') ? !this[prop] : flag; 1308 1309 if ( !this.disabled ) { 1310 this[prop] = state; 1311 } 1312 1313 if (state) { 1314 this.setAttribute('aria-' + prop, true); 1315 } else { 1316 this.removeAttribute('aria-' + prop); 1317 } 1318 }; 1319 }, 1320 1321 /** 1322 * Toggles the checked state on options within the menu 1323 * Potentially scoped down to visible elements from filteredInputs 1324 * @param {boolean} flag checked property to set 1325 * @param {object} group option group that was clicked, if any 1326 * @param {boolean}
1326 filteredInputs does not toggle hidden inputs if filtering. 1327 */ 1328 _toggleChecked: function(flag, group, filteredInputs) { 1329 var self = this; 1330 var $element = self.element; 1331 var $inputs = (group && group.length) ? group : self.$inputs; 1332 1333 if (filteredInputs) { 1334 $inputs = self._isOpen 1335 ? $inputs.closest('li').not('.ui-multiselect-excluded').find('input').not(':disabled') 1336 : $inputs.not(':disabled'); 1337 } 1338 1339 // toggle state on inputs 1340 $inputs.each(self._toggleState('checked', flag)); 1341 1342 // Give the first input focus 1343 $inputs.eq(0).focus(); 1344 1345 // update button text 1346 self.update(); 1347 1348 // Create a plain object of the values that actually changed 1349 var inputValues = {}; 1350 $inputs.each( function() { 1351 inputValues[this.value] = true; 1352 }); 1353 1354 // toggle state on original option tags 1355 $element.find('option') 1356 .each( function() { 1357 if (!this.disabled && inputValues[this.value]) { 1358 self._toggleState('selected', flag).call(this); 1359 } 1360 }); 1361 1362 // trigger the change event on the select 1363 if ($inputs.length) { 1364 $element.trigger('change'); 1365 } 1366 }, 1367 1368 /** 1369 * Toggles disabled state on the widget and underlying select or for just one option group. 1370 * Will also disable all individual options if the disableInputsOnToggle option is set 1371 * @param {boolean} flag true if disabling widget 1372 * @param {number | string} groupID index or label of option group to disable 1373 */ 1374 _toggleDisabled: function(flag, groupID) { 1375 var disabledClass = 'ui-state-disabled'; // used for styling only 1376 1377 if (this.$button) { 1378 this.$button.prop({'disabled': flag, 'aria-disabled': flag})[flag ? 'addClass' : 'removeClass'](disabledClass); 1379 } 1380 1381 if (this.options.disableInputsOnToggle) { 1382 // Apply the ui-multiselect-disabled class name to identify which 1383 // input elements this widget disabled (not pre-disabled) 1384 // so that they can be restored if the widget is re-enabled. 1385 var $inputs = (typeof groupID === 'undefined') ? this.$inputs : this._multiselectOptgroupFilter(groupID).find('input'); 1386 var msDisabledClass = 'ui-multiselect-disabled'; 1387 if (flag) { 1388 var matchedInputs = $inputs.filter(':enabled').get(); 1389 for (var x = 0, len = matchedInputs.length; x < len; x++) { 1390 matchedInputs[x].setAttribute('disabled', 'disabled'); 1391 matchedInputs[x].setAttribute('aria-disabled', 'disabled'); 1392 matchedInputs[x].classList.add(msDisabledClass); 1393 matchedInputs[x].parentNode.classList.add(disabledClass); 1394 } 1395 } else { 1396 var matchedInputs = $inputs.filter('.' + msDisabledClass + ':disabled').get(); 1397 for (var x = 0, len = matchedInputs.length; x < len; x++) { 1398 matchedInputs[x].removeAttribute('disabled'); 1399 matchedInputs[x].removeAttribute('aria-disabled'); 1400 matchedInputs[x].classList.remove(msDisabledClass); 1401 matchedInputs[x].parentNode.classList.remove(disabledClass); 1402 } 1403 } 1404 } 1405 1406 var $select = (typeof groupID === 'undefined') ? this.element : this._nativeOptgroupFilter(groupID).find('option'); 1407 $select.prop({ 1408 'disabled': flag, 1409 'aria-disabled': flag, 1410 }); 1411 }, 1412 1413 /** 1414 * Opens the menu, possibly with effects 1415 * Calls methods to set position and resize as well 1416 */ 1417 open: function() { 1418 var $button = this.$button; 1419 1420 // bail if the multiselect open event returns false, this widget is disabled, or is already open 1421 if (this._trigger('beforeopen') === false || $button.hasClass('ui-state-disabled') || this._isOpen || !!this.options.listbox) { 1422 return; 1423 } 1424 1425 var $menu = this.$menu; 1426 var $header = this.$header; 1427 var $labels = this.$labels; 1428 var $inputs = this.$inputs.filter(':checked:not(.ui-state-disabled)'); 1429 var options = this.options; 1430 var effect = options.openEffect; 1431 var scrollX = window.pageXOffset; 1432 var scrollY = window.pageYOffset; 1433 1434 // set the scroll of the checkbox container 1435 this.$checkboxes.scrollTop(0); 1436 1437 // Show the menu, set its dimensions, and position it. 1438 $menu.css('display', 'block'); 1439 this._setMenuWidth(); 1440 this._setMenuHeight(); 1441 this.position(); 1442 1443 // Do any specified open animation effect after positioning the menu. 1444 if (!!effect) { 1445 // Menu must be hidden for some effects (e.g. fade) to work. 1446 $menu.css('display', 'none'); 1447 if (typeof effect == 'string') { 1448 $menu.show(effect, this.speed); 1449 } else if (typeof effect == 'object' && effect.constructor == Array) { 1450 $menu.show(effect[0], effect[1] || this.speed); 1451 } else if (typeof effect == 'object' && effect.constructor == Object) { 1452 $menu.show(effect); 1453 } 1454 } 1455 1456 // focus the first not disabled option or filter input if available 1457 var filter = $header.find('.ui-multiselect-filter'); 1458 if (filter.length) { 1459 filter.first().find('input').trigger('focus'); 1460 } else if ($inputs.length) {
1461 $inputs.eq(0).trigger('focus').parent('label').eq(0).trigger('mouseover').trigger('mouseenter'); 1462 } else if ($labels.length) { 1463 $labels.filter(':not(.ui-state-disabled)').eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus'); 1464 } else { 1465 $header.find('a').first().trigger('focus'); 1466 } 1467 1468 // Restore window scroll position if altered by setting element focus 1469 window.scrollTo(scrollX, scrollY); 1470 1471 $button.addClass('ui-state-active'); 1472 this._isOpen = true; 1473 this._trigger('open'); 1474 }, 1475 1476 // Close the menu 1477 close: function() { 1478 // bail if the multiselect close event returns false 1479 if (this._trigger('beforeclose') === false || !!this.options.listbox) { 1480 return; 1481 } 1482 1483 var $menu = this.$menu; 1484 var options = this.options; 1485 var effect = options.closeEffect; 1486 var $button = this.$button; 1487 1488 // hide the menu, maybe with a speed/effect combo 1489 if (!!effect) { 1490 if (typeof effect == 'string') { 1491 $menu.hide(effect, this.speed); 1492 } else if (typeof effect == 'object' && effect.constructor == Array) { 1493 $menu.hide(effect[0], effect[1] || this.speed); 1494 } else if (typeof effect == 'object' && effect.constructor == Object) { 1495 $menu.hide(effect); 1496 } 1497 } else { 1498 $menu.css('display', 'none'); 1499 } 1500 1501 $button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave'); 1502 this.element.trigger('blur'); // For jQuery Validate 1503 this._isOpen = false; 1504 this._trigger('close'); 1505 $button.trigger('focus'); 1506 }, 1507 1508 /** 1509 * Positions the menu relative to the button. 1510 */ 1511 position: function() { 1512 var $button = this.$button; 1513 1514 // Save the button height so that we can determine when it has changed due to adding/removing selections. 1515 this._savedButtonHeight = $button.outerHeight(false); 1516 1517 if ($.ui && $.ui.position) { 1518 this.$menu.position(this.options.position); 1519 } else { 1520 var pos = {}; 1521 1522 pos.top = $button.offset().top + this._savedButtonHeight; 1523 pos.left = $button.offset().left; 1524 1525 this.$menu.offset(pos); 1526 } 1527 }, 1528 1529 // Enable widget 1530 enable: function(groupID) { 1531 this._toggleDisabled(false, groupID); 1532 }, 1533 1534 // Disable widget 1535 disable: function(groupID) { 1536 this._toggleDisabled(true, groupID); 1537 }, 1538 1539 /** 1540 * Checks all options or those in an option group 1541 * Accounts for maxSelected possibly being set. 1542 * @param {(number|string)} groupID index or label of option group to check all for. 1543 */ 1544 checkAll: function(groupID) { 1545 this._trigger('beforeCheckAll'); 1546 1547 if (this.options.maxSelected) { 1548 return; 1549 } 1550 1551 if (typeof groupID === 'undefined') { // groupID could be 0 1552 this._toggleChecked(true); 1553 } else { 1554 this._toggleChecked(true, this._multiselectOptgroupFilter(groupID).find('input')); 1555 } 1556 1557 this._trigger('checkAll'); 1558 }, 1559 1560 /** 1561 * Unchecks all options or those in an option group 1562 * @param {(number|string)} groupID index or label of option group to uncheck all for. 1563 */ 1564 uncheckAll: function(groupID) { 1565 this._trigger('beforeUncheckAll'); 1566 1567 if (typeof groupID === 'undefined') { // groupID could be 0 1568 this._toggleChecked(false); 1569 } else { 1570 this._toggleChecked(false, this._multiselectOptgroupFilter(groupID).find('input')); 1571 } 1572 if ( !this.element[0].multiple && !this.$inputs.filter(':checked').length) { 1573 // Forces the underlying single-select to have no options selected. 1574 this.element[0].selectedIndex = -1; 1575 } 1576 1577 this._trigger('uncheckAll'); 1578 }, 1579 1580 /** 1581 * Flips all options or those in an option group. 1582 * Accounts for maxSelected possibly being set. 1583 * @param {(number|string)} groupID index or label of option group to flip all for. 1584 */ 1585 flipAll: function(groupID) { 1586 this._trigger('beforeFlipAll'); 1587 1588 var gotID = (typeof groupID !== 'undefined'); // groupID could be 0 1589 var maxSelected = this.options.maxSelected; 1590 var inputCount = this.$inputs.length; 1591 var checkedCount = this.$inputs.filter(':checked').length; 1592 var $filteredOptgroupInputs = gotID ? this._multiselectOptgroupFilter(groupID).find('input') : null; 1593 var gInputCount = gotID ? $filteredOptgroupInputs.length : 0; 1594 var gCheckedCount = gotID ? $filteredOptgroupInputs.filter(':checked').length : 0; 1595 1596 if (!maxSelected 1597 || maxSelected >= (gotID ? checkedCount - gCheckedCount + gInputCount - gCheckedCount : inputCount - checkedCount ) ) { 1598 if (gotID) {
1599 this._toggleChecked('!', $filteredOptgroupInputs); 1600 } else { 1601 this._toggleChecked('!'); 1602 } 1603 this._trigger('flipAll'); 1604 } else { 1605 this.buttonMessage('<center><b>Flip All Not Permitted.</b></center>'); 1606 } 1607 }, 1608 1609 /** 1610 * Collapses all option groups or just the one specified. 1611 * @param {(number|string)} groupID index or label of option group to collapse. 1612 */ 1613 collapseAll: function(groupID) { 1614 this._trigger('beforeCollapseAll'); 1615 1616 var $optgroups = (typeof groupID === 'undefined') // groupID could be 0 1617 ? this.$checkboxes.find('.ui-multiselect-optgroup') 1618 : this._multiselectOptgroupFilter(groupID); 1619 1620 $optgroups.addClass('ui-multiselect-collapsed') 1621 .children('.ui-multiselect-collapser').attr('title', this.linkInfo.expand.title ).html( this.linkInfo.expand.icon ); 1622 1623 this._trigger('collapseAll'); 1624 }, 1625 1626 /** 1627 * Expands all option groups or just the one specified. 1628 * @param {(number|string)} groupID index or label of option group to expand. 1629 */ 1630 expandAll: function(groupID) { 1631 this._trigger('beforeExpandAll'); 1632 1633 var $optgroups = (typeof groupID === 'undefined') // groupID could be 0 1634 ? this.$checkboxes.find('.ui-multiselect-optgroup') 1635 : this._multiselectOptgroupFilter(groupID); 1636 1637 $optgroups.removeClass('ui-multiselect-collapsed') 1638 .children('.ui-multiselect-collapser').attr('title', this.linkInfo.collapse.title ).html( this.linkInfo.collapse.icon ); 1639 1640 this._trigger('expandAll'); 1641 }, 1642 1643 /** 1644 * Flashes a message in the button caption for 1 second. 1645 * Useful for very short warning messages to the user. 1646 * @param {string} message HTML to show in the button. 1647 */ 1648 buttonMessage: function(message) { 1649 var self = this; 1650 self.$buttonlabel.html(message); 1651 setTimeout( function() { 1652 self.update(); 1653 }, 1000 ); 1654 }, 1655 1656 /** 1657 * Provides a list of all checked options 1658 * @return {array} list of inputs 1659 */ 1660 getChecked: function() { 1661 return this.$inputs.filter(':checked'); 1662 }, 1663 1664 /** 1665 * Provides a list of all options that are not checked 1666 * @return {array} list of inputs 1667 */ 1668 getUnchecked: function() { 1669 return this.$inputs.filter(':not(:checked)'); 1670 }, 1671 1672 /** 1673 * Destroys the widget instance 1674 * @return {object} reference to widget 1675 */ 1676 destroy: function() { 1677 // remove classes + data 1678 $.Widget.prototype.destroy.call(this); 1679 1680 // unbind events 1681 this.document.off(this._namespaceID); 1682 $(this.element[0].form).off(this._namespaceID); 1683 1684 if (!this.options.listbox) { 1685 this.$button.remove(); 1686 } 1687 this.$menu.remove(); 1688 this.element.show(); 1689 1690 return this; 1691 }, 1692 1693 /** 1694 * @return {boolean} indicates whether the menu is open 1695 */ 1696 isOpen: function() { 1697 return this._isOpen; 1698 }, 1699 1700 /** 1701 * @return {object} jQuery object for menu 1702 */ 1703 widget: function() { 1704 return this.$menu; 1705 }, 1706 1707 /** 1708 * @return {string} namespaceID for use with external event handlers. 1709 */ 1710 getNamespaceID: function() { 1711 return this._namespaceID; 1712 }, 1713 1714 /** 1715 * @return {object} jQuery object for button 1716 */ 1717 getButton: function() { 1718 return this.$button; 1719 }, 1720 1721 /** 1722 * Essentially an alias for widget 1723 * @return {object} jQuery object for menu 1724 */ 1725 getMenu: function() { 1726 return this.$menu; 1727 }, 1728 1729 /** 1730 * @return {array} List of the option labels 1731 */ 1732 getLabels: function() { 1733 return this.$labels; 1734 }, 1735 1736 /** 1737 * @return {array} List of option groups that are collapsed 1738 */ 1739 getCollapsed: function() { 1740 return this.$checkboxes.find('.ui-multiselect-collapsed'); 1741 }, 1742 1743 /** 1744 * Sets the value of the underlying select then resyncs the menu. 1745 * @param {(string|array)} newValue value(s) to set the underlying select to. 1746 * @return {any} the underlying select when a value is provied, or eles the value of the select 1747 */ 1748 value: function(newValue) { 1749 if (typeof newValue !== 'undefined') { 1750 this.element.val(newValue); 1751 this.resync(); 1752 return this.element; 1753 } else { 1754 return this.element.val(); 1755 } 1756 }, 1757 1758 /** 1759 * Determines if HTML content is allowed for the given element type 1760 * @param {string} element to check 1761 * @return {boolean} true if html content is allowed 1762 */ 1763 htmlAllowedFor: function(element) { 1764 return this.options.htmlText.indexOf(element) > -1; 1765 }, 1766 1767 /** 1768 * Adds an option to the widget and underlying select 1769 * @param {object} attributes hash to be added to the option 1770 * @param {string} text label for the option 1771 * @param {(number|string)} groupID index or label of option group to add the option to 1772 */ 1773 addOption: function(attributes, text, groupID) { 1774 var self = this; 1775 var textFxn = self.htmlAllowedFor('options') ? 'html' : 'text'; 1776 var $option = $( document.createElement('option') ).attr(attributes)[textFxn](text); 1777 var optionNode = $option.get(0); 1778 1779 if (typeof groupID === 'undefined') { // groupID could be 0 1780 self.element.append($option); 1781 self.$checkboxes.append(self._makeOption(optionNode)); 1782 } else { 1783 self._nativeOptgroupFilter(groupID).append($option); 1784 self._multiselectOptgroupFilter(groupID).append(self._makeOption(optionNode)); 1785 } 1786 1787 self._updateCache(); 1788 }, 1789 1790 /** 1791 * Finds an optgroup in the native select by index or label using the tag name 1792 * @param {(number|string)} groupID index or label of option group to find 1793 * @return {object} matching option groups 1794 */ 1795 _nativeOptgroupFilter: function(groupID) { 1796 return this.element.children('OPTGROUP').filter( function(index) { 1797 return (typeof groupID === 'number' ? index === groupID : this.getAttribute('label') === groupID); 1798 }); 1799 }, 1800 1801 /** 1802 * Finds an optgroup in the multiselect widget by index or label 1803 * @param {(number|string)} groupID index or label of option group to find 1804 * @return {object} matching option groups 1805 */ 1806 _multiselectOptgroupFilter: function(groupID) { 1807 return this.$menu.find('.ui-multiselect-optgroup').filter( function(index) { 1808 return (typeof groupID === 'number' ? index === groupID : this.getElementsByClassName('ui-multiselect-grouplabel')[0].textContent === groupID); 1809 }); 1810 }, 1811 1812 /** 1813 * Removes an option from the widget and underlying select 1814 * @param {string} value attribute corresponding to option being removed 1815 */ 1816 removeOption: function(value) { 1817 if (!value) { 1818 return; 1819 } 1820 this.element.find('option[value=' + value + ']').remove(); 1821 this.$labels.find('input[value=' + value + ']').parents('li').remove(); 1822 1823 this._updateCache(); 1824 }, 1825 1826 /** 1827 * Reacts to options being changed 1828 * Delegates to various handlers 1829 * @param {string} key into the options hash 1830 * @param {any} value to be assigned to that option 1831 */ 1832 _setOption: function(key, value) { 1833 var $header = this.$header; 1834 var $menu = this.$menu; 1835 1836 switch (key) { 1837 case 'header': 1838 if (typeof value === 'boolean') { 1839 $header.toggle( value ); 1840 } else if (typeof value === 'string') { 1841 this.$headerLinkContainer.children('li:not(:last-child)').remove(); 1842 this.$headerLinkContainer.prepend('<li>' + value + '</li>'); 1843 } 1844 break; 1845 case 'checkAllText': 1846 case 'uncheckAllText': 1847 case 'flipAllText': 1848 case 'collapseAllText': 1849 case 'expandAllText': 1850 if (key !== 'checkAllText' || !this.options.maxSelected) { 1851 // eq(-1) finds the last span 1852 $header.find('a.' + this.linkInfo[key.replace('Text', '')]['class'] + ' span').eq(-1).html(value); 1853 } 1854 break; 1855 case 'checkAllIcon': 1856 case 'uncheckAllIcon': 1857 case 'flipAllIcon': 1858 case 'collapseAllIcon': 1859 case 'expandAllIcon': 1860 if (key !== 'checkAllIcon' || !this.options.maxSelected) { 1861 // eq(0) finds the first span 1862 $header.find('a.' + this.linkInfo[key.replace('Icon', '')]['class'] + ' span').eq(0).replaceWith(value); 1863 } 1864 break; 1865 case 'openIcon': 1866 $menu.find('span.ui-multiselect-open').html(value); 1867 break; 1868 case 'closeIcon': 1869 $menu.find('a.ui-multiselect-close').html(value); 1870 break; 1871 case 'buttonWidth': 1872 case 'menuWidth': 1873 this.options[key] = value; 1874 this._setButtonWidth(true); // true forces recalc of cached value. 1875 this._setMenuWidth(true); // true forces recalc of cached value. 1876 break; 1877 case 'menuHeight': 1878 this.options[key] = value; 1879 this._setMenuHeight(true); // true forces recalc of cached value. 1880 break; 1881 case 'selectedText': 1882 case 'selectedList': 1883 case 'maxSelected': 1884 case 'noneSelectedText': 1885 case 'selectedListSeparator': 1886 this.options[key] = value; // these all need to update immediately for the update() call 1887 this.update(true); 1888 break; 1889 case 'classes': 1890 $menu.add(this.$button).removeClass(this.options.classes).addClass(value); 1891 break; 1892 case 'multiple': 1893 var $element = this.element; 1894 if (!!$element[0].multiple !== value) { 1895 $menu.toggleClass('ui-multiselect-multiple', value).toggleClass('ui-multiselect-single', !value); 1896 $element[0].multiple = value; 1897 this.uncheckAll(); 1898 this.refresh(); 1899 } 1900 break; 1901 case 'position': 1902 if (value !== null && !$.isEmptyObject(value) ) { 1903 this.options.position = value; 1904 } 1905 this.position(); 1906 break; 1907 case 'zIndex': 1908 this.options.zIndex = value; 1909 this.$menu.css('z-index', value); 1910 break; 1911 default: 1912 this.options[key] = value; 1913 } 1914 $.Widget.prototype._setOption.apply(this, arguments); // eslint-disable-line prefer-rest-params 1915 }, 1916 _parse2px: parse2px, 1917 1918 }); 1919 1920 // Fix for jQuery UI modal dialogs 1921 // https://api.jqueryui.com/dialog/#method-_allowInteraction 1922 // https://learn.jquery.com/jquery-ui/widget-factory/extending-widgets/ 1923 if ($.ui && 'dialog' in $.ui) { 1924 $.widget( 'ui.dialog', $.ui.dialog, { 1925 _allowInteraction: function( event ) { 1926 if ( this._super( event ) || $( event.target ).closest('.ui-multiselect-menu' ).length ) { 1927 return true; 1928 } 1929 }, 1930 }); 1931 } 1932})(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.