From 18486cb60cd0718e2f7b60bbd3976e95a2a94875 Mon Sep 17 00:00:00 2001 From: Alexander Schmitz Date: Tue, 14 Jul 2015 22:11:35 -0400 Subject: Tabs: Move tabs into widgets directory Ref #13885 --- ui/tabs.js | 902 ----------------------------------------------------- ui/widgets/tabs.js | 902 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 902 insertions(+), 902 deletions(-) delete mode 100644 ui/tabs.js create mode 100644 ui/widgets/tabs.js (limited to 'ui') diff --git a/ui/tabs.js b/ui/tabs.js deleted file mode 100644 index e2cfa3ae0..000000000 --- a/ui/tabs.js +++ /dev/null @@ -1,902 +0,0 @@ -/*! - * jQuery UI Tabs @VERSION - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Tabs -//>>group: Widgets -//>>description: Transforms a set of container elements into a tab structure. -//>>docs: http://api.jqueryui.com/tabs/ -//>>demos: http://jqueryui.com/tabs/ -//>>css.structure: ../themes/base/core.css -//>>css.structure: ../themes/base/tabs.css -//>>css.theme: ../themes/base/theme.css - -( function( factory ) { - if ( typeof define === "function" && define.amd ) { - - // AMD. Register as an anonymous module. - define( [ - "jquery", - "./escape-selector", - "./keycode", - "./safe-active-element", - "./unique-id", - "./version", - "./widget" - ], factory ); - } else { - - // Browser globals - factory( jQuery ); - } -}( function( $ ) { - -$.widget( "ui.tabs", { - version: "@VERSION", - delay: 300, - options: { - active: null, - classes: { - "ui-tabs": "ui-corner-all", - "ui-tabs-nav": "ui-corner-all", - "ui-tab": "ui-corner-top", - "ui-tabs-panel": "ui-corner-bottom" - }, - collapsible: false, - event: "click", - heightStyle: "content", - hide: null, - show: null, - - // callbacks - activate: null, - beforeActivate: null, - beforeLoad: null, - load: null - }, - - _isLocal: ( function() { - var rhash = /#.*$/; - - return function( anchor ) { - var anchorUrl, locationUrl; - - anchorUrl = anchor.href.replace( rhash, "" ); - locationUrl = location.href.replace( rhash, "" ); - - // decoding may throw an error if the URL isn't UTF-8 (#9518) - try { - anchorUrl = decodeURIComponent( anchorUrl ); - } catch ( error ) {} - try { - locationUrl = decodeURIComponent( locationUrl ); - } catch ( error ) {} - - return anchor.hash.length > 1 && anchorUrl === locationUrl; - }; - } )(), - - _create: function() { - var that = this, - options = this.options; - - this.running = false; - - this._addClass( "ui-tabs", "ui-widget ui-widget-content" ); - this._toggleClass( "ui-tabs-collapsible", null, options.collapsible ); - - this._processTabs(); - options.active = this._initialActive(); - - // Take disabling tabs via class attribute from HTML - // into account and update option properly. - if ( $.isArray( options.disabled ) ) { - options.disabled = $.unique( options.disabled.concat( - $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) { - return that.tabs.index( li ); - } ) - ) ).sort(); - } - - // check for length avoids error when initializing empty list - if ( this.options.active !== false && this.anchors.length ) { - this.active = this._findActive( options.active ); - } else { - this.active = $(); - } - - this._refresh(); - - if ( this.active.length ) { - this.load( options.active ); - } - }, - - _initialActive: function() { - var active = this.options.active, - collapsible = this.options.collapsible, - locationHash = location.hash.substring( 1 ); - - if ( active === null ) { - // check the fragment identifier in the URL - if ( locationHash ) { - this.tabs.each( function( i, tab ) { - if ( $( tab ).attr( "aria-controls" ) === locationHash ) { - active = i; - return false; - } - } ); - } - - // check for a tab marked active via a class - if ( active === null ) { - active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) ); - } - - // no active tab, set to false - if ( active === null || active === -1 ) { - active = this.tabs.length ? 0 : false; - } - } - - // handle numbers: negative, out of range - if ( active !== false ) { - active = this.tabs.index( this.tabs.eq( active ) ); - if ( active === -1 ) { - active = collapsible ? false : 0; - } - } - - // don't allow collapsible: false and active: false - if ( !collapsible && active === false && this.anchors.length ) { - active = 0; - } - - return active; - }, - - _getCreateEventData: function() { - return { - tab: this.active, - panel: !this.active.length ? $() : this._getPanelForTab( this.active ) - }; - }, - - _tabKeydown: function( event ) { - var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ), - selectedIndex = this.tabs.index( focusedTab ), - goingForward = true; - - if ( this._handlePageNav( event ) ) { - return; - } - - switch ( event.keyCode ) { - case $.ui.keyCode.RIGHT: - case $.ui.keyCode.DOWN: - selectedIndex++; - break; - case $.ui.keyCode.UP: - case $.ui.keyCode.LEFT: - goingForward = false; - selectedIndex--; - break; - case $.ui.keyCode.END: - selectedIndex = this.anchors.length - 1; - break; - case $.ui.keyCode.HOME: - selectedIndex = 0; - break; - case $.ui.keyCode.SPACE: - // Activate only, no collapsing - event.preventDefault(); - clearTimeout( this.activating ); - this._activate( selectedIndex ); - return; - case $.ui.keyCode.ENTER: - // Toggle (cancel delayed activation, allow collapsing) - event.preventDefault(); - clearTimeout( this.activating ); - // Determine if we should collapse or activate - this._activate( selectedIndex === this.options.active ? false : selectedIndex ); - return; - default: - return; - } - - // Focus the appropriate tab, based on which key was pressed - event.preventDefault(); - clearTimeout( this.activating ); - selectedIndex = this._focusNextTab( selectedIndex, goingForward ); - - // Navigating with control/command key will prevent automatic activation - if ( !event.ctrlKey && !event.metaKey ) { - - // Update aria-selected immediately so that AT think the tab is already selected. - // Otherwise AT may confuse the user by stating that they need to activate the tab, - // but the tab will already be activated by the time the announcement finishes. - focusedTab.attr( "aria-selected", "false" ); - this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" ); - - this.activating = this._delay( function() { - this.option( "active", selectedIndex ); - }, this.delay ); - } - }, - - _panelKeydown: function( event ) { - if ( this._handlePageNav( event ) ) { - return; - } - - // Ctrl+up moves focus to the current tab - if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { - event.preventDefault(); - this.active.trigger( "focus" ); - } - }, - - // Alt+page up/down moves focus to the previous/next tab (and activates) - _handlePageNav: function( event ) { - if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { - this._activate( this._focusNextTab( this.options.active - 1, false ) ); - return true; - } - if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { - this._activate( this._focusNextTab( this.options.active + 1, true ) ); - return true; - } - }, - - _findNextTab: function( index, goingForward ) { - var lastTabIndex = this.tabs.length - 1; - - function constrain() { - if ( index > lastTabIndex ) { - index = 0; - } - if ( index < 0 ) { - index = lastTabIndex; - } - return index; - } - - while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { - index = goingForward ? index + 1 : index - 1; - } - - return index; - }, - - _focusNextTab: function( index, goingForward ) { - index = this._findNextTab( index, goingForward ); - this.tabs.eq( index ).trigger( "focus" ); - return index; - }, - - _setOption: function( key, value ) { - if ( key === "active" ) { - // _activate() will handle invalid values and update this.options - this._activate( value ); - return; - } - - if ( key === "disabled" ) { - // don't use the widget factory's disabled handling - this._setupDisabled( value ); - return; - } - - this._super( key, value ); - - if ( key === "collapsible" ) { - this._toggleClass( "ui-tabs-collapsible", null, value ); - - // Setting collapsible: false while collapsed; open first panel - if ( !value && this.options.active === false ) { - this._activate( 0 ); - } - } - - if ( key === "event" ) { - this._setupEvents( value ); - } - - if ( key === "heightStyle" ) { - this._setupHeightStyle( value ); - } - }, - - _sanitizeSelector: function( hash ) { - return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; - }, - - refresh: function() { - var options = this.options, - lis = this.tablist.children( ":has(a[href])" ); - - // get disabled tabs from class attribute from HTML - // this will get converted to a boolean if needed in _refresh() - options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) { - return lis.index( tab ); - } ); - - this._processTabs(); - - // was collapsed or no tabs - if ( options.active === false || !this.anchors.length ) { - options.active = false; - this.active = $(); - // was active, but active tab is gone - } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) { - // all remaining tabs are disabled - if ( this.tabs.length === options.disabled.length ) { - options.active = false; - this.active = $(); - // activate previous tab - } else { - this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) ); - } - // was active, active tab still exists - } else { - // make sure active index is correct - options.active = this.tabs.index( this.active ); - } - - this._refresh(); - }, - - _refresh: function() { - this._setupDisabled( this.options.disabled ); - this._setupEvents( this.options.event ); - this._setupHeightStyle( this.options.heightStyle ); - - this.tabs.not( this.active ).attr( { - "aria-selected": "false", - "aria-expanded": "false", - tabIndex: -1 - } ); - this.panels.not( this._getPanelForTab( this.active ) ) - .hide() - .attr( { - "aria-hidden": "true" - } ); - - // Make sure one tab is in the tab order - if ( !this.active.length ) { - this.tabs.eq( 0 ).attr( "tabIndex", 0 ); - } else { - this.active - .attr( { - "aria-selected": "true", - "aria-expanded": "true", - tabIndex: 0 - } ); - this._addClass( this.active, "ui-tabs-active", "ui-state-active" ); - this._getPanelForTab( this.active ) - .show() - .attr( { - "aria-hidden": "false" - } ); - } - }, - - _processTabs: function() { - var that = this, - prevTabs = this.tabs, - prevAnchors = this.anchors, - prevPanels = this.panels; - - this.tablist = this._getList().attr( "role", "tablist" ); - this._addClass( this.tablist, "ui-tabs-nav", - "ui-helper-reset ui-helper-clearfix ui-widget-header" ); - - // Prevent users from focusing disabled tabs via click - this.tablist - .on( "mousedown" + this.eventNamespace, "> li", function( event ) { - if ( $( this ).is( ".ui-state-disabled" ) ) { - event.preventDefault(); - } - } ) - - // support: IE <9 - // Preventing the default action in mousedown doesn't prevent IE - // from focusing the element, so if the anchor gets focused, blur. - // We don't have to worry about focusing the previously focused - // element since clicking on a non-focusable element should focus - // the body anyway. - .on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() { - if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { - this.blur(); - } - } ); - - this.tabs = this.tablist.find( "> li:has(a[href])" ) - .attr( { - role: "tab", - tabIndex: -1 - } ); - this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" ); - - this.anchors = this.tabs.map( function() { - return $( "a", this )[ 0 ]; - } ) - .attr( { - role: "presentation", - tabIndex: -1 - } ); - this._addClass( this.anchors, "ui-tabs-anchor" ); - - this.panels = $(); - - this.anchors.each( function( i, anchor ) { - var selector, panel, panelId, - anchorId = $( anchor ).uniqueId().attr( "id" ), - tab = $( anchor ).closest( "li" ), - originalAriaControls = tab.attr( "aria-controls" ); - - // inline tab - if ( that._isLocal( anchor ) ) { - selector = anchor.hash; - panelId = selector.substring( 1 ); - panel = that.element.find( that._sanitizeSelector( selector ) ); - // remote tab - } else { - // If the tab doesn't already have aria-controls, - // generate an id by using a throw-away element - panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id; - selector = "#" + panelId; - panel = that.element.find( selector ); - if ( !panel.length ) { - panel = that._createPanel( panelId ); - panel.insertAfter( that.panels[ i - 1 ] || that.tablist ); - } - panel.attr( "aria-live", "polite" ); - } - - if ( panel.length ) { - that.panels = that.panels.add( panel ); - } - if ( originalAriaControls ) { - tab.data( "ui-tabs-aria-controls", originalAriaControls ); - } - tab.attr( { - "aria-controls": panelId, - "aria-labelledby": anchorId - } ); - panel.attr( "aria-labelledby", anchorId ); - } ); - - this.panels.attr( "role", "tabpanel" ); - this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" ); - - // Avoid memory leaks (#10056) - if ( prevTabs ) { - this._off( prevTabs.not( this.tabs ) ); - this._off( prevAnchors.not( this.anchors ) ); - this._off( prevPanels.not( this.panels ) ); - } - }, - - // allow overriding how to find the list for rare usage scenarios (#7715) - _getList: function() { - return this.tablist || this.element.find( "ol, ul" ).eq( 0 ); - }, - - _createPanel: function( id ) { - return $( "
" ) - .attr( "id", id ) - .data( "ui-tabs-destroy", true ); - }, - - _setupDisabled: function( disabled ) { - var currentItem, li, i; - - if ( $.isArray( disabled ) ) { - if ( !disabled.length ) { - disabled = false; - } else if ( disabled.length === this.anchors.length ) { - disabled = true; - } - } - - // disable tabs - for ( i = 0; ( li = this.tabs[ i ] ); i++ ) { - currentItem = $( li ); - if ( disabled === true || $.inArray( i, disabled ) !== -1 ) { - currentItem.attr( "aria-disabled", "true" ); - this._addClass( currentItem, null, "ui-state-disabled" ); - } else { - currentItem.removeAttr( "aria-disabled" ); - this._removeClass( currentItem, null, "ui-state-disabled" ); - } - } - - this.options.disabled = disabled; - }, - - _setupEvents: function( event ) { - var events = {}; - if ( event ) { - $.each( event.split( " " ), function( index, eventName ) { - events[ eventName ] = "_eventHandler"; - } ); - } - - this._off( this.anchors.add( this.tabs ).add( this.panels ) ); - // Always prevent the default action, even when disabled - this._on( true, this.anchors, { - click: function( event ) { - event.preventDefault(); - } - } ); - this._on( this.anchors, events ); - this._on( this.tabs, { keydown: "_tabKeydown" } ); - this._on( this.panels, { keydown: "_panelKeydown" } ); - - this._focusable( this.tabs ); - this._hoverable( this.tabs ); - }, - - _setupHeightStyle: function( heightStyle ) { - var maxHeight, - parent = this.element.parent(); - - if ( heightStyle === "fill" ) { - maxHeight = parent.height(); - maxHeight -= this.element.outerHeight() - this.element.height(); - - this.element.siblings( ":visible" ).each( function() { - var elem = $( this ), - position = elem.css( "position" ); - - if ( position === "absolute" || position === "fixed" ) { - return; - } - maxHeight -= elem.outerHeight( true ); - } ); - - this.element.children().not( this.panels ).each( function() { - maxHeight -= $( this ).outerHeight( true ); - } ); - - this.panels.each( function() { - $( this ).height( Math.max( 0, maxHeight - - $( this ).innerHeight() + $( this ).height() ) ); - } ) - .css( "overflow", "auto" ); - } else if ( heightStyle === "auto" ) { - maxHeight = 0; - this.panels.each( function() { - maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() ); - } ).height( maxHeight ); - } - }, - - _eventHandler: function( event ) { - var options = this.options, - active = this.active, - anchor = $( event.currentTarget ), - tab = anchor.closest( "li" ), - clickedIsActive = tab[ 0 ] === active[ 0 ], - collapsing = clickedIsActive && options.collapsible, - toShow = collapsing ? $() : this._getPanelForTab( tab ), - toHide = !active.length ? $() : this._getPanelForTab( active ), - eventData = { - oldTab: active, - oldPanel: toHide, - newTab: collapsing ? $() : tab, - newPanel: toShow - }; - - event.preventDefault(); - - if ( tab.hasClass( "ui-state-disabled" ) || - // tab is already loading - tab.hasClass( "ui-tabs-loading" ) || - // can't switch durning an animation - this.running || - // click on active header, but not collapsible - ( clickedIsActive && !options.collapsible ) || - // allow canceling activation - ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { - return; - } - - options.active = collapsing ? false : this.tabs.index( tab ); - - this.active = clickedIsActive ? $() : tab; - if ( this.xhr ) { - this.xhr.abort(); - } - - if ( !toHide.length && !toShow.length ) { - $.error( "jQuery UI Tabs: Mismatching fragment identifier." ); - } - - if ( toShow.length ) { - this.load( this.tabs.index( tab ), event ); - } - this._toggle( event, eventData ); - }, - - // handles show/hide for selecting tabs - _toggle: function( event, eventData ) { - var that = this, - toShow = eventData.newPanel, - toHide = eventData.oldPanel; - - this.running = true; - - function complete() { - that.running = false; - that._trigger( "activate", event, eventData ); - } - - function show() { - that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" ); - - if ( toShow.length && that.options.show ) { - that._show( toShow, that.options.show, complete ); - } else { - toShow.show(); - complete(); - } - } - - // start out by hiding, then showing, then completing - if ( toHide.length && this.options.hide ) { - this._hide( toHide, this.options.hide, function() { - that._removeClass( eventData.oldTab.closest( "li" ), - "ui-tabs-active", "ui-state-active" ); - show(); - } ); - } else { - this._removeClass( eventData.oldTab.closest( "li" ), - "ui-tabs-active", "ui-state-active" ); - toHide.hide(); - show(); - } - - toHide.attr( "aria-hidden", "true" ); - eventData.oldTab.attr( { - "aria-selected": "false", - "aria-expanded": "false" - } ); - // If we're switching tabs, remove the old tab from the tab order. - // If we're opening from collapsed state, remove the previous tab from the tab order. - // If we're collapsing, then keep the collapsing tab in the tab order. - if ( toShow.length && toHide.length ) { - eventData.oldTab.attr( "tabIndex", -1 ); - } else if ( toShow.length ) { - this.tabs.filter( function() { - return $( this ).attr( "tabIndex" ) === 0; - } ) - .attr( "tabIndex", -1 ); - } - - toShow.attr( "aria-hidden", "false" ); - eventData.newTab.attr( { - "aria-selected": "true", - "aria-expanded": "true", - tabIndex: 0 - } ); - }, - - _activate: function( index ) { - var anchor, - active = this._findActive( index ); - - // trying to activate the already active panel - if ( active[ 0 ] === this.active[ 0 ] ) { - return; - } - - // trying to collapse, simulate a click on the current active header - if ( !active.length ) { - active = this.active; - } - - anchor = active.find( ".ui-tabs-anchor" )[ 0 ]; - this._eventHandler( { - target: anchor, - currentTarget: anchor, - preventDefault: $.noop - } ); - }, - - _findActive: function( index ) { - return index === false ? $() : this.tabs.eq( index ); - }, - - _getIndex: function( index ) { - // meta-function to give users option to provide a href string instead of a numerical index. - if ( typeof index === "string" ) { - index = this.anchors.index( this.anchors.filter( "[href$='" + $.ui.escapeSelector( index ) + "']" ) ); - } - - return index; - }, - - _destroy: function() { - if ( this.xhr ) { - this.xhr.abort(); - } - - this.tablist - .removeAttr( "role" ) - .off( this.eventNamespace ); - - this.anchors - .removeAttr( "role tabIndex" ) - .removeUniqueId(); - - this.tabs.add( this.panels ).each( function() { - if ( $.data( this, "ui-tabs-destroy" ) ) { - $( this ).remove(); - } else { - $( this ).removeAttr( "role tabIndex " + - "aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" ); - } - } ); - - this.tabs.each( function() { - var li = $( this ), - prev = li.data( "ui-tabs-aria-controls" ); - if ( prev ) { - li - .attr( "aria-controls", prev ) - .removeData( "ui-tabs-aria-controls" ); - } else { - li.removeAttr( "aria-controls" ); - } - } ); - - this.panels.show(); - - if ( this.options.heightStyle !== "content" ) { - this.panels.css( "height", "" ); - } - }, - - enable: function( index ) { - var disabled = this.options.disabled; - if ( disabled === false ) { - return; - } - - if ( index === undefined ) { - disabled = false; - } else { - index = this._getIndex( index ); - if ( $.isArray( disabled ) ) { - disabled = $.map( disabled, function( num ) { - return num !== index ? num : null; - } ); - } else { - disabled = $.map( this.tabs, function( li, num ) { - return num !== index ? num : null; - } ); - } - } - this._setupDisabled( disabled ); - }, - - disable: function( index ) { - var disabled = this.options.disabled; - if ( disabled === true ) { - return; - } - - if ( index === undefined ) { - disabled = true; - } else { - index = this._getIndex( index ); - if ( $.inArray( index, disabled ) !== -1 ) { - return; - } - if ( $.isArray( disabled ) ) { - disabled = $.merge( [ index ], disabled ).sort(); - } else { - disabled = [ index ]; - } - } - this._setupDisabled( disabled ); - }, - - load: function( index, event ) { - index = this._getIndex( index ); - var that = this, - tab = this.tabs.eq( index ), - anchor = tab.find( ".ui-tabs-anchor" ), - panel = this._getPanelForTab( tab ), - eventData = { - tab: tab, - panel: panel - }, - complete = function( jqXHR, status ) { - if ( status === "abort" ) { - that.panels.stop( false, true ); - } - - that._removeClass( tab, "ui-tabs-loading" ); - panel.removeAttr( "aria-busy" ); - - if ( jqXHR === that.xhr ) { - delete that.xhr; - } - }; - - // not remote - if ( this._isLocal( anchor[ 0 ] ) ) { - return; - } - - this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) ); - - // support: jQuery <1.8 - // jQuery <1.8 returns false if the request is canceled in beforeSend, - // but as of 1.8, $.ajax() always returns a jqXHR object. - if ( this.xhr && this.xhr.statusText !== "canceled" ) { - this._addClass( tab, "ui-tabs-loading" ); - panel.attr( "aria-busy", "true" ); - - this.xhr - .done( function( response, status, jqXHR ) { - // support: jQuery <1.8 - // http://bugs.jquery.com/ticket/11778 - setTimeout( function() { - panel.html( response ); - that._trigger( "load", event, eventData ); - - complete( jqXHR, status ); - }, 1 ); - } ) - .fail( function( jqXHR, status ) { - // support: jQuery <1.8 - // http://bugs.jquery.com/ticket/11778 - setTimeout( function() { - complete( jqXHR, status ); - }, 1 ); - } ); - } - }, - - _ajaxSettings: function( anchor, event, eventData ) { - var that = this; - return { - url: anchor.attr( "href" ), - beforeSend: function( jqXHR, settings ) { - return that._trigger( "beforeLoad", event, - $.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) ); - } - }; - }, - - _getPanelForTab: function( tab ) { - var id = $( tab ).attr( "aria-controls" ); - return this.element.find( this._sanitizeSelector( "#" + id ) ); - } -} ); - -// DEPRECATED -// TODO: Switch return back to widget declaration at top of file when this is removed -if ( $.uiBackCompat !== false ) { - - // Backcompat for ui-tab class (now ui-tabs-tab) - $.widget( "ui.tabs", $.ui.tabs, { - _processTabs: function() { - this._superApply( arguments ); - this._addClass( this.tabs, "ui-tab" ); - } - } ); -} - -return $.ui.tabs; - -} ) ); diff --git a/ui/widgets/tabs.js b/ui/widgets/tabs.js new file mode 100644 index 000000000..0b7bf5850 --- /dev/null +++ b/ui/widgets/tabs.js @@ -0,0 +1,902 @@ +/*! + * jQuery UI Tabs @VERSION + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Tabs +//>>group: Widgets +//>>description: Transforms a set of container elements into a tab structure. +//>>docs: http://api.jqueryui.com/tabs/ +//>>demos: http://jqueryui.com/tabs/ +//>>css.structure: ../themes/base/core.css +//>>css.structure: ../themes/base/tabs.css +//>>css.theme: ../themes/base/theme.css + +( function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define( [ + "jquery", + "../escape-selector", + "../keycode", + "../safe-active-element", + "../unique-id", + "../version", + "../widget" + ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}( function( $ ) { + +$.widget( "ui.tabs", { + version: "@VERSION", + delay: 300, + options: { + active: null, + classes: { + "ui-tabs": "ui-corner-all", + "ui-tabs-nav": "ui-corner-all", + "ui-tab": "ui-corner-top", + "ui-tabs-panel": "ui-corner-bottom" + }, + collapsible: false, + event: "click", + heightStyle: "content", + hide: null, + show: null, + + // callbacks + activate: null, + beforeActivate: null, + beforeLoad: null, + load: null + }, + + _isLocal: ( function() { + var rhash = /#.*$/; + + return function( anchor ) { + var anchorUrl, locationUrl; + + anchorUrl = anchor.href.replace( rhash, "" ); + locationUrl = location.href.replace( rhash, "" ); + + // decoding may throw an error if the URL isn't UTF-8 (#9518) + try { + anchorUrl = decodeURIComponent( anchorUrl ); + } catch ( error ) {} + try { + locationUrl = decodeURIComponent( locationUrl ); + } catch ( error ) {} + + return anchor.hash.length > 1 && anchorUrl === locationUrl; + }; + } )(), + + _create: function() { + var that = this, + options = this.options; + + this.running = false; + + this._addClass( "ui-tabs", "ui-widget ui-widget-content" ); + this._toggleClass( "ui-tabs-collapsible", null, options.collapsible ); + + this._processTabs(); + options.active = this._initialActive(); + + // Take disabling tabs via class attribute from HTML + // into account and update option properly. + if ( $.isArray( options.disabled ) ) { + options.disabled = $.unique( options.disabled.concat( + $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) { + return that.tabs.index( li ); + } ) + ) ).sort(); + } + + // check for length avoids error when initializing empty list + if ( this.options.active !== false && this.anchors.length ) { + this.active = this._findActive( options.active ); + } else { + this.active = $(); + } + + this._refresh(); + + if ( this.active.length ) { + this.load( options.active ); + } + }, + + _initialActive: function() { + var active = this.options.active, + collapsible = this.options.collapsible, + locationHash = location.hash.substring( 1 ); + + if ( active === null ) { + // check the fragment identifier in the URL + if ( locationHash ) { + this.tabs.each( function( i, tab ) { + if ( $( tab ).attr( "aria-controls" ) === locationHash ) { + active = i; + return false; + } + } ); + } + + // check for a tab marked active via a class + if ( active === null ) { + active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) ); + } + + // no active tab, set to false + if ( active === null || active === -1 ) { + active = this.tabs.length ? 0 : false; + } + } + + // handle numbers: negative, out of range + if ( active !== false ) { + active = this.tabs.index( this.tabs.eq( active ) ); + if ( active === -1 ) { + active = collapsible ? false : 0; + } + } + + // don't allow collapsible: false and active: false + if ( !collapsible && active === false && this.anchors.length ) { + active = 0; + } + + return active; + }, + + _getCreateEventData: function() { + return { + tab: this.active, + panel: !this.active.length ? $() : this._getPanelForTab( this.active ) + }; + }, + + _tabKeydown: function( event ) { + var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ), + selectedIndex = this.tabs.index( focusedTab ), + goingForward = true; + + if ( this._handlePageNav( event ) ) { + return; + } + + switch ( event.keyCode ) { + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + selectedIndex++; + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + goingForward = false; + selectedIndex--; + break; + case $.ui.keyCode.END: + selectedIndex = this.anchors.length - 1; + break; + case $.ui.keyCode.HOME: + selectedIndex = 0; + break; + case $.ui.keyCode.SPACE: + // Activate only, no collapsing + event.preventDefault(); + clearTimeout( this.activating ); + this._activate( selectedIndex ); + return; + case $.ui.keyCode.ENTER: + // Toggle (cancel delayed activation, allow collapsing) + event.preventDefault(); + clearTimeout( this.activating ); + // Determine if we should collapse or activate + this._activate( selectedIndex === this.options.active ? false : selectedIndex ); + return; + default: + return; + } + + // Focus the appropriate tab, based on which key was pressed + event.preventDefault(); + clearTimeout( this.activating ); + selectedIndex = this._focusNextTab( selectedIndex, goingForward ); + + // Navigating with control/command key will prevent automatic activation + if ( !event.ctrlKey && !event.metaKey ) { + + // Update aria-selected immediately so that AT think the tab is already selected. + // Otherwise AT may confuse the user by stating that they need to activate the tab, + // but the tab will already be activated by the time the announcement finishes. + focusedTab.attr( "aria-selected", "false" ); + this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" ); + + this.activating = this._delay( function() { + this.option( "active", selectedIndex ); + }, this.delay ); + } + }, + + _panelKeydown: function( event ) { + if ( this._handlePageNav( event ) ) { + return; + } + + // Ctrl+up moves focus to the current tab + if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { + event.preventDefault(); + this.active.trigger( "focus" ); + } + }, + + // Alt+page up/down moves focus to the previous/next tab (and activates) + _handlePageNav: function( event ) { + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { + this._activate( this._focusNextTab( this.options.active - 1, false ) ); + return true; + } + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { + this._activate( this._focusNextTab( this.options.active + 1, true ) ); + return true; + } + }, + + _findNextTab: function( index, goingForward ) { + var lastTabIndex = this.tabs.length - 1; + + function constrain() { + if ( index > lastTabIndex ) { + index = 0; + } + if ( index < 0 ) { + index = lastTabIndex; + } + return index; + } + + while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { + index = goingForward ? index + 1 : index - 1; + } + + return index; + }, + + _focusNextTab: function( index, goingForward ) { + index = this._findNextTab( index, goingForward ); + this.tabs.eq( index ).trigger( "focus" ); + return index; + }, + + _setOption: function( key, value ) { + if ( key === "active" ) { + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } + + if ( key === "disabled" ) { + // don't use the widget factory's disabled handling + this._setupDisabled( value ); + return; + } + + this._super( key, value ); + + if ( key === "collapsible" ) { + this._toggleClass( "ui-tabs-collapsible", null, value ); + + // Setting collapsible: false while collapsed; open first panel + if ( !value && this.options.active === false ) { + this._activate( 0 ); + } + } + + if ( key === "event" ) { + this._setupEvents( value ); + } + + if ( key === "heightStyle" ) { + this._setupHeightStyle( value ); + } + }, + + _sanitizeSelector: function( hash ) { + return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; + }, + + refresh: function() { + var options = this.options, + lis = this.tablist.children( ":has(a[href])" ); + + // get disabled tabs from class attribute from HTML + // this will get converted to a boolean if needed in _refresh() + options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) { + return lis.index( tab ); + } ); + + this._processTabs(); + + // was collapsed or no tabs + if ( options.active === false || !this.anchors.length ) { + options.active = false; + this.active = $(); + // was active, but active tab is gone + } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) { + // all remaining tabs are disabled + if ( this.tabs.length === options.disabled.length ) { + options.active = false; + this.active = $(); + // activate previous tab + } else { + this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) ); + } + // was active, active tab still exists + } else { + // make sure active index is correct + options.active = this.tabs.index( this.active ); + } + + this._refresh(); + }, + + _refresh: function() { + this._setupDisabled( this.options.disabled ); + this._setupEvents( this.options.event ); + this._setupHeightStyle( this.options.heightStyle ); + + this.tabs.not( this.active ).attr( { + "aria-selected": "false", + "aria-expanded": "false", + tabIndex: -1 + } ); + this.panels.not( this._getPanelForTab( this.active ) ) + .hide() + .attr( { + "aria-hidden": "true" + } ); + + // Make sure one tab is in the tab order + if ( !this.active.length ) { + this.tabs.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active + .attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ); + this._addClass( this.active, "ui-tabs-active", "ui-state-active" ); + this._getPanelForTab( this.active ) + .show() + .attr( { + "aria-hidden": "false" + } ); + } + }, + + _processTabs: function() { + var that = this, + prevTabs = this.tabs, + prevAnchors = this.anchors, + prevPanels = this.panels; + + this.tablist = this._getList().attr( "role", "tablist" ); + this._addClass( this.tablist, "ui-tabs-nav", + "ui-helper-reset ui-helper-clearfix ui-widget-header" ); + + // Prevent users from focusing disabled tabs via click + this.tablist + .on( "mousedown" + this.eventNamespace, "> li", function( event ) { + if ( $( this ).is( ".ui-state-disabled" ) ) { + event.preventDefault(); + } + } ) + + // support: IE <9 + // Preventing the default action in mousedown doesn't prevent IE + // from focusing the element, so if the anchor gets focused, blur. + // We don't have to worry about focusing the previously focused + // element since clicking on a non-focusable element should focus + // the body anyway. + .on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() { + if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { + this.blur(); + } + } ); + + this.tabs = this.tablist.find( "> li:has(a[href])" ) + .attr( { + role: "tab", + tabIndex: -1 + } ); + this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" ); + + this.anchors = this.tabs.map( function() { + return $( "a", this )[ 0 ]; + } ) + .attr( { + role: "presentation", + tabIndex: -1 + } ); + this._addClass( this.anchors, "ui-tabs-anchor" ); + + this.panels = $(); + + this.anchors.each( function( i, anchor ) { + var selector, panel, panelId, + anchorId = $( anchor ).uniqueId().attr( "id" ), + tab = $( anchor ).closest( "li" ), + originalAriaControls = tab.attr( "aria-controls" ); + + // inline tab + if ( that._isLocal( anchor ) ) { + selector = anchor.hash; + panelId = selector.substring( 1 ); + panel = that.element.find( that._sanitizeSelector( selector ) ); + // remote tab + } else { + // If the tab doesn't already have aria-controls, + // generate an id by using a throw-away element + panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id; + selector = "#" + panelId; + panel = that.element.find( selector ); + if ( !panel.length ) { + panel = that._createPanel( panelId ); + panel.insertAfter( that.panels[ i - 1 ] || that.tablist ); + } + panel.attr( "aria-live", "polite" ); + } + + if ( panel.length ) { + that.panels = that.panels.add( panel ); + } + if ( originalAriaControls ) { + tab.data( "ui-tabs-aria-controls", originalAriaControls ); + } + tab.attr( { + "aria-controls": panelId, + "aria-labelledby": anchorId + } ); + panel.attr( "aria-labelledby", anchorId ); + } ); + + this.panels.attr( "role", "tabpanel" ); + this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" ); + + // Avoid memory leaks (#10056) + if ( prevTabs ) { + this._off( prevTabs.not( this.tabs ) ); + this._off( prevAnchors.not( this.anchors ) ); + this._off( prevPanels.not( this.panels ) ); + } + }, + + // allow overriding how to find the list for rare usage scenarios (#7715) + _getList: function() { + return this.tablist || this.element.find( "ol, ul" ).eq( 0 ); + }, + + _createPanel: function( id ) { + return $( "
" ) + .attr( "id", id ) + .data( "ui-tabs-destroy", true ); + }, + + _setupDisabled: function( disabled ) { + var currentItem, li, i; + + if ( $.isArray( disabled ) ) { + if ( !disabled.length ) { + disabled = false; + } else if ( disabled.length === this.anchors.length ) { + disabled = true; + } + } + + // disable tabs + for ( i = 0; ( li = this.tabs[ i ] ); i++ ) { + currentItem = $( li ); + if ( disabled === true || $.inArray( i, disabled ) !== -1 ) { + currentItem.attr( "aria-disabled", "true" ); + this._addClass( currentItem, null, "ui-state-disabled" ); + } else { + currentItem.removeAttr( "aria-disabled" ); + this._removeClass( currentItem, null, "ui-state-disabled" ); + } + } + + this.options.disabled = disabled; + }, + + _setupEvents: function( event ) { + var events = {}; + if ( event ) { + $.each( event.split( " " ), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; + } ); + } + + this._off( this.anchors.add( this.tabs ).add( this.panels ) ); + // Always prevent the default action, even when disabled + this._on( true, this.anchors, { + click: function( event ) { + event.preventDefault(); + } + } ); + this._on( this.anchors, events ); + this._on( this.tabs, { keydown: "_tabKeydown" } ); + this._on( this.panels, { keydown: "_panelKeydown" } ); + + this._focusable( this.tabs ); + this._hoverable( this.tabs ); + }, + + _setupHeightStyle: function( heightStyle ) { + var maxHeight, + parent = this.element.parent(); + + if ( heightStyle === "fill" ) { + maxHeight = parent.height(); + maxHeight -= this.element.outerHeight() - this.element.height(); + + this.element.siblings( ":visible" ).each( function() { + var elem = $( this ), + position = elem.css( "position" ); + + if ( position === "absolute" || position === "fixed" ) { + return; + } + maxHeight -= elem.outerHeight( true ); + } ); + + this.element.children().not( this.panels ).each( function() { + maxHeight -= $( this ).outerHeight( true ); + } ); + + this.panels.each( function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + } ) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.panels.each( function() { + maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() ); + } ).height( maxHeight ); + } + }, + + _eventHandler: function( event ) { + var options = this.options, + active = this.active, + anchor = $( event.currentTarget ), + tab = anchor.closest( "li" ), + clickedIsActive = tab[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : this._getPanelForTab( tab ), + toHide = !active.length ? $() : this._getPanelForTab( active ), + eventData = { + oldTab: active, + oldPanel: toHide, + newTab: collapsing ? $() : tab, + newPanel: toShow + }; + + event.preventDefault(); + + if ( tab.hasClass( "ui-state-disabled" ) || + // tab is already loading + tab.hasClass( "ui-tabs-loading" ) || + // can't switch durning an animation + this.running || + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; + } + + options.active = collapsing ? false : this.tabs.index( tab ); + + this.active = clickedIsActive ? $() : tab; + if ( this.xhr ) { + this.xhr.abort(); + } + + if ( !toHide.length && !toShow.length ) { + $.error( "jQuery UI Tabs: Mismatching fragment identifier." ); + } + + if ( toShow.length ) { + this.load( this.tabs.index( tab ), event ); + } + this._toggle( event, eventData ); + }, + + // handles show/hide for selecting tabs + _toggle: function( event, eventData ) { + var that = this, + toShow = eventData.newPanel, + toHide = eventData.oldPanel; + + this.running = true; + + function complete() { + that.running = false; + that._trigger( "activate", event, eventData ); + } + + function show() { + that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" ); + + if ( toShow.length && that.options.show ) { + that._show( toShow, that.options.show, complete ); + } else { + toShow.show(); + complete(); + } + } + + // start out by hiding, then showing, then completing + if ( toHide.length && this.options.hide ) { + this._hide( toHide, this.options.hide, function() { + that._removeClass( eventData.oldTab.closest( "li" ), + "ui-tabs-active", "ui-state-active" ); + show(); + } ); + } else { + this._removeClass( eventData.oldTab.closest( "li" ), + "ui-tabs-active", "ui-state-active" ); + toHide.hide(); + show(); + } + + toHide.attr( "aria-hidden", "true" ); + eventData.oldTab.attr( { + "aria-selected": "false", + "aria-expanded": "false" + } ); + // If we're switching tabs, remove the old tab from the tab order. + // If we're opening from collapsed state, remove the previous tab from the tab order. + // If we're collapsing, then keep the collapsing tab in the tab order. + if ( toShow.length && toHide.length ) { + eventData.oldTab.attr( "tabIndex", -1 ); + } else if ( toShow.length ) { + this.tabs.filter( function() { + return $( this ).attr( "tabIndex" ) === 0; + } ) + .attr( "tabIndex", -1 ); + } + + toShow.attr( "aria-hidden", "false" ); + eventData.newTab.attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ); + }, + + _activate: function( index ) { + var anchor, + active = this._findActive( index ); + + // trying to activate the already active panel + if ( active[ 0 ] === this.active[ 0 ] ) { + return; + } + + // trying to collapse, simulate a click on the current active header + if ( !active.length ) { + active = this.active; + } + + anchor = active.find( ".ui-tabs-anchor" )[ 0 ]; + this._eventHandler( { + target: anchor, + currentTarget: anchor, + preventDefault: $.noop + } ); + }, + + _findActive: function( index ) { + return index === false ? $() : this.tabs.eq( index ); + }, + + _getIndex: function( index ) { + // meta-function to give users option to provide a href string instead of a numerical index. + if ( typeof index === "string" ) { + index = this.anchors.index( this.anchors.filter( "[href$='" + $.ui.escapeSelector( index ) + "']" ) ); + } + + return index; + }, + + _destroy: function() { + if ( this.xhr ) { + this.xhr.abort(); + } + + this.tablist + .removeAttr( "role" ) + .off( this.eventNamespace ); + + this.anchors + .removeAttr( "role tabIndex" ) + .removeUniqueId(); + + this.tabs.add( this.panels ).each( function() { + if ( $.data( this, "ui-tabs-destroy" ) ) { + $( this ).remove(); + } else { + $( this ).removeAttr( "role tabIndex " + + "aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" ); + } + } ); + + this.tabs.each( function() { + var li = $( this ), + prev = li.data( "ui-tabs-aria-controls" ); + if ( prev ) { + li + .attr( "aria-controls", prev ) + .removeData( "ui-tabs-aria-controls" ); + } else { + li.removeAttr( "aria-controls" ); + } + } ); + + this.panels.show(); + + if ( this.options.heightStyle !== "content" ) { + this.panels.css( "height", "" ); + } + }, + + enable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === false ) { + return; + } + + if ( index === undefined ) { + disabled = false; + } else { + index = this._getIndex( index ); + if ( $.isArray( disabled ) ) { + disabled = $.map( disabled, function( num ) { + return num !== index ? num : null; + } ); + } else { + disabled = $.map( this.tabs, function( li, num ) { + return num !== index ? num : null; + } ); + } + } + this._setupDisabled( disabled ); + }, + + disable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === true ) { + return; + } + + if ( index === undefined ) { + disabled = true; + } else { + index = this._getIndex( index ); + if ( $.inArray( index, disabled ) !== -1 ) { + return; + } + if ( $.isArray( disabled ) ) { + disabled = $.merge( [ index ], disabled ).sort(); + } else { + disabled = [ index ]; + } + } + this._setupDisabled( disabled ); + }, + + load: function( index, event ) { + index = this._getIndex( index ); + var that = this, + tab = this.tabs.eq( index ), + anchor = tab.find( ".ui-tabs-anchor" ), + panel = this._getPanelForTab( tab ), + eventData = { + tab: tab, + panel: panel + }, + complete = function( jqXHR, status ) { + if ( status === "abort" ) { + that.panels.stop( false, true ); + } + + that._removeClass( tab, "ui-tabs-loading" ); + panel.removeAttr( "aria-busy" ); + + if ( jqXHR === that.xhr ) { + delete that.xhr; + } + }; + + // not remote + if ( this._isLocal( anchor[ 0 ] ) ) { + return; + } + + this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) ); + + // support: jQuery <1.8 + // jQuery <1.8 returns false if the request is canceled in beforeSend, + // but as of 1.8, $.ajax() always returns a jqXHR object. + if ( this.xhr && this.xhr.statusText !== "canceled" ) { + this._addClass( tab, "ui-tabs-loading" ); + panel.attr( "aria-busy", "true" ); + + this.xhr + .done( function( response, status, jqXHR ) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout( function() { + panel.html( response ); + that._trigger( "load", event, eventData ); + + complete( jqXHR, status ); + }, 1 ); + } ) + .fail( function( jqXHR, status ) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout( function() { + complete( jqXHR, status ); + }, 1 ); + } ); + } + }, + + _ajaxSettings: function( anchor, event, eventData ) { + var that = this; + return { + url: anchor.attr( "href" ), + beforeSend: function( jqXHR, settings ) { + return that._trigger( "beforeLoad", event, + $.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) ); + } + }; + }, + + _getPanelForTab: function( tab ) { + var id = $( tab ).attr( "aria-controls" ); + return this.element.find( this._sanitizeSelector( "#" + id ) ); + } +} ); + +// DEPRECATED +// TODO: Switch return back to widget declaration at top of file when this is removed +if ( $.uiBackCompat !== false ) { + + // Backcompat for ui-tab class (now ui-tabs-tab) + $.widget( "ui.tabs", $.ui.tabs, { + _processTabs: function() { + this._superApply( arguments ); + this._addClass( this.tabs, "ui-tab" ); + } + } ); +} + +return $.ui.tabs; + +} ) ); -- cgit v1.2.3