2015-08-17 17:00:26 -07:00
/ * *
* @ file
* Manages page tabbing modifications made by modules .
* /
/ * *
* Allow modules to respond to the constrain event .
*
* @ event drupalTabbingConstrained
* /
/ * *
* Allow modules to respond to the tabbingContext release event .
*
* @ event drupalTabbingContextReleased
* /
/ * *
* Allow modules to respond to the constrain event .
*
* @ event drupalTabbingContextActivated
* /
/ * *
* Allow modules to respond to the constrain event .
*
* @ event drupalTabbingContextDeactivated
* /
( function ( $ , Drupal ) {
2015-10-21 21:44:50 -07:00
'use strict' ;
2015-08-17 17:00:26 -07:00
/ * *
* Provides an API for managing page tabbing order modifications .
*
* @ constructor Drupal ~ TabbingManager
* /
function TabbingManager ( ) {
/ * *
* Tabbing sets are stored as a stack . The active set is at the top of the
* stack . We use a JavaScript array as if it were a stack ; we consider the
* first element to be the bottom and the last element to be the top . This
* allows us to use JavaScript ' s built - in Array . push ( ) and Array . pop ( )
* methods .
*
* @ type { Array . < Drupal ~ TabbingContext > }
* /
this . stack = [ ] ;
}
/ * *
* Add public methods to the TabbingManager class .
* /
$ . extend ( TabbingManager . prototype , /** @lends Drupal~TabbingManager# */ {
/ * *
* Constrain tabbing to the specified set of elements only .
*
* Makes elements outside of the specified set of elements unreachable via
* the tab key .
*
* @ param { jQuery } elements
* The set of elements to which tabbing should be constrained . Can also
* be a jQuery - compatible selector string .
*
* @ return { Drupal ~ TabbingContext }
2016-04-20 09:56:34 -07:00
* The TabbingContext instance .
2015-08-17 17:00:26 -07:00
*
* @ fires event : drupalTabbingConstrained
* /
constrain : function ( elements ) {
// Deactivate all tabbingContexts to prepare for the new constraint. A
// tabbingContext instance will only be reactivated if the stack is
// unwound to it in the _unwindStack() method.
var il = this . stack . length ;
for ( var i = 0 ; i < il ; i ++ ) {
this . stack [ i ] . deactivate ( ) ;
}
// The "active tabbing set" are the elements tabbing should be constrained
// to.
var $elements = $ ( elements ) . find ( ':tabbable' ) . addBack ( ':tabbable' ) ;
var tabbingContext = new TabbingContext ( {
// The level is the current height of the stack before this new
// tabbingContext is pushed on top of the stack.
level : this . stack . length ,
$tabbableElements : $elements
} ) ;
this . stack . push ( tabbingContext ) ;
// Activates the tabbingContext; this will manipulate the DOM to constrain
// tabbing.
tabbingContext . activate ( ) ;
// Allow modules to respond to the constrain event.
$ ( document ) . trigger ( 'drupalTabbingConstrained' , tabbingContext ) ;
return tabbingContext ;
} ,
/ * *
* Restores a former tabbingContext when an active one is released .
*
* The TabbingManager stack of tabbingContext instances will be unwound
* from the top - most released tabbingContext down to the first non - released
* tabbingContext instance . This non - released instance is then activated .
* /
release : function ( ) {
// Unwind as far as possible: find the topmost non-released
// tabbingContext.
var toActivate = this . stack . length - 1 ;
while ( toActivate >= 0 && this . stack [ toActivate ] . released ) {
toActivate -- ;
}
// Delete all tabbingContexts after the to be activated one. They have
// already been deactivated, so their effect on the DOM has been reversed.
this . stack . splice ( toActivate + 1 ) ;
// Get topmost tabbingContext, if one exists, and activate it.
if ( toActivate >= 0 ) {
this . stack [ toActivate ] . activate ( ) ;
}
} ,
/ * *
2015-12-02 11:38:43 -08:00
* Makes all elements outside of the tabbingContext ' s set untabbable .
2015-08-17 17:00:26 -07:00
*
* Elements made untabbable have their original tabindex and autofocus
* values stored so that they might be restored later when this
* tabbingContext is deactivated .
*
* @ param { Drupal ~ TabbingContext } tabbingContext
* The TabbingContext instance that has been activated .
* /
activate : function ( tabbingContext ) {
var $set = tabbingContext . $tabbableElements ;
var level = tabbingContext . level ;
// Determine which elements are reachable via tabbing by default.
var $disabledSet = $ ( ':tabbable' )
// Exclude elements of the active tabbing set.
. not ( $set ) ;
// Set the disabled set on the tabbingContext.
tabbingContext . $disabledElements = $disabledSet ;
// Record the tabindex for each element, so we can restore it later.
var il = $disabledSet . length ;
for ( var i = 0 ; i < il ; i ++ ) {
this . recordTabindex ( $disabledSet . eq ( i ) , level ) ;
}
// Make all tabbable elements outside of the active tabbing set
// unreachable.
$disabledSet
. prop ( 'tabindex' , - 1 )
. prop ( 'autofocus' , false ) ;
// Set focus on an element in the tabbingContext's set of tabbable
// elements. First, check if there is an element with an autofocus
// attribute. Select the last one from the DOM order.
var $hasFocus = $set . filter ( '[autofocus]' ) . eq ( - 1 ) ;
// If no element in the tabbable set has an autofocus attribute, select
// the first element in the set.
if ( $hasFocus . length === 0 ) {
$hasFocus = $set . eq ( 0 ) ;
}
$hasFocus . trigger ( 'focus' ) ;
} ,
/ * *
* Restores that tabbable state of a tabbingContext ' s disabled elements .
*
* Elements that were made untabbable have their original tabindex and
* autofocus values restored .
*
* @ param { Drupal ~ TabbingContext } tabbingContext
* The TabbingContext instance that has been deactivated .
* /
deactivate : function ( tabbingContext ) {
var $set = tabbingContext . $disabledElements ;
var level = tabbingContext . level ;
var il = $set . length ;
for ( var i = 0 ; i < il ; i ++ ) {
this . restoreTabindex ( $set . eq ( i ) , level ) ;
}
} ,
/ * *
* Records the tabindex and autofocus values of an untabbable element .
*
* @ param { jQuery } $el
* The set of elements that have been disabled .
* @ param { number } level
* The stack level for which the tabindex attribute should be recorded .
* /
recordTabindex : function ( $el , level ) {
var tabInfo = $el . data ( 'drupalOriginalTabIndices' ) || { } ;
tabInfo [ level ] = {
tabindex : $el [ 0 ] . getAttribute ( 'tabindex' ) ,
autofocus : $el [ 0 ] . hasAttribute ( 'autofocus' )
} ;
$el . data ( 'drupalOriginalTabIndices' , tabInfo ) ;
} ,
/ * *
* Restores the tabindex and autofocus values of a reactivated element .
*
* @ param { jQuery } $el
* The element that is being reactivated .
* @ param { number } level
* The stack level for which the tabindex attribute should be restored .
* /
restoreTabindex : function ( $el , level ) {
var tabInfo = $el . data ( 'drupalOriginalTabIndices' ) ;
if ( tabInfo && tabInfo [ level ] ) {
var data = tabInfo [ level ] ;
if ( data . tabindex ) {
$el [ 0 ] . setAttribute ( 'tabindex' , data . tabindex ) ;
}
// If the element did not have a tabindex at this stack level then
// remove it.
else {
$el [ 0 ] . removeAttribute ( 'tabindex' ) ;
}
if ( data . autofocus ) {
$el [ 0 ] . setAttribute ( 'autofocus' , 'autofocus' ) ;
}
// Clean up $.data.
if ( level === 0 ) {
// Remove all data.
$el . removeData ( 'drupalOriginalTabIndices' ) ;
}
else {
// Remove the data for this stack level and higher.
var levelToDelete = level ;
while ( tabInfo . hasOwnProperty ( levelToDelete ) ) {
delete tabInfo [ levelToDelete ] ;
levelToDelete ++ ;
}
$el . data ( 'drupalOriginalTabIndices' , tabInfo ) ;
}
}
}
} ) ;
/ * *
* Stores a set of tabbable elements .
*
* This constraint can be removed with the release ( ) method .
*
* @ constructor Drupal ~ TabbingContext
*
* @ param { object } options
* A set of initiating values
* @ param { number } options . level
* The level in the TabbingManager ' s stack of this tabbingContext .
* @ param { jQuery } options . $tabbableElements
* The DOM elements that should be reachable via the tab key when this
* tabbingContext is active .
* @ param { jQuery } options . $disabledElements
* The DOM elements that should not be reachable via the tab key when this
* tabbingContext is active .
* @ param { bool } options . released
* A released tabbingContext can never be activated again . It will be
* cleaned up when the TabbingManager unwinds its stack .
* @ param { bool } options . active
* When true , the tabbable elements of this tabbingContext will be reachable
* via the tab key and the disabled elements will not . Only one
* tabbingContext can be active at a time .
* /
function TabbingContext ( options ) {
$ . extend ( this , /** @lends Drupal~TabbingContext# */ {
/ * *
* @ type { ? number }
* /
level : null ,
/ * *
* @ type { jQuery }
* /
$tabbableElements : $ ( ) ,
/ * *
* @ type { jQuery }
* /
$disabledElements : $ ( ) ,
/ * *
* @ type { bool }
* /
released : false ,
/ * *
* @ type { bool }
* /
active : false
} , options ) ;
}
/ * *
* Add public methods to the TabbingContext class .
* /
$ . extend ( TabbingContext . prototype , /** @lends Drupal~TabbingContext# */ {
/ * *
* Releases this TabbingContext .
*
* Once a TabbingContext object is released , it can never be activated
* again .
*
* @ fires event : drupalTabbingContextReleased
* /
release : function ( ) {
if ( ! this . released ) {
this . deactivate ( ) ;
this . released = true ;
Drupal . tabbingManager . release ( this ) ;
// Allow modules to respond to the tabbingContext release event.
$ ( document ) . trigger ( 'drupalTabbingContextReleased' , this ) ;
}
} ,
/ * *
* Activates this TabbingContext .
*
* @ fires event : drupalTabbingContextActivated
* /
activate : function ( ) {
// A released TabbingContext object can never be activated again.
if ( ! this . active && ! this . released ) {
this . active = true ;
Drupal . tabbingManager . activate ( this ) ;
// Allow modules to respond to the constrain event.
$ ( document ) . trigger ( 'drupalTabbingContextActivated' , this ) ;
}
} ,
/ * *
* Deactivates this TabbingContext .
*
* @ fires event : drupalTabbingContextDeactivated
* /
deactivate : function ( ) {
if ( this . active ) {
this . active = false ;
Drupal . tabbingManager . deactivate ( this ) ;
// Allow modules to respond to the constrain event.
$ ( document ) . trigger ( 'drupalTabbingContextDeactivated' , this ) ;
}
}
} ) ;
// Mark this behavior as processed on the first pass and return if it is
// already processed.
if ( Drupal . tabbingManager ) {
return ;
}
/ * *
* @ type { Drupal ~ TabbingManager }
* /
Drupal . tabbingManager = new TabbingManager ( ) ;
} ( jQuery , Drupal ) ) ;