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

Complexity

Total Complexity 185
Complexity/F 2.94

Size

Lines of Code 906
Function Count 63

Duplication

Duplicated Lines 34
Ratio 3.75 %

Importance

Changes 0
Metric Value
cc 0
nc 0
dl 34
loc 906
rs 2.1818
c 0
b 0
f 0
wmc 185
mnd 3
bc 155
fnc 63
bpm 2.4603
cpm 2.9365
noi 2

1 Function

Rating   Name   Duplication   Size   Complexity  
A tabs.js ➔ ?!? 0 20 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like resources/lib/jquery-ui/ui/widgets/tabs.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 Tabs 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: Tabs
11
//>>group: Widgets
12
//>>description: Transforms a set of container elements into a tab structure.
13
//>>docs: http://api.jqueryui.com/tabs/
14
//>>demos: http://jqueryui.com/tabs/
15
//>>css.structure: ../../themes/base/core.css
16
//>>css.structure: ../../themes/base/tabs.css
17
//>>css.theme: ../../themes/base/theme.css
18
19
( function( factory ) {
20
	if ( typeof define === "function" && define.amd ) {
21
22
		// AMD. Register as an anonymous module.
23
		define( [
24
			"jquery",
25
			"../escape-selector",
26
			"../keycode",
27
			"../safe-active-element",
28
			"../unique-id",
29
			"../version",
30
			"../widget"
31
		], factory );
32
	} else {
33
34
		// Browser globals
35
		factory( jQuery );
36
	}
37
}( function( $ ) {
38
39
$.widget( "ui.tabs", {
40
	version: "1.12.1",
41
	delay: 300,
42
	options: {
43
		active: null,
44
		classes: {
45
			"ui-tabs": "ui-corner-all",
46
			"ui-tabs-nav": "ui-corner-all",
47
			"ui-tabs-panel": "ui-corner-bottom",
48
			"ui-tabs-tab": "ui-corner-top"
49
		},
50
		collapsible: false,
51
		event: "click",
52
		heightStyle: "content",
53
		hide: null,
54
		show: null,
55
56
		// Callbacks
57
		activate: null,
58
		beforeActivate: null,
59
		beforeLoad: null,
60
		load: null
61
	},
62
63
	_isLocal: ( function() {
64
		var rhash = /#.*$/;
65
66
		return function( anchor ) {
67
			var anchorUrl, locationUrl;
68
69
			anchorUrl = anchor.href.replace( rhash, "" );
70
			locationUrl = location.href.replace( rhash, "" );
71
72
			// Decoding may throw an error if the URL isn't UTF-8 (#9518)
73
			try {
74
				anchorUrl = decodeURIComponent( anchorUrl );
75
			} catch ( error ) {}
76
			try {
77
				locationUrl = decodeURIComponent( locationUrl );
78
			} catch ( error ) {}
79
80
			return anchor.hash.length > 1 && anchorUrl === locationUrl;
81
		};
82
	} )(),
83
84
	_create: function() {
85
		var that = this,
86
			options = this.options;
87
88
		this.running = false;
89
90
		this._addClass( "ui-tabs", "ui-widget ui-widget-content" );
91
		this._toggleClass( "ui-tabs-collapsible", null, options.collapsible );
92
93
		this._processTabs();
94
		options.active = this._initialActive();
95
96
		// Take disabling tabs via class attribute from HTML
97
		// into account and update option properly.
98
		if ( $.isArray( options.disabled ) ) {
99
			options.disabled = $.unique( options.disabled.concat(
100
				$.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) {
101
					return that.tabs.index( li );
102
				} )
103
			) ).sort();
104
		}
105
106
		// Check for length avoids error when initializing empty list
107
		if ( this.options.active !== false && this.anchors.length ) {
108
			this.active = this._findActive( options.active );
109
		} else {
110
			this.active = $();
111
		}
112
113
		this._refresh();
114
115
		if ( this.active.length ) {
116
			this.load( options.active );
117
		}
118
	},
119
120
	_initialActive: function() {
121
		var active = this.options.active,
122
			collapsible = this.options.collapsible,
123
			locationHash = location.hash.substring( 1 );
124
125
		if ( active === null ) {
126
127
			// check the fragment identifier in the URL
128
			if ( locationHash ) {
129
				this.tabs.each( function( i, tab ) {
130
					if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if $(tab).attr("aria-controls") === locationHash is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
131
						active = i;
132
						return false;
133
					}
134
				} );
135
			}
136
137
			// Check for a tab marked active via a class
138
			if ( active === null ) {
139
				active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) );
140
			}
141
142
			// No active tab, set to false
143
			if ( active === null || active === -1 ) {
144
				active = this.tabs.length ? 0 : false;
145
			}
146
		}
147
148
		// Handle numbers: negative, out of range
149
		if ( active !== false ) {
150
			active = this.tabs.index( this.tabs.eq( active ) );
151
			if ( active === -1 ) {
152
				active = collapsible ? false : 0;
153
			}
154
		}
155
156
		// Don't allow collapsible: false and active: false
157
		if ( !collapsible && active === false && this.anchors.length ) {
158
			active = 0;
159
		}
160
161
		return active;
162
	},
