jquery.ui.tabs.js 34KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367
  1. /*!
  2. * jQuery UI Tabs v1.9 stable
  3. * http://jqueryui.com
  4. *
  5. * Copyright 2012 jQuery Foundation and other contributors
  6. * Released under the MIT license.
  7. * http://jquery.org/license
  8. *
  9. * http://api.jqueryui.com/tabs/
  10. *
  11. * Depends:
  12. * jquery.ui.core.js
  13. * jquery.ui.widget.js
  14. */
  15. (function( $, undefined ) {
  16. var tabId = 0,
  17. rhash = /#.*$/;
  18. function getNextTabId() {
  19. return ++tabId;
  20. }
  21. function isLocal( anchor ) {
  22. return anchor.hash.length > 1 &&
  23. anchor.href.replace( rhash, "" ) ===
  24. location.href.replace( rhash, "" )
  25. // support: Safari 5.1
  26. // Safari 5.1 doesn't encode spaces in window.location
  27. // but it does encode spaces from anchors (#8777)
  28. .replace( /\s/g, "%20" );
  29. }
  30. $.widget( "ui.tabs", {
  31. version: "@VERSION",
  32. delay: 300,
  33. options: {
  34. active: null,
  35. collapsible: false,
  36. event: "click",
  37. heightStyle: "content",
  38. hide: null,
  39. show: null,
  40. // callbacks
  41. activate: null,
  42. beforeActivate: null,
  43. beforeLoad: null,
  44. load: null
  45. },
  46. _create: function() {
  47. var that = this,
  48. options = this.options,
  49. active = options.active,
  50. locationHash = location.hash.substring( 1 );
  51. this.running = false;
  52. this.element
  53. .addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" )
  54. .toggleClass( "ui-tabs-collapsible", options.collapsible )
  55. // Prevent users from focusing disabled tabs via click
  56. .delegate( ".ui-tabs-nav > li", "mousedown" + this.eventNamespace, function( event ) {
  57. if ( $( this ).is( ".ui-state-disabled" ) ) {
  58. event.preventDefault();
  59. }
  60. })
  61. // support: IE <9
  62. // Preventing the default action in mousedown doesn't prevent IE
  63. // from focusing the element, so if the anchor gets focused, blur.
  64. // We don't have to worry about focusing the previously focused
  65. // element since clicking on a non-focusable element should focus
  66. // the body anyway.
  67. .delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace, function() {
  68. if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
  69. this.blur();
  70. }
  71. });
  72. this._processTabs();
  73. if ( active === null ) {
  74. // check the fragment identifier in the URL
  75. if ( locationHash ) {
  76. this.tabs.each(function( i, tab ) {
  77. if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
  78. active = i;
  79. return false;
  80. }
  81. });
  82. }
  83. // check for a tab marked active via a class
  84. if ( active === null ) {
  85. active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) );
  86. }
  87. // no active tab, set to false
  88. if ( active === null || active === -1 ) {
  89. active = this.tabs.length ? 0 : false;
  90. }
  91. }
  92. // handle numbers: negative, out of range
  93. if ( active !== false ) {
  94. active = this.tabs.index( this.tabs.eq( active ) );
  95. if ( active === -1 ) {
  96. active = options.collapsible ? false : 0;
  97. }
  98. }
  99. options.active = active;
  100. // don't allow collapsible: false and active: false
  101. if ( !options.collapsible && options.active === false && this.anchors.length ) {
  102. options.active = 0;
  103. }
  104. // Take disabling tabs via class attribute from HTML
  105. // into account and update option properly.
  106. if ( $.isArray( options.disabled ) ) {
  107. options.disabled = $.unique( options.disabled.concat(
  108. $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) {
  109. return that.tabs.index( li );
  110. })
  111. ) ).sort();
  112. }
  113. // check for length avoids error when initializing empty list
  114. if ( this.options.active !== false && this.anchors.length ) {
  115. this.active = this._findActive( this.options.active );
  116. } else {
  117. this.active = $();
  118. }
  119. this._refresh();
  120. if ( this.active.length ) {
  121. this.load( options.active );
  122. }
  123. },
  124. _getCreateEventData: function() {
  125. return {
  126. tab: this.active,
  127. panel: !this.active.length ? $() : this._getPanelForTab( this.active )
  128. };
  129. },
  130. _tabKeydown: function( event ) {
  131. var focusedTab = $( this.document[0].activeElement ).closest( "li" ),
  132. selectedIndex = this.tabs.index( focusedTab ),
  133. goingForward = true;
  134. if ( this._handlePageNav( event ) ) {
  135. return;
  136. }
  137. switch ( event.keyCode ) {
  138. case $.ui.keyCode.RIGHT:
  139. case $.ui.keyCode.DOWN:
  140. selectedIndex++;
  141. break;
  142. case $.ui.keyCode.UP:
  143. case $.ui.keyCode.LEFT:
  144. goingForward = false;
  145. selectedIndex--;
  146. break;
  147. case $.ui.keyCode.END:
  148. selectedIndex = this.anchors.length - 1;
  149. break;
  150. case $.ui.keyCode.HOME:
  151. selectedIndex = 0;
  152. break;
  153. case $.ui.keyCode.SPACE:
  154. // Activate only, no collapsing
  155. event.preventDefault();
  156. clearTimeout( this.activating );
  157. this._activate( selectedIndex );
  158. return;
  159. case $.ui.keyCode.ENTER:
  160. // Toggle (cancel delayed activation, allow collapsing)
  161. event.preventDefault();
  162. clearTimeout( this.activating );
  163. // Determine if we should collapse or activate
  164. this._activate( selectedIndex === this.options.active ? false : selectedIndex );
  165. return;
  166. default:
  167. return;
  168. }
  169. // Focus the appropriate tab, based on which key was pressed
  170. event.preventDefault();
  171. clearTimeout( this.activating );
  172. selectedIndex = this._focusNextTab( selectedIndex, goingForward );
  173. // Navigating with control key will prevent automatic activation
  174. if ( !event.ctrlKey ) {
  175. // Update aria-selected immediately so that AT think the tab is already selected.
  176. // Otherwise AT may confuse the user by stating that they need to activate the tab,
  177. // but the tab will already be activated by the time the announcement finishes.
  178. focusedTab.attr( "aria-selected", "false" );
  179. this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
  180. this.activating = this._delay(function() {
  181. this.option( "active", selectedIndex );
  182. }, this.delay );
  183. }
  184. },
  185. _panelKeydown: function( event ) {
  186. if ( this._handlePageNav( event ) ) {
  187. return;
  188. }
  189. // Ctrl+up moves focus to the current tab
  190. if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
  191. event.preventDefault();
  192. this.active.focus();
  193. }
  194. },
  195. // Alt+page up/down moves focus to the previous/next tab (and activates)
  196. _handlePageNav: function( event ) {
  197. if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
  198. this._activate( this._focusNextTab( this.options.active - 1, false ) );
  199. return true;
  200. }
  201. if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
  202. this._activate( this._focusNextTab( this.options.active + 1, true ) );
  203. return true;
  204. }
  205. },
  206. _findNextTab: function( index, goingForward ) {
  207. var lastTabIndex = this.tabs.length - 1;
  208. function constrain() {
  209. if ( index > lastTabIndex ) {
  210. index = 0;
  211. }
  212. if ( index < 0 ) {
  213. index = lastTabIndex;
  214. }
  215. return index;
  216. }
  217. while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
  218. index = goingForward ? index + 1 : index - 1;
  219. }
  220. return index;
  221. },
  222. _focusNextTab: function( index, goingForward ) {
  223. index = this._findNextTab( index, goingForward );
  224. this.tabs.eq( index ).focus();
  225. return index;
  226. },
  227. _setOption: function( key, value ) {
  228. if ( key === "active" ) {
  229. // _activate() will handle invalid values and update this.options
  230. this._activate( value );
  231. return;
  232. }
  233. if ( key === "disabled" ) {
  234. // don't use the widget factory's disabled handling
  235. this._setupDisabled( value );
  236. return;
  237. }
  238. this._super( key, value);
  239. if ( key === "collapsible" ) {
  240. this.element.toggleClass( "ui-tabs-collapsible", value );
  241. // Setting collapsible: false while collapsed; open first panel
  242. if ( !value && this.options.active === false ) {
  243. this._activate( 0 );
  244. }
  245. }
  246. if ( key === "event" ) {
  247. this._setupEvents( value );
  248. }
  249. if ( key === "heightStyle" ) {
  250. this._setupHeightStyle( value );
  251. }
  252. },
  253. _tabId: function( tab ) {
  254. return tab.attr( "aria-controls" ) || "ui-tabs-" + getNextTabId();
  255. },
  256. _sanitizeSelector: function( hash ) {
  257. return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
  258. },
  259. refresh: function() {
  260. var options = this.options,
  261. lis = this.tablist.children( ":has(a[href])" );
  262. // get disabled tabs from class attribute from HTML
  263. // this will get converted to a boolean if needed in _refresh()
  264. options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) {
  265. return lis.index( tab );
  266. });
  267. this._processTabs();
  268. // was collapsed or no tabs
  269. if ( options.active === false || !this.anchors.length ) {
  270. options.active = false;
  271. this.active = $();
  272. // was active, but active tab is gone
  273. } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) {
  274. // all remaining tabs are disabled
  275. if ( this.tabs.length === options.disabled.length ) {
  276. options.active = false;
  277. this.active = $();
  278. // activate previous tab
  279. } else {
  280. this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) );
  281. }
  282. // was active, active tab still exists
  283. } else {
  284. // make sure active index is correct
  285. options.active = this.tabs.index( this.active );
  286. }
  287. this._refresh();
  288. },
  289. _refresh: function() {
  290. this._setupDisabled( this.options.disabled );
  291. this._setupEvents( this.options.event );
  292. this._setupHeightStyle( this.options.heightStyle );
  293. this.tabs.not( this.active ).attr({
  294. "aria-selected": "false",
  295. tabIndex: -1
  296. });
  297. this.panels.not( this._getPanelForTab( this.active ) )
  298. .hide()
  299. .attr({
  300. "aria-expanded": "false",
  301. "aria-hidden": "true"
  302. });
  303. // Make sure one tab is in the tab order
  304. if ( !this.active.length ) {
  305. this.tabs.eq( 0 ).attr( "tabIndex", 0 );
  306. } else {
  307. this.active
  308. .addClass( "ui-tabs-active ui-state-active" )
  309. .attr({
  310. "aria-selected": "true",
  311. tabIndex: 0
  312. });
  313. this._getPanelForTab( this.active )
  314. .show()
  315. .attr({
  316. "aria-expanded": "true",
  317. "aria-hidden": "false"
  318. });
  319. }
  320. },
  321. _processTabs: function() {
  322. var that = this;
  323. this.tablist = this._getList()
  324. .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
  325. .attr( "role", "tablist" );
  326. this.tabs = this.tablist.find( "> li:has(a[href])" )
  327. .addClass( "ui-state-default ui-corner-top" )
  328. .attr({
  329. role: "tab",
  330. tabIndex: -1
  331. });
  332. this.anchors = this.tabs.map(function() {
  333. return $( "a", this )[ 0 ];
  334. })
  335. .addClass( "ui-tabs-anchor" )
  336. .attr({
  337. role: "presentation",
  338. tabIndex: -1
  339. });
  340. this.panels = $();
  341. this.anchors.each(function( i, anchor ) {
  342. var selector, panel, panelId,
  343. anchorId = $( anchor ).uniqueId().attr( "id" ),
  344. tab = $( anchor ).closest( "li" ),
  345. originalAriaControls = tab.attr( "aria-controls" );
  346. // inline tab
  347. if ( isLocal( anchor ) ) {
  348. selector = anchor.hash;
  349. panel = that.element.find( that._sanitizeSelector( selector ) );
  350. // remote tab
  351. } else {
  352. panelId = that._tabId( tab );
  353. selector = "#" + panelId;
  354. panel = that.element.find( selector );
  355. if ( !panel.length ) {
  356. panel = that._createPanel( panelId );
  357. panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
  358. }
  359. panel.attr( "aria-live", "polite" );
  360. }
  361. if ( panel.length) {
  362. that.panels = that.panels.add( panel );
  363. }
  364. if ( originalAriaControls ) {
  365. tab.data( "ui-tabs-aria-controls", originalAriaControls );
  366. }
  367. tab.attr({
  368. "aria-controls": selector.substring( 1 ),
  369. "aria-labelledby": anchorId
  370. });
  371. panel.attr( "aria-labelledby", anchorId );
  372. });
  373. this.panels
  374. .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
  375. .attr( "role", "tabpanel" );
  376. },
  377. // allow overriding how to find the list for rare usage scenarios (#7715)
  378. _getList: function() {
  379. return this.element.find( "ol,ul" ).eq( 0 );
  380. },
  381. _createPanel: function( id ) {
  382. return $( "<div>" )
  383. .attr( "id", id )
  384. .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
  385. .data( "ui-tabs-destroy", true );
  386. },
  387. _setupDisabled: function( disabled ) {
  388. if ( $.isArray( disabled ) ) {
  389. if ( !disabled.length ) {
  390. disabled = false;
  391. } else if ( disabled.length === this.anchors.length ) {
  392. disabled = true;
  393. }
  394. }
  395. // disable tabs
  396. for ( var i = 0, li; ( li = this.tabs[ i ] ); i++ ) {
  397. if ( disabled === true || $.inArray( i, disabled ) !== -1 ) {
  398. $( li )
  399. .addClass( "ui-state-disabled" )
  400. .attr( "aria-disabled", "true" );
  401. } else {
  402. $( li )
  403. .removeClass( "ui-state-disabled" )
  404. .removeAttr( "aria-disabled" );
  405. }
  406. }
  407. this.options.disabled = disabled;
  408. },
  409. _setupEvents: function( event ) {
  410. var events = {
  411. click: function( event ) {
  412. event.preventDefault();
  413. }
  414. };
  415. if ( event ) {
  416. $.each( event.split(" "), function( index, eventName ) {
  417. events[ eventName ] = "_eventHandler";
  418. });
  419. }
  420. this._off( this.anchors.add( this.tabs ).add( this.panels ) );
  421. this._on( this.anchors, events );
  422. this._on( this.tabs, { keydown: "_tabKeydown" } );
  423. this._on( this.panels, { keydown: "_panelKeydown" } );
  424. this._focusable( this.tabs );
  425. this._hoverable( this.tabs );
  426. },
  427. _setupHeightStyle: function( heightStyle ) {
  428. var maxHeight, overflow,
  429. parent = this.element.parent();
  430. if ( heightStyle === "fill" ) {
  431. // IE 6 treats height like minHeight, so we need to turn off overflow
  432. // in order to get a reliable height
  433. // we use the minHeight support test because we assume that only
  434. // browsers that don't support minHeight will treat height as minHeight
  435. if ( !$.support.minHeight ) {
  436. overflow = parent.css( "overflow" );
  437. parent.css( "overflow", "hidden");
  438. }
  439. maxHeight = parent.height();
  440. this.element.siblings( ":visible" ).each(function() {
  441. var elem = $( this ),
  442. position = elem.css( "position" );
  443. if ( position === "absolute" || position === "fixed" ) {
  444. return;
  445. }
  446. maxHeight -= elem.outerHeight( true );
  447. });
  448. if ( overflow ) {
  449. parent.css( "overflow", overflow );
  450. }
  451. this.element.children().not( this.panels ).each(function() {
  452. maxHeight -= $( this ).outerHeight( true );
  453. });
  454. this.panels.each(function() {
  455. $( this ).height( Math.max( 0, maxHeight -
  456. $( this ).innerHeight() + $( this ).height() ) );
  457. })
  458. .css( "overflow", "auto" );
  459. } else if ( heightStyle === "auto" ) {
  460. maxHeight = 0;
  461. this.panels.each(function() {
  462. maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
  463. }).height( maxHeight );
  464. }
  465. },
  466. _eventHandler: function( event ) {
  467. var options = this.options,
  468. active = this.active,
  469. anchor = $( event.currentTarget ),
  470. tab = anchor.closest( "li" ),
  471. clickedIsActive = tab[ 0 ] === active[ 0 ],
  472. collapsing = clickedIsActive && options.collapsible,
  473. toShow = collapsing ? $() : this._getPanelForTab( tab ),
  474. toHide = !active.length ? $() : this._getPanelForTab( active ),
  475. eventData = {
  476. oldTab: active,
  477. oldPanel: toHide,
  478. newTab: collapsing ? $() : tab,
  479. newPanel: toShow
  480. };
  481. event.preventDefault();
  482. if ( tab.hasClass( "ui-state-disabled" ) ||
  483. // tab is already loading
  484. tab.hasClass( "ui-tabs-loading" ) ||
  485. // can't switch durning an animation
  486. this.running ||
  487. // click on active header, but not collapsible
  488. ( clickedIsActive && !options.collapsible ) ||
  489. // allow canceling activation
  490. ( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
  491. return;
  492. }
  493. options.active = collapsing ? false : this.tabs.index( tab );
  494. this.active = clickedIsActive ? $() : tab;
  495. if ( this.xhr ) {
  496. this.xhr.abort();
  497. }
  498. if ( !toHide.length && !toShow.length ) {
  499. $.error( "jQuery UI Tabs: Mismatching fragment identifier." );
  500. }
  501. if ( toShow.length ) {
  502. this.load( this.tabs.index( tab ), event );
  503. }
  504. this._toggle( event, eventData );
  505. },
  506. // handles show/hide for selecting tabs
  507. _toggle: function( event, eventData ) {
  508. var that = this,
  509. toShow = eventData.newPanel,
  510. toHide = eventData.oldPanel;
  511. this.running = true;
  512. function complete() {
  513. that.running = false;
  514. that._trigger( "activate", event, eventData );
  515. }
  516. function show() {
  517. eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" );
  518. if ( toShow.length && that.options.show ) {
  519. that._show( toShow, that.options.show, complete );
  520. } else {
  521. toShow.show();
  522. complete();
  523. }
  524. }
  525. // start out by hiding, then showing, then completing
  526. if ( toHide.length && this.options.hide ) {
  527. this._hide( toHide, this.options.hide, function() {
  528. eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
  529. show();
  530. });
  531. } else {
  532. eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
  533. toHide.hide();
  534. show();
  535. }
  536. toHide.attr({
  537. "aria-expanded": "false",
  538. "aria-hidden": "true"
  539. });
  540. eventData.oldTab.attr( "aria-selected", "false" );
  541. // If we're switching tabs, remove the old tab from the tab order.
  542. // If we're opening from collapsed state, remove the previous tab from the tab order.
  543. // If we're collapsing, then keep the collapsing tab in the tab order.
  544. if ( toShow.length && toHide.length ) {
  545. eventData.oldTab.attr( "tabIndex", -1 );
  546. } else if ( toShow.length ) {
  547. this.tabs.filter(function() {
  548. return $( this ).attr( "tabIndex" ) === 0;
  549. })
  550. .attr( "tabIndex", -1 );
  551. }
  552. toShow.attr({
  553. "aria-expanded": "true",
  554. "aria-hidden": "false"
  555. });
  556. eventData.newTab.attr({
  557. "aria-selected": "true",
  558. tabIndex: 0
  559. });
  560. },
  561. _activate: function( index ) {
  562. var anchor,
  563. active = this._findActive( index );
  564. // trying to activate the already active panel
  565. if ( active[ 0 ] === this.active[ 0 ] ) {
  566. return;
  567. }
  568. // trying to collapse, simulate a click on the current active header
  569. if ( !active.length ) {
  570. active = this.active;
  571. }
  572. anchor = active.find( ".ui-tabs-anchor" )[ 0 ];
  573. this._eventHandler({
  574. target: anchor,
  575. currentTarget: anchor,
  576. preventDefault: $.noop
  577. });
  578. },
  579. _findActive: function( index ) {
  580. return index === false ? $() : this.tabs.eq( index );
  581. },
  582. _getIndex: function( index ) {
  583. // meta-function to give users option to provide a href string instead of a numerical index.
  584. if ( typeof index === "string" ) {
  585. index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) );
  586. }
  587. return index;
  588. },
  589. _destroy: function() {
  590. if ( this.xhr ) {
  591. this.xhr.abort();
  592. }
  593. this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" );
  594. this.tablist
  595. .removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
  596. .removeAttr( "role" );
  597. this.anchors
  598. .removeClass( "ui-tabs-anchor" )
  599. .removeAttr( "role" )
  600. .removeAttr( "tabIndex" )
  601. .removeData( "href.tabs" )
  602. .removeData( "load.tabs" )
  603. .removeUniqueId();
  604. this.tabs.add( this.panels ).each(function() {
  605. if ( $.data( this, "ui-tabs-destroy" ) ) {
  606. $( this ).remove();
  607. } else {
  608. $( this )
  609. .removeClass( "ui-state-default ui-state-active ui-state-disabled " +
  610. "ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel" )
  611. .removeAttr( "tabIndex" )
  612. .removeAttr( "aria-live" )
  613. .removeAttr( "aria-busy" )
  614. .removeAttr( "aria-selected" )
  615. .removeAttr( "aria-labelledby" )
  616. .removeAttr( "aria-hidden" )
  617. .removeAttr( "aria-expanded" )
  618. .removeAttr( "role" );
  619. }
  620. });
  621. this.tabs.each(function() {
  622. var li = $( this ),
  623. prev = li.data( "ui-tabs-aria-controls" );
  624. if ( prev ) {
  625. li.attr( "aria-controls", prev );
  626. } else {
  627. li.removeAttr( "aria-controls" );
  628. }
  629. });
  630. this.panels.show();
  631. if ( this.options.heightStyle !== "content" ) {
  632. this.panels.css( "height", "" );
  633. }
  634. },
  635. enable: function( index ) {
  636. var disabled = this.options.disabled;
  637. if ( disabled === false ) {
  638. return;
  639. }
  640. if ( index === undefined ) {
  641. disabled = false;
  642. } else {
  643. index = this._getIndex( index );
  644. if ( $.isArray( disabled ) ) {
  645. disabled = $.map( disabled, function( num ) {
  646. return num !== index ? num : null;
  647. });
  648. } else {
  649. disabled = $.map( this.tabs, function( li, num ) {
  650. return num !== index ? num : null;
  651. });
  652. }
  653. }
  654. this._setupDisabled( disabled );
  655. },
  656. disable: function( index ) {
  657. var disabled = this.options.disabled;
  658. if ( disabled === true ) {
  659. return;
  660. }
  661. if ( index === undefined ) {
  662. disabled = true;
  663. } else {
  664. index = this._getIndex( index );
  665. if ( $.inArray( index, disabled ) !== -1 ) {
  666. return;
  667. }
  668. if ( $.isArray( disabled ) ) {
  669. disabled = $.merge( [ index ], disabled ).sort();
  670. } else {
  671. disabled = [ index ];
  672. }
  673. }
  674. this._setupDisabled( disabled );
  675. },
  676. load: function( index, event ) {
  677. index = this._getIndex( index );
  678. var that = this,
  679. tab = this.tabs.eq( index ),
  680. anchor = tab.find( ".ui-tabs-anchor" ),
  681. panel = this._getPanelForTab( tab ),
  682. eventData = {
  683. tab: tab,
  684. panel: panel
  685. };
  686. // not remote
  687. if ( isLocal( anchor[ 0 ] ) ) {
  688. return;
  689. }
  690. this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) );
  691. // support: jQuery <1.8
  692. // jQuery <1.8 returns false if the request is canceled in beforeSend,
  693. // but as of 1.8, $.ajax() always returns a jqXHR object.
  694. if ( this.xhr && this.xhr.statusText !== "canceled" ) {
  695. tab.addClass( "ui-tabs-loading" );
  696. panel.attr( "aria-busy", "true" );
  697. this.xhr
  698. .success(function( response ) {
  699. // support: jQuery <1.8
  700. // http://bugs.jquery.com/ticket/11778
  701. setTimeout(function() {
  702. panel.html( response );
  703. that._trigger( "load", event, eventData );
  704. }, 1 );
  705. })
  706. .complete(function( jqXHR, status ) {
  707. // support: jQuery <1.8
  708. // http://bugs.jquery.com/ticket/11778
  709. setTimeout(function() {
  710. if ( status === "abort" ) {
  711. that.panels.stop( false, true );
  712. }
  713. tab.removeClass( "ui-tabs-loading" );
  714. panel.removeAttr( "aria-busy" );
  715. if ( jqXHR === that.xhr ) {
  716. delete that.xhr;
  717. }
  718. }, 1 );
  719. });
  720. }
  721. },
  722. // TODO: Remove this function in 1.10 when ajaxOptions is removed
  723. _ajaxSettings: function( anchor, event, eventData ) {
  724. var that = this;
  725. return {
  726. url: anchor.attr( "href" ),
  727. beforeSend: function( jqXHR, settings ) {
  728. return that._trigger( "beforeLoad", event,
  729. $.extend( { jqXHR : jqXHR, ajaxSettings: settings }, eventData ) );
  730. }
  731. };
  732. },
  733. _getPanelForTab: function( tab ) {
  734. var id = $( tab ).attr( "aria-controls" );
  735. return this.element.find( this._sanitizeSelector( "#" + id ) );
  736. }
  737. });
  738. // DEPRECATED
  739. if ( $.uiBackCompat !== false ) {
  740. // helper method for a lot of the back compat extensions
  741. $.ui.tabs.prototype._ui = function( tab, panel ) {
  742. return {
  743. tab: tab,
  744. panel: panel,
  745. index: this.anchors.index( tab )
  746. };
  747. };
  748. // url method
  749. $.widget( "ui.tabs", $.ui.tabs, {
  750. url: function( index, url ) {
  751. this.anchors.eq( index ).attr( "href", url );
  752. }
  753. });
  754. // TODO: Remove _ajaxSettings() method when removing this extension
  755. // ajaxOptions and cache options
  756. $.widget( "ui.tabs", $.ui.tabs, {
  757. options: {
  758. ajaxOptions: null,
  759. cache: false
  760. },
  761. _create: function() {
  762. this._super();
  763. var that = this;
  764. this._on({ tabsbeforeload: function( event, ui ) {
  765. // tab is already cached
  766. if ( $.data( ui.tab[ 0 ], "cache.tabs" ) ) {
  767. event.preventDefault();
  768. return;
  769. }
  770. ui.jqXHR.success(function() {
  771. if ( that.options.cache ) {
  772. $.data( ui.tab[ 0 ], "cache.tabs", true );
  773. }
  774. });
  775. }});
  776. },
  777. _ajaxSettings: function( anchor, event, ui ) {
  778. var ajaxOptions = this.options.ajaxOptions;
  779. return $.extend( {}, ajaxOptions, {
  780. error: function( xhr, status ) {
  781. try {
  782. // Passing index avoid a race condition when this method is
  783. // called after the user has selected another tab.
  784. // Pass the anchor that initiated this request allows
  785. // loadError to manipulate the tab content panel via $(a.hash)
  786. ajaxOptions.error(
  787. xhr, status, ui.tab.closest( "li" ).index(), ui.tab[ 0 ] );
  788. }
  789. catch ( error ) {}
  790. }
  791. }, this._superApply( arguments ) );
  792. },
  793. _setOption: function( key, value ) {
  794. // reset cache if switching from cached to not cached
  795. if ( key === "cache" && value === false ) {
  796. this.anchors.removeData( "cache.tabs" );
  797. }
  798. this._super( key, value );
  799. },
  800. _destroy: function() {
  801. this.anchors.removeData( "cache.tabs" );
  802. this._super();
  803. },
  804. url: function( index ){
  805. this.anchors.eq( index ).removeData( "cache.tabs" );
  806. this._superApply( arguments );
  807. }
  808. });
  809. // abort method
  810. $.widget( "ui.tabs", $.ui.tabs, {
  811. abort: function() {
  812. if ( this.xhr ) {
  813. this.xhr.abort();
  814. }
  815. }
  816. });
  817. // spinner
  818. $.widget( "ui.tabs", $.ui.tabs, {
  819. options: {
  820. spinner: "<em>Loading&#8230;</em>"
  821. },
  822. _create: function() {
  823. this._super();
  824. this._on({
  825. tabsbeforeload: function( event, ui ) {
  826. // Don't react to nested tabs or tabs that don't use a spinner
  827. if ( event.target !== this.element[ 0 ] ||
  828. !this.options.spinner ) {
  829. return;
  830. }
  831. var span = ui.tab.find( "span" ),
  832. html = span.html();
  833. span.html( this.options.spinner );
  834. ui.jqXHR.complete(function() {
  835. span.html( html );
  836. });
  837. }
  838. });
  839. }
  840. });
  841. // enable/disable events
  842. $.widget( "ui.tabs", $.ui.tabs, {
  843. options: {
  844. enable: null,
  845. disable: null
  846. },
  847. enable: function( index ) {
  848. var options = this.options,
  849. trigger;
  850. if ( index && options.disabled === true ||
  851. ( $.isArray( options.disabled ) && $.inArray( index, options.disabled ) !== -1 ) ) {
  852. trigger = true;
  853. }
  854. this._superApply( arguments );
  855. if ( trigger ) {
  856. this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
  857. }
  858. },
  859. disable: function( index ) {
  860. var options = this.options,
  861. trigger;
  862. if ( index && options.disabled === false ||
  863. ( $.isArray( options.disabled ) && $.inArray( index, options.disabled ) === -1 ) ) {
  864. trigger = true;
  865. }
  866. this._superApply( arguments );
  867. if ( trigger ) {
  868. this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
  869. }
  870. }
  871. });
  872. // add/remove methods and events
  873. $.widget( "ui.tabs", $.ui.tabs, {
  874. options: {
  875. add: null,
  876. remove: null,
  877. tabTemplate: "<li><a href='#{href}'><span>#{label}</span></a></li>"
  878. },
  879. add: function( url, label, index ) {
  880. if ( index === undefined ) {
  881. index = this.anchors.length;
  882. }
  883. var doInsertAfter, panel,
  884. options = this.options,
  885. li = $( options.tabTemplate
  886. .replace( /#\{href\}/g, url )
  887. .replace( /#\{label\}/g, label ) ),
  888. id = !url.indexOf( "#" ) ?
  889. url.replace( "#", "" ) :
  890. this._tabId( li );
  891. li.addClass( "ui-state-default ui-corner-top" ).data( "ui-tabs-destroy", true );
  892. li.attr( "aria-controls", id );
  893. doInsertAfter = index >= this.tabs.length;
  894. // try to find an existing element before creating a new one
  895. panel = this.element.find( "#" + id );
  896. if ( !panel.length ) {
  897. panel = this._createPanel( id );
  898. if ( doInsertAfter ) {
  899. if ( index > 0 ) {
  900. panel.insertAfter( this.panels.eq( -1 ) );
  901. } else {
  902. panel.appendTo( this.element );
  903. }
  904. } else {
  905. panel.insertBefore( this.panels[ index ] );
  906. }
  907. }
  908. panel.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ).hide();
  909. if ( doInsertAfter ) {
  910. li.appendTo( this.tablist );
  911. } else {
  912. li.insertBefore( this.tabs[ index ] );
  913. }
  914. options.disabled = $.map( options.disabled, function( n ) {
  915. return n >= index ? ++n : n;
  916. });
  917. this.refresh();
  918. if ( this.tabs.length === 1 && options.active === false ) {
  919. this.option( "active", 0 );
  920. }
  921. this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
  922. return this;
  923. },
  924. remove: function( index ) {
  925. index = this._getIndex( index );
  926. var options = this.options,
  927. tab = this.tabs.eq( index ).remove(),
  928. panel = this._getPanelForTab( tab ).remove();
  929. // If selected tab was removed focus tab to the right or
  930. // in case the last tab was removed the tab to the left.
  931. // We check for more than 2 tabs, because if there are only 2,
  932. // then when we remove this tab, there will only be one tab left
  933. // so we don't need to detect which tab to activate.
  934. if ( tab.hasClass( "ui-tabs-active" ) && this.anchors.length > 2 ) {
  935. this._activate( index + ( index + 1 < this.anchors.length ? 1 : -1 ) );
  936. }
  937. options.disabled = $.map(
  938. $.grep( options.disabled, function( n ) {
  939. return n !== index;
  940. }),
  941. function( n ) {
  942. return n >= index ? --n : n;
  943. });
  944. this.refresh();
  945. this._trigger( "remove", null, this._ui( tab.find( "a" )[ 0 ], panel[ 0 ] ) );
  946. return this;
  947. }
  948. });
  949. // length method
  950. $.widget( "ui.tabs", $.ui.tabs, {
  951. length: function() {
  952. return this.anchors.length;
  953. }
  954. });
  955. // panel ids (idPrefix option + title attribute)
  956. $.widget( "ui.tabs", $.ui.tabs, {
  957. options: {
  958. idPrefix: "ui-tabs-"
  959. },
  960. _tabId: function( tab ) {
  961. var a = tab.is( "li" ) ? tab.find( "a[href]" ) : tab;
  962. a = a[0];
  963. return $( a ).closest( "li" ).attr( "aria-controls" ) ||
  964. a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF\-]/g, "" ) ||
  965. this.options.idPrefix + getNextTabId();
  966. }
  967. });
  968. // _createPanel method
  969. $.widget( "ui.tabs", $.ui.tabs, {
  970. options: {
  971. panelTemplate: "<div></div>"
  972. },
  973. _createPanel: function( id ) {
  974. return $( this.options.panelTemplate )
  975. .attr( "id", id )
  976. .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
  977. .data( "ui-tabs-destroy", true );
  978. }
  979. });
  980. // selected option
  981. $.widget( "ui.tabs", $.ui.tabs, {
  982. _create: function() {
  983. var options = this.options;
  984. if ( options.active === null && options.selected !== undefined ) {
  985. options.active = options.selected === -1 ? false : options.selected;
  986. }
  987. this._super();
  988. options.selected = options.active;
  989. if ( options.selected === false ) {
  990. options.selected = -1;
  991. }
  992. },
  993. _setOption: function( key, value ) {
  994. if ( key !== "selected" ) {
  995. return this._super( key, value );
  996. }
  997. var options = this.options;
  998. this._super( "active", value === -1 ? false : value );
  999. options.selected = options.active;
  1000. if ( options.selected === false ) {
  1001. options.selected = -1;
  1002. }
  1003. },
  1004. _eventHandler: function() {
  1005. this._superApply( arguments );
  1006. this.options.selected = this.options.active;
  1007. if ( this.options.selected === false ) {
  1008. this.options.selected = -1;
  1009. }
  1010. }
  1011. });
  1012. // show and select event
  1013. $.widget( "ui.tabs", $.ui.tabs, {
  1014. options: {
  1015. show: null,
  1016. select: null
  1017. },
  1018. _create: function() {
  1019. this._super();
  1020. if ( this.options.active !== false ) {
  1021. this._trigger( "show", null, this._ui(
  1022. this.active.find( ".ui-tabs-anchor" )[ 0 ],
  1023. this._getPanelForTab( this.active )[ 0 ] ) );
  1024. }
  1025. },
  1026. _trigger: function( type, event, data ) {
  1027. var tab, panel,
  1028. ret = this._superApply( arguments );
  1029. if ( !ret ) {
  1030. return false;
  1031. }
  1032. if ( type === "beforeActivate" ) {
  1033. tab = data.newTab.length ? data.newTab : data.oldTab;
  1034. panel = data.newPanel.length ? data.newPanel : data.oldPanel;
  1035. ret = this._super( "select", event, {
  1036. tab: tab.find( ".ui-tabs-anchor" )[ 0],
  1037. panel: panel[ 0 ],
  1038. index: tab.closest( "li" ).index()
  1039. });
  1040. } else if ( type === "activate" && data.newTab.length ) {
  1041. ret = this._super( "show", event, {
  1042. tab: data.newTab.find( ".ui-tabs-anchor" )[ 0 ],
  1043. panel: data.newPanel[ 0 ],
  1044. index: data.newTab.closest( "li" ).index()
  1045. });
  1046. }
  1047. return ret;
  1048. }
  1049. });
  1050. // select method
  1051. $.widget( "ui.tabs", $.ui.tabs, {
  1052. select: function( index ) {
  1053. index = this._getIndex( index );
  1054. if ( index === -1 ) {
  1055. if ( this.options.collapsible && this.options.selected !== -1 ) {
  1056. index = this.options.selected;
  1057. } else {
  1058. return;
  1059. }
  1060. }
  1061. this.anchors.eq( index ).trigger( this.options.event + this.eventNamespace );
  1062. }
  1063. });
  1064. // cookie option
  1065. (function() {
  1066. var listId = 0;
  1067. $.widget( "ui.tabs", $.ui.tabs, {
  1068. options: {
  1069. cookie: null // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true }
  1070. },
  1071. _create: function() {
  1072. var options = this.options,
  1073. active;
  1074. if ( options.active == null && options.cookie ) {
  1075. active = parseInt( this._cookie(), 10 );
  1076. if ( active === -1 ) {
  1077. active = false;
  1078. }
  1079. options.active = active;
  1080. }
  1081. this._super();
  1082. },
  1083. _cookie: function( active ) {
  1084. var cookie = [ this.cookie ||
  1085. ( this.cookie = this.options.cookie.name || "ui-tabs-" + (++listId) ) ];
  1086. if ( arguments.length ) {
  1087. cookie.push( active === false ? -1 : active );
  1088. cookie.push( this.options.cookie );
  1089. }
  1090. return $.cookie.apply( null, cookie );
  1091. },
  1092. _refresh: function() {
  1093. this._super();
  1094. if ( this.options.cookie ) {
  1095. this._cookie( this.options.active, this.options.cookie );
  1096. }
  1097. },
  1098. _eventHandler: function() {
  1099. this._superApply( arguments );
  1100. if ( this.options.cookie ) {
  1101. this._cookie( this.options.active, this.options.cookie );
  1102. }
  1103. },
  1104. _destroy: function() {
  1105. this._super();
  1106. if ( this.options.cookie ) {
  1107. this._cookie( null, this.options.cookie );
  1108. }
  1109. }
  1110. });
  1111. })();
  1112. // load event
  1113. $.widget( "ui.tabs", $.ui.tabs, {
  1114. _trigger: function( type, event, data ) {
  1115. var _data = $.extend( {}, data );
  1116. if ( type === "load" ) {
  1117. _data.panel = _data.panel[ 0 ];
  1118. _data.tab = _data.tab.find( ".ui-tabs-anchor" )[ 0 ];
  1119. }
  1120. return this._super( type, event, _data );
  1121. }
  1122. });
  1123. // fx option
  1124. // The new animation options (show, hide) conflict with the old show callback.
  1125. // The old fx option wins over show/hide anyway (always favor back-compat).
  1126. // If a user wants to use the new animation API, they must give up the old API.
  1127. $.widget( "ui.tabs", $.ui.tabs, {
  1128. options: {
  1129. fx: null // e.g. { height: "toggle", opacity: "toggle", duration: 200 }
  1130. },
  1131. _getFx: function() {
  1132. var hide, show,
  1133. fx = this.options.fx;
  1134. if ( fx ) {
  1135. if ( $.isArray( fx ) ) {
  1136. hide = fx[ 0 ];
  1137. show = fx[ 1 ];
  1138. } else {
  1139. hide = show = fx;
  1140. }
  1141. }
  1142. return fx ? { show: show, hide: hide } : null;
  1143. },
  1144. _toggle: function( event, eventData ) {
  1145. var that = this,
  1146. toShow = eventData.newPanel,
  1147. toHide = eventData.oldPanel,
  1148. fx = this._getFx();
  1149. if ( !fx ) {
  1150. return this._super( event, eventData );
  1151. }
  1152. that.running = true;
  1153. function complete() {
  1154. that.running = false;
  1155. that._trigger( "activate", event, eventData );
  1156. }
  1157. function show() {
  1158. eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" );
  1159. if ( toShow.length && fx.show ) {
  1160. toShow
  1161. .animate( fx.show, fx.show.duration, function() {
  1162. complete();
  1163. });
  1164. } else {
  1165. toShow.show();
  1166. complete();
  1167. }
  1168. }
  1169. // start out by hiding, then showing, then completing
  1170. if ( toHide.length && fx.hide ) {
  1171. toHide.animate( fx.hide, fx.hide.duration, function() {
  1172. eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
  1173. show();
  1174. });
  1175. } else {
  1176. eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
  1177. toHide.hide();
  1178. show();
  1179. }
  1180. }
  1181. });
  1182. }
  1183. })( jQuery );