9b98d5dd292b3085f3a35f41597c26f3064728d1
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 for (var selector
in settings
.states
) {
20 for (var state
in settings
.states
[selector
]) {
21 new states
.Dependent({
23 state
: states
.State
.sanitize(state
),
24 dependees
: settings
.states
[selector
][state
]
29 // Execute all postponed functions now.
30 while (states
.postponed
.length
) {
31 (states
.postponed
.shift())();
37 * Object representing an element that depends on other elements.
40 * Object with the following keys (all of which are required):
41 * - element: A jQuery object of the dependent element
42 * - state: A State object describing the state that is dependent
43 * - dependees: An object with dependency specifications. Lists all elements
44 * that this element depends on.
46 states
.Dependent = function (args
) {
47 $.extend(this, { values
: {}, oldValue
: undefined }, args
);
49 for (var selector
in this.dependees
) {
50 this.initializeDependee(selector
, this.dependees
[selector
]);
55 * Comparison functions for comparing the value of an element with the
56 * specification from the dependency settings. If the object type can't be
57 * found in this list, the === operator is used by default.
59 states
.Dependent
.comparisons
= {
60 'RegExp': function (reference
, value
) {
61 return reference
.test(value
);
63 'Function': function (reference
, value
) {
64 // The "reference" variable is a comparison function.
65 return reference(value
);
67 'Number': function (reference
, value
) {
68 // If "reference" is a number and "value" is a string, then cast reference
69 // as a string before applying the strict comparison in compare(). Otherwise
70 // numeric keys in the form's #states array fail to match string values
71 // returned from jQuery's val().
72 return (value
.constructor.name
=== 'String') ? compare(String(reference
), value
) : compare(reference
, value
);
76 states
.Dependent
.prototype = {
78 * Initializes one of the elements this dependent depends on.
81 * The CSS selector describing the dependee.
82 * @param dependeeStates
83 * The list of states that have to be monitored for tracking the
84 * dependee's compliance status.
86 initializeDependee: function (selector
, dependeeStates
) {
89 // Cache for the states of this dependee.
90 self
.values
[selector
] = {};
92 $.each(dependeeStates
, function (state
, value
) {
93 state
= states
.State
.sanitize(state
);
95 // Initialize the value of this state.
96 self
.values
[selector
][state
.pristine
] = undefined;
98 // Monitor state changes of the specified state for this dependee.
99 $(selector
).bind('state:' + state
, function (e
) {
100 var complies
= self
.compare(value
, e
.value
);
101 self
.update(selector
, state
, complies
);
104 // Make sure the event we just bound ourselves to is actually fired.
105 new states
.Trigger({ selector
: selector
, state
: state
});
110 * Compares a value with a reference value.
113 * The value used for reference.
115 * The value to compare with the reference value.
117 * true, undefined or false.
119 compare: function (reference
, value
) {
120 if (reference
.constructor.name
in states
.Dependent
.comparisons
) {
121 // Use a custom compare function for certain reference value types.
122 return states
.Dependent
.comparisons
[reference
.constructor.name
](reference
, value
);
125 // Do a plain comparison otherwise.
126 return compare(reference
, value
);
131 * Update the value of a dependee's state.
134 * CSS selector describing the dependee.
136 * A State object describing the dependee's updated state.
138 * The new value for the dependee's updated state.
140 update: function (selector
, state
, value
) {
141 // Only act when the 'new' value is actually new.
142 if (value
!== this.values
[selector
][state
.pristine
]) {
143 this.values
[selector
][state
.pristine
] = value
;
149 * Triggers change events in case a state changed.
151 reevaluate: function () {
152 var value
= undefined;
154 // Merge all individual values to find out whether this dependee complies.
155 for (var selector
in this.values
) {
156 for (var state
in this.values
[selector
]) {
157 state
= states
.State
.sanitize(state
);
158 var complies
= this.values
[selector
][state
.pristine
];
159 value
= ternary(value
, invert(complies
, state
.invert
));
163 // Only invoke a state change event when the value actually changed.
164 if (value
!== this.oldValue
) {
165 // Store the new value so that we can compare later whether the value
167 this.oldValue
= value
;
169 // Normalize the value to match the normalized state name.
170 value
= invert(value
, this.state
.invert
);
172 // By adding "trigger: true", we ensure that state changes don't go into
174 this.element
.trigger({ type
: 'state:' + this.state
, value
: value
, trigger
: true });
179 states
.Trigger = function (args
) {
180 $.extend(this, args
);
182 if (this.state
in states
.Trigger
.states
) {
183 this.element
= $(this.selector
);
185 // Only call the trigger initializer when it wasn't yet attached to this
186 // element. Otherwise we'd end up with duplicate events.
187 if (!this.element
.data('trigger:' + this.state
)) {
193 states
.Trigger
.prototype = {
194 initialize: function () {
196 var trigger
= states
.Trigger
.states
[this.state
];
198 if (typeof trigger
== 'function') {
199 // We have a custom trigger initialization function.
200 trigger
.call(window
, this.element
);
203 $.each(trigger
, function (event
, valueFn
) {
204 self
.defaultTrigger(event
, valueFn
);
208 // Mark this trigger as initialized for this element.
209 this.element
.data('trigger:' + this.state
, true);
212 defaultTrigger: function (event
, valueFn
) {
214 var oldValue
= valueFn
.call(this.element
);
216 // Attach the event callback.
217 this.element
.bind(event
, function (e
) {
218 var value
= valueFn
.call(self
.element
, e
);
219 // Only trigger the event if the value has actually changed.
220 if (oldValue
!== value
) {
221 self
.element
.trigger({ type
: 'state:' + self
.state
, value
: value
, oldValue
: oldValue
});
226 states
.postponed
.push(function () {
227 // Trigger the event once for initialization purposes.
228 self
.element
.trigger({ type
: 'state:' + self
.state
, value
: oldValue
, oldValue
: undefined });
234 * This list of states contains functions that are used to monitor the state
235 * of an element. Whenever an element depends on the state of another element,
236 * one of these trigger functions is added to the dependee so that the
237 * dependent element can be updated.
239 states
.Trigger
.states
= {
240 // 'empty' describes the state to be monitored
242 // 'keyup' is the (native DOM) event that we watch for.
243 'keyup': function () {
244 // The function associated to that trigger returns the new value for the
246 return this.val() == '';
251 'change': function () {
252 return this.attr('checked');
256 // For radio buttons, only return the value if the radio button is selected.
258 'keyup': function () {
259 // Radio buttons share the same :input[name="key"] selector.
260 if (this.length
> 1) {
261 // Initial checked value of radios is undefined, so we return false.
262 return this.filter(':checked').val() || false;
266 'change': function () {
267 // Radio buttons share the same :input[name="key"] selector.
268 if (this.length
> 1) {
269 // Initial checked value of radios is undefined, so we return false.
270 return this.filter(':checked').val() || false;
277 'collapsed': function(e
) {
278 return (e
!== undefined && 'value' in e
) ? e
.value
: this.is('.collapsed');
285 * A state object is used for describing the state and performing aliasing.
287 states
.State = function(state
) {
288 // We may need the original unresolved name later.
289 this.pristine
= this.name
= state
;
291 // Normalize the state name.
293 // Iteratively remove exclamation marks and invert the value.
294 while (this.name
.charAt(0) == '!') {
295 this.name
= this.name
.substring(1);
296 this.invert
= !this.invert
;
299 // Replace the state with its normalized name.
300 if (this.name
in states
.State
.aliases
) {
301 this.name
= states
.State
.aliases
[this.name
];
310 * Create a new State object by sanitizing the passed value.
312 states
.State
.sanitize = function (state
) {
313 if (state
instanceof states
.State
) {
317 return new states
.State(state
);
322 * This list of aliases is used to normalize states and associates negated names
323 * with their respective inverse state.
325 states
.State
.aliases
= {
326 'enabled': '!disabled',
327 'invisible': '!visible',
329 'untouched': '!touched',
330 'optional': '!required',
332 'unchecked': '!checked',
333 'irrelevant': '!relevant',
334 'expanded': '!collapsed',
335 'readwrite': '!readonly'
338 states
.State
.prototype = {
342 * Ensures that just using the state object returns the name.
344 toString: function() {
350 * Global state change handlers. These are bound to "document" to cover all
351 * elements whose state changes. Events sent to elements within the page
352 * bubble up to these handlers. We use this system so that themes and modules
353 * can override these state change handlers for particular parts of a page.
356 $(document
).bind('state:disabled', function(e
) {
357 // Only act when this change was triggered by a dependency and not by the
358 // element monitoring itself.
361 .attr('disabled', e
.value
)
362 .filter('.form-element')
363 .closest('.form-item, .form-submit, .form-wrapper')[e
.value
? 'addClass' : 'removeClass']('form-disabled');
365 // Note: WebKit nightlies don't reflect that change correctly.
366 // See https://bugs.webkit.org/show_bug.cgi?id=23789
370 $(document
).bind('state:required', function(e
) {
373 $(e
.target
).closest('.form-item, .form-wrapper').find('label').append('<span class="form-required">*</span>');
376 $(e
.target
).closest('.form-item, .form-wrapper').find('label .form-required').remove();
381 $(document
).bind('state:visible', function(e
) {
383 $(e
.target
).closest('.form-item, .form-submit, .form-wrapper')[e
.value
? 'show' : 'hide']();
387 $(document
).bind('state:checked', function(e
) {
389 $(e
.target
).attr('checked', e
.value
);
393 $(document
).bind('state:collapsed', function(e
) {
395 if ($(e
.target
).is('.collapsed') !== e
.value
) {
396 $('> legend a', e
.target
).click();
403 * These are helper functions implementing addition "operators" and don't
404 * implement any logic that is particular to states.
407 // Bitwise AND with a third undefined state.
408 function ternary (a
, b
) {
409 return a
=== undefined ? b
: (b
=== undefined ? a
: a
&& b
);
412 // Inverts a (if it's not undefined) when invert is true.
413 function invert (a
, invert
) {
414 return (invert
&& a
!== undefined) ? !a
: a
;
417 // Compares two values while ignoring undefined values.
418 function compare (a
, b
) {
419 return (a
=== b
) ? (a
=== undefined ? a
: true) : (a
=== undefined || b
=== undefined);