163
164
	_getCreateEventData: function() {
165
		return {
166
			tab: this.active,
167
			panel: !this.active.length ? $() : this._getPanelForTab( this.active )
168
		};
169
	},
170
171
	_tabKeydown: function( event ) {
172
		var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ),
173
			selectedIndex = this.tabs.index( focusedTab ),
174
			goingForward = true;
175
176
		if ( this._handlePageNav( event ) ) {
177
			return;
178
		}
179
180
		switch ( event.keyCode ) {
181
		case $.ui.keyCode.RIGHT:
182
		case $.ui.keyCode.DOWN:
183
			selectedIndex++;
184
			break;
185
		case $.ui.keyCode.UP:
186
		case $.ui.keyCode.LEFT:
187
			goingForward = false;
188
			selectedIndex--;
189
			break;
190
		case $.ui.keyCode.END:
191
			selectedIndex = this.anchors.length - 1;
192
			break;
193
		case $.ui.keyCode.HOME:
194
			selectedIndex = 0;
195
			break;
196
		case $.ui.keyCode.SPACE:
197
198
			// Activate only, no collapsing
199
			event.preventDefault();
200
			clearTimeout( this.activating );
201
			this._activate( selectedIndex );
202
			return;
203
		case $.ui.keyCode.ENTER:
204
205
			// Toggle (cancel delayed activation, allow collapsing)
206
			event.preventDefault();
207
			clearTimeout( this.activating );
208
209
			// Determine if we should collapse or activate
210
			this._activate( selectedIndex === this.options.active ? false : selectedIndex );
211
			return;
212
		default:
213
			return;
214
		}
215
216
		// Focus the appropriate tab, based on which key was pressed
217
		event.preventDefault();
218
		clearTimeout( this.activating );
219
		selectedIndex = this._focusNextTab( selectedIndex, goingForward );
220
221
		// Navigating with control/command key will prevent automatic activation
222
		if ( !event.ctrlKey && !event.metaKey ) {
223
224
			// Update aria-selected immediately so that AT think the tab is already selected.
225
			// Otherwise AT may confuse the user by stating that they need to activate the tab,
226
			// but the tab will already be activated by the time the announcement finishes.
227
			focusedTab.attr( "aria-selected", "false" );
228
			this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
229
230
			this.activating = this._delay( function() {
231
				this.option( "active", selectedIndex );
232
			}, this.delay );
233
		}
234
	},
235
236
	_panelKeydown: function( event ) {
237
		if ( this._handlePageNav( event ) ) {
238
			return;
239
		}
240
241
		// Ctrl+up moves focus to the current tab
242
		if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
243
			event.preventDefault();
244
			this.active.trigger( "focus" );
245
		}
246
	},
247
248
	// Alt+page up/down moves focus to the previous/next tab (and activates)
249
	_handlePageNav: function( event ) {
250
		if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
251
			this._activate( this._focusNextTab( this.options.active - 1, false ) );
252
			return true;
253
		}
254
		if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if event.altKey && event.ke... $.ui.keyCode.PAGE_DOWN is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
255
			this._activate( this._focusNextTab( this.options.active + 1, true ) );
256
			return true;
257
		}
