| Total Complexity | 289 |
| Complexity/F | 3.11 |
| Lines of Code | 1908 |
| Function Count | 93 |
| Duplicated Lines | 27 |
| Ratio | 1.42 % |
| Changes | 0 | ||
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like static/org.openpsa.core/jquery-ui-multiselect-widget-3.0.0/src/jquery.multiselect.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | /* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, boss:true, undef:true, curly:true, browser:true, jquery:true */ |
||
| 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} 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 when 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 > 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} 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 | View Code Duplication | 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 | View Code Duplication | 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 | })(jQuery); |
||
| 1927 |
This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.