danielebarchiesi@0
|
1 (function ($) {
|
danielebarchiesi@0
|
2
|
danielebarchiesi@0
|
3 /**
|
danielebarchiesi@0
|
4 * The base States namespace.
|
danielebarchiesi@0
|
5 *
|
danielebarchiesi@0
|
6 * Having the local states variable allows us to use the States namespace
|
danielebarchiesi@0
|
7 * without having to always declare "Drupal.states".
|
danielebarchiesi@0
|
8 */
|
danielebarchiesi@0
|
9 var states = Drupal.states = {
|
danielebarchiesi@0
|
10 // An array of functions that should be postponed.
|
danielebarchiesi@0
|
11 postponed: []
|
danielebarchiesi@0
|
12 };
|
danielebarchiesi@0
|
13
|
danielebarchiesi@0
|
14 /**
|
danielebarchiesi@0
|
15 * Attaches the states.
|
danielebarchiesi@0
|
16 */
|
danielebarchiesi@0
|
17 Drupal.behaviors.states = {
|
danielebarchiesi@0
|
18 attach: function (context, settings) {
|
danielebarchiesi@0
|
19 var $context = $(context);
|
danielebarchiesi@0
|
20 for (var selector in settings.states) {
|
danielebarchiesi@0
|
21 for (var state in settings.states[selector]) {
|
danielebarchiesi@0
|
22 new states.Dependent({
|
danielebarchiesi@0
|
23 element: $context.find(selector),
|
danielebarchiesi@0
|
24 state: states.State.sanitize(state),
|
danielebarchiesi@0
|
25 constraints: settings.states[selector][state]
|
danielebarchiesi@0
|
26 });
|
danielebarchiesi@0
|
27 }
|
danielebarchiesi@0
|
28 }
|
danielebarchiesi@0
|
29
|
danielebarchiesi@0
|
30 // Execute all postponed functions now.
|
danielebarchiesi@0
|
31 while (states.postponed.length) {
|
danielebarchiesi@0
|
32 (states.postponed.shift())();
|
danielebarchiesi@0
|
33 }
|
danielebarchiesi@0
|
34 }
|
danielebarchiesi@0
|
35 };
|
danielebarchiesi@0
|
36
|
danielebarchiesi@0
|
37 /**
|
danielebarchiesi@0
|
38 * Object representing an element that depends on other elements.
|
danielebarchiesi@0
|
39 *
|
danielebarchiesi@0
|
40 * @param args
|
danielebarchiesi@0
|
41 * Object with the following keys (all of which are required):
|
danielebarchiesi@0
|
42 * - element: A jQuery object of the dependent element
|
danielebarchiesi@0
|
43 * - state: A State object describing the state that is dependent
|
danielebarchiesi@0
|
44 * - constraints: An object with dependency specifications. Lists all elements
|
danielebarchiesi@0
|
45 * that this element depends on. It can be nested and can contain arbitrary
|
danielebarchiesi@0
|
46 * AND and OR clauses.
|
danielebarchiesi@0
|
47 */
|
danielebarchiesi@0
|
48 states.Dependent = function (args) {
|
danielebarchiesi@0
|
49 $.extend(this, { values: {}, oldValue: null }, args);
|
danielebarchiesi@0
|
50
|
danielebarchiesi@0
|
51 this.dependees = this.getDependees();
|
danielebarchiesi@0
|
52 for (var selector in this.dependees) {
|
danielebarchiesi@0
|
53 this.initializeDependee(selector, this.dependees[selector]);
|
danielebarchiesi@0
|
54 }
|
danielebarchiesi@0
|
55 };
|
danielebarchiesi@0
|
56
|
danielebarchiesi@0
|
57 /**
|
danielebarchiesi@0
|
58 * Comparison functions for comparing the value of an element with the
|
danielebarchiesi@0
|
59 * specification from the dependency settings. If the object type can't be
|
danielebarchiesi@0
|
60 * found in this list, the === operator is used by default.
|
danielebarchiesi@0
|
61 */
|
danielebarchiesi@0
|
62 states.Dependent.comparisons = {
|
danielebarchiesi@0
|
63 'RegExp': function (reference, value) {
|
danielebarchiesi@0
|
64 return reference.test(value);
|
danielebarchiesi@0
|
65 },
|
danielebarchiesi@0
|
66 'Function': function (reference, value) {
|
danielebarchiesi@0
|
67 // The "reference" variable is a comparison function.
|
danielebarchiesi@0
|
68 return reference(value);
|
danielebarchiesi@0
|
69 },
|
danielebarchiesi@0
|
70 'Number': function (reference, value) {
|
danielebarchiesi@0
|
71 // If "reference" is a number and "value" is a string, then cast reference
|
danielebarchiesi@0
|
72 // as a string before applying the strict comparison in compare(). Otherwise
|
danielebarchiesi@0
|
73 // numeric keys in the form's #states array fail to match string values
|
danielebarchiesi@0
|
74 // returned from jQuery's val().
|
danielebarchiesi@0
|
75 return (typeof value === 'string') ? compare(reference.toString(), value) : compare(reference, value);
|
danielebarchiesi@0
|
76 }
|
danielebarchiesi@0
|
77 };
|
danielebarchiesi@0
|
78
|
danielebarchiesi@0
|
79 states.Dependent.prototype = {
|
danielebarchiesi@0
|
80 /**
|
danielebarchiesi@0
|
81 * Initializes one of the elements this dependent depends on.
|
danielebarchiesi@0
|
82 *
|
danielebarchiesi@0
|
83 * @param selector
|
danielebarchiesi@0
|
84 * The CSS selector describing the dependee.
|
danielebarchiesi@0
|
85 * @param dependeeStates
|
danielebarchiesi@0
|
86 * The list of states that have to be monitored for tracking the
|
danielebarchiesi@0
|
87 * dependee's compliance status.
|
danielebarchiesi@0
|
88 */
|
danielebarchiesi@0
|
89 initializeDependee: function (selector, dependeeStates) {
|
danielebarchiesi@0
|
90 var state;
|
danielebarchiesi@0
|
91
|
danielebarchiesi@0
|
92 // Cache for the states of this dependee.
|
danielebarchiesi@0
|
93 this.values[selector] = {};
|
danielebarchiesi@0
|
94
|
danielebarchiesi@0
|
95 for (var i in dependeeStates) {
|
danielebarchiesi@0
|
96 if (dependeeStates.hasOwnProperty(i)) {
|
danielebarchiesi@0
|
97 state = dependeeStates[i];
|
danielebarchiesi@0
|
98 // Make sure we're not initializing this selector/state combination twice.
|
danielebarchiesi@0
|
99 if ($.inArray(state, dependeeStates) === -1) {
|
danielebarchiesi@0
|
100 continue;
|
danielebarchiesi@0
|
101 }
|
danielebarchiesi@0
|
102
|
danielebarchiesi@0
|
103 state = states.State.sanitize(state);
|
danielebarchiesi@0
|
104
|
danielebarchiesi@0
|
105 // Initialize the value of this state.
|
danielebarchiesi@0
|
106 this.values[selector][state.name] = null;
|
danielebarchiesi@0
|
107
|
danielebarchiesi@0
|
108 // Monitor state changes of the specified state for this dependee.
|
danielebarchiesi@0
|
109 $(selector).bind('state:' + state, $.proxy(function (e) {
|
danielebarchiesi@0
|
110 this.update(selector, state, e.value);
|
danielebarchiesi@0
|
111 }, this));
|
danielebarchiesi@0
|
112
|
danielebarchiesi@0
|
113 // Make sure the event we just bound ourselves to is actually fired.
|
danielebarchiesi@0
|
114 new states.Trigger({ selector: selector, state: state });
|
danielebarchiesi@0
|
115 }
|
danielebarchiesi@0
|
116 }
|
danielebarchiesi@0
|
117 },
|
danielebarchiesi@0
|
118
|
danielebarchiesi@0
|
119 /**
|
danielebarchiesi@0
|
120 * Compares a value with a reference value.
|
danielebarchiesi@0
|
121 *
|
danielebarchiesi@0
|
122 * @param reference
|
danielebarchiesi@0
|
123 * The value used for reference.
|
danielebarchiesi@0
|
124 * @param selector
|
danielebarchiesi@0
|
125 * CSS selector describing the dependee.
|
danielebarchiesi@0
|
126 * @param state
|
danielebarchiesi@0
|
127 * A State object describing the dependee's updated state.
|
danielebarchiesi@0
|
128 *
|
danielebarchiesi@0
|
129 * @return
|
danielebarchiesi@0
|
130 * true or false.
|
danielebarchiesi@0
|
131 */
|
danielebarchiesi@0
|
132 compare: function (reference, selector, state) {
|
danielebarchiesi@0
|
133 var value = this.values[selector][state.name];
|
danielebarchiesi@0
|
134 if (reference.constructor.name in states.Dependent.comparisons) {
|
danielebarchiesi@0
|
135 // Use a custom compare function for certain reference value types.
|
danielebarchiesi@0
|
136 return states.Dependent.comparisons[reference.constructor.name](reference, value);
|
danielebarchiesi@0
|
137 }
|
danielebarchiesi@0
|
138 else {
|
danielebarchiesi@0
|
139 // Do a plain comparison otherwise.
|
danielebarchiesi@0
|
140 return compare(reference, value);
|
danielebarchiesi@0
|
141 }
|
danielebarchiesi@0
|
142 },
|
danielebarchiesi@0
|
143
|
danielebarchiesi@0
|
144 /**
|
danielebarchiesi@0
|
145 * Update the value of a dependee's state.
|
danielebarchiesi@0
|
146 *
|
danielebarchiesi@0
|
147 * @param selector
|
danielebarchiesi@0
|
148 * CSS selector describing the dependee.
|
danielebarchiesi@0
|
149 * @param state
|
danielebarchiesi@0
|
150 * A State object describing the dependee's updated state.
|
danielebarchiesi@0
|
151 * @param value
|
danielebarchiesi@0
|
152 * The new value for the dependee's updated state.
|
danielebarchiesi@0
|
153 */
|
danielebarchiesi@0
|
154 update: function (selector, state, value) {
|
danielebarchiesi@0
|
155 // Only act when the 'new' value is actually new.
|
danielebarchiesi@0
|
156 if (value !== this.values[selector][state.name]) {
|
danielebarchiesi@0
|
157 this.values[selector][state.name] = value;
|
danielebarchiesi@0
|
158 this.reevaluate();
|
danielebarchiesi@0
|
159 }
|
danielebarchiesi@0
|
160 },
|
danielebarchiesi@0
|
161
|
danielebarchiesi@0
|
162 /**
|
danielebarchiesi@0
|
163 * Triggers change events in case a state changed.
|
danielebarchiesi@0
|
164 */
|
danielebarchiesi@0
|
165 reevaluate: function () {
|
danielebarchiesi@0
|
166 // Check whether any constraint for this dependent state is satisifed.
|
danielebarchiesi@0
|
167 var value = this.verifyConstraints(this.constraints);
|
danielebarchiesi@0
|
168
|
danielebarchiesi@0
|
169 // Only invoke a state change event when the value actually changed.
|
danielebarchiesi@0
|
170 if (value !== this.oldValue) {
|
danielebarchiesi@0
|
171 // Store the new value so that we can compare later whether the value
|
danielebarchiesi@0
|
172 // actually changed.
|
danielebarchiesi@0
|
173 this.oldValue = value;
|
danielebarchiesi@0
|
174
|
danielebarchiesi@0
|
175 // Normalize the value to match the normalized state name.
|
danielebarchiesi@0
|
176 value = invert(value, this.state.invert);
|
danielebarchiesi@0
|
177
|
danielebarchiesi@0
|
178 // By adding "trigger: true", we ensure that state changes don't go into
|
danielebarchiesi@0
|
179 // infinite loops.
|
danielebarchiesi@0
|
180 this.element.trigger({ type: 'state:' + this.state, value: value, trigger: true });
|
danielebarchiesi@0
|
181 }
|
danielebarchiesi@0
|
182 },
|
danielebarchiesi@0
|
183
|
danielebarchiesi@0
|
184 /**
|
danielebarchiesi@0
|
185 * Evaluates child constraints to determine if a constraint is satisfied.
|
danielebarchiesi@0
|
186 *
|
danielebarchiesi@0
|
187 * @param constraints
|
danielebarchiesi@0
|
188 * A constraint object or an array of constraints.
|
danielebarchiesi@0
|
189 * @param selector
|
danielebarchiesi@0
|
190 * The selector for these constraints. If undefined, there isn't yet a
|
danielebarchiesi@0
|
191 * selector that these constraints apply to. In that case, the keys of the
|
danielebarchiesi@0
|
192 * object are interpreted as the selector if encountered.
|
danielebarchiesi@0
|
193 *
|
danielebarchiesi@0
|
194 * @return
|
danielebarchiesi@0
|
195 * true or false, depending on whether these constraints are satisfied.
|
danielebarchiesi@0
|
196 */
|
danielebarchiesi@0
|
197 verifyConstraints: function(constraints, selector) {
|
danielebarchiesi@0
|
198 var result;
|
danielebarchiesi@0
|
199 if ($.isArray(constraints)) {
|
danielebarchiesi@0
|
200 // This constraint is an array (OR or XOR).
|
danielebarchiesi@0
|
201 var hasXor = $.inArray('xor', constraints) === -1;
|
danielebarchiesi@0
|
202 for (var i = 0, len = constraints.length; i < len; i++) {
|
danielebarchiesi@0
|
203 if (constraints[i] != 'xor') {
|
danielebarchiesi@0
|
204 var constraint = this.checkConstraints(constraints[i], selector, i);
|
danielebarchiesi@0
|
205 // Return if this is OR and we have a satisfied constraint or if this
|
danielebarchiesi@0
|
206 // is XOR and we have a second satisfied constraint.
|
danielebarchiesi@0
|
207 if (constraint && (hasXor || result)) {
|
danielebarchiesi@0
|
208 return hasXor;
|
danielebarchiesi@0
|
209 }
|
danielebarchiesi@0
|
210 result = result || constraint;
|
danielebarchiesi@0
|
211 }
|
danielebarchiesi@0
|
212 }
|
danielebarchiesi@0
|
213 }
|
danielebarchiesi@0
|
214 // Make sure we don't try to iterate over things other than objects. This
|
danielebarchiesi@0
|
215 // shouldn't normally occur, but in case the condition definition is bogus,
|
danielebarchiesi@0
|
216 // we don't want to end up with an infinite loop.
|
danielebarchiesi@0
|
217 else if ($.isPlainObject(constraints)) {
|
danielebarchiesi@0
|
218 // This constraint is an object (AND).
|
danielebarchiesi@0
|
219 for (var n in constraints) {
|
danielebarchiesi@0
|
220 if (constraints.hasOwnProperty(n)) {
|
danielebarchiesi@0
|
221 result = ternary(result, this.checkConstraints(constraints[n], selector, n));
|
danielebarchiesi@0
|
222 // False and anything else will evaluate to false, so return when any
|
danielebarchiesi@0
|
223 // false condition is found.
|
danielebarchiesi@0
|
224 if (result === false) { return false; }
|
danielebarchiesi@0
|
225 }
|
danielebarchiesi@0
|
226 }
|
danielebarchiesi@0
|
227 }
|
danielebarchiesi@0
|
228 return result;
|
danielebarchiesi@0
|
229 },
|
danielebarchiesi@0
|
230
|
danielebarchiesi@0
|
231 /**
|
danielebarchiesi@0
|
232 * Checks whether the value matches the requirements for this constraint.
|
danielebarchiesi@0
|
233 *
|
danielebarchiesi@0
|
234 * @param value
|
danielebarchiesi@0
|
235 * Either the value of a state or an array/object of constraints. In the
|
danielebarchiesi@0
|
236 * latter case, resolving the constraint continues.
|
danielebarchiesi@0
|
237 * @param selector
|
danielebarchiesi@0
|
238 * The selector for this constraint. If undefined, there isn't yet a
|
danielebarchiesi@0
|
239 * selector that this constraint applies to. In that case, the state key is
|
danielebarchiesi@0
|
240 * propagates to a selector and resolving continues.
|
danielebarchiesi@0
|
241 * @param state
|
danielebarchiesi@0
|
242 * The state to check for this constraint. If undefined, resolving
|
danielebarchiesi@0
|
243 * continues.
|
danielebarchiesi@0
|
244 * If both selector and state aren't undefined and valid non-numeric
|
danielebarchiesi@0
|
245 * strings, a lookup for the actual value of that selector's state is
|
danielebarchiesi@0
|
246 * performed. This parameter is not a State object but a pristine state
|
danielebarchiesi@0
|
247 * string.
|
danielebarchiesi@0
|
248 *
|
danielebarchiesi@0
|
249 * @return
|
danielebarchiesi@0
|
250 * true or false, depending on whether this constraint is satisfied.
|
danielebarchiesi@0
|
251 */
|
danielebarchiesi@0
|
252 checkConstraints: function(value, selector, state) {
|
danielebarchiesi@0
|
253 // Normalize the last parameter. If it's non-numeric, we treat it either as
|
danielebarchiesi@0
|
254 // a selector (in case there isn't one yet) or as a trigger/state.
|
danielebarchiesi@0
|
255 if (typeof state !== 'string' || (/[0-9]/).test(state[0])) {
|
danielebarchiesi@0
|
256 state = null;
|
danielebarchiesi@0
|
257 }
|
danielebarchiesi@0
|
258 else if (typeof selector === 'undefined') {
|
danielebarchiesi@0
|
259 // Propagate the state to the selector when there isn't one yet.
|
danielebarchiesi@0
|
260 selector = state;
|
danielebarchiesi@0
|
261 state = null;
|
danielebarchiesi@0
|
262 }
|
danielebarchiesi@0
|
263
|
danielebarchiesi@0
|
264 if (state !== null) {
|
danielebarchiesi@0
|
265 // constraints is the actual constraints of an element to check for.
|
danielebarchiesi@0
|
266 state = states.State.sanitize(state);
|
danielebarchiesi@0
|
267 return invert(this.compare(value, selector, state), state.invert);
|
danielebarchiesi@0
|
268 }
|
danielebarchiesi@0
|
269 else {
|
danielebarchiesi@0
|
270 // Resolve this constraint as an AND/OR operator.
|
danielebarchiesi@0
|
271 return this.verifyConstraints(value, selector);
|
danielebarchiesi@0
|
272 }
|
danielebarchiesi@0
|
273 },
|
danielebarchiesi@0
|
274
|
danielebarchiesi@0
|
275 /**
|
danielebarchiesi@0
|
276 * Gathers information about all required triggers.
|
danielebarchiesi@0
|
277 */
|
danielebarchiesi@0
|
278 getDependees: function() {
|
danielebarchiesi@0
|
279 var cache = {};
|
danielebarchiesi@0
|
280 // Swivel the lookup function so that we can record all available selector-
|
danielebarchiesi@0
|
281 // state combinations for initialization.
|
danielebarchiesi@0
|
282 var _compare = this.compare;
|
danielebarchiesi@0
|
283 this.compare = function(reference, selector, state) {
|
danielebarchiesi@0
|
284 (cache[selector] || (cache[selector] = [])).push(state.name);
|
danielebarchiesi@0
|
285 // Return nothing (=== undefined) so that the constraint loops are not
|
danielebarchiesi@0
|
286 // broken.
|
danielebarchiesi@0
|
287 };
|
danielebarchiesi@0
|
288
|
danielebarchiesi@0
|
289 // This call doesn't actually verify anything but uses the resolving
|
danielebarchiesi@0
|
290 // mechanism to go through the constraints array, trying to look up each
|
danielebarchiesi@0
|
291 // value. Since we swivelled the compare function, this comparison returns
|
danielebarchiesi@0
|
292 // undefined and lookup continues until the very end. Instead of lookup up
|
danielebarchiesi@0
|
293 // the value, we record that combination of selector and state so that we
|
danielebarchiesi@0
|
294 // can initialize all triggers.
|
danielebarchiesi@0
|
295 this.verifyConstraints(this.constraints);
|
danielebarchiesi@0
|
296 // Restore the original function.
|
danielebarchiesi@0
|
297 this.compare = _compare;
|
danielebarchiesi@0
|
298
|
danielebarchiesi@0
|
299 return cache;
|
danielebarchiesi@0
|
300 }
|
danielebarchiesi@0
|
301 };
|
danielebarchiesi@0
|
302
|
danielebarchiesi@0
|
303 states.Trigger = function (args) {
|
danielebarchiesi@0
|
304 $.extend(this, args);
|
danielebarchiesi@0
|
305
|
danielebarchiesi@0
|
306 if (this.state in states.Trigger.states) {
|
danielebarchiesi@0
|
307 this.element = $(this.selector);
|
danielebarchiesi@0
|
308
|
danielebarchiesi@0
|
309 // Only call the trigger initializer when it wasn't yet attached to this
|
danielebarchiesi@0
|
310 // element. Otherwise we'd end up with duplicate events.
|
danielebarchiesi@0
|
311 if (!this.element.data('trigger:' + this.state)) {
|
danielebarchiesi@0
|
312 this.initialize();
|
danielebarchiesi@0
|
313 }
|
danielebarchiesi@0
|
314 }
|
danielebarchiesi@0
|
315 };
|
danielebarchiesi@0
|
316
|
danielebarchiesi@0
|
317 states.Trigger.prototype = {
|
danielebarchiesi@0
|
318 initialize: function () {
|
danielebarchiesi@0
|
319 var trigger = states.Trigger.states[this.state];
|
danielebarchiesi@0
|
320
|
danielebarchiesi@0
|
321 if (typeof trigger == 'function') {
|
danielebarchiesi@0
|
322 // We have a custom trigger initialization function.
|
danielebarchiesi@0
|
323 trigger.call(window, this.element);
|
danielebarchiesi@0
|
324 }
|
danielebarchiesi@0
|
325 else {
|
danielebarchiesi@0
|
326 for (var event in trigger) {
|
danielebarchiesi@0
|
327 if (trigger.hasOwnProperty(event)) {
|
danielebarchiesi@0
|
328 this.defaultTrigger(event, trigger[event]);
|
danielebarchiesi@0
|
329 }
|
danielebarchiesi@0
|
330 }
|
danielebarchiesi@0
|
331 }
|
danielebarchiesi@0
|
332
|
danielebarchiesi@0
|
333 // Mark this trigger as initialized for this element.
|
danielebarchiesi@0
|
334 this.element.data('trigger:' + this.state, true);
|
danielebarchiesi@0
|
335 },
|
danielebarchiesi@0
|
336
|
danielebarchiesi@0
|
337 defaultTrigger: function (event, valueFn) {
|
danielebarchiesi@0
|
338 var oldValue = valueFn.call(this.element);
|
danielebarchiesi@0
|
339
|
danielebarchiesi@0
|
340 // Attach the event callback.
|
danielebarchiesi@0
|
341 this.element.bind(event, $.proxy(function (e) {
|
danielebarchiesi@0
|
342 var value = valueFn.call(this.element, e);
|
danielebarchiesi@0
|
343 // Only trigger the event if the value has actually changed.
|
danielebarchiesi@0
|
344 if (oldValue !== value) {
|
danielebarchiesi@0
|
345 this.element.trigger({ type: 'state:' + this.state, value: value, oldValue: oldValue });
|
danielebarchiesi@0
|
346 oldValue = value;
|
danielebarchiesi@0
|
347 }
|
danielebarchiesi@0
|
348 }, this));
|
danielebarchiesi@0
|
349
|
danielebarchiesi@0
|
350 states.postponed.push($.proxy(function () {
|
danielebarchiesi@0
|
351 // Trigger the event once for initialization purposes.
|
danielebarchiesi@0
|
352 this.element.trigger({ type: 'state:' + this.state, value: oldValue, oldValue: null });
|
danielebarchiesi@0
|
353 }, this));
|
danielebarchiesi@0
|
354 }
|
danielebarchiesi@0
|
355 };
|
danielebarchiesi@0
|
356
|
danielebarchiesi@0
|
357 /**
|
danielebarchiesi@0
|
358 * This list of states contains functions that are used to monitor the state
|
danielebarchiesi@0
|
359 * of an element. Whenever an element depends on the state of another element,
|
danielebarchiesi@0
|
360 * one of these trigger functions is added to the dependee so that the
|
danielebarchiesi@0
|
361 * dependent element can be updated.
|
danielebarchiesi@0
|
362 */
|
danielebarchiesi@0
|
363 states.Trigger.states = {
|
danielebarchiesi@0
|
364 // 'empty' describes the state to be monitored
|
danielebarchiesi@0
|
365 empty: {
|
danielebarchiesi@0
|
366 // 'keyup' is the (native DOM) event that we watch for.
|
danielebarchiesi@0
|
367 'keyup': function () {
|
danielebarchiesi@0
|
368 // The function associated to that trigger returns the new value for the
|
danielebarchiesi@0
|
369 // state.
|
danielebarchiesi@0
|
370 return this.val() == '';
|
danielebarchiesi@0
|
371 }
|
danielebarchiesi@0
|
372 },
|
danielebarchiesi@0
|
373
|
danielebarchiesi@0
|
374 checked: {
|
danielebarchiesi@0
|
375 'change': function () {
|
danielebarchiesi@0
|
376 return this.attr('checked');
|
danielebarchiesi@0
|
377 }
|
danielebarchiesi@0
|
378 },
|
danielebarchiesi@0
|
379
|
danielebarchiesi@0
|
380 // For radio buttons, only return the value if the radio button is selected.
|
danielebarchiesi@0
|
381 value: {
|
danielebarchiesi@0
|
382 'keyup': function () {
|
danielebarchiesi@0
|
383 // Radio buttons share the same :input[name="key"] selector.
|
danielebarchiesi@0
|
384 if (this.length > 1) {
|
danielebarchiesi@0
|
385 // Initial checked value of radios is undefined, so we return false.
|
danielebarchiesi@0
|
386 return this.filter(':checked').val() || false;
|
danielebarchiesi@0
|
387 }
|
danielebarchiesi@0
|
388 return this.val();
|
danielebarchiesi@0
|
389 },
|
danielebarchiesi@0
|
390 'change': function () {
|
danielebarchiesi@0
|
391 // Radio buttons share the same :input[name="key"] selector.
|
danielebarchiesi@0
|
392 if (this.length > 1) {
|
danielebarchiesi@0
|
393 // Initial checked value of radios is undefined, so we return false.
|
danielebarchiesi@0
|
394 return this.filter(':checked').val() || false;
|
danielebarchiesi@0
|
395 }
|
danielebarchiesi@0
|
396 return this.val();
|
danielebarchiesi@0
|
397 }
|
danielebarchiesi@0
|
398 },
|
danielebarchiesi@0
|
399
|
danielebarchiesi@0
|
400 collapsed: {
|
danielebarchiesi@0
|
401 'collapsed': function(e) {
|
danielebarchiesi@0
|
402 return (typeof e !== 'undefined' && 'value' in e) ? e.value : this.is('.collapsed');
|
danielebarchiesi@0
|
403 }
|
danielebarchiesi@0
|
404 }
|
danielebarchiesi@0
|
405 };
|
danielebarchiesi@0
|
406
|
danielebarchiesi@0
|
407
|
danielebarchiesi@0
|
408 /**
|
danielebarchiesi@0
|
409 * A state object is used for describing the state and performing aliasing.
|
danielebarchiesi@0
|
410 */
|
danielebarchiesi@0
|
411 states.State = function(state) {
|
danielebarchiesi@0
|
412 // We may need the original unresolved name later.
|
danielebarchiesi@0
|
413 this.pristine = this.name = state;
|
danielebarchiesi@0
|
414
|
danielebarchiesi@0
|
415 // Normalize the state name.
|
danielebarchiesi@0
|
416 while (true) {
|
danielebarchiesi@0
|
417 // Iteratively remove exclamation marks and invert the value.
|
danielebarchiesi@0
|
418 while (this.name.charAt(0) == '!') {
|
danielebarchiesi@0
|
419 this.name = this.name.substring(1);
|
danielebarchiesi@0
|
420 this.invert = !this.invert;
|
danielebarchiesi@0
|
421 }
|
danielebarchiesi@0
|
422
|
danielebarchiesi@0
|
423 // Replace the state with its normalized name.
|
danielebarchiesi@0
|
424 if (this.name in states.State.aliases) {
|
danielebarchiesi@0
|
425 this.name = states.State.aliases[this.name];
|
danielebarchiesi@0
|
426 }
|
danielebarchiesi@0
|
427 else {
|
danielebarchiesi@0
|
428 break;
|
danielebarchiesi@0
|
429 }
|
danielebarchiesi@0
|
430 }
|
danielebarchiesi@0
|
431 };
|
danielebarchiesi@0
|
432
|
danielebarchiesi@0
|
433 /**
|
danielebarchiesi@0
|
434 * Creates a new State object by sanitizing the passed value.
|
danielebarchiesi@0
|
435 */
|
danielebarchiesi@0
|
436 states.State.sanitize = function (state) {
|
danielebarchiesi@0
|
437 if (state instanceof states.State) {
|
danielebarchiesi@0
|
438 return state;
|
danielebarchiesi@0
|
439 }
|
danielebarchiesi@0
|
440 else {
|
danielebarchiesi@0
|
441 return new states.State(state);
|
danielebarchiesi@0
|
442 }
|
danielebarchiesi@0
|
443 };
|
danielebarchiesi@0
|
444
|
danielebarchiesi@0
|
445 /**
|
danielebarchiesi@0
|
446 * This list of aliases is used to normalize states and associates negated names
|
danielebarchiesi@0
|
447 * with their respective inverse state.
|
danielebarchiesi@0
|
448 */
|
danielebarchiesi@0
|
449 states.State.aliases = {
|
danielebarchiesi@0
|
450 'enabled': '!disabled',
|
danielebarchiesi@0
|
451 'invisible': '!visible',
|
danielebarchiesi@0
|
452 'invalid': '!valid',
|
danielebarchiesi@0
|
453 'untouched': '!touched',
|
danielebarchiesi@0
|
454 'optional': '!required',
|
danielebarchiesi@0
|
455 'filled': '!empty',
|
danielebarchiesi@0
|
456 'unchecked': '!checked',
|
danielebarchiesi@0
|
457 'irrelevant': '!relevant',
|
danielebarchiesi@0
|
458 'expanded': '!collapsed',
|
danielebarchiesi@0
|
459 'readwrite': '!readonly'
|
danielebarchiesi@0
|
460 };
|
danielebarchiesi@0
|
461
|
danielebarchiesi@0
|
462 states.State.prototype = {
|
danielebarchiesi@0
|
463 invert: false,
|
danielebarchiesi@0
|
464
|
danielebarchiesi@0
|
465 /**
|
danielebarchiesi@0
|
466 * Ensures that just using the state object returns the name.
|
danielebarchiesi@0
|
467 */
|
danielebarchiesi@0
|
468 toString: function() {
|
danielebarchiesi@0
|
469 return this.name;
|
danielebarchiesi@0
|
470 }
|
danielebarchiesi@0
|
471 };
|
danielebarchiesi@0
|
472
|
danielebarchiesi@0
|
473 /**
|
danielebarchiesi@0
|
474 * Global state change handlers. These are bound to "document" to cover all
|
danielebarchiesi@0
|
475 * elements whose state changes. Events sent to elements within the page
|
danielebarchiesi@0
|
476 * bubble up to these handlers. We use this system so that themes and modules
|
danielebarchiesi@0
|
477 * can override these state change handlers for particular parts of a page.
|
danielebarchiesi@0
|
478 */
|
danielebarchiesi@0
|
479 $(document).bind('state:disabled', function(e) {
|
danielebarchiesi@0
|
480 // Only act when this change was triggered by a dependency and not by the
|
danielebarchiesi@0
|
481 // element monitoring itself.
|
danielebarchiesi@0
|
482 if (e.trigger) {
|
danielebarchiesi@0
|
483 $(e.target)
|
danielebarchiesi@0
|
484 .attr('disabled', e.value)
|
danielebarchiesi@0
|
485 .closest('.form-item, .form-submit, .form-wrapper').toggleClass('form-disabled', e.value)
|
danielebarchiesi@0
|
486 .find('select, input, textarea').attr('disabled', e.value);
|
danielebarchiesi@0
|
487
|
danielebarchiesi@0
|
488 // Note: WebKit nightlies don't reflect that change correctly.
|
danielebarchiesi@0
|
489 // See https://bugs.webkit.org/show_bug.cgi?id=23789
|
danielebarchiesi@0
|
490 }
|
danielebarchiesi@0
|
491 });
|
danielebarchiesi@0
|
492
|
danielebarchiesi@0
|
493 $(document).bind('state:required', function(e) {
|
danielebarchiesi@0
|
494 if (e.trigger) {
|
danielebarchiesi@0
|
495 if (e.value) {
|
danielebarchiesi@0
|
496 $(e.target).closest('.form-item, .form-wrapper').find('label').append('<span class="form-required">*</span>');
|
danielebarchiesi@0
|
497 }
|
danielebarchiesi@0
|
498 else {
|
danielebarchiesi@0
|
499 $(e.target).closest('.form-item, .form-wrapper').find('label .form-required').remove();
|
danielebarchiesi@0
|
500 }
|
danielebarchiesi@0
|
501 }
|
danielebarchiesi@0
|
502 });
|
danielebarchiesi@0
|
503
|
danielebarchiesi@0
|
504 $(document).bind('state:visible', function(e) {
|
danielebarchiesi@0
|
505 if (e.trigger) {
|
danielebarchiesi@0
|
506 $(e.target).closest('.form-item, .form-submit, .form-wrapper').toggle(e.value);
|
danielebarchiesi@0
|
507 }
|
danielebarchiesi@0
|
508 });
|
danielebarchiesi@0
|
509
|
danielebarchiesi@0
|
510 $(document).bind('state:checked', function(e) {
|
danielebarchiesi@0
|
511 if (e.trigger) {
|
danielebarchiesi@0
|
512 $(e.target).attr('checked', e.value);
|
danielebarchiesi@0
|
513 }
|
danielebarchiesi@0
|
514 });
|
danielebarchiesi@0
|
515
|
danielebarchiesi@0
|
516 $(document).bind('state:collapsed', function(e) {
|
danielebarchiesi@0
|
517 if (e.trigger) {
|
danielebarchiesi@0
|
518 if ($(e.target).is('.collapsed') !== e.value) {
|
danielebarchiesi@0
|
519 $('> legend a', e.target).click();
|
danielebarchiesi@0
|
520 }
|
danielebarchiesi@0
|
521 }
|
danielebarchiesi@0
|
522 });
|
danielebarchiesi@0
|
523
|
danielebarchiesi@0
|
524 /**
|
danielebarchiesi@0
|
525 * These are helper functions implementing addition "operators" and don't
|
danielebarchiesi@0
|
526 * implement any logic that is particular to states.
|
danielebarchiesi@0
|
527 */
|
danielebarchiesi@0
|
528
|
danielebarchiesi@0
|
529 // Bitwise AND with a third undefined state.
|
danielebarchiesi@0
|
530 function ternary (a, b) {
|
danielebarchiesi@0
|
531 return typeof a === 'undefined' ? b : (typeof b === 'undefined' ? a : a && b);
|
danielebarchiesi@0
|
532 }
|
danielebarchiesi@0
|
533
|
danielebarchiesi@0
|
534 // Inverts a (if it's not undefined) when invert is true.
|
danielebarchiesi@0
|
535 function invert (a, invert) {
|
danielebarchiesi@0
|
536 return (invert && typeof a !== 'undefined') ? !a : a;
|
danielebarchiesi@0
|
537 }
|
danielebarchiesi@0
|
538
|
danielebarchiesi@0
|
539 // Compares two values while ignoring undefined values.
|
danielebarchiesi@0
|
540 function compare (a, b) {
|
danielebarchiesi@0
|
541 return (a === b) ? (typeof a === 'undefined' ? a : true) : (typeof a === 'undefined' || typeof b === 'undefined');
|
danielebarchiesi@0
|
542 }
|
danielebarchiesi@0
|
543
|
danielebarchiesi@0
|
544 })(jQuery);
|