258
	},
259
260
	_findNextTab: function( index, goingForward ) {
261
		var lastTabIndex = this.tabs.length - 1;
262
263
		function constrain() {
264
			if ( index > lastTabIndex ) {
265
				index = 0;
266
			}
267
			if ( index < 0 ) {
268
				index = lastTabIndex;
269
			}
270
			return index;
271
		}
272
273
		while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
274
			index = goingForward ? index + 1 : index - 1;
275
		}
276
277
		return index;
278
	},
279
280
	_focusNextTab: function( index, goingForward ) {
281
		index = this._findNextTab( index, goingForward );
282
		this.tabs.eq( index ).trigger( "focus" );
283
		return index;
284
	},
285
286
	_setOption: function( key, value ) {
287
		if ( key === "active" ) {
288
289
			// _activate() will handle invalid values and update this.options
290
			this._activate( value );
291
			return;
292
		}
293
294
		this._super( key, value );
295
296
		if ( key === "collapsible" ) {
297
			this._toggleClass( "ui-tabs-collapsible", null, value );
298
299
			// Setting collapsible: false while collapsed; open first panel
300
			if ( !value && this.options.active === false ) {
301
				this._activate( 0 );
302
			}
303
		}
304
305
		if ( key === "event" ) {
306
			this._setupEvents( value );
307
		}
308
309
		if ( key === "heightStyle" ) {
310
			this._setupHeightStyle( value );
311
		}
312
	},
313
314
	_sanitizeSelector: function( hash ) {
315
		return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
316
	},
317
318
	refresh: function() {
319
		var options = this.options,
320
			lis = this.tablist.children( ":has(a[href])" );
321
322
		// Get disabled tabs from class attribute from HTML
323
		// this will get converted to a boolean if needed in _refresh()
324
		options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) {
325
			return lis.index( tab );
326
		} );
327
328
		this._processTabs();
329
330
		// Was collapsed or no tabs
331
		if ( options.active === false || !this.anchors.length ) {
332
			options.active = false;
333
			this.active = $();
334
335
		// was active, but active tab is gone
336
		} else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) {
337
338
			// all remaining tabs are disabled
339
			if ( this.tabs.length === options.disabled.length ) {
340
				options.active = false;
341
				this.active = $();
342
343
			// activate previous tab
344
			} else {
345
				this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) );
346
			}
347
348
		// was active, active tab still exists
349
		} else {
350
351
			// make sure active index is correct
352
			options.active = this.tabs.index( this.active );
353
		}
354
355
		this._refresh();
356
	},
357
358
	_refresh: function() {
359
		this._setOptionDisabled( this.options.disabled );
360
		this._setupEvents( this.options.event );
361
		this._setupHeightStyle( this.options.heightStyle );
362
363
		this.tabs.not( this.active ).attr( {
364
			"aria-selected": "false",
365
			"aria-expanded": "false",
366
			tabIndex: -1
367
		} );
368
		this.panels.not( this._getPanelForTab( this.active ) )
369
			.hide()
370
			.attr( {
371
				"aria-hidden": "true"
372
			} );
373
374
		// Make sure one tab is in the tab order
375
		if ( !this.active.length ) {
376
			this.tabs.eq( 0 ).attr( "tabIndex", 0 );
377
		} else {
378
			this.active
379
				.attr( {
380
					"aria-selected": "true",
381
					"aria-expanded": "true",
382
					tabIndex: 0
383
				} );
384
			this._addClass( this.active, "ui-tabs-active", "ui-state-active" );
385
			this._getPanelForTab( this.active )
386
				.show()
387
				.attr( {
388
					"aria-hidden": "false"
389
				} );
390
		}
391
	},
