5a920362 |
1 | (function ($) { |
2 | |
3 | /** |
4 | * The base States namespace. |
5 | * |
6 | * Having the local states variable allows us to use the States namespace |
7 | * without having to always declare "Drupal.states". |
8 | */ |
9 | var states = Drupal.states = { |
10 | // An array of functions that should be postponed. |
11 | postponed: [] |
12 | }; |
13 | |
14 | /** |
15 | * Attaches the states. |
16 | */ |
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({ |
22 | element: $(selector), |
23 | state: states.State.sanitize(state), |
24 | dependees: settings.states[selector][state] |
25 | }); |
26 | } |
27 | } |
28 | |
29 | // Execute all postponed functions now. |
30 | while (states.postponed.length) { |
31 | (states.postponed.shift())(); |
32 | } |
33 | } |
34 | }; |
35 | |
36 | /** |
37 | * Object representing an element that depends on other elements. |
38 | * |
39 | * @param args |
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. |
45 | */ |
46 | states.Dependent = function (args) { |
47 | $.extend(this, { values: {}, oldValue: undefined }, args); |
48 | |
49 | for (var selector in this.dependees) { |
50 | this.initializeDependee(selector, this.dependees[selector]); |
51 | } |
52 | }; |
53 | |
54 | /** |
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. |
58 | */ |
59 | states.Dependent.comparisons = { |
60 | 'RegExp': function (reference, value) { |
61 | return reference.test(value); |
62 | }, |
63 | 'Function': function (reference, value) { |
64 | // The "reference" variable is a comparison function. |
65 | return reference(value); |
66 | }, |
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); |
73 | } |
74 | }; |
75 | |
76 | states.Dependent.prototype = { |
77 | /** |
78 | * Initializes one of the elements this dependent depends on. |
79 | * |
80 | * @param selector |
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. |
85 | */ |
86 | initializeDependee: function (selector, dependeeStates) { |
87 | var self = this; |
88 | |
89 | // Cache for the states of this dependee. |
90 | self.values[selector] = {}; |
91 | |
92 | $.each(dependeeStates, function (state, value) { |
93 | state = states.State.sanitize(state); |
94 | |
95 | // Initialize the value of this state. |
96 | self.values[selector][state.pristine] = undefined; |
97 | |
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); |
102 | }); |
103 | |
104 | // Make sure the event we just bound ourselves to is actually fired. |
105 | new states.Trigger({ selector: selector, state: state }); |
106 | }); |
107 | }, |
108 | |
109 | /** |
110 | * Compares a value with a reference value. |
111 | * |
112 | * @param reference |
113 | * The value used for reference. |
114 | * @param value |
115 | * The value to compare with the reference value. |
116 | * @return |
117 | * true, undefined or false. |
118 | */ |
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); |
123 | } |
124 | else { |
125 | // Do a plain comparison otherwise. |
126 | return compare(reference, value); |
127 | } |
128 | }, |
129 | |
130 | /** |
131 | * Update the value of a dependee's state. |
132 | * |
133 | * @param selector |
134 | * CSS selector describing the dependee. |
135 | * @param state |
136 | * A State object describing the dependee's updated state. |
137 | * @param value |
138 | * The new value for the dependee's updated state. |
139 | */ |
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; |
144 | this.reevaluate(); |
145 | } |
146 | }, |
147 | |
148 | /** |
149 | * Triggers change events in case a state changed. |
150 | */ |
151 | reevaluate: function () { |
152 | var value = undefined; |
153 | |
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)); |
160 | } |
161 | } |
162 | |
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 |
166 | // actually changed. |
167 | this.oldValue = value; |
168 | |
169 | // Normalize the value to match the normalized state name. |
170 | value = invert(value, this.state.invert); |
171 | |
172 | // By adding "trigger: true", we ensure that state changes don't go into |
173 | // infinite loops. |
174 | this.element.trigger({ type: 'state:' + this.state, value: value, trigger: true }); |
175 | } |
176 | } |
177 | }; |
178 | |
179 | states.Trigger = function (args) { |
180 | $.extend(this, args); |
181 | |
182 | if (this.state in states.Trigger.states) { |
183 | this.element = $(this.selector); |
184 | |
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)) { |
188 | this.initialize(); |
189 | } |
190 | } |
191 | }; |
192 | |
193 | states.Trigger.prototype = { |
194 | initialize: function () { |
195 | var self = this; |
196 | var trigger = states.Trigger.states[this.state]; |
197 | |
198 | if (typeof trigger == 'function') { |
199 | // We have a custom trigger initialization function. |
200 | trigger.call(window, this.element); |
201 | } |
202 | else { |
203 | $.each(trigger, function (event, valueFn) { |
204 | self.defaultTrigger(event, valueFn); |
205 | }); |
206 | } |
207 | |
208 | // Mark this trigger as initialized for this element. |
209 | this.element.data('trigger:' + this.state, true); |
210 | }, |
211 | |
212 | defaultTrigger: function (event, valueFn) { |
213 | var self = this; |
214 | var oldValue = valueFn.call(this.element); |
215 | |
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 }); |
222 | oldValue = value; |
223 | } |
224 | }); |
225 | |
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 }); |
229 | }); |
230 | } |
231 | }; |
232 | |
233 | /** |
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. |
238 | */ |
239 | states.Trigger.states = { |
240 | // 'empty' describes the state to be monitored |
241 | empty: { |
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 |
245 | // state. |
246 | return this.val() == ''; |
247 | } |
248 | }, |
249 | |
250 | checked: { |
251 | 'change': function () { |
252 | return this.attr('checked'); |
253 | } |
254 | }, |
255 | |
256 | // For radio buttons, only return the value if the radio button is selected. |
257 | value: { |
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; |
263 | } |
264 | return this.val(); |
265 | }, |
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; |
271 | } |
272 | return this.val(); |
273 | } |
274 | }, |
275 | |
276 | collapsed: { |
277 | 'collapsed': function(e) { |
278 | return (e !== undefined && 'value' in e) ? e.value : this.is('.collapsed'); |
279 | } |
280 | } |
281 | }; |
282 | |
283 | |
284 | /** |
285 | * A state object is used for describing the state and performing aliasing. |
286 | */ |
287 | states.State = function(state) { |
288 | // We may need the original unresolved name later. |
289 | this.pristine = this.name = state; |
290 | |
291 | // Normalize the state name. |
292 | while (true) { |
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; |
297 | } |
298 | |
299 | // Replace the state with its normalized name. |
300 | if (this.name in states.State.aliases) { |
301 | this.name = states.State.aliases[this.name]; |
302 | } |
303 | else { |
304 | break; |
305 | } |
306 | } |
307 | }; |
308 | |
309 | /** |
310 | * Create a new State object by sanitizing the passed value. |
311 | */ |
312 | states.State.sanitize = function (state) { |
313 | if (state instanceof states.State) { |
314 | return state; |
315 | } |
316 | else { |
317 | return new states.State(state); |
318 | } |
319 | }; |
320 | |
321 | /** |
322 | * This list of aliases is used to normalize states and associates negated names |
323 | * with their respective inverse state. |
324 | */ |
325 | states.State.aliases = { |
326 | 'enabled': '!disabled', |
327 | 'invisible': '!visible', |
328 | 'invalid': '!valid', |
329 | 'untouched': '!touched', |
330 | 'optional': '!required', |
331 | 'filled': '!empty', |
332 | 'unchecked': '!checked', |
333 | 'irrelevant': '!relevant', |
334 | 'expanded': '!collapsed', |
335 | 'readwrite': '!readonly' |
336 | }; |
337 | |
338 | states.State.prototype = { |
339 | invert: false, |
340 | |
341 | /** |
342 | * Ensures that just using the state object returns the name. |
343 | */ |
344 | toString: function() { |
345 | return this.name; |
346 | } |
347 | }; |
348 | |
349 | /** |
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. |
354 | */ |
355 | { |
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. |
359 | if (e.trigger) { |
360 | $(e.target) |
361 | .attr('disabled', e.value) |
362 | .filter('.form-element') |
363 | .closest('.form-item, .form-submit, .form-wrapper')[e.value ? 'addClass' : 'removeClass']('form-disabled'); |
364 | |
365 | // Note: WebKit nightlies don't reflect that change correctly. |
366 | // See https://bugs.webkit.org/show_bug.cgi?id=23789 |
367 | } |
368 | }); |
369 | |
370 | $(document).bind('state:required', function(e) { |
371 | if (e.trigger) { |
372 | if (e.value) { |
373 | $(e.target).closest('.form-item, .form-wrapper').find('label').append('<span class="form-required">*</span>'); |
374 | } |
375 | else { |
376 | $(e.target).closest('.form-item, .form-wrapper').find('label .form-required').remove(); |
377 | } |
378 | } |
379 | }); |
380 | |
381 | $(document).bind('state:visible', function(e) { |
382 | if (e.trigger) { |
383 | $(e.target).closest('.form-item, .form-submit, .form-wrapper')[e.value ? 'show' : 'hide'](); |
384 | } |
385 | }); |
386 | |
387 | $(document).bind('state:checked', function(e) { |
388 | if (e.trigger) { |
389 | $(e.target).attr('checked', e.value); |
390 | } |
391 | }); |
392 | |
393 | $(document).bind('state:collapsed', function(e) { |
394 | if (e.trigger) { |
395 | if ($(e.target).is('.collapsed') !== e.value) { |
396 | $('> legend a', e.target).click(); |
397 | } |
398 | } |
399 | }); |
400 | } |
401 | |
402 | /** |
403 | * These are helper functions implementing addition "operators" and don't |
404 | * implement any logic that is particular to states. |
405 | */ |
406 | { |
407 | // Bitwise AND with a third undefined state. |
408 | function ternary (a, b) { |
409 | return a === undefined ? b : (b === undefined ? a : a && b); |
410 | }; |
411 | |
412 | // Inverts a (if it's not undefined) when invert is true. |
413 | function invert (a, invert) { |
414 | return (invert && a !== undefined) ? !a : a; |
415 | }; |
416 | |
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); |
420 | } |
421 | } |
422 | |
423 | })(jQuery); |