4 * The base States namespace.
6 * Having the local states variable allows us to use the States namespace
7 * without having to always declare "Drupal.states".
9 var states
= Drupal
.states
= {
10 // An array of functions that should be postponed.
15 * Attaches the states.
17 Drupal
.behaviors
.states
= {
18 attach: function (context
, settings
) {
19 var $context
= $(context
);
20 for (var selector
in settings
.states
) {
21 for (var state
in settings
.states
[selector
]) {
22 new states
.Dependent({
23 element
: $context
.find(selector
),
24 state
: states
.State
.sanitize(state
),
25 constraints
: settings
.states
[selector
][state
]
30 // Execute all postponed functions now.
31 while (states
.postponed
.length
) {
32 (states
.postponed
.shift())();
38 * Object representing an element that depends on other elements.
41 * Object with the following keys (all of which are required):
42 * - element: A jQuery object of the dependent element
43 * - state: A State object describing the state that is dependent
44 * - constraints: An object with dependency specifications. Lists all elements
45 * that this element depends on. It can be nested and can contain arbitrary
48 states
.Dependent = function (args
) {
49 $.extend(this, { values
: {}, oldValue
: null }, args
);
51 this.dependees
= this.getDependees();
52 for (var selector
in this.dependees
) {
53 this.initializeDependee(selector
, this.dependees
[selector
]);
58 * Comparison functions for comparing the value of an element with the
59 * specification from the dependency settings. If the object type can't be
60 * found in this list, the === operator is used by default.
62 states
.Dependent
.comparisons
= {
63 'RegExp': function (reference
, value
) {
64 return reference
.test(value
);
66 'Function': function (reference
, value
) {
67 // The "reference" variable is a comparison function.
68 return reference(value
);
70 'Number': function (reference
, value
) {
71 // If "reference" is a number and "value" is a string, then cast reference
72 // as a string before applying the strict comparison in compare(). Otherwise
73 // numeric keys in the form's #states array fail to match string values
74 // returned from jQuery's val().
75 return (typeof value
=== 'string') ? compare(reference
.toString(), value
) : compare(reference
, value
);
79 states
.Dependent
.prototype = {
81 * Initializes one of the elements this dependent depends on.
84 * The CSS selector describing the dependee.
85 * @param dependeeStates
86 * The list of states that have to be monitored for tracking the
87 * dependee's compliance status.
89 initializeDependee: function (selector
, dependeeStates
) {
92 // Cache for the states of this dependee.
93 this.values
[selector
] = {};
95 for (var i
in dependeeStates
) {
96 if (dependeeStates
.hasOwnProperty(i
)) {
97 state
= dependeeStates
[i
];
98 // Make sure we're not initializing this selector/state combination twice.
99 if ($.inArray(state
, dependeeStates
) === -1) {
103 state
= states
.State
.sanitize(state
);
105 // Initialize the value of this state.
106 this.values
[selector
][state
.name
] = null;
108 // Monitor state changes of the specified state for this dependee.
109 $(selector
).bind('state:' + state
, $.proxy(function (e
) {
110 this.update(selector
, state
, e
.value
);
113 // Make sure the event we just bound ourselves to is actually fired.
114 new states
.Trigger({ selector
: selector
, state
: state
});
120 * Compares a value with a reference value.
123 * The value used for reference.
125 * CSS selector describing the dependee.
127 * A State object describing the dependee's updated state.
132 compare: function (reference
, selector
, state
) {
133 var value
= this.values
[selector
][state
.name
];
134 if (reference
.constructor.name
in states
.Dependent
.comparisons
) {
135 // Use a custom compare function for certain reference value types.
136 return states
.Dependent
.comparisons
[reference
.constructor.name
](reference
, value
);
139 // Do a plain comparison otherwise.
140 return compare(reference
, value
);
145 * Update the value of a dependee's state.
148 * CSS selector describing the dependee.
150 * A State object describing the dependee's updated state.
152 * The new value for the dependee's updated state.
154 update: function (selector
, state
, value
) {
155 // Only act when the 'new' value is actually new.
156 if (value
!== this.values
[selector
][state
.name
]) {
157 this.values
[selector
][state
.name
] = value
;
163 * Triggers change events in case a state changed.
165 reevaluate: function () {
166 // Check whether any constraint for this dependent state is satisifed.
167 var value
= this.verifyConstraints(this.constraints
);
169 // Only invoke a state change event when the value actually changed.
170 if (value
!== this.oldValue
) {
171 // Store the new value so that we can compare later whether the value
173 this.oldValue
= value
;
175 // Normalize the value to match the normalized state name.
176 value
= invert(value
, this.state
.invert
);
178 // By adding "trigger: true", we ensure that state changes don't go into
180 this.element
.trigger({ type
: 'state:' + this.state
, value
: value
, trigger
: true });
185 * Evaluates child constraints to determine if a constraint is satisfied.
188 * A constraint object or an array of constraints.
190 * The selector for these constraints. If undefined, there isn't yet a
191 * selector that these constraints apply to. In that case, the keys of the
192 * object are interpreted as the selector if encountered.
195 * true or false, depending on whether these constraints are satisfied.
197 verifyConstraints: function(constraints
, selector
) {
199 if ($.isArray(constraints
)) {
200 // This constraint is an array (OR or XOR).
201 var hasXor
= $.inArray('xor', constraints
) === -1;
202 for (var i
= 0, len
= constraints
.length
; i
< len
; i
++) {
203 if (constraints
[i
] != 'xor') {
204 var constraint
= this.checkConstraints(constraints
[i
], selector
, i
);
205 // Return if this is OR and we have a satisfied constraint or if this
206 // is XOR and we have a second satisfied constraint.
207 if (constraint
&& (hasXor
|| result
)) {
210 result
= result
|| constraint
;
214 // Make sure we don't try to iterate over things other than objects. This
215 // shouldn't normally occur, but in case the condition definition is bogus,
216 // we don't want to end up with an infinite loop.
217 else if ($.isPlainObject(constraints
)) {
218 // This constraint is an object (AND).
219 for (var n
in constraints
) {
220 if (constraints
.hasOwnProperty(n
)) {
221 result
= ternary(result
, this.checkConstraints(constraints
[n
], selector
, n
));
222 // False and anything else will evaluate to false, so return when any
223 // false condition is found.
224 if (result
=== false) { return false; }
232 * Checks whether the value matches the requirements for this constraint.
235 * Either the value of a state or an array/object of constraints. In the
236 * latter case, resolving the constraint continues.
238 * The selector for this constraint. If undefined, there isn't yet a
239 * selector that this constraint applies to. In that case, the state key is
240 * propagates to a selector and resolving continues.
242 * The state to check for this constraint. If undefined, resolving
244 * If both selector and state aren't undefined and valid non-numeric
245 * strings, a lookup for the actual value of that selector's state is
246 * performed. This parameter is not a State object but a pristine state
250 * true or false, depending on whether this constraint is satisfied.
252 checkConstraints: function(value
, selector
, state
) {
253 // Normalize the last parameter. If it's non-numeric, we treat it either as
254 // a selector (in case there isn't one yet) or as a trigger/state.
255 if (typeof state
!== 'string' || (/[0-9]/).test(state
[0])) {
258 else if (typeof selector
=== 'undefined') {
259 // Propagate the state to the selector when there isn't one yet.
264 if (state
!== null) {
265 // constraints is the actual constraints of an element to check for.
266 state
= states
.State
.sanitize(state
);
267 return invert(this.compare(value
, selector
, state
), state
.invert
);
270 // Resolve this constraint as an AND/OR operator.
271 return this.verifyConstraints(value
, selector
);
276 * Gathers information about all required triggers.
278 getDependees: function() {
280 // Swivel the lookup function so that we can record all available selector-
281 // state combinations for initialization.
282 var _compare
= this.compare
;
283 this.compare = function(reference
, selector
, state
) {
284 (cache
[selector
] || (cache
[selector
] = [])).push(state
.name
);
285 // Return nothing (=== undefined) so that the constraint loops are not
289 // This call doesn't actually verify anything but uses the resolving
290 // mechanism to go through the constraints array, trying to look up each
291 // value. Since we swivelled the compare function, this comparison returns
292 // undefined and lookup continues until the very end. Instead of lookup up
293 // the value, we record that combination of selector and state so that we
294 // can initialize all triggers.
295 this.verifyConstraints(this.constraints
);
296 // Restore the original function.
297 this.compare
= _compare
;
303 states
.Trigger = function (args
) {
304 $.extend(this, args
);
306 if (this.state
in states
.Trigger
.states
) {
307 this.element
= $(this.selector
);
309 // Only call the trigger initializer when it wasn't yet attached to this
310 // element. Otherwise we'd end up with duplicate events.
311 if (!this.element
.data('trigger:' + this.state
)) {
317 states
.Trigger
.prototype = {
318 initialize: function () {
319 var trigger
= states
.Trigger
.states
[this.state
];
321 if (typeof trigger
== 'function') {
322 // We have a custom trigger initialization function.
323 trigger
.call(window
, this.element
);
326 for (var event
in trigger
) {
327 if (trigger
.hasOwnProperty(event
)) {
328 this.defaultTrigger(event
, trigger
[event
]);
333 // Mark this trigger as initialized for this element.
334 this.element
.data('trigger:' + this.state
, true);
337 defaultTrigger: function (event
, valueFn
) {
338 var oldValue
= valueFn
.call(this.element
);
340 // Attach the event callback.
341 this.element
.bind(event
, $.proxy(function (e
) {
342 var value
= valueFn
.call(this.element
, e
);
343 // Only trigger the event if the value has actually changed.
344 if (oldValue
!== value
) {
345 this.element
.trigger({ type
: 'state:' + this.state
, value
: value
, oldValue
: oldValue
});
350 states
.postponed
.push($.proxy(function () {
351 // Trigger the event once for initialization purposes.
352 this.element
.trigger({ type
: 'state:' + this.state
, value
: oldValue
, oldValue
: null });
358 * This list of states contains functions that are used to monitor the state
359 * of an element. Whenever an element depends on the state of another element,
360 * one of these trigger functions is added to the dependee so that the
361 * dependent element can be updated.
363 states
.Trigger
.states
= {
364 // 'empty' describes the state to be monitored
366 // 'keyup' is the (native DOM) event that we watch for.
367 'keyup': function () {
368 // The function associated to that trigger returns the new value for the
370 return this.val() == '';
375 'change': function () {
376 return this.attr('checked');
380 // For radio buttons, only return the value if the radio button is selected.
382 'keyup': function () {
383 // Radio buttons share the same :input[name="key"] selector.
384 if (this.length
> 1) {
385 // Initial checked value of radios is undefined, so we return false.
386 return this.filter(':checked').val() || false;
390 'change': function () {
391 // Radio buttons share the same :input[name="key"] selector.
392 if (this.length
> 1) {
393 // Initial checked value of radios is undefined, so we return false.
394 return this.filter(':checked').val() || false;
401 'collapsed': function(e
) {
402 return (typeof e
!== 'undefined' && 'value' in e
) ? e
.value
: this.is('.collapsed');
409 * A state object is used for describing the state and performing aliasing.
411 states
.State = function(state
) {
412 // We may need the original unresolved name later.
413 this.pristine
= this.name
= state
;
415 // Normalize the state name.
417 // Iteratively remove exclamation marks and invert the value.
418 while (this.name
.charAt(0) == '!') {
419 this.name
= this.name
.substring(1);
420 this.invert
= !this.invert
;
423 // Replace the state with its normalized name.
424 if (this.name
in states
.State
.aliases
) {
425 this.name
= states
.State
.aliases
[this.name
];
434 * Creates a new State object by sanitizing the passed value.
436 states
.State
.sanitize = function (state
) {
437 if (state
instanceof states
.State
) {
441 return new states
.State(state
);
446 * This list of aliases is used to normalize states and associates negated names
447 * with their respective inverse state.
449 states
.State
.aliases
= {
450 'enabled': '!disabled',
451 'invisible': '!visible',
453 'untouched': '!touched',
454 'optional': '!required',
456 'unchecked': '!checked',
457 'irrelevant': '!relevant',
458 'expanded': '!collapsed',
459 'readwrite': '!readonly'
462 states
.State
.prototype = {
466 * Ensures that just using the state object returns the name.
468 toString: function() {
474 * Global state change handlers. These are bound to "document" to cover all
475 * elements whose state changes. Events sent to elements within the page
476 * bubble up to these handlers. We use this system so that themes and modules
477 * can override these state change handlers for particular parts of a page.
479 $(document
).bind('state:disabled', function(e
) {
480 // Only act when this change was triggered by a dependency and not by the
481 // element monitoring itself.
484 .attr('disabled', e
.value
)
485 .closest('.form-item, .form-submit, .form-wrapper').toggleClass('form-disabled', e
.value
)
486 .find('select, input, textarea').attr('disabled', e
.value
);
488 // Note: WebKit nightlies don't reflect that change correctly.
489 // See https://bugs.webkit.org/show_bug.cgi?id=23789
493 $(document
).bind('state:required', function(e
) {
496 $(e
.target
).closest('.form-item, .form-wrapper').find('label').append('<span class="form-required">*</span>');
499 $(e
.target
).closest('.form-item, .form-wrapper').find('label .form-required').remove();
504 $(document
).bind('state:visible', function(e
) {
506 $(e
.target
).closest('.form-item, .form-submit, .form-wrapper').toggle(e
.value
);
510 $(document
).bind('state:checked', function(e
) {
512 $(e
.target
).attr('checked', e
.value
);
516 $(document
).bind('state:collapsed', function(e
) {
518 if ($(e
.target
).is('.collapsed') !== e
.value
) {
519 $('> legend a', e
.target
).click();
525 * These are helper functions implementing addition "operators" and don't
526 * implement any logic that is particular to states.
529 // Bitwise AND with a third undefined state.
530 function ternary (a
, b
) {
531 return typeof a
=== 'undefined' ? b
: (typeof b
=== 'undefined' ? a
: a
&& b
);
534 // Inverts a (if it's not undefined) when invert is true.
535 function invert (a
, invert
) {
536 return (invert
&& typeof a
!== 'undefined') ? !a
: a
;
539 // Compares two values while ignoring undefined values.
540 function compare (a
, b
) {
541 return (a
=== b
) ? (typeof a
=== 'undefined' ? a
: true) : (typeof a
=== 'undefined' || typeof b
=== 'undefined');