392
393
	_processTabs: function() {
394
		var that = this,
395
			prevTabs = this.tabs,
396
			prevAnchors = this.anchors,
397
			prevPanels = this.panels;
398
399
		this.tablist = this._getList().attr( "role", "tablist" );
400
		this._addClass( this.tablist, "ui-tabs-nav",
401
			"ui-helper-reset ui-helper-clearfix ui-widget-header" );
402
403
		// Prevent users from focusing disabled tabs via click
404
		this.tablist
405
			.on( "mousedown" + this.eventNamespace, "> li", function( event ) {
406
				if ( $( this ).is( ".ui-state-disabled" ) ) {
407
					event.preventDefault();
408
				}
409
			} )
410
411
			// Support: IE <9
412
			// Preventing the default action in mousedown doesn't prevent IE
413
			// from focusing the element, so if the anchor gets focused, blur.
414
			// We don't have to worry about focusing the previously focused
415
			// element since clicking on a non-focusable element should focus
416
			// the body anyway.
417
			.on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() {
418
				if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
419
					this.blur();
420
				}
421
			} );
422
423
		this.tabs = this.tablist.find( "> li:has(a[href])" )
424
			.attr( {
425
				role: "tab",
426
				tabIndex: -1
427
			} );
428
		this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" );
429
430
		this.anchors = this.tabs.map( function() {
431
			return $( "a", this )[ 0 ];
432
		} )
433
			.attr( {
434
				role: "presentation",
435
				tabIndex: -1
436
			} );
437
		this._addClass( this.anchors, "ui-tabs-anchor" );
438
439
		this.panels = $();
440
441
		this.anchors.each( function( i, anchor ) {
442
			var selector, panel, panelId,
443
				anchorId = $( anchor ).uniqueId().attr( "id" ),
444
				tab = $( anchor ).closest( "li" ),
445
				originalAriaControls = tab.attr( "aria-controls" );
446
447
			// Inline tab
448
			if ( that._isLocal( anchor ) ) {
449
				selector = anchor.hash;
450
				panelId = selector.substring( 1 );
451
				panel = that.element.find( that._sanitizeSelector( selector ) );
452
453
			// remote tab
454
			} else {
455
456
				// If the tab doesn't already have aria-controls,
457
				// generate an id by using a throw-away element
458
				panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id;
459
				selector = "#" + panelId;
460
				panel = that.element.find( selector );
461
				if ( !panel.length ) {
462
					panel = that._createPanel( panelId );
463
					panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
464
				}
465
				panel.attr( "aria-live", "polite" );
466
			}
467
468
			if ( panel.length ) {
469
				that.panels = that.panels.add( panel );
470
			}
471
			if ( originalAriaControls ) {
472
				tab.data( "ui-tabs-aria-controls", originalAriaControls );
473
			}
474
			tab.attr( {
475
				"aria-controls": panelId,
476
				"aria-labelledby": anchorId
477
			} );
478
			panel.attr( "aria-labelledby", anchorId );
479
		} );
480
481
		this.panels.attr( "role", "tabpanel" );
482
		this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" );
483
484
		// Avoid memory leaks (#10056)
485
		if ( prevTabs ) {
486
			this._off( prevTabs.not( this.tabs ) );
487
			this._off( prevAnchors.not( this.anchors ) );
488
			this._off( prevPanels.not( this.panels ) );
489
		}
490
	},
491
492
	// Allow overriding how to find the list for rare usage scenarios (#7715)
493
	_getList: function() {
494
		return this.tablist || this.element.find( "ol, ul" ).eq( 0 );
495
	},
496
497
	_createPanel: function( id ) {
498
		return $( "<div>" )
499
			.attr( "id", id )
500
			.data( "ui-tabs-destroy", true );
501
	},
502
503
	_setOptionDisabled: function( disabled ) {
504
		var currentItem, li, i;
505
506
		if ( $.isArray( disabled ) ) {
507
			if ( !disabled.length ) {
508
				disabled = false;
509
			} else if ( disabled.length === this.anchors.length ) {
510
				disabled = true;
511
			}
512
		}
513
514
		// Disable tabs
515
		for ( i = 0; ( li = this.tabs[ i ] ); i++ ) {
516
			currentItem = $( li );
517
			if ( disabled === true || $.inArray( i, disabled ) !== -1 ) {
518
				currentItem.attr( "aria-disabled", "true" );
519
				this._addClass( currentItem, null, "ui-state-disabled" );
520
			} else {
521
				currentItem.removeAttr( "aria-disabled" );
522
				this._removeClass( currentItem, null, "ui-state-disabled" );
523
			}
524
		}
525
526
		this.options.disabled = disabled;
527
528
		this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null,
529
			disabled === true );
