annotate core/misc/states.es6.js @ 19:fa3358dc1485 tip

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