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