530
	},
531
532
	_setupEvents: function( event ) {
533
		var events = {};
534
		if ( event ) {
535
			$.each( event.split( " " ), function( index, eventName ) {
536
				events[ eventName ] = "_eventHandler";
537
			} );
538
		}
539
540
		this._off( this.anchors.add( this.tabs ).add( this.panels ) );
541
542
		// Always prevent the default action, even when disabled
543
		this._on( true, this.anchors, {
544
			click: function( event ) {
545
				event.preventDefault();
546
			}
547
		} );
548
		this._on( this.anchors, events );
549
		this._on( this.tabs, { keydown: "_tabKeydown" } );
550
		this._on( this.panels, { keydown: "_panelKeydown" } );
551
552
		this._focusable( this.tabs );
553
		this._hoverable( this.tabs );
554
	},
555
556 View Code Duplication
	_setupHeightStyle: function( heightStyle ) {
557
		var maxHeight,
558
			parent = this.element.parent();
559
560
		if ( heightStyle === "fill" ) {
561
			maxHeight = parent.height();
562
			maxHeight -= this.element.outerHeight() - this.element.height();
563
564
			this.element.siblings( ":visible" ).each( function() {
565
				var elem = $( this ),
566
					position = elem.css( "position" );
567
568
				if ( position === "absolute" || position === "fixed" ) {
569
					return;
570
				}
571
				maxHeight -= elem.outerHeight( true );
572
			} );
573
574
			this.element.children().not( this.panels ).each( function() {
575
				maxHeight -= $( this ).outerHeight( true );
576
			} );
577
578
			this.panels.each( function() {
579
				$( this ).height( Math.max( 0, maxHeight -
580
					$( this ).innerHeight() + $( this ).height() ) );
581
			} )
582
				.css( "overflow", "auto" );
583
		} else if ( heightStyle === "auto" ) {
584
			maxHeight = 0;
585
			this.panels.each( function() {
586
				maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
587
			} ).height( maxHeight );
588
		}
589
	},
