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