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