590
591
	_eventHandler: function( event ) {
592
		var options = this.options,
593
			active = this.active,
594
			anchor = $( event.currentTarget ),
595
			tab = anchor.closest( "li" ),
596
			clickedIsActive = tab[ 0 ] === active[ 0 ],
597
			collapsing = clickedIsActive && options.collapsible,
598
			toShow = collapsing ? $() : this._getPanelForTab( tab ),
599
			toHide = !active.length ? $() : this._getPanelForTab( active ),
600
			eventData = {
601
				oldTab: active,
602
				oldPanel: toHide,
603
				newTab: collapsing ? $() : tab,
604
				newPanel: toShow
605
			};
606
607
		event.preventDefault();
608
609
		if ( tab.hasClass( "ui-state-disabled" ) ||
610
611
				// tab is already loading
612
				tab.hasClass( "ui-tabs-loading" ) ||
613
614
				// can't switch durning an animation
615
				this.running ||
616
617
				// click on active header, but not collapsible
618
				( clickedIsActive && !options.collapsible ) ||
619
620
				// allow canceling activation
621
				( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
622
			return;
623
		}
624
625
		options.active = collapsing ? false : this.tabs.index( tab );
626
627
		this.active = clickedIsActive ? $() : tab;
628
		if ( this.xhr ) {
629
			this.xhr.abort();
630
		}
631
632
		if ( !toHide.length && !toShow.length ) {
633
			$.error( "jQuery UI Tabs: Mismatching fragment identifier." );
634
		}
635
636
		if ( toShow.length ) {
637
			this.load( this.tabs.index( tab ), event );
638
		}
639
		this._toggle( event, eventData );
640
	},
641
642
	// Handles show/hide for selecting tabs
643
	_toggle: function( event, eventData ) {
644
		var that = this,
645
			toShow = eventData.newPanel,
646
			toHide = eventData.oldPanel;
647
648
		this.running = true;
649
650
		function complete() {
651
			that.running = false;
652
			that._trigger( "activate", event, eventData );
653
		}
654
655
		function show() {
656
			that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" );
657
658
			if ( toShow.length && that.options.show ) {
659
				that._show( toShow, that.options.show, complete );
660
			} else {
661
				toShow.show();
662
				complete();
663
			}
664
		}
665
666
		// Start out by hiding, then showing, then completing
667
		if ( toHide.length && this.options.hide ) {
668
			this._hide( toHide, this.options.hide, function() {
669
				that._removeClass( eventData.oldTab.closest( "li" ),
670
					"ui-tabs-active", "ui-state-active" );
671
				show();
672
			} );
673
		} else {
674
			this._removeClass( eventData.oldTab.closest( "li" ),
675
				"ui-tabs-active", "ui-state-active" );
676
			toHide.hide();
677
			show();
678
		}
679
680
		toHide.attr( "aria-hidden", "true" );
681
		eventData.oldTab.attr( {
682
			"aria-selected": "false",
683
			"aria-expanded": "false"
684
		} );
685
686
		// If we're switching tabs, remove the old tab from the tab order.
687
		// If we're opening from collapsed state, remove the previous tab from the tab order.
688
		// If we're collapsing, then keep the collapsing tab in the tab order.
689
		if ( toShow.length && toHide.length ) {
690
			eventData.oldTab.attr( "tabIndex", -1 );
691
		} else if ( toShow.length ) {
692
			this.tabs.filter( function() {
693
				return $( this ).attr( "tabIndex" ) === 0;
694
			} )
695
				.attr( "tabIndex", -1 );
696
		}
697
698
		toShow.attr( "aria-hidden", "false" );
699
		eventData.newTab.attr( {
700
			"aria-selected": "true",
701
			"aria-expanded": "true",
702
			tabIndex: 0
703
		} );
704
	},
705
706
	_activate: function( index ) {
707
		var anchor,
708
			active = this._findActive( index );
709
710
		// Trying to activate the already active panel
711
		if ( active[ 0 ] === this.active[ 0 ] ) {
712
			return;
713
		}
714
715
		// Trying to collapse, simulate a click on the current active header
716
		if ( !active.length ) {
717
			active = this.active;
718
		}
719
720
		anchor = active.find( ".ui-tabs-anchor" )[ 0 ];
721
		this._eventHandler( {
722
			target: anchor,
723
			currentTarget: anchor,
724
			preventDefault: $.noop
725
		} );
726
	},
727
728
	_findActive: function( index ) {
729
		return index === false ? $() : this.tabs.eq( index );
730
	},
731
732
	_getIndex: function( index ) {
733
734
		// meta-function to give users option to provide a href string instead of a numerical index.
735
		if ( typeof index === "string" ) {
736
			index = this.anchors.index( this.anchors.filter( "[href$='" +
737
				$.ui.escapeSelector( index ) + "']" ) );
738
		}
739
740
		return index;
741
	},
742
743
	_destroy: function() {
744
		if ( this.xhr ) {
745
			this.xhr.abort();
746
		}
747
748
		this.tablist
749
			.removeAttr( "role" )
750
			.off( this.eventNamespace );
751
752
		this.anchors
753
			.removeAttr( "role tabIndex" )
754
			.removeUniqueId();
755
756
		this.tabs.add( this.panels ).each( function() {
757
			if ( $.data( this, "ui-tabs-destroy" ) ) {
758
				$( this ).remove();
759
			} else {
760
				$( this ).removeAttr( "role tabIndex " +
761
					"aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" );
762
			}
763
		} );
764
765
		this.tabs.each( function() {
766
			var li = $( this ),
767
				prev = li.data( "ui-tabs-aria-controls" );
768
			if ( prev ) {
769
				li
770
					.attr( "aria-controls", prev )
771
					.removeData( "ui-tabs-aria-controls" );
772
			} else {
773
				li.removeAttr( "aria-controls" );
774
			}
775
		} );
776
777
		this.panels.show();
778
779
		if ( this.options.heightStyle !== "content" ) {
780
			this.panels.css( "height", "" );
781
		}
782
	},
783
784
	enable: function( index ) {
785
		var disabled = this.options.disabled;
786
		if ( disabled === false ) {
787
			return;
788
		}
789
790
		if ( index === undefined ) {
791
			disabled = false;
792
		} else {
793
			index = this._getIndex( index );
794
			if ( $.isArray( disabled ) ) {
795
				disabled = $.map( disabled, function( num ) {
796
					return num !== index ? num : null;
797
				} );
798
			} else {
799
				disabled = $.map( this.tabs, function( li, num ) {
800
					return num !== index ? num : null;
801
				} );
802
			}
803
		}
804
		this._setOptionDisabled( disabled );
805
	},
806
807
	disable: function( index ) {
808
		var disabled = this.options.disabled;
809
		if ( disabled === true ) {
810
			return;
811
		}
812
813
		if ( index === undefined ) {
814
			disabled = true;
815
		} else {
816
			index = this._getIndex( index );
817
			if ( $.inArray( index, disabled ) !== -1 ) {
818
				return;
819
			}
820
			if ( $.isArray( disabled ) ) {
821
				disabled = $.merge( [ index ], disabled ).sort();
822
			} else {
823
				disabled = [ index ];
824
			}
825
		}
826
		this._setOptionDisabled( disabled );
827
	},
828
829
	load: function( index, event ) {
830
		index = this._getIndex( index );
831
		var that = this,
832
			tab = this.tabs.eq( index ),
833
			anchor = tab.find( ".ui-tabs-anchor" ),
834
			panel = this._getPanelForTab( tab ),
835
			eventData = {
836
				tab: tab,
837
				panel: panel
838
			},
839
			complete = function( jqXHR, status ) {
840
				if ( status === "abort" ) {
841
					that.panels.stop( false, true );
842
				}
843
844
				that._removeClass( tab, "ui-tabs-loading" );
845
				panel.removeAttr( "aria-busy" );
846
847
				if ( jqXHR === that.xhr ) {
848
					delete that.xhr;
849
				}
850
			};
851
852
		// Not remote
853
		if ( this._isLocal( anchor[ 0 ] ) ) {
854
			return;
855
		}
856
857
		this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) );
