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.