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