858
859
		// Support: jQuery <1.8
860
		// jQuery <1.8 returns false if the request is canceled in beforeSend,
861
		// but as of 1.8, $.ajax() always returns a jqXHR object.
862
		if ( this.xhr && this.xhr.statusText !== "canceled" ) {
863
			this._addClass( tab, "ui-tabs-loading" );
864
			panel.attr( "aria-busy", "true" );
865
866
			this.xhr
867
				.done( function( response, status, jqXHR ) {
868
869
					// support: jQuery <1.8
870
					// http://bugs.jquery.com/ticket/11778
871
					setTimeout( function() {
872
						panel.html( response );
873
						that._trigger( "load", event, eventData );
874
875
						complete( jqXHR, status );
876
					}, 1 );
877
				} )
878
				.fail( function( jqXHR, status ) {
879
880
					// support: jQuery <1.8
881
					// http://bugs.jquery.com/ticket/11778
882
					setTimeout( function() {
883
						complete( jqXHR, status );
884
					}, 1 );
885
				} );
886
		}
887
	},
888
889
	_ajaxSettings: function( anchor, event, eventData ) {
890
		var that = this;
891
		return {
892
893
			// Support: IE <11 only
894
			// Strip any hash that exists to prevent errors with the Ajax request
895
			url: anchor.attr( "href" ).replace( /#.*$/, "" ),
896
			beforeSend: function( jqXHR, settings ) {
897
				return that._trigger( "beforeLoad", event,
898
					$.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) );
899
			}
900
		};
901
	},
902
903
	_getPanelForTab: function( tab ) {
904
		var id = $( tab ).attr( "aria-controls" );
905
		return this.element.find( this._sanitizeSelector( "#" + id ) );
906
	}
907
} );
908
909
// DEPRECATED
910
// TODO: Switch return back to widget declaration at top of file when this is removed
911
if ( $.uiBackCompat !== false ) {
912
913
	// Backcompat for ui-tab class (now ui-tabs-tab)
914
	$.widget( "ui.tabs", $.ui.tabs, {
915
		_processTabs: function() {
916
			this._superApply( arguments );
917
			this._addClass( this.tabs, "ui-tab" );
918
		}
919
	} );
920
}
921
922
return $.ui.tabs;
923
924
} ) );
925