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 ) ,
dependees : settings . states [ selector ] [ state ]
} ) ;
}
}
// 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
2009-10-16 19:20:34 +00:00
* - dependees : An object with dependency specifications . Lists all elements
* that this element depends on .
* /
2010-04-29 03:52:04 +00:00
states . Dependent = function ( args ) {
2009-10-16 19:20:34 +00:00
$ . extend ( this , { values : { } , oldValue : undefined } , args ) ;
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().
return ( value . constructor . name === 'String' ) ? compare ( String ( reference ) , 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 ) {
var self = this ;
// Cache for the states of this dependee.
self . values [ selector ] = { } ;
$ . each ( dependeeStates , function ( state , value ) {
state = states . State . sanitize ( state ) ;
// Initialize the value of this state.
self . values [ selector ] [ state . pristine ] = undefined ;
// Monitor state changes of the specified state for this dependee.
$ ( selector ) . bind ( 'state:' + state , function ( e ) {
var complies = self . compare ( value , e . value ) ;
self . update ( selector , state , complies ) ;
} ) ;
// Make sure the event we just bound ourselves to is actually fired.
new states . Trigger ( { selector : selector , state : state } ) ;
} ) ;
} ,
/ * *
* Compares a value with a reference value .
*
* @ param reference
* The value used for reference .
* @ param value
* The value to compare with the reference value .
* @ return
* true , undefined or false .
* /
compare : function ( reference , value ) {
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.
if ( value !== this . values [ selector ] [ state . pristine ] ) {
this . values [ selector ] [ state . pristine ] = value ;
this . reevaluate ( ) ;
}
} ,
/ * *
* Triggers change events in case a state changed .
* /
reevaluate : function ( ) {
var value = undefined ;
// Merge all individual values to find out whether this dependee complies.
for ( var selector in this . values ) {
for ( var state in this . values [ selector ] ) {
state = states . State . sanitize ( state ) ;
var complies = this . values [ selector ] [ state . pristine ] ;
value = ternary ( value , invert ( complies , state . invert ) ) ;
}
}
// 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 } ) ;
}
}
} ;
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 self = this ;
var trigger = states . Trigger . states [ this . state ] ;
if ( typeof trigger == 'function' ) {
// We have a custom trigger initialization function.
trigger . call ( window , this . element ) ;
}
else {
$ . each ( trigger , function ( event , valueFn ) {
self . defaultTrigger ( event , valueFn ) ;
} ) ;
}
// Mark this trigger as initialized for this element.
this . element . data ( 'trigger:' + this . state , true ) ;
} ,
defaultTrigger : function ( event , valueFn ) {
var self = this ;
var oldValue = valueFn . call ( this . element ) ;
// Attach the event callback.
this . element . bind ( event , function ( e ) {
var value = valueFn . call ( self . element , e ) ;
// Only trigger the event if the value has actually changed.
if ( oldValue !== value ) {
self . element . trigger ( { type : 'state:' + self . state , value : value , oldValue : oldValue } ) ;
oldValue = value ;
}
} ) ;
states . postponed . push ( function ( ) {
// Trigger the event once for initialization purposes.
self . element . trigger ( { type : 'state:' + self . state , value : oldValue , oldValue : undefined } ) ;
} ) ;
}
} ;
/ * *
* 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 ) {
return ( e !== undefined && 'value' in e ) ? e . value : this . is ( '.collapsed' ) ;
}
}
} ;
/ * *
* 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 ;
}
}
} ;
/ * *
* Create a new State object by sanitizing the passed value .
* /
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 .
* /
{
$ ( 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' )
2010-12-06 16:10:29 +00:00
. closest ( '.form-item, .form-submit, .form-wrapper' ) [ e . value ? 'addClass' : 'removeClass' ] ( 'form-disabled' ) ;
2009-10-16 19:20:34 +00:00
// Note: WebKit nightlies don't reflect that change correctly.
// See https://bugs.webkit.org/show_bug.cgi?id=23789
}
} ) ;
$ ( document ) . bind ( 'state:required' , function ( e ) {
if ( e . trigger ) {
2011-04-12 20:00:57 +00:00
if ( e . value ) {
2011-07-05 13:44:58 +00:00
$ ( e . target ) . closest ( '.form-item, .form-wrapper' ) . find ( 'label' ) . append ( '<abbr class="form-required" title="' + Drupal . t ( 'This field is required.' ) + '">*</abbr>' ) ;
2011-04-12 20:00:57 +00:00
}
else {
$ ( e . target ) . closest ( '.form-item, .form-wrapper' ) . find ( 'label .form-required' ) . remove ( ) ;
}
2009-10-16 19:20:34 +00:00
}
} ) ;
$ ( document ) . bind ( 'state:visible' , function ( e ) {
if ( e . trigger ) {
2010-12-06 16:10:29 +00:00
$ ( e . target ) . closest ( '.form-item, .form-submit, .form-wrapper' ) [ e . value ? 'show' : 'hide' ] ( ) ;
2009-10-16 19:20:34 +00:00
}
} ) ;
$ ( 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 ( ) ;
}
}
} ) ;
}
/ * *
* These are helper functions implementing addition "operators" and don ' t
* implement any logic that is particular to states .
* /
{
// Bitwise AND with a third undefined state.
function ternary ( a , b ) {
return a === undefined ? b : ( b === undefined ? a : a && b ) ;
} ;
// Inverts a (if it's not undefined) when invert is true.
function invert ( a , invert ) {
return ( invert && a !== undefined ) ? ! a : a ;
} ;
// Compares two values while ignoring undefined values.
function compare ( a , b ) {
return ( a === b ) ? ( a === undefined ? a : true ) : ( a === undefined || b === undefined ) ;
}
}
} ) ( jQuery ) ;