resources/lib/jquery-ui/ui/widgets/selectmenu.js   F
last analyzed

Complexity

Total Complexity 110
Complexity/F 2.39

Size

Lines of Code 667
Function Count 46

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
nc 32768
dl 0
loc 667
rs 2.5184
c 0
b 0
f 0
wmc 110
mnd 2
bc 96
fnc 46
bpm 2.0869
cpm 2.3913
noi 0

1 Function

Rating   Name   Duplication   Size   Complexity  
B selectmenu.js ➔ ?!? 0 646 1

How to fix   Complexity   

Complexity

Complex classes like resources/lib/jquery-ui/ui/widgets/selectmenu.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
/*!
2
 * jQuery UI Selectmenu 1.12.1
3
 * http://jqueryui.com
4
 *
5
 * Copyright jQuery Foundation and other contributors
6
 * Released under the MIT license.
7
 * http://jquery.org/license
8
 */
9
10
//>>label: Selectmenu
11
//>>group: Widgets
12
// jscs:disable maximumLineLength
13
//>>description: Duplicates and extends the functionality of a native HTML select element, allowing it to be customizable in behavior and appearance far beyond the limitations of a native select.
14
// jscs:enable maximumLineLength
15
//>>docs: http://api.jqueryui.com/selectmenu/
16
//>>demos: http://jqueryui.com/selectmenu/
17
//>>css.structure: ../../themes/base/core.css
18
//>>css.structure: ../../themes/base/selectmenu.css, ../../themes/base/button.css
19
//>>css.theme: ../../themes/base/theme.css
20
21
( function( factory ) {
22
	if ( typeof define === "function" && define.amd ) {
23
24
		// AMD. Register as an anonymous module.
25
		define( [
26
			"jquery",
27
			"./menu",
28
			"../escape-selector",
29
			"../form-reset-mixin",
30
			"../keycode",
31
			"../labels",
32
			"../position",
33
			"../unique-id",
34
			"../version",
35
			"../widget"
36
		], factory );
37
	} else {
38
39
		// Browser globals
40
		factory( jQuery );
41
	}
42
}( function( $ ) {
43
44
return $.widget( "ui.selectmenu", [ $.ui.formResetMixin, {
45
	version: "1.12.1",
46
	defaultElement: "<select>",
47
	options: {
48
		appendTo: null,
49
		classes: {
50
			"ui-selectmenu-button-open": "ui-corner-top",
51
			"ui-selectmenu-button-closed": "ui-corner-all"
52
		},
53
		disabled: null,
54
		icons: {
55
			button: "ui-icon-triangle-1-s"
56
		},
57
		position: {
58
			my: "left top",
59
			at: "left bottom",
60
			collision: "none"
61
		},
62
		width: false,
63
64
		// Callbacks
65
		change: null,
66
		close: null,
67
		focus: null,
68
		open: null,
69
		select: null
70
	},
71
72
	_create: function() {
73
		var selectmenuId = this.element.uniqueId().attr( "id" );
74
		this.ids = {
75
			element: selectmenuId,
76
			button: selectmenuId + "-button",
77
			menu: selectmenuId + "-menu"
78
		};
79
80
		this._drawButton();
81
		this._drawMenu();
82
		this._bindFormResetHandler();
83
84
		this._rendered = false;
85
		this.menuItems = $();
86
	},
87
88
	_drawButton: function() {
89
		var icon,
90
			that = this,
91
			item = this._parseOption(
92
				this.element.find( "option:selected" ),
93
				this.element[ 0 ].selectedIndex
94
			);
95
96
		// Associate existing label with the new button
97
		this.labels = this.element.labels().attr( "for", this.ids.button );
98
		this._on( this.labels, {
99
			click: function( event ) {
100
				this.button.focus();
101
				event.preventDefault();
102
			}
103
		} );
104
105
		// Hide original select element
106
		this.element.hide();
107
108
		// Create button
109
		this.button = $( "<span>", {
110
			tabindex: this.options.disabled ? -1 : 0,
111
			id: this.ids.button,
112
			role: "combobox",
113
			"aria-expanded": "false",
114
			"aria-autocomplete": "list",
115
			"aria-owns": this.ids.menu,
116
			"aria-haspopup": "true",
117
			title: this.element.attr( "title" )
118
		} )
119
			.insertAfter( this.element );
120
121
		this._addClass( this.button, "ui-selectmenu-button ui-selectmenu-button-closed",
122
			"ui-button ui-widget" );
123
124
		icon = $( "<span>" ).appendTo( this.button );
125
		this._addClass( icon, "ui-selectmenu-icon", "ui-icon " + this.options.icons.button );
126
		this.buttonItem = this._renderButtonItem( item )
127
			.appendTo( this.button );
128
129
		if ( this.options.width !== false ) {
130
			this._resizeButton();
131
		}
132
133
		this._on( this.button, this._buttonEvents );
134
		this.button.one( "focusin", function() {
135
136
			// Delay rendering the menu items until the button receives focus.
137
			// The menu may have already been rendered via a programmatic open.
138
			if ( !that._rendered ) {
139
				that._refreshMenu();
140
			}
141
		} );
142
	},
143
144
	_drawMenu: function() {
145
		var that = this;
146
147
		// Create menu
148
		this.menu = $( "<ul>", {
149
			"aria-hidden": "true",
150
			"aria-labelledby": this.ids.button,
151
			id: this.ids.menu
152
		} );
153
154
		// Wrap menu
155
		this.menuWrap = $( "<div>" ).append( this.menu );
156
		this._addClass( this.menuWrap, "ui-selectmenu-menu", "ui-front" );
157
		this.menuWrap.appendTo( this._appendTo() );
158
159
		// Initialize menu widget
160
		this.menuInstance = this.menu
161
			.menu( {
162
				classes: {
163
					"ui-menu": "ui-corner-bottom"
164
				},
165
				role: "listbox",
166
				select: function( event, ui ) {
167
					event.preventDefault();
168
169
					// Support: IE8
170
					// If the item was selected via a click, the text selection
171
					// will be destroyed in IE
172
					that._setSelection();
173
174
					that._select( ui.item.data( "ui-selectmenu-item" ), event );
175
				},
176
				focus: function( event, ui ) {
177
					var item = ui.item.data( "ui-selectmenu-item" );
178
179
					// Prevent inital focus from firing and check if its a newly focused item
180
					if ( that.focusIndex != null && item.index !== that.focusIndex ) {
181
						that._trigger( "focus", event, { item: item } );
182
						if ( !that.isOpen ) {
183
							that._select( item, event );
184
						}
185
					}
186
					that.focusIndex = item.index;
187
188
					that.button.attr( "aria-activedescendant",
189
						that.menuItems.eq( item.index ).attr( "id" ) );
190
				}
191
			} )
192
			.menu( "instance" );
193
194
		// Don't close the menu on mouseleave
195
		this.menuInstance._off( this.menu, "mouseleave" );
196
197
		// Cancel the menu's collapseAll on document click
198
		this.menuInstance._closeOnDocumentClick = function() {
199
			return false;
200
		};
201
202
		// Selects often contain empty items, but never contain dividers
203
		this.menuInstance._isDivider = function() {
204
			return false;
205
		};
206
	},
207
208
	refresh: function() {
209
		this._refreshMenu();
210
		this.buttonItem.replaceWith(
211
			this.buttonItem = this._renderButtonItem(
212
213
				// Fall back to an empty object in case there are no options
214
				this._getSelectedItem().data( "ui-selectmenu-item" ) || {}
215
			)
216
		);
217
		if ( this.options.width === null ) {
218
			this._resizeButton();
219
		}
220
	},
221
222
	_refreshMenu: function() {
223
		var item,
224
			options = this.element.find( "option" );
225
226
		this.menu.empty();
227
228
		this._parseOptions( options );
229
		this._renderMenu( this.menu, this.items );
230
231
		this.menuInstance.refresh();
232
		this.menuItems = this.menu.find( "li" )
233
			.not( ".ui-selectmenu-optgroup" )
234
				.find( ".ui-menu-item-wrapper" );
235
236
		this._rendered = true;
237
238
		if ( !options.length ) {
239
			return;
240
		}
241
242
		item = this._getSelectedItem();
243
244
		// Update the menu to have the correct item focused
245
		this.menuInstance.focus( null, item );
246
		this._setAria( item.data( "ui-selectmenu-item" ) );
247
248
		// Set disabled state
249
		this._setOption( "disabled", this.element.prop( "disabled" ) );
250
	},
251
252
	open: function( event ) {
253
		if ( this.options.disabled ) {
254
			return;
255
		}
256
257
		// If this is the first time the menu is being opened, render the items
258
		if ( !this._rendered ) {
259
			this._refreshMenu();
260
		} else {
261
262
			// Menu clears focus on close, reset focus to selected item
263
			this._removeClass( this.menu.find( ".ui-state-active" ), null, "ui-state-active" );
264
			this.menuInstance.focus( null, this._getSelectedItem() );
265
		}
266
267
		// If there are no options, don't open the menu
268
		if ( !this.menuItems.length ) {
269
			return;
270
		}
271
272
		this.isOpen = true;
273
		this._toggleAttr();
274
		this._resizeMenu();
275
		this._position();
276
277
		this._on( this.document, this._documentClick );
278
279
		this._trigger( "open", event );
280
	},
281
282
	_position: function() {
283
		this.menuWrap.position( $.extend( { of: this.button }, this.options.position ) );
284
	},
285
286
	close: function( event ) {
287
		if ( !this.isOpen ) {
288
			return;
289
		}
290
291
		this.isOpen = false;
292
		this._toggleAttr();
293
294
		this.range = null;
295
		this._off( this.document );
296
297
		this._trigger( "close", event );
298
	},
299
300
	widget: function() {
301
		return this.button;
302
	},
303
304
	menuWidget: function() {
305
		return this.menu;
306
	},
307
308
	_renderButtonItem: function( item ) {
309
		var buttonItem = $( "<span>" );
310
311
		this._setText( buttonItem, item.label );
312
		this._addClass( buttonItem, "ui-selectmenu-text" );
313
314
		return buttonItem;
315
	},
316
317
	_renderMenu: function( ul, items ) {
318
		var that = this,
319
			currentOptgroup = "";
320
321
		$.each( items, function( index, item ) {
322
			var li;
323
324
			if ( item.optgroup !== currentOptgroup ) {
325
				li = $( "<li>", {
326
					text: item.optgroup
327
				} );
328
				that._addClass( li, "ui-selectmenu-optgroup", "ui-menu-divider" +
329
					( item.element.parent( "optgroup" ).prop( "disabled" ) ?
330
						" ui-state-disabled" :
331
						"" ) );
332
333
				li.appendTo( ul );
334
335
				currentOptgroup = item.optgroup;
336
			}
337
338
			that._renderItemData( ul, item );
339
		} );
340
	},
341
342
	_renderItemData: function( ul, item ) {
343
		return this._renderItem( ul, item ).data( "ui-selectmenu-item", item );
344
	},
345
346
	_renderItem: function( ul, item ) {
347
		var li = $( "<li>" ),
348
			wrapper = $( "<div>", {
349
				title: item.element.attr( "title" )
350
			} );
351
352
		if ( item.disabled ) {
353
			this._addClass( li, null, "ui-state-disabled" );
354
		}
355
		this._setText( wrapper, item.label );
356
357
		return li.append( wrapper ).appendTo( ul );
358
	},
359
360
	_setText: function( element, value ) {
361
		if ( value ) {
362
			element.text( value );
363
		} else {
364
			element.html( "&#160;" );
365
		}
366
	},
367
368
	_move: function( direction, event ) {
369
		var item, next,
370
			filter = ".ui-menu-item";
371
372
		if ( this.isOpen ) {
373
			item = this.menuItems.eq( this.focusIndex ).parent( "li" );
374
		} else {
375
			item = this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" );
376
			filter += ":not(.ui-state-disabled)";
377
		}
378
379
		if ( direction === "first" || direction === "last" ) {
380
			next = item[ direction === "first" ? "prevAll" : "nextAll" ]( filter ).eq( -1 );
381
		} else {
382
			next = item[ direction + "All" ]( filter ).eq( 0 );
383
		}
384
385
		if ( next.length ) {
386
			this.menuInstance.focus( event, next );
387
		}
388
	},
389
390
	_getSelectedItem: function() {
391
		return this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" );
392
	},
393
394
	_toggle: function( event ) {
395
		this[ this.isOpen ? "close" : "open" ]( event );
396
	},
397
398
	_setSelection: function() {
399
		var selection;
400
401
		if ( !this.range ) {
402
			return;
403
		}
404
405
		if ( window.getSelection ) {
406
			selection = window.getSelection();
407
			selection.removeAllRanges();
408
			selection.addRange( this.range );
409
410
		// Support: IE8
411
		} else {
412
			this.range.select();
413
		}
414
415
		// Support: IE
416
		// Setting the text selection kills the button focus in IE, but
417
		// restoring the focus doesn't kill the selection.
418
		this.button.focus();
419
	},
420
421
	_documentClick: {
422
		mousedown: function( event ) {
423
			if ( !this.isOpen ) {
424
				return;
425
			}
426
427
			if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" +
428
					$.ui.escapeSelector( this.ids.button ) ).length ) {
429
				this.close( event );
430
			}
431
		}
432
	},
433
434
	_buttonEvents: {
435
436
		// Prevent text selection from being reset when interacting with the selectmenu (#10144)
437
		mousedown: function() {
438
			var selection;
439
440
			if ( window.getSelection ) {
441
				selection = window.getSelection();
442
				if ( selection.rangeCount ) {
443
					this.range = selection.getRangeAt( 0 );
444
				}
445
446
			// Support: IE8
447
			} else {
448
				this.range = document.selection.createRange();
449
			}
450
		},
451
452
		click: function( event ) {
453
			this._setSelection();
454
			this._toggle( event );
455
		},
456
457
		keydown: function( event ) {
458
			var preventDefault = true;
459
			switch ( event.keyCode ) {
460
			case $.ui.keyCode.TAB:
461
			case $.ui.keyCode.ESCAPE:
462
				this.close( event );
463
				preventDefault = false;
464
				break;
465
			case $.ui.keyCode.ENTER:
466
				if ( this.isOpen ) {
467
					this._selectFocusedItem( event );
468
				}
469
				break;
470
			case $.ui.keyCode.UP:
471
				if ( event.altKey ) {
472
					this._toggle( event );
473
				} else {
474
					this._move( "prev", event );
475
				}
476
				break;
477
			case $.ui.keyCode.DOWN:
478
				if ( event.altKey ) {
479
					this._toggle( event );
480
				} else {
481
					this._move( "next", event );
482
				}
483
				break;
484
			case $.ui.keyCode.SPACE:
485
				if ( this.isOpen ) {
486
					this._selectFocusedItem( event );
487
				} else {
488
					this._toggle( event );
489
				}
490
				break;
491
			case $.ui.keyCode.LEFT:
492
				this._move( "prev", event );
493
				break;
494
			case $.ui.keyCode.RIGHT:
495
				this._move( "next", event );
496
				break;
497
			case $.ui.keyCode.HOME:
498
			case $.ui.keyCode.PAGE_UP:
499
				this._move( "first", event );
500
				break;
501
			case $.ui.keyCode.END:
502
			case $.ui.keyCode.PAGE_DOWN:
503
				this._move( "last", event );
504
				break;
505
			default:
506
				this.menu.trigger( event );
507
				preventDefault = false;
508
			}
509
510
			if ( preventDefault ) {
511
				event.preventDefault();
512
			}
513
		}
514
	},
515
516
	_selectFocusedItem: function( event ) {
517
		var item = this.menuItems.eq( this.focusIndex ).parent( "li" );
518
		if ( !item.hasClass( "ui-state-disabled" ) ) {
519
			this._select( item.data( "ui-selectmenu-item" ), event );
520
		}
521
	},
522
523
	_select: function( item, event ) {
524
		var oldIndex = this.element[ 0 ].selectedIndex;
525
526
		// Change native select element
527
		this.element[ 0 ].selectedIndex = item.index;
528
		this.buttonItem.replaceWith( this.buttonItem = this._renderButtonItem( item ) );
529
		this._setAria( item );
530
		this._trigger( "select", event, { item: item } );
531
532
		if ( item.index !== oldIndex ) {
533
			this._trigger( "change", event, { item: item } );
534
		}
535
536
		this.close( event );
537
	},
538
539
	_setAria: function( item ) {
540
		var id = this.menuItems.eq( item.index ).attr( "id" );
541
542
		this.button.attr( {
543
			"aria-labelledby": id,
544
			"aria-activedescendant": id
545
		} );
546
		this.menu.attr( "aria-activedescendant", id );
547
	},
548
549
	_setOption: function( key, value ) {
550
		if ( key === "icons" ) {
551
			var icon = this.button.find( "span.ui-icon" );
552
			this._removeClass( icon, null, this.options.icons.button )
553
				._addClass( icon, null, value.button );
554
		}
555
556
		this._super( key, value );
557
558
		if ( key === "appendTo" ) {
559
			this.menuWrap.appendTo( this._appendTo() );
560
		}
561
562
		if ( key === "width" ) {
563
			this._resizeButton();
564
		}
565
	},
566
567
	_setOptionDisabled: function( value ) {
568
		this._super( value );
569
570
		this.menuInstance.option( "disabled", value );
571
		this.button.attr( "aria-disabled", value );
572
		this._toggleClass( this.button, null, "ui-state-disabled", value );
573
574
		this.element.prop( "disabled", value );
575
		if ( value ) {
576
			this.button.attr( "tabindex", -1 );
577
			this.close();
578
		} else {
579
			this.button.attr( "tabindex", 0 );
580
		}
581
	},
582
583
	_appendTo: function() {
584
		var element = this.options.appendTo;
585
586
		if ( element ) {
587
			element = element.jquery || element.nodeType ?
588
				$( element ) :
589
				this.document.find( element ).eq( 0 );
590
		}
591
592
		if ( !element || !element[ 0 ] ) {
593
			element = this.element.closest( ".ui-front, dialog" );
594
		}
595
596
		if ( !element.length ) {
597
			element = this.document[ 0 ].body;
598
		}
599
600
		return element;
601
	},
602
603
	_toggleAttr: function() {
604
		this.button.attr( "aria-expanded", this.isOpen );
605
606
		// We can't use two _toggleClass() calls here, because we need to make sure
607
		// we always remove classes first and add them second, otherwise if both classes have the
608
		// same theme class, it will be removed after we add it.
609
		this._removeClass( this.button, "ui-selectmenu-button-" +
610
			( this.isOpen ? "closed" : "open" ) )
611
			._addClass( this.button, "ui-selectmenu-button-" +
612
				( this.isOpen ? "open" : "closed" ) )
613
			._toggleClass( this.menuWrap, "ui-selectmenu-open", null, this.isOpen );
614
615
		this.menu.attr( "aria-hidden", !this.isOpen );
616
	},
617
618
	_resizeButton: function() {
619
		var width = this.options.width;
620
621
		// For `width: false`, just remove inline style and stop
622
		if ( width === false ) {
623
			this.button.css( "width", "" );
624
			return;
625
		}
626
627
		// For `width: null`, match the width of the original element
628
		if ( width === null ) {
629
			width = this.element.show().outerWidth();
630
			this.element.hide();
631
		}
632
633
		this.button.outerWidth( width );
634
	},
635
636
	_resizeMenu: function() {
637
		this.menu.outerWidth( Math.max(
638
			this.button.outerWidth(),
639
640
			// Support: IE10
641
			// IE10 wraps long text (possibly a rounding bug)
642
			// so we add 1px to avoid the wrapping
643
			this.menu.width( "" ).outerWidth() + 1
644
		) );
645
	},
646
647
	_getCreateOptions: function() {
648
		var options = this._super();
649
650
		options.disabled = this.element.prop( "disabled" );
651
652
		return options;
653
	},
654
655
	_parseOptions: function( options ) {
656
		var that = this,
657
			data = [];
658
		options.each( function( index, item ) {
659
			data.push( that._parseOption( $( item ), index ) );
660
		} );
661
		this.items = data;
662
	},
663
664
	_parseOption: function( option, index ) {
665
		var optgroup = option.parent( "optgroup" );
666
667
		return {
668
			element: option,
669
			index: index,
670
			value: option.val(),
671
			label: option.text(),
672
			optgroup: optgroup.attr( "label" ) || "",
673
			disabled: optgroup.prop( "disabled" ) || option.prop( "disabled" )
674
		};
675
	},
676
677
	_destroy: function() {
678
		this._unbindFormResetHandler();
679
		this.menuWrap.remove();
680
		this.button.remove();
681
		this.element.show();
682
		this.element.removeUniqueId();
683
		this.labels.attr( "for", this.ids.element );
684
	}
685
} ] );
686
687
} ) );
688