2009-10-16 19:20:34 +00:00
( function ( $ ) {
/ * *
* The base States namespace .
*
* Having the local states variable allows us to use the States namespace
* without having to always declare "Drupal.states" .
* /
var states = Drupal . states = {
// An array of functions that should be postponed.
postponed : [ ]
} ;
/ * *
* Attaches the states .
* /
Drupal . behaviors . states = {
attach : function ( context , settings ) {
for ( var selector in settings . states ) {
for ( var state in settings . states [ selector ] ) {
2010-04-29 03:52:04 +00:00
new states . Dependent ( {
2009-10-16 19:20:34 +00:00
element : $ ( selector ) ,
state : states . State . sanitize ( state ) ,
2012-01-31 08:03:32 +00:00
constraints : settings . states [ selector ] [ state ]
2009-10-16 19:20:34 +00:00
} ) ;
}
}
// Execute all postponed functions now.
while ( states . postponed . length ) {
( states . postponed . shift ( ) ) ( ) ;
}
}
} ;
/ * *
* Object representing an element that depends on other elements .
*
* @ param args
* Object with the following keys ( all of which are required ) :
2010-04-29 03:52:04 +00:00
* - element : A jQuery object of the dependent element
* - state : A State object describing the state that is dependent
2012-01-31 08:03:32 +00:00
* - constraints : An object with dependency specifications . Lists all elements
* that this element depends on . It can be nested and can contain arbitrary
* AND and OR clauses .
2009-10-16 19:20:34 +00:00
* /
2010-04-29 03:52:04 +00:00
states . Dependent = function ( args ) {
2012-01-31 08:03:32 +00:00
$ . extend ( this , { values : { } , oldValue : null } , args ) ;
2009-10-16 19:20:34 +00:00
2012-01-31 08:03:32 +00:00
this . dependees = this . getDependees ( ) ;
2009-10-16 19:20:34 +00:00
for ( var selector in this . dependees ) {
this . initializeDependee ( selector , this . dependees [ selector ] ) ;
}
} ;
/ * *
* Comparison functions for comparing the value of an element with the
* specification from the dependency settings . If the object type can ' t be
* found in this list , the === operator is used by default .
* /
2010-04-29 03:52:04 +00:00
states . Dependent . comparisons = {
2009-10-16 19:20:34 +00:00
'RegExp' : function ( reference , value ) {
return reference . test ( value ) ;
} ,
'Function' : function ( reference , value ) {
// The "reference" variable is a comparison function.
return reference ( value ) ;
2011-09-28 03:31:27 +00:00
} ,
'Number' : function ( reference , value ) {
// If "reference" is a number and "value" is a string, then cast reference
// as a string before applying the strict comparison in compare(). Otherwise
// numeric keys in the form's #states array fail to match string values
// returned from jQuery's val().
2012-01-31 08:03:32 +00:00
return ( typeof value === 'string' ) ? compare ( reference . toString ( ) , value ) : compare ( reference , value ) ;
2009-10-16 19:20:34 +00:00
}
} ;
2010-04-29 03:52:04 +00:00
states . Dependent . prototype = {
2009-10-16 19:20:34 +00:00
/ * *
2010-04-29 03:52:04 +00:00
* Initializes one of the elements this dependent depends on .
2009-10-16 19:20:34 +00:00
*
* @ param selector
* The CSS selector describing the dependee .
* @ param dependeeStates
* The list of states that have to be monitored for tracking the
* dependee ' s compliance status .
* /
initializeDependee : function ( selector , dependeeStates ) {
2012-01-31 08:03:32 +00:00
var state ;
2009-10-16 19:20:34 +00:00
// Cache for the states of this dependee.
2012-01-31 08:03:32 +00:00
this . values [ selector ] = { } ;
2009-10-16 19:20:34 +00:00
2012-01-31 08:03:32 +00:00
for ( var i in dependeeStates ) {
if ( dependeeStates . hasOwnProperty ( i ) ) {
state = dependeeStates [ i ] ;
// Make sure we're not initializing this selector/state combination twice.
if ( $ . inArray ( state , dependeeStates ) === - 1 ) {
continue ;
}
2009-10-16 19:20:34 +00:00
2012-01-31 08:03:32 +00:00
state = states . State . sanitize ( state ) ;
2009-10-16 19:20:34 +00:00
2012-01-31 08:03:32 +00:00
// Initialize the value of this state.
this . values [ selector ] [ state . name ] = null ;
2009-10-16 19:20:34 +00:00
2012-01-31 08:03:32 +00:00
// Monitor state changes of the specified state for this dependee.
$ ( selector ) . bind ( 'state:' + state , $ . proxy ( function ( e ) {
this . update ( selector , state , e . value ) ;
} , this ) ) ;
// Make sure the event we just bound ourselves to is actually fired.
new states . Trigger ( { selector : selector , state : state } ) ;
}
}
2009-10-16 19:20:34 +00:00
} ,
/ * *
* Compares a value with a reference value .
*
* @ param reference
* The value used for reference .
2012-01-31 08:03:32 +00:00
* @ param selector
* CSS selector describing the dependee .
* @ param state
* A State object describing the dependee ' s updated state .
*
2009-10-16 19:20:34 +00:00
* @ return
2012-01-31 08:03:32 +00:00
* true or false .
2009-10-16 19:20:34 +00:00
* /
2012-01-31 08:03:32 +00:00
compare : function ( reference , selector , state ) {
var value = this . values [ selector ] [ state . name ] ;
2010-04-29 03:52:04 +00:00
if ( reference . constructor . name in states . Dependent . comparisons ) {
2009-10-16 19:20:34 +00:00
// Use a custom compare function for certain reference value types.
2010-04-29 03:52:04 +00:00
return states . Dependent . comparisons [ reference . constructor . name ] ( reference , value ) ;
2009-10-16 19:20:34 +00:00
}
else {
// Do a plain comparison otherwise.
return compare ( reference , value ) ;
}
} ,
/ * *
* Update the value of a dependee ' s state .
*
* @ param selector
* CSS selector describing the dependee .
* @ param state
* A State object describing the dependee ' s updated state .
* @ param value
* The new value for the dependee ' s updated state .
* /
update : function ( selector , state , value ) {
// Only act when the 'new' value is actually new.
2012-01-31 08:03:32 +00:00
if ( value !== this . values [ selector ] [ state . name ] ) {
this . values [ selector ] [ state . name ] = value ;
2009-10-16 19:20:34 +00:00
this . reevaluate ( ) ;
}
} ,
/ * *
* Triggers change events in case a state changed .
* /
reevaluate : function ( ) {
2012-01-31 08:03:32 +00:00
// Check whether any constraint for this dependent state is satisifed.
var value = this . verifyConstraints ( this . constraints ) ;
2009-10-16 19:20:34 +00:00
// Only invoke a state change event when the value actually changed.
if ( value !== this . oldValue ) {
// Store the new value so that we can compare later whether the value
// actually changed.
this . oldValue = value ;
// Normalize the value to match the normalized state name.
value = invert ( value , this . state . invert ) ;
// By adding "trigger: true", we ensure that state changes don't go into
// infinite loops.
this . element . trigger ( { type : 'state:' + this . state , value : value , trigger : true } ) ;
}
2012-01-31 08:03:32 +00:00
} ,
/ * *
* Evaluates child constraints to determine if a constraint is satisfied .
*
* @ param constraints
* A constraint object or an array of constraints .
* @ param selector
* The selector for these constraints . If undefined , there isn ' t yet a
* selector that these constraints apply to . In that case , the keys of the
* object are interpreted as the selector if encountered .
*
* @ return
* true or false , depending on whether these constraints are satisfied .
* /
verifyConstraints : function ( constraints , selector ) {
var result ;
if ( $ . isArray ( constraints ) ) {
// This constraint is an array (OR or XOR).
var hasXor = $ . inArray ( 'xor' , constraints ) === - 1 ;
for ( var i = 0 , len = constraints . length ; i < len ; i ++ ) {
if ( constraints [ i ] != 'xor' ) {
var constraint = this . checkConstraints ( constraints [ i ] , selector , i ) ;
// Return if this is OR and we have a satisfied constraint or if this
// is XOR and we have a second satisfied constraint.
if ( constraint && ( hasXor || result ) ) {
return hasXor ;
}
result = result || constraint ;
}
}
}
// Make sure we don't try to iterate over things other than objects. This
// shouldn't normally occur, but in case the condition definition is bogus,
// we don't want to end up with an infinite loop.
else if ( $ . isPlainObject ( constraints ) ) {
// This constraint is an object (AND).
for ( var n in constraints ) {
if ( constraints . hasOwnProperty ( n ) ) {
result = ternary ( result , this . checkConstraints ( constraints [ n ] , selector , n ) ) ;
// False and anything else will evaluate to false, so return when any
// false condition is found.
if ( result === false ) { return false ; }
}
}
}
return result ;
} ,
/ * *
* Checks whether the value matches the requirements for this constraint .
*
* @ param value
* Either the value of a state or an array / object of constraints . In the
* latter case , resolving the constraint continues .
* @ param selector
* The selector for this constraint . If undefined , there isn ' t yet a
* selector that this constraint applies to . In that case , the state key is
* propagates to a selector and resolving continues .
* @ param state
* The state to check for this constraint . If undefined , resolving
* continues .
* If both selector and state aren ' t undefined and valid non - numeric
* strings , a lookup for the actual value of that selector ' s state is
* performed . This parameter is not a State object but a pristine state
* string .
*
* @ return
* true or false , depending on whether this constraint is satisfied .
* /
checkConstraints : function ( value , selector , state ) {
// Normalize the last parameter. If it's non-numeric, we treat it either as
// a selector (in case there isn't one yet) or as a trigger/state.
if ( typeof state !== 'string' || ( /[0-9]/ ) . test ( state [ 0 ] ) ) {
state = null ;
}
else if ( typeof selector === 'undefined' ) {
// Propagate the state to the selector when there isn't one yet.
selector = state ;
state = null ;
}
if ( state !== null ) {
// constraints is the actual constraints of an element to check for.
state = states . State . sanitize ( state ) ;
return invert ( this . compare ( value , selector , state ) , state . invert ) ;
}
else {
// Resolve this constraint as an AND/OR operator.
return this . verifyConstraints ( value , selector ) ;
}
} ,
/ * *
* Gathers information about all required triggers .
* /
getDependees : function ( ) {
var cache = { } ;
// Swivel the lookup function so that we can record all available selector-
// state combinations for initialization.
var _compare = this . compare ;
this . compare = function ( reference , selector , state ) {
( cache [ selector ] || ( cache [ selector ] = [ ] ) ) . push ( state . name ) ;
// Return nothing (=== undefined) so that the constraint loops are not
// broken.
} ;
// This call doesn't actually verify anything but uses the resolving
// mechanism to go through the constraints array, trying to look up each
// value. Since we swivelled the compare function, this comparison returns
// undefined and lookup continues until the very end. Instead of lookup up
// the value, we record that combination of selector and state so that we
// can initialize all triggers.
this . verifyConstraints ( this . constraints ) ;
// Restore the original function.
this . compare = _compare ;
return cache ;
2009-10-16 19:20:34 +00:00
}
} ;
states . Trigger = function ( args ) {
$ . extend ( this , args ) ;
if ( this . state in states . Trigger . states ) {
this . element = $ ( this . selector ) ;
// Only call the trigger initializer when it wasn't yet attached to this
// element. Otherwise we'd end up with duplicate events.
if ( ! this . element . data ( 'trigger:' + this . state ) ) {
this . initialize ( ) ;
}
}
} ;
states . Trigger . prototype = {
initialize : function ( ) {
var trigger = states . Trigger . states [ this . state ] ;
if ( typeof trigger == 'function' ) {
// We have a custom trigger initialization function.
trigger . call ( window , this . element ) ;
}
else {
2012-01-31 08:03:32 +00:00
for ( var event in trigger ) {
if ( trigger . hasOwnProperty ( event ) ) {
this . defaultTrigger ( event , trigger [ event ] ) ;
}
}
2009-10-16 19:20:34 +00:00
}
// Mark this trigger as initialized for this element.
this . element . data ( 'trigger:' + this . state , true ) ;
} ,
defaultTrigger : function ( event , valueFn ) {
var oldValue = valueFn . call ( this . element ) ;
// Attach the event callback.
2012-01-31 08:03:32 +00:00
this . element . bind ( event , $ . proxy ( function ( e ) {
var value = valueFn . call ( this . element , e ) ;
2009-10-16 19:20:34 +00:00
// Only trigger the event if the value has actually changed.
if ( oldValue !== value ) {
2012-01-31 08:03:32 +00:00
this . element . trigger ( { type : 'state:' + this . state , value : value , oldValue : oldValue } ) ;
2009-10-16 19:20:34 +00:00
oldValue = value ;
}
2012-01-31 08:03:32 +00:00
} , this ) ) ;
2009-10-16 19:20:34 +00:00
2012-01-31 08:03:32 +00:00
states . postponed . push ( $ . proxy ( function ( ) {
2009-10-16 19:20:34 +00:00
// Trigger the event once for initialization purposes.
2012-01-31 08:03:32 +00:00
this . element . trigger ( { type : 'state:' + this . state , value : oldValue , oldValue : null } ) ;
} , this ) ) ;
2009-10-16 19:20:34 +00:00
}
} ;
/ * *
* This list of states contains functions that are used to monitor the state
* of an element . Whenever an element depends on the state of another element ,
* one of these trigger functions is added to the dependee so that the
2010-04-29 03:52:04 +00:00
* dependent element can be updated .
2009-10-16 19:20:34 +00:00
* /
states . Trigger . states = {
// 'empty' describes the state to be monitored
empty : {
// 'keyup' is the (native DOM) event that we watch for.
'keyup' : function ( ) {
// The function associated to that trigger returns the new value for the
// state.
return this . val ( ) == '' ;
}
} ,
checked : {
'change' : function ( ) {
2012-01-18 04:24:44 +00:00
// prop() and attr() only takes the first element into account. To support
// selectors matching multiple checkboxes, iterate over all and return
// whether any is checked.
var checked = false ;
this . each ( function ( ) {
// Use prop() here as we want a boolean of the checkbox state.
// @see http://api.jquery.com/prop/
checked = $ ( this ) . prop ( 'checked' ) ;
// Break the each() loop if this is checked.
return ! checked ;
} ) ;
return checked ;
2009-10-16 19:20:34 +00:00
}
} ,
2010-04-30 19:30:44 +00:00
// For radio buttons, only return the value if the radio button is selected.
2009-10-16 19:20:34 +00:00
value : {
'keyup' : function ( ) {
2010-04-30 19:30:44 +00:00
// Radio buttons share the same :input[name="key"] selector.
if ( this . length > 1 ) {
// Initial checked value of radios is undefined, so we return false.
return this . filter ( ':checked' ) . val ( ) || false ;
}
2009-10-16 19:20:34 +00:00
return this . val ( ) ;
2010-02-18 19:24:55 +00:00
} ,
'change' : function ( ) {
2010-04-30 19:30:44 +00:00
// Radio buttons share the same :input[name="key"] selector.
if ( this . length > 1 ) {
// Initial checked value of radios is undefined, so we return false.
return this . filter ( ':checked' ) . val ( ) || false ;
}
2010-02-18 19:24:55 +00:00
return this . val ( ) ;
2009-10-16 19:20:34 +00:00
}
} ,
collapsed : {
'collapsed' : function ( e ) {
2012-01-31 08:03:32 +00:00
return ( typeof e !== 'undefined' && 'value' in e ) ? e . value : this . is ( '.collapsed' ) ;
2009-10-16 19:20:34 +00:00
}
}
} ;
/ * *
* A state object is used for describing the state and performing aliasing .
* /
states . State = function ( state ) {
// We may need the original unresolved name later.
this . pristine = this . name = state ;
// Normalize the state name.
while ( true ) {
// Iteratively remove exclamation marks and invert the value.
while ( this . name . charAt ( 0 ) == '!' ) {
this . name = this . name . substring ( 1 ) ;
this . invert = ! this . invert ;
}
// Replace the state with its normalized name.
if ( this . name in states . State . aliases ) {
this . name = states . State . aliases [ this . name ] ;
}
else {
break ;
}
}
} ;
/ * *
2012-01-31 08:03:32 +00:00
* Creates a new State object by sanitizing the passed value .
2009-10-16 19:20:34 +00:00
* /
states . State . sanitize = function ( state ) {
if ( state instanceof states . State ) {
return state ;
}
else {
return new states . State ( state ) ;
}
} ;
/ * *
* This list of aliases is used to normalize states and associates negated names
* with their respective inverse state .
* /
states . State . aliases = {
'enabled' : '!disabled' ,
'invisible' : '!visible' ,
'invalid' : '!valid' ,
'untouched' : '!touched' ,
'optional' : '!required' ,
'filled' : '!empty' ,
'unchecked' : '!checked' ,
'irrelevant' : '!relevant' ,
'expanded' : '!collapsed' ,
'readwrite' : '!readonly'
} ;
states . State . prototype = {
invert : false ,
/ * *
* Ensures that just using the state object returns the name .
* /
toString : function ( ) {
return this . name ;
}
} ;
/ * *
* Global state change handlers . These are bound to "document" to cover all
* elements whose state changes . Events sent to elements within the page
* bubble up to these handlers . We use this system so that themes and modules
* can override these state change handlers for particular parts of a page .
* /
2012-01-31 08:03:32 +00:00
$ ( document ) . bind ( 'state:disabled' , function ( e ) {
// Only act when this change was triggered by a dependency and not by the
// element monitoring itself.
if ( e . trigger ) {
$ ( e . target )
. attr ( 'disabled' , e . value )
. filter ( '.form-element' )
. closest ( '.form-item, .form-submit, .form-wrapper' ) [ e . value ? 'addClass' : 'removeClass' ] ( 'form-disabled' ) ;
// Note: WebKit nightlies don't reflect that change correctly.
// See https://bugs.webkit.org/show_bug.cgi?id=23789
}
} ) ;
2009-10-16 19:20:34 +00:00
2012-01-31 08:03:32 +00:00
$ ( document ) . bind ( 'state:required' , function ( e ) {
if ( e . trigger ) {
if ( e . value ) {
$ ( e . target ) . closest ( '.form-item, .form-wrapper' ) . find ( 'label' ) . append ( '<abbr class="form-required" title="' + Drupal . t ( 'This field is required.' ) + '">*</abbr>' ) ;
2009-10-16 19:20:34 +00:00
}
2012-01-31 08:03:32 +00:00
else {
$ ( e . target ) . closest ( '.form-item, .form-wrapper' ) . find ( 'label .form-required' ) . remove ( ) ;
2009-10-16 19:20:34 +00:00
}
2012-01-31 08:03:32 +00:00
}
} ) ;
2009-10-16 19:20:34 +00:00
2012-01-31 08:03:32 +00:00
$ ( document ) . bind ( 'state:visible' , function ( e ) {
if ( e . trigger ) {
$ ( e . target ) . closest ( '.form-item, .form-submit, .form-wrapper' ) [ e . value ? 'show' : 'hide' ] ( ) ;
}
} ) ;
$ ( document ) . bind ( 'state:checked' , function ( e ) {
if ( e . trigger ) {
$ ( e . target ) . attr ( 'checked' , e . value ) ;
}
} ) ;
$ ( document ) . bind ( 'state:collapsed' , function ( e ) {
if ( e . trigger ) {
if ( $ ( e . target ) . is ( '.collapsed' ) !== e . value ) {
$ ( '> legend a' , e . target ) . click ( ) ;
2009-10-16 19:20:34 +00:00
}
2012-01-31 08:03:32 +00:00
}
} ) ;
2009-10-16 19:20:34 +00:00
/ * *
* These are helper functions implementing addition "operators" and don ' t
* implement any logic that is particular to states .
* /
2012-01-31 08:03:32 +00:00
// Bitwise AND with a third undefined state.
function ternary ( a , b ) {
return typeof a === 'undefined' ? b : ( typeof b === 'undefined' ? a : a && b ) ;
}
// Inverts a (if it's not undefined) when invert is true.
function invert ( a , invert ) {
return ( invert && typeof a !== 'undefined' ) ? ! a : a ;
}
// Compares two values while ignoring undefined values.
function compare ( a , b ) {
return ( a === b ) ? ( typeof a === 'undefined' ? a : true ) : ( typeof a === 'undefined' || typeof b === 'undefined' ) ;
2009-10-16 19:20:34 +00:00
}
} ) ( jQuery ) ;