Chris@0: /** Chris@0: * @file Chris@0: * Drupal's states library. Chris@0: */ Chris@0: Chris@17: (function($, Drupal) { Chris@0: /** Chris@0: * The base States namespace. Chris@0: * Chris@0: * Having the local states variable allows us to use the States namespace Chris@0: * without having to always declare "Drupal.states". Chris@0: * Chris@0: * @namespace Drupal.states Chris@0: */ Chris@14: const states = { Chris@0: /** Chris@0: * An array of functions that should be postponed. Chris@0: */ Chris@0: postponed: [], Chris@0: }; Chris@0: Chris@14: Drupal.states = states; Chris@14: Chris@0: /** Chris@17: * Inverts a (if it's not undefined) when invertState is true. Chris@17: * Chris@17: * @function Drupal.states~invert Chris@17: * Chris@17: * @param {*} a Chris@17: * The value to maybe invert. Chris@17: * @param {bool} invertState Chris@17: * Whether to invert state or not. Chris@17: * Chris@17: * @return {bool} Chris@17: * The result. Chris@17: */ Chris@17: function invert(a, invertState) { Chris@17: return invertState && typeof a !== 'undefined' ? !a : a; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Compares two values while ignoring undefined values. Chris@17: * Chris@17: * @function Drupal.states~compare Chris@17: * Chris@17: * @param {*} a Chris@17: * Value a. Chris@17: * @param {*} b Chris@17: * Value b. Chris@17: * Chris@17: * @return {bool} Chris@17: * The comparison result. Chris@17: */ Chris@17: function compare(a, b) { Chris@17: if (a === b) { Chris@17: return typeof a === 'undefined' ? a : true; Chris@17: } Chris@17: Chris@17: return typeof a === 'undefined' || typeof b === 'undefined'; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Bitwise AND with a third undefined state. Chris@17: * Chris@17: * @function Drupal.states~ternary Chris@17: * Chris@17: * @param {*} a Chris@17: * Value a. Chris@17: * @param {*} b Chris@17: * Value b Chris@17: * Chris@17: * @return {bool} Chris@17: * The result. Chris@17: */ Chris@17: function ternary(a, b) { Chris@17: if (typeof a === 'undefined') { Chris@17: return b; Chris@17: } Chris@17: if (typeof b === 'undefined') { Chris@17: return a; Chris@17: } Chris@17: Chris@17: return a && b; Chris@17: } Chris@17: Chris@17: /** Chris@0: * Attaches the states. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches states behaviors. Chris@0: */ Chris@0: Drupal.behaviors.states = { Chris@0: attach(context, settings) { Chris@0: const $states = $(context).find('[data-drupal-states]'); Chris@0: const il = $states.length; Chris@0: for (let i = 0; i < il; i++) { Chris@17: const config = JSON.parse( Chris@17: $states[i].getAttribute('data-drupal-states'), Chris@17: ); Chris@17: Object.keys(config || {}).forEach(state => { Chris@14: new states.Dependent({ Chris@14: element: $($states[i]), Chris@14: state: states.State.sanitize(state), Chris@14: constraints: config[state], Chris@14: }); Chris@14: }); Chris@0: } Chris@0: Chris@0: // Execute all postponed functions now. Chris@0: while (states.postponed.length) { Chris@17: states.postponed.shift()(); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Object representing an element that depends on other elements. Chris@0: * Chris@0: * @constructor Drupal.states.Dependent Chris@0: * Chris@0: * @param {object} args Chris@0: * Object with the following keys (all of which are required) Chris@0: * @param {jQuery} args.element Chris@0: * A jQuery object of the dependent element Chris@0: * @param {Drupal.states.State} args.state Chris@0: * A State object describing the state that is dependent Chris@0: * @param {object} args.constraints Chris@0: * An object with dependency specifications. Lists all elements that this Chris@0: * element depends on. It can be nested and can contain Chris@0: * arbitrary AND and OR clauses. Chris@0: */ Chris@17: states.Dependent = function(args) { Chris@0: $.extend(this, { values: {}, oldValue: null }, args); Chris@0: Chris@0: this.dependees = this.getDependees(); Chris@17: Object.keys(this.dependees || {}).forEach(selector => { Chris@14: this.initializeDependee(selector, this.dependees[selector]); Chris@14: }); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Comparison functions for comparing the value of an element with the Chris@0: * specification from the dependency settings. If the object type can't be Chris@0: * found in this list, the === operator is used by default. Chris@0: * Chris@0: * @name Drupal.states.Dependent.comparisons Chris@0: * Chris@0: * @prop {function} RegExp Chris@0: * @prop {function} Function Chris@0: * @prop {function} Number Chris@0: */ Chris@0: states.Dependent.comparisons = { Chris@0: RegExp(reference, value) { Chris@0: return reference.test(value); Chris@0: }, Chris@0: Function(reference, value) { Chris@0: // The "reference" variable is a comparison function. Chris@0: return reference(value); Chris@0: }, Chris@0: Number(reference, value) { Chris@0: // If "reference" is a number and "value" is a string, then cast Chris@0: // reference as a string before applying the strict comparison in Chris@0: // compare(). Chris@0: // Otherwise numeric keys in the form's #states array fail to match Chris@0: // string values returned from jQuery's val(). Chris@17: return typeof value === 'string' Chris@17: ? compare(reference.toString(), value) Chris@17: : compare(reference, value); Chris@0: }, Chris@0: }; Chris@0: Chris@0: states.Dependent.prototype = { Chris@0: /** Chris@0: * Initializes one of the elements this dependent depends on. Chris@0: * Chris@0: * @memberof Drupal.states.Dependent# Chris@0: * Chris@0: * @param {string} selector Chris@0: * The CSS selector describing the dependee. Chris@0: * @param {object} dependeeStates Chris@0: * The list of states that have to be monitored for tracking the Chris@0: * dependee's compliance status. Chris@0: */ Chris@0: initializeDependee(selector, dependeeStates) { Chris@0: // Cache for the states of this dependee. Chris@0: this.values[selector] = {}; Chris@0: Chris@17: Object.keys(dependeeStates).forEach(i => { Chris@17: let state = dependeeStates[i]; Chris@17: // Make sure we're not initializing this selector/state combination Chris@17: // twice. Chris@17: if ($.inArray(state, dependeeStates) === -1) { Chris@17: return; Chris@17: } Chris@0: Chris@17: state = states.State.sanitize(state); Chris@0: Chris@17: // Initialize the value of this state. Chris@17: this.values[selector][state.name] = null; Chris@0: Chris@17: // Monitor state changes of the specified state for this dependee. Chris@17: $(selector).on(`state:${state}`, { selector, state }, e => { Chris@17: this.update(e.data.selector, e.data.state, e.value); Chris@17: }); Chris@0: Chris@17: // Make sure the event we just bound ourselves to is actually fired. Chris@17: new states.Trigger({ selector, state }); Chris@17: }); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Compares a value with a reference value. Chris@0: * Chris@0: * @memberof Drupal.states.Dependent# Chris@0: * Chris@0: * @param {object} reference Chris@0: * The value used for reference. Chris@0: * @param {string} selector Chris@0: * CSS selector describing the dependee. Chris@0: * @param {Drupal.states.State} state Chris@0: * A State object describing the dependee's updated state. Chris@0: * Chris@0: * @return {bool} Chris@0: * true or false. Chris@0: */ Chris@0: compare(reference, selector, state) { Chris@0: const value = this.values[selector][state.name]; Chris@0: if (reference.constructor.name in states.Dependent.comparisons) { Chris@0: // Use a custom compare function for certain reference value types. Chris@17: return states.Dependent.comparisons[reference.constructor.name]( Chris@17: reference, Chris@17: value, Chris@17: ); Chris@0: } Chris@0: Chris@17: // Do a plain comparison otherwise. Chris@0: return compare(reference, value); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Update the value of a dependee's state. Chris@0: * Chris@0: * @memberof Drupal.states.Dependent# Chris@0: * Chris@0: * @param {string} selector Chris@0: * CSS selector describing the dependee. Chris@0: * @param {Drupal.states.state} state Chris@0: * A State object describing the dependee's updated state. Chris@0: * @param {string} value Chris@0: * The new value for the dependee's updated state. Chris@0: */ Chris@0: update(selector, state, value) { Chris@0: // Only act when the 'new' value is actually new. Chris@0: if (value !== this.values[selector][state.name]) { Chris@0: this.values[selector][state.name] = value; Chris@0: this.reevaluate(); Chris@0: } Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Triggers change events in case a state changed. Chris@0: * Chris@0: * @memberof Drupal.states.Dependent# Chris@0: */ Chris@0: reevaluate() { Chris@0: // Check whether any constraint for this dependent state is satisfied. Chris@0: let value = this.verifyConstraints(this.constraints); Chris@0: Chris@0: // Only invoke a state change event when the value actually changed. Chris@0: if (value !== this.oldValue) { Chris@0: // Store the new value so that we can compare later whether the value Chris@0: // actually changed. Chris@0: this.oldValue = value; Chris@0: Chris@0: // Normalize the value to match the normalized state name. Chris@0: value = invert(value, this.state.invert); Chris@0: Chris@0: // By adding "trigger: true", we ensure that state changes don't go into Chris@0: // infinite loops. Chris@17: this.element.trigger({ Chris@17: type: `state:${this.state}`, Chris@17: value, Chris@17: trigger: true, Chris@17: }); Chris@0: } Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Evaluates child constraints to determine if a constraint is satisfied. Chris@0: * Chris@0: * @memberof Drupal.states.Dependent# Chris@0: * Chris@0: * @param {object|Array} constraints Chris@0: * A constraint object or an array of constraints. Chris@0: * @param {string} selector Chris@0: * The selector for these constraints. If undefined, there isn't yet a Chris@0: * selector that these constraints apply to. In that case, the keys of the Chris@0: * object are interpreted as the selector if encountered. Chris@0: * Chris@0: * @return {bool} Chris@0: * true or false, depending on whether these constraints are satisfied. Chris@0: */ Chris@0: verifyConstraints(constraints, selector) { Chris@0: let result; Chris@0: if ($.isArray(constraints)) { Chris@0: // This constraint is an array (OR or XOR). Chris@0: const hasXor = $.inArray('xor', constraints) === -1; Chris@0: const len = constraints.length; Chris@0: for (let i = 0; i < len; i++) { Chris@0: if (constraints[i] !== 'xor') { Chris@17: const constraint = this.checkConstraints( Chris@17: constraints[i], Chris@17: selector, Chris@17: i, Chris@17: ); Chris@0: // Return if this is OR and we have a satisfied constraint or if Chris@0: // this is XOR and we have a second satisfied constraint. Chris@0: if (constraint && (hasXor || result)) { Chris@0: return hasXor; Chris@0: } Chris@0: result = result || constraint; Chris@0: } Chris@0: } Chris@0: } Chris@0: // Make sure we don't try to iterate over things other than objects. This Chris@0: // shouldn't normally occur, but in case the condition definition is Chris@0: // bogus, we don't want to end up with an infinite loop. Chris@0: else if ($.isPlainObject(constraints)) { Chris@0: // This constraint is an object (AND). Chris@14: // eslint-disable-next-line no-restricted-syntax Chris@0: for (const n in constraints) { Chris@0: if (constraints.hasOwnProperty(n)) { Chris@17: result = ternary( Chris@17: result, Chris@17: this.checkConstraints(constraints[n], selector, n), Chris@17: ); Chris@0: // False and anything else will evaluate to false, so return when Chris@0: // any false condition is found. Chris@0: if (result === false) { Chris@0: return false; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: return result; Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Checks whether the value matches the requirements for this constraint. Chris@0: * Chris@0: * @memberof Drupal.states.Dependent# Chris@0: * Chris@0: * @param {string|Array|object} value Chris@0: * Either the value of a state or an array/object of constraints. In the Chris@0: * latter case, resolving the constraint continues. Chris@0: * @param {string} [selector] Chris@0: * The selector for this constraint. If undefined, there isn't yet a Chris@0: * selector that this constraint applies to. In that case, the state key Chris@0: * is propagates to a selector and resolving continues. Chris@0: * @param {Drupal.states.State} [state] Chris@0: * The state to check for this constraint. If undefined, resolving Chris@0: * continues. If both selector and state aren't undefined and valid Chris@0: * non-numeric strings, a lookup for the actual value of that selector's Chris@0: * state is performed. This parameter is not a State object but a pristine Chris@0: * state string. Chris@0: * Chris@0: * @return {bool} Chris@0: * true or false, depending on whether this constraint is satisfied. Chris@0: */ Chris@0: checkConstraints(value, selector, state) { Chris@0: // Normalize the last parameter. If it's non-numeric, we treat it either Chris@0: // as a selector (in case there isn't one yet) or as a trigger/state. Chris@17: if (typeof state !== 'string' || /[0-9]/.test(state[0])) { Chris@0: state = null; Chris@17: } else if (typeof selector === 'undefined') { Chris@0: // Propagate the state to the selector when there isn't one yet. Chris@0: selector = state; Chris@0: state = null; Chris@0: } Chris@0: Chris@0: if (state !== null) { Chris@0: // Constraints is the actual constraints of an element to check for. Chris@0: state = states.State.sanitize(state); Chris@0: return invert(this.compare(value, selector, state), state.invert); Chris@0: } Chris@0: Chris@17: // Resolve this constraint as an AND/OR operator. Chris@0: return this.verifyConstraints(value, selector); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Gathers information about all required triggers. Chris@0: * Chris@0: * @memberof Drupal.states.Dependent# Chris@0: * Chris@0: * @return {object} Chris@0: * An object describing the required triggers. Chris@0: */ Chris@0: getDependees() { Chris@0: const cache = {}; Chris@0: // Swivel the lookup function so that we can record all available Chris@0: // selector- state combinations for initialization. Chris@0: const _compare = this.compare; Chris@17: this.compare = function(reference, selector, state) { Chris@0: (cache[selector] || (cache[selector] = [])).push(state.name); Chris@0: // Return nothing (=== undefined) so that the constraint loops are not Chris@0: // broken. Chris@0: }; Chris@0: Chris@0: // This call doesn't actually verify anything but uses the resolving Chris@0: // mechanism to go through the constraints array, trying to look up each Chris@0: // value. Since we swivelled the compare function, this comparison returns Chris@0: // undefined and lookup continues until the very end. Instead of lookup up Chris@0: // the value, we record that combination of selector and state so that we Chris@0: // can initialize all triggers. Chris@0: this.verifyConstraints(this.constraints); Chris@0: // Restore the original function. Chris@0: this.compare = _compare; Chris@0: Chris@0: return cache; Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * @constructor Drupal.states.Trigger Chris@0: * Chris@0: * @param {object} args Chris@0: * Trigger arguments. Chris@0: */ Chris@17: states.Trigger = function(args) { Chris@0: $.extend(this, args); Chris@0: Chris@0: if (this.state in states.Trigger.states) { Chris@0: this.element = $(this.selector); Chris@0: Chris@0: // Only call the trigger initializer when it wasn't yet attached to this Chris@0: // element. Otherwise we'd end up with duplicate events. Chris@0: if (!this.element.data(`trigger:${this.state}`)) { Chris@0: this.initialize(); Chris@0: } Chris@0: } Chris@0: }; Chris@0: Chris@0: states.Trigger.prototype = { Chris@0: /** Chris@0: * @memberof Drupal.states.Trigger# Chris@0: */ Chris@0: initialize() { Chris@0: const trigger = states.Trigger.states[this.state]; Chris@0: Chris@0: if (typeof trigger === 'function') { Chris@0: // We have a custom trigger initialization function. Chris@0: trigger.call(window, this.element); Chris@17: } else { Chris@17: Object.keys(trigger || {}).forEach(event => { Chris@14: this.defaultTrigger(event, trigger[event]); Chris@14: }); Chris@0: } Chris@0: Chris@0: // Mark this trigger as initialized for this element. Chris@0: this.element.data(`trigger:${this.state}`, true); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * @memberof Drupal.states.Trigger# Chris@0: * Chris@0: * @param {jQuery.Event} event Chris@0: * The event triggered. Chris@0: * @param {function} valueFn Chris@0: * The function to call. Chris@0: */ Chris@0: defaultTrigger(event, valueFn) { Chris@0: let oldValue = valueFn.call(this.element); Chris@0: Chris@0: // Attach the event callback. Chris@17: this.element.on( Chris@17: event, Chris@17: $.proxy(function(e) { Chris@17: const value = valueFn.call(this.element, e); Chris@17: // Only trigger the event if the value has actually changed. Chris@17: if (oldValue !== value) { Chris@17: this.element.trigger({ Chris@17: type: `state:${this.state}`, Chris@17: value, Chris@17: oldValue, Chris@17: }); Chris@17: oldValue = value; Chris@17: } Chris@17: }, this), Chris@17: ); Chris@0: Chris@17: states.postponed.push( Chris@17: $.proxy(function() { Chris@17: // Trigger the event once for initialization purposes. Chris@17: this.element.trigger({ Chris@17: type: `state:${this.state}`, Chris@17: value: oldValue, Chris@17: oldValue: null, Chris@17: }); Chris@17: }, this), Chris@17: ); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * This list of states contains functions that are used to monitor the state Chris@0: * of an element. Whenever an element depends on the state of another element, Chris@0: * one of these trigger functions is added to the dependee so that the Chris@0: * dependent element can be updated. Chris@0: * Chris@0: * @name Drupal.states.Trigger.states Chris@0: * Chris@0: * @prop empty Chris@0: * @prop checked Chris@0: * @prop value Chris@0: * @prop collapsed Chris@0: */ Chris@0: states.Trigger.states = { Chris@0: // 'empty' describes the state to be monitored. Chris@0: empty: { Chris@0: // 'keyup' is the (native DOM) event that we watch for. Chris@0: keyup() { Chris@0: // The function associated with that trigger returns the new value for Chris@0: // the state. Chris@0: return this.val() === ''; Chris@0: }, Chris@0: }, Chris@0: Chris@0: checked: { Chris@0: change() { Chris@0: // prop() and attr() only takes the first element into account. To Chris@0: // support selectors matching multiple checkboxes, iterate over all and Chris@0: // return whether any is checked. Chris@0: let checked = false; Chris@17: this.each(function() { Chris@0: // Use prop() here as we want a boolean of the checkbox state. Chris@0: // @see http://api.jquery.com/prop/ Chris@0: checked = $(this).prop('checked'); Chris@0: // Break the each() loop if this is checked. Chris@0: return !checked; Chris@0: }); Chris@0: return checked; Chris@0: }, Chris@0: }, Chris@0: Chris@0: // For radio buttons, only return the value if the radio button is selected. Chris@0: value: { Chris@0: keyup() { Chris@0: // Radio buttons share the same :input[name="key"] selector. Chris@0: if (this.length > 1) { Chris@0: // Initial checked value of radios is undefined, so we return false. Chris@0: return this.filter(':checked').val() || false; Chris@0: } Chris@0: return this.val(); Chris@0: }, Chris@0: change() { Chris@0: // Radio buttons share the same :input[name="key"] selector. Chris@0: if (this.length > 1) { Chris@0: // Initial checked value of radios is undefined, so we return false. Chris@0: return this.filter(':checked').val() || false; Chris@0: } Chris@0: return this.val(); Chris@0: }, Chris@0: }, Chris@0: Chris@0: collapsed: { Chris@0: collapsed(e) { Chris@17: return typeof e !== 'undefined' && 'value' in e Chris@17: ? e.value Chris@17: : !this.is('[open]'); Chris@0: }, Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * A state object is used for describing the state and performing aliasing. Chris@0: * Chris@0: * @constructor Drupal.states.State Chris@0: * Chris@0: * @param {string} state Chris@0: * The name of the state. Chris@0: */ Chris@17: states.State = function(state) { Chris@0: /** Chris@0: * Original unresolved name. Chris@0: */ Chris@14: this.pristine = state; Chris@14: this.name = state; Chris@0: Chris@0: // Normalize the state name. Chris@0: let process = true; Chris@0: do { Chris@0: // Iteratively remove exclamation marks and invert the value. Chris@0: while (this.name.charAt(0) === '!') { Chris@0: this.name = this.name.substring(1); Chris@0: this.invert = !this.invert; Chris@0: } Chris@0: Chris@0: // Replace the state with its normalized name. Chris@0: if (this.name in states.State.aliases) { Chris@0: this.name = states.State.aliases[this.name]; Chris@17: } else { Chris@0: process = false; Chris@0: } Chris@0: } while (process); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Creates a new State object by sanitizing the passed value. Chris@0: * Chris@0: * @name Drupal.states.State.sanitize Chris@0: * Chris@0: * @param {string|Drupal.states.State} state Chris@0: * A state object or the name of a state. Chris@0: * Chris@0: * @return {Drupal.states.state} Chris@0: * A state object. Chris@0: */ Chris@17: states.State.sanitize = function(state) { Chris@0: if (state instanceof states.State) { Chris@0: return state; Chris@0: } Chris@0: Chris@0: return new states.State(state); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * This list of aliases is used to normalize states and associates negated Chris@0: * names with their respective inverse state. Chris@0: * Chris@0: * @name Drupal.states.State.aliases Chris@0: */ Chris@0: states.State.aliases = { Chris@0: enabled: '!disabled', Chris@0: invisible: '!visible', Chris@0: invalid: '!valid', Chris@0: untouched: '!touched', Chris@0: optional: '!required', Chris@0: filled: '!empty', Chris@0: unchecked: '!checked', Chris@0: irrelevant: '!relevant', Chris@0: expanded: '!collapsed', Chris@0: open: '!collapsed', Chris@0: closed: 'collapsed', Chris@0: readwrite: '!readonly', Chris@0: }; Chris@0: Chris@0: states.State.prototype = { Chris@0: /** Chris@0: * @memberof Drupal.states.State# Chris@0: */ Chris@0: invert: false, Chris@0: Chris@0: /** Chris@0: * Ensures that just using the state object returns the name. Chris@0: * Chris@0: * @memberof Drupal.states.State# Chris@0: * Chris@0: * @return {string} Chris@0: * The name of the state. Chris@0: */ Chris@0: toString() { Chris@0: return this.name; Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Global state change handlers. These are bound to "document" to cover all Chris@0: * elements whose state changes. Events sent to elements within the page Chris@0: * bubble up to these handlers. We use this system so that themes and modules Chris@0: * can override these state change handlers for particular parts of a page. Chris@0: */ Chris@0: Chris@0: const $document = $(document); Chris@17: $document.on('state:disabled', e => { Chris@0: // Only act when this change was triggered by a dependency and not by the Chris@0: // element monitoring itself. Chris@0: if (e.trigger) { Chris@0: $(e.target) Chris@0: .prop('disabled', e.value) Chris@14: .closest('.js-form-item, .js-form-submit, .js-form-wrapper') Chris@14: .toggleClass('form-disabled', e.value) Chris@14: .find('select, input, textarea') Chris@14: .prop('disabled', e.value); Chris@0: Chris@0: // Note: WebKit nightlies don't reflect that change correctly. Chris@0: // See https://bugs.webkit.org/show_bug.cgi?id=23789 Chris@0: } Chris@0: }); Chris@0: Chris@17: $document.on('state:required', e => { Chris@0: if (e.trigger) { Chris@0: if (e.value) { Chris@0: const label = `label${e.target.id ? `[for=${e.target.id}]` : ''}`; Chris@17: const $label = $(e.target) Chris@18: .attr({ required: 'required', 'aria-required': 'true' }) Chris@17: .closest('.js-form-item, .js-form-wrapper') Chris@17: .find(label); Chris@0: // Avoids duplicate required markers on initialization. Chris@0: if (!$label.hasClass('js-form-required').length) { Chris@0: $label.addClass('js-form-required form-required'); Chris@0: } Chris@17: } else { Chris@14: $(e.target) Chris@14: .removeAttr('required aria-required') Chris@14: .closest('.js-form-item, .js-form-wrapper') Chris@14: .find('label.js-form-required') Chris@14: .removeClass('js-form-required form-required'); Chris@0: } Chris@0: } Chris@0: }); Chris@0: Chris@17: $document.on('state:visible', e => { Chris@0: if (e.trigger) { Chris@17: $(e.target) Chris@17: .closest('.js-form-item, .js-form-submit, .js-form-wrapper') Chris@17: .toggle(e.value); Chris@0: } Chris@0: }); Chris@0: Chris@17: $document.on('state:checked', e => { Chris@0: if (e.trigger) { Chris@0: $(e.target).prop('checked', e.value); Chris@0: } Chris@0: }); Chris@0: Chris@17: $document.on('state:collapsed', e => { Chris@0: if (e.trigger) { Chris@0: if ($(e.target).is('[open]') === e.value) { Chris@17: $(e.target) Chris@17: .find('> summary') Chris@17: .trigger('click'); Chris@0: } Chris@0: } Chris@0: }); Chris@17: })(jQuery, Drupal);