/[drupal]/drupal/misc/states.js
ViewVC logotype

Contents of /drupal/misc/states.js

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (show annotations) (download) (as text)
Fri Oct 16 19:20:34 2009 UTC (6 weeks ago) by dries
Branch: MAIN
CVS Tags: DRUPAL-7-0-UNSTABLE-10, HEAD
File MIME type: text/javascript
- Patch #557272 by kkaefer, Rob Loach, quicksketch: added FAPI JavaScript States system.
1 // $Id$
2 (function ($) {
3
4 /**
5 * The base States namespace.
6 *
7 * Having the local states variable allows us to use the States namespace
8 * without having to always declare "Drupal.states".
9 */
10 var states = Drupal.states = {
11 // An array of functions that should be postponed.
12 postponed: []
13 };
14
15 /**
16 * Attaches the states.
17 */
18 Drupal.behaviors.states = {
19 attach: function (context, settings) {
20 for (var selector in settings.states) {
21 for (var state in settings.states[selector]) {
22 new states.Dependant({
23 element: $(selector),
24 state: states.State.sanitize(state),
25 dependees: settings.states[selector][state]
26 });
27 }
28 }
29
30 // Execute all postponed functions now.
31 while (states.postponed.length) {
32 (states.postponed.shift())();
33 }
34 }
35 };
36
37 /**
38 * Object representing an element that depends on other elements.
39 *
40 * @param args
41 * Object with the following keys (all of which are required):
42 * - element: A jQuery object of the dependant element
43 * - state: A State object describing the state that is dependant
44 * - dependees: An object with dependency specifications. Lists all elements
45 * that this element depends on.
46 */
47 states.Dependant = function (args) {
48 $.extend(this, { values: {}, oldValue: undefined }, args);
49
50 for (var selector in this.dependees) {
51 this.initializeDependee(selector, this.dependees[selector]);
52 }
53 };
54
55 /**
56 * Comparison functions for comparing the value of an element with the
57 * specification from the dependency settings. If the object type can't be
58 * found in this list, the === operator is used by default.
59 */
60 states.Dependant.comparisons = {
61 'RegExp': function (reference, value) {
62 return reference.test(value);
63 },
64 'Function': function (reference, value) {
65 // The "reference" variable is a comparison function.
66 return reference(value);
67 }
68 };
69
70 states.Dependant.prototype = {
71 /**
72 * Initializes one of the elements this dependant depends on.
73 *
74 * @param selector
75 * The CSS selector describing the dependee.
76 * @param dependeeStates
77 * The list of states that have to be monitored for tracking the
78 * dependee's compliance status.
79 */
80 initializeDependee: function (selector, dependeeStates) {
81 var self = this;
82
83 // Cache for the states of this dependee.
84 self.values[selector] = {};
85
86 $.each(dependeeStates, function (state, value) {
87 state = states.State.sanitize(state);
88
89 // Initialize the value of this state.
90 self.values[selector][state.pristine] = undefined;
91
92 // Monitor state changes of the specified state for this dependee.
93 $(selector).bind('state:' + state, function (e) {
94 var complies = self.compare(value, e.value);
95 self.update(selector, state, complies);
96 });
97
98 // Make sure the event we just bound ourselves to is actually fired.
99 new states.Trigger({ selector: selector, state: state });
100 });
101 },
102
103 /**
104 * Compares a value with a reference value.
105 *
106 * @param reference
107 * The value used for reference.
108 * @param value
109 * The value to compare with the reference value.
110 * @return
111 * true, undefined or false.
112 */
113 compare: function (reference, value) {
114 if (reference.constructor.name in states.Dependant.comparisons) {
115 // Use a custom compare function for certain reference value types.
116 return states.Dependant.comparisons[reference.constructor.name](reference, value);
117 }
118 else {
119 // Do a plain comparison otherwise.
120 return compare(reference, value);
121 }
122 },
123
124 /**
125 * Update the value of a dependee's state.
126 *
127 * @param selector
128 * CSS selector describing the dependee.
129 * @param state
130 * A State object describing the dependee's updated state.
131 * @param value
132 * The new value for the dependee's updated state.
133 */
134 update: function (selector, state, value) {
135 // Only act when the 'new' value is actually new.
136 if (value !== this.values[selector][state.pristine]) {
137 this.values[selector][state.pristine] = value;
138 this.reevaluate();
139 }
140 },
141
142 /**
143 * Triggers change events in case a state changed.
144 */
145 reevaluate: function () {
146 var value = undefined;
147
148 // Merge all individual values to find out whether this dependee complies.
149 for (var selector in this.values) {
150 for (var state in this.values[selector]) {
151 state = states.State.sanitize(state);
152 var complies = this.values[selector][state.pristine];
153 value = ternary(value, invert(complies, state.invert));
154 }
155 }
156
157 // Only invoke a state change event when the value actually changed.
158 if (value !== this.oldValue) {
159 // Store the new value so that we can compare later whether the value
160 // actually changed.
161 this.oldValue = value;
162
163 // Normalize the value to match the normalized state name.
164 value = invert(value, this.state.invert);
165
166 // By adding "trigger: true", we ensure that state changes don't go into
167 // infinite loops.
168 this.element.trigger({ type: 'state:' + this.state, value: value, trigger: true });
169 }
170 }
171 };
172
173 states.Trigger = function (args) {
174 $.extend(this, args);
175
176 if (this.state in states.Trigger.states) {
177 this.element = $(this.selector);
178
179 // Only call the trigger initializer when it wasn't yet attached to this
180 // element. Otherwise we'd end up with duplicate events.
181 if (!this.element.data('trigger:' + this.state)) {
182 this.initialize();
183 }
184 }
185 };
186
187 states.Trigger.prototype = {
188 initialize: function () {
189 var self = this;
190 var trigger = states.Trigger.states[this.state];
191
192 if (typeof trigger == 'function') {
193 // We have a custom trigger initialization function.
194 trigger.call(window, this.element);
195 }
196 else {
197 $.each(trigger, function (event, valueFn) {
198 self.defaultTrigger(event, valueFn);
199 });
200 }
201
202 // Mark this trigger as initialized for this element.
203 this.element.data('trigger:' + this.state, true);
204 },
205
206 defaultTrigger: function (event, valueFn) {
207 var self = this;
208 var oldValue = valueFn.call(this.element);
209
210 // Attach the event callback.
211 this.element.bind(event, function (e) {
212 var value = valueFn.call(self.element, e);
213 // Only trigger the event if the value has actually changed.
214 if (oldValue !== value) {
215 self.element.trigger({ type: 'state:' + self.state, value: value, oldValue: oldValue });
216 oldValue = value;
217 }
218 });
219
220 states.postponed.push(function () {
221 // Trigger the event once for initialization purposes.
222 self.element.trigger({ type: 'state:' + self.state, value: oldValue, oldValue: undefined });
223 });
224 }
225 };
226
227 /**
228 * This list of states contains functions that are used to monitor the state
229 * of an element. Whenever an element depends on the state of another element,
230 * one of these trigger functions is added to the dependee so that the
231 * dependant element can be updated.
232 */
233 states.Trigger.states = {
234 // 'empty' describes the state to be monitored
235 empty: {
236 // 'keyup' is the (native DOM) event that we watch for.
237 'keyup': function () {
238 // The function associated to that trigger returns the new value for the
239 // state.
240 return this.val() == '';
241 }
242 },
243
244 checked: {
245 'change': function () {
246 return this.attr('checked');
247 }
248 },
249
250 value: {
251 'keyup': function () {
252 return this.val();
253 }
254 },
255
256 collapsed: {
257 'collapsed': function(e) {
258 return (e !== undefined && 'value' in e) ? e.value : this.is('.collapsed');
259 }
260 }
261 };
262
263
264 /**
265 * A state object is used for describing the state and performing aliasing.
266 */
267 states.State = function(state) {
268 // We may need the original unresolved name later.
269 this.pristine = this.name = state;
270
271 // Normalize the state name.
272 while (true) {
273 // Iteratively remove exclamation marks and invert the value.
274 while (this.name.charAt(0) == '!') {
275 this.name = this.name.substring(1);
276 this.invert = !this.invert;
277 }
278
279 // Replace the state with its normalized name.
280 if (this.name in states.State.aliases) {
281 this.name = states.State.aliases[this.name];
282 }
283 else {
284 break;
285 }
286 }
287 };
288
289 /**
290 * Create a new State object by sanitizing the passed value.
291 */
292 states.State.sanitize = function (state) {
293 if (state instanceof states.State) {
294 return state;
295 }
296 else {
297 return new states.State(state);
298 }
299 };
300
301 /**
302 * This list of aliases is used to normalize states and associates negated names
303 * with their respective inverse state.
304 */
305 states.State.aliases = {
306 'enabled': '!disabled',
307 'invisible': '!visible',
308 'invalid': '!valid',
309 'untouched': '!touched',
310 'optional': '!required',
311 'filled': '!empty',
312 'unchecked': '!checked',
313 'irrelevant': '!relevant',
314 'expanded': '!collapsed',
315 'readwrite': '!readonly'
316 };
317
318 states.State.prototype = {
319 invert: false,
320
321 /**
322 * Ensures that just using the state object returns the name.
323 */
324 toString: function() {
325 return this.name;
326 }
327 };
328
329 /**
330 * Global state change handlers. These are bound to "document" to cover all
331 * elements whose state changes. Events sent to elements within the page
332 * bubble up to these handlers. We use this system so that themes and modules
333 * can override these state change handlers for particular parts of a page.
334 */
335 {
336 $(document).bind('state:disabled', function(e) {
337 // Only act when this change was triggered by a dependency and not by the
338 // element monitoring itself.
339 if (e.trigger) {
340 $(e.target)
341 .attr('disabled', e.value)
342 .filter('.form-element')
343 .closest('.form-item, .form-wrapper')[e.value ? 'addClass' : 'removeClass']('form-disabled');
344
345 // Note: WebKit nightlies don't reflect that change correctly.
346 // See https://bugs.webkit.org/show_bug.cgi?id=23789
347 }
348 });
349
350 $(document).bind('state:required', function(e) {
351 if (e.trigger) {
352 $(e.target).closest('.form-item, .form-wrapper')[e.value ? 'addClass' : 'removeClass']('form-required');
353 }
354 });
355
356 $(document).bind('state:visible', function(e) {
357 if (e.trigger) {
358 $(e.target).closest('.form-item, .form-wrapper')[e.value ? 'show' : 'hide']();
359 }
360 });
361
362 $(document).bind('state:checked', function(e) {
363 if (e.trigger) {
364 $(e.target).attr('checked', e.value);
365 }
366 });
367
368 $(document).bind('state:collapsed', function(e) {
369 if (e.trigger) {
370 if ($(e.target).is('.collapsed') !== e.value) {
371 $('> legend a', e.target).click();
372 }
373 }
374 });
375 }
376
377 /**
378 * These are helper functions implementing addition "operators" and don't
379 * implement any logic that is particular to states.
380 */
381 {
382 // Bitwise AND with a third undefined state.
383 function ternary (a, b) {
384 return a === undefined ? b : (b === undefined ? a : a && b);
385 };
386
387 // Inverts a (if it's not undefined) when invert is true.
388 function invert (a, invert) {
389 return (invert && a !== undefined) ? !a : a;
390 };
391
392 // Compares two values while ignoring undefined values.
393 function compare (a, b) {
394 return (a === b) ? (a === undefined ? a : true) : (a === undefined || b === undefined);
395 }
396 }
397
398 })(jQuery);

  ViewVC Help
Powered by ViewVC 1.1.2