PageSourceSearch

https://www.transitionresourceguide.ca/core/misc/states.js?v=10.6.13

js transitionresourceguide.ca collected 2026-09-24 12:09:04 UTC 23,322 bytes, 769 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.