comparison core/misc/states.es6.js @ 4:a9cd425dd02b

Update, including to Drupal core 8.6.10
author Chris Cannam
date Thu, 28 Feb 2019 13:11:55 +0000
parents c75dbcec494b
children 12f9dff5fda9
comparison
equal deleted inserted replaced
3:307d7a7fd348 4:a9cd425dd02b
1 /** 1 /**
2 * @file 2 * @file
3 * Drupal's states library. 3 * Drupal's states library.
4 */ 4 */
5 5
6 (function ($, Drupal) { 6 (function($, Drupal) {
7 /** 7 /**
8 * The base States namespace. 8 * The base States namespace.
9 * 9 *
10 * Having the local states variable allows us to use the States namespace 10 * Having the local states variable allows us to use the States namespace
11 * without having to always declare "Drupal.states". 11 * without having to always declare "Drupal.states".
12 * 12 *
13 * @namespace Drupal.states 13 * @namespace Drupal.states
14 */ 14 */
15 const states = { 15 const states = {
16
17 /** 16 /**
18 * An array of functions that should be postponed. 17 * An array of functions that should be postponed.
19 */ 18 */
20 postponed: [], 19 postponed: [],
21 }; 20 };
22 21
23 Drupal.states = states; 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 {bool} invertState
32 * Whether to invert state or not.
33 *
34 * @return {bool}
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 {bool}
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 {bool}
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 }
24 85
25 /** 86 /**
26 * Attaches the states. 87 * Attaches the states.
27 * 88 *
28 * @type {Drupal~behavior} 89 * @type {Drupal~behavior}
33 Drupal.behaviors.states = { 94 Drupal.behaviors.states = {
34 attach(context, settings) { 95 attach(context, settings) {
35 const $states = $(context).find('[data-drupal-states]'); 96 const $states = $(context).find('[data-drupal-states]');
36 const il = $states.length; 97 const il = $states.length;
37 for (let i = 0; i < il; i++) { 98 for (let i = 0; i < il; i++) {
38 const config = JSON.parse($states[i].getAttribute('data-drupal-states')); 99 const config = JSON.parse(
39 Object.keys(config || {}).forEach((state) => { 100 $states[i].getAttribute('data-drupal-states'),
101 );
102 Object.keys(config || {}).forEach(state => {
40 new states.Dependent({ 103 new states.Dependent({
41 element: $($states[i]), 104 element: $($states[i]),
42 state: states.State.sanitize(state), 105 state: states.State.sanitize(state),
43 constraints: config[state], 106 constraints: config[state],
44 }); 107 });
45 }); 108 });
46 } 109 }
47 110
48 // Execute all postponed functions now. 111 // Execute all postponed functions now.
49 while (states.postponed.length) { 112 while (states.postponed.length) {
50 (states.postponed.shift())(); 113 states.postponed.shift()();
51 } 114 }
52 }, 115 },
53 }; 116 };
54 117
55 /** 118 /**
66 * @param {object} args.constraints 129 * @param {object} args.constraints
67 * An object with dependency specifications. Lists all elements that this 130 * An object with dependency specifications. Lists all elements that this
68 * element depends on. It can be nested and can contain 131 * element depends on. It can be nested and can contain
69 * arbitrary AND and OR clauses. 132 * arbitrary AND and OR clauses.
70 */ 133 */
71 states.Dependent = function (args) { 134 states.Dependent = function(args) {
72 $.extend(this, { values: {}, oldValue: null }, args); 135 $.extend(this, { values: {}, oldValue: null }, args);
73 136
74 this.dependees = this.getDependees(); 137 this.dependees = this.getDependees();
75 Object.keys(this.dependees || {}).forEach((selector) => { 138 Object.keys(this.dependees || {}).forEach(selector => {
76 this.initializeDependee(selector, this.dependees[selector]); 139 this.initializeDependee(selector, this.dependees[selector]);
77 }); 140 });
78 }; 141 };
79 142
80 /** 143 /**
100 // If "reference" is a number and "value" is a string, then cast 163 // If "reference" is a number and "value" is a string, then cast
101 // reference as a string before applying the strict comparison in 164 // reference as a string before applying the strict comparison in
102 // compare(). 165 // compare().
103 // Otherwise numeric keys in the form's #states array fail to match 166 // Otherwise numeric keys in the form's #states array fail to match
104 // string values returned from jQuery's val(). 167 // string values returned from jQuery's val().
105 return (typeof value === 'string') ? compare(reference.toString(), value) : compare(reference, value); 168 return typeof value === 'string'
169 ? compare(reference.toString(), value)
170 : compare(reference, value);
106 }, 171 },
107 }; 172 };
108 173
109 states.Dependent.prototype = { 174 states.Dependent.prototype = {
110
111 /** 175 /**
112 * Initializes one of the elements this dependent depends on. 176 * Initializes one of the elements this dependent depends on.
113 * 177 *
114 * @memberof Drupal.states.Dependent# 178 * @memberof Drupal.states.Dependent#
115 * 179 *
118 * @param {object} dependeeStates 182 * @param {object} dependeeStates
119 * The list of states that have to be monitored for tracking the 183 * The list of states that have to be monitored for tracking the
120 * dependee's compliance status. 184 * dependee's compliance status.
121 */ 185 */
122 initializeDependee(selector, dependeeStates) { 186 initializeDependee(selector, dependeeStates) {
123 let state;
124 const self = this;
125
126 function stateEventHandler(e) {
127 self.update(e.data.selector, e.data.state, e.value);
128 }
129
130 // Cache for the states of this dependee. 187 // Cache for the states of this dependee.
131 this.values[selector] = {}; 188 this.values[selector] = {};
132 189
133 // eslint-disable-next-line no-restricted-syntax 190 Object.keys(dependeeStates).forEach(i => {
134 for (const i in dependeeStates) { 191 let state = dependeeStates[i];
135 if (dependeeStates.hasOwnProperty(i)) { 192 // Make sure we're not initializing this selector/state combination
136 state = dependeeStates[i]; 193 // twice.
137 // Make sure we're not initializing this selector/state combination 194 if ($.inArray(state, dependeeStates) === -1) {
138 // twice. 195 return;
139 if ($.inArray(state, dependeeStates) === -1) {
140 continue;
141 }
142
143 state = states.State.sanitize(state);
144
145 // Initialize the value of this state.
146 this.values[selector][state.name] = null;
147
148 // Monitor state changes of the specified state for this dependee.
149 $(selector).on(`state:${state}`, { selector, state }, stateEventHandler);
150
151 // Make sure the event we just bound ourselves to is actually fired.
152 new states.Trigger({ selector, state });
153 } 196 }
154 } 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 });
155 }, 211 },
156 212
157 /** 213 /**
158 * Compares a value with a reference value. 214 * Compares a value with a reference value.
159 * 215 *
171 */ 227 */
172 compare(reference, selector, state) { 228 compare(reference, selector, state) {
173 const value = this.values[selector][state.name]; 229 const value = this.values[selector][state.name];
174 if (reference.constructor.name in states.Dependent.comparisons) { 230 if (reference.constructor.name in states.Dependent.comparisons) {
175 // Use a custom compare function for certain reference value types. 231 // Use a custom compare function for certain reference value types.
176 return states.Dependent.comparisons[reference.constructor.name](reference, value); 232 return states.Dependent.comparisons[reference.constructor.name](
177 } 233 reference,
178 234 value,
179 // Do a plain comparison otherwise. 235 );
236 }
237
238 // Do a plain comparison otherwise.
180 return compare(reference, value); 239 return compare(reference, value);
181 }, 240 },
182 241
183 /** 242 /**
184 * Update the value of a dependee's state. 243 * Update the value of a dependee's state.
218 // Normalize the value to match the normalized state name. 277 // Normalize the value to match the normalized state name.
219 value = invert(value, this.state.invert); 278 value = invert(value, this.state.invert);
220 279
221 // By adding "trigger: true", we ensure that state changes don't go into 280 // By adding "trigger: true", we ensure that state changes don't go into
222 // infinite loops. 281 // infinite loops.
223 this.element.trigger({ type: `state:${this.state}`, value, trigger: true }); 282 this.element.trigger({
283 type: `state:${this.state}`,
284 value,
285 trigger: true,
286 });
224 } 287 }
225 }, 288 },
226 289
227 /** 290 /**
228 * Evaluates child constraints to determine if a constraint is satisfied. 291 * Evaluates child constraints to determine if a constraint is satisfied.
245 // This constraint is an array (OR or XOR). 308 // This constraint is an array (OR or XOR).
246 const hasXor = $.inArray('xor', constraints) === -1; 309 const hasXor = $.inArray('xor', constraints) === -1;
247 const len = constraints.length; 310 const len = constraints.length;
248 for (let i = 0; i < len; i++) { 311 for (let i = 0; i < len; i++) {
249 if (constraints[i] !== 'xor') { 312 if (constraints[i] !== 'xor') {
250 const constraint = this.checkConstraints(constraints[i], selector, i); 313 const constraint = this.checkConstraints(
314 constraints[i],
315 selector,
316 i,
317 );
251 // Return if this is OR and we have a satisfied constraint or if 318 // Return if this is OR and we have a satisfied constraint or if
252 // this is XOR and we have a second satisfied constraint. 319 // this is XOR and we have a second satisfied constraint.
253 if (constraint && (hasXor || result)) { 320 if (constraint && (hasXor || result)) {
254 return hasXor; 321 return hasXor;
255 } 322 }
263 else if ($.isPlainObject(constraints)) { 330 else if ($.isPlainObject(constraints)) {
264 // This constraint is an object (AND). 331 // This constraint is an object (AND).
265 // eslint-disable-next-line no-restricted-syntax 332 // eslint-disable-next-line no-restricted-syntax
266 for (const n in constraints) { 333 for (const n in constraints) {
267 if (constraints.hasOwnProperty(n)) { 334 if (constraints.hasOwnProperty(n)) {
268 result = ternary(result, this.checkConstraints(constraints[n], selector, n)); 335 result = ternary(
336 result,
337 this.checkConstraints(constraints[n], selector, n),
338 );
269 // False and anything else will evaluate to false, so return when 339 // False and anything else will evaluate to false, so return when
270 // any false condition is found. 340 // any false condition is found.
271 if (result === false) { 341 if (result === false) {
272 return false; 342 return false;
273 } 343 }
300 * true or false, depending on whether this constraint is satisfied. 370 * true or false, depending on whether this constraint is satisfied.
301 */ 371 */
302 checkConstraints(value, selector, state) { 372 checkConstraints(value, selector, state) {
303 // Normalize the last parameter. If it's non-numeric, we treat it either 373 // Normalize the last parameter. If it's non-numeric, we treat it either
304 // as a selector (in case there isn't one yet) or as a trigger/state. 374 // as a selector (in case there isn't one yet) or as a trigger/state.
305 if (typeof state !== 'string' || (/[0-9]/).test(state[0])) { 375 if (typeof state !== 'string' || /[0-9]/.test(state[0])) {
306 state = null; 376 state = null;
307 } 377 } else if (typeof selector === 'undefined') {
308 else if (typeof selector === 'undefined') {
309 // Propagate the state to the selector when there isn't one yet. 378 // Propagate the state to the selector when there isn't one yet.
310 selector = state; 379 selector = state;
311 state = null; 380 state = null;
312 } 381 }
313 382
315 // Constraints is the actual constraints of an element to check for. 384 // Constraints is the actual constraints of an element to check for.
316 state = states.State.sanitize(state); 385 state = states.State.sanitize(state);
317 return invert(this.compare(value, selector, state), state.invert); 386 return invert(this.compare(value, selector, state), state.invert);
318 } 387 }
319 388
320 // Resolve this constraint as an AND/OR operator. 389 // Resolve this constraint as an AND/OR operator.
321 return this.verifyConstraints(value, selector); 390 return this.verifyConstraints(value, selector);
322 }, 391 },
323 392
324 /** 393 /**
325 * Gathers information about all required triggers. 394 * Gathers information about all required triggers.
332 getDependees() { 401 getDependees() {
333 const cache = {}; 402 const cache = {};
334 // Swivel the lookup function so that we can record all available 403 // Swivel the lookup function so that we can record all available
335 // selector- state combinations for initialization. 404 // selector- state combinations for initialization.
336 const _compare = this.compare; 405 const _compare = this.compare;
337 this.compare = function (reference, selector, state) { 406 this.compare = function(reference, selector, state) {
338 (cache[selector] || (cache[selector] = [])).push(state.name); 407 (cache[selector] || (cache[selector] = [])).push(state.name);
339 // Return nothing (=== undefined) so that the constraint loops are not 408 // Return nothing (=== undefined) so that the constraint loops are not
340 // broken. 409 // broken.
341 }; 410 };
342 411
358 * @constructor Drupal.states.Trigger 427 * @constructor Drupal.states.Trigger
359 * 428 *
360 * @param {object} args 429 * @param {object} args
361 * Trigger arguments. 430 * Trigger arguments.
362 */ 431 */
363 states.Trigger = function (args) { 432 states.Trigger = function(args) {
364 $.extend(this, args); 433 $.extend(this, args);
365 434
366 if (this.state in states.Trigger.states) { 435 if (this.state in states.Trigger.states) {
367 this.element = $(this.selector); 436 this.element = $(this.selector);
368 437
373 } 442 }
374 } 443 }
375 }; 444 };
376 445
377 states.Trigger.prototype = { 446 states.Trigger.prototype = {
378
379 /** 447 /**
380 * @memberof Drupal.states.Trigger# 448 * @memberof Drupal.states.Trigger#
381 */ 449 */
382 initialize() { 450 initialize() {
383 const trigger = states.Trigger.states[this.state]; 451 const trigger = states.Trigger.states[this.state];
384 452
385 if (typeof trigger === 'function') { 453 if (typeof trigger === 'function') {
386 // We have a custom trigger initialization function. 454 // We have a custom trigger initialization function.
387 trigger.call(window, this.element); 455 trigger.call(window, this.element);
388 } 456 } else {
389 else { 457 Object.keys(trigger || {}).forEach(event => {
390 Object.keys(trigger || {}).forEach((event) => {
391 this.defaultTrigger(event, trigger[event]); 458 this.defaultTrigger(event, trigger[event]);
392 }); 459 });
393 } 460 }
394 461
395 // Mark this trigger as initialized for this element. 462 // Mark this trigger as initialized for this element.
406 */ 473 */
407 defaultTrigger(event, valueFn) { 474 defaultTrigger(event, valueFn) {
408 let oldValue = valueFn.call(this.element); 475 let oldValue = valueFn.call(this.element);
409 476
410 // Attach the event callback. 477 // Attach the event callback.
411 this.element.on(event, $.proxy(function (e) { 478 this.element.on(
412 const value = valueFn.call(this.element, e); 479 event,
413 // Only trigger the event if the value has actually changed. 480 $.proxy(function(e) {
414 if (oldValue !== value) { 481 const value = valueFn.call(this.element, e);
415 this.element.trigger({ type: `state:${this.state}`, value, oldValue }); 482 // Only trigger the event if the value has actually changed.
416 oldValue = value; 483 if (oldValue !== value) {
417 } 484 this.element.trigger({
418 }, this)); 485 type: `state:${this.state}`,
419 486 value,
420 states.postponed.push($.proxy(function () { 487 oldValue,
421 // Trigger the event once for initialization purposes. 488 });
422 this.element.trigger({ type: `state:${this.state}`, value: oldValue, oldValue: null }); 489 oldValue = value;
423 }, this)); 490 }
491 }, this),
492 );
493
494 states.postponed.push(
495 $.proxy(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 }, this),
503 );
424 }, 504 },
425 }; 505 };
426 506
427 /** 507 /**
428 * This list of states contains functions that are used to monitor the state 508 * This list of states contains functions that are used to monitor the state
452 change() { 532 change() {
453 // prop() and attr() only takes the first element into account. To 533 // prop() and attr() only takes the first element into account. To
454 // support selectors matching multiple checkboxes, iterate over all and 534 // support selectors matching multiple checkboxes, iterate over all and
455 // return whether any is checked. 535 // return whether any is checked.
456 let checked = false; 536 let checked = false;
457 this.each(function () { 537 this.each(function() {
458 // Use prop() here as we want a boolean of the checkbox state. 538 // Use prop() here as we want a boolean of the checkbox state.
459 // @see http://api.jquery.com/prop/ 539 // @see http://api.jquery.com/prop/
460 checked = $(this).prop('checked'); 540 checked = $(this).prop('checked');
461 // Break the each() loop if this is checked. 541 // Break the each() loop if this is checked.
462 return !checked; 542 return !checked;
485 }, 565 },
486 }, 566 },
487 567
488 collapsed: { 568 collapsed: {
489 collapsed(e) { 569 collapsed(e) {
490 return (typeof e !== 'undefined' && 'value' in e) ? e.value : !this.is('[open]'); 570 return typeof e !== 'undefined' && 'value' in e
571 ? e.value
572 : !this.is('[open]');
491 }, 573 },
492 }, 574 },
493 }; 575 };
494 576
495 /** 577 /**
498 * @constructor Drupal.states.State 580 * @constructor Drupal.states.State
499 * 581 *
500 * @param {string} state 582 * @param {string} state
501 * The name of the state. 583 * The name of the state.
502 */ 584 */
503 states.State = function (state) { 585 states.State = function(state) {
504 /** 586 /**
505 * Original unresolved name. 587 * Original unresolved name.
506 */ 588 */
507 this.pristine = state; 589 this.pristine = state;
508 this.name = state; 590 this.name = state;
517 } 599 }
518 600
519 // Replace the state with its normalized name. 601 // Replace the state with its normalized name.
520 if (this.name in states.State.aliases) { 602 if (this.name in states.State.aliases) {
521 this.name = states.State.aliases[this.name]; 603 this.name = states.State.aliases[this.name];
522 } 604 } else {
523 else {
524 process = false; 605 process = false;
525 } 606 }
526 } while (process); 607 } while (process);
527 }; 608 };
528 609
535 * A state object or the name of a state. 616 * A state object or the name of a state.
536 * 617 *
537 * @return {Drupal.states.state} 618 * @return {Drupal.states.state}
538 * A state object. 619 * A state object.
539 */ 620 */
540 states.State.sanitize = function (state) { 621 states.State.sanitize = function(state) {
541 if (state instanceof states.State) { 622 if (state instanceof states.State) {
542 return state; 623 return state;
543 } 624 }
544 625
545 return new states.State(state); 626 return new states.State(state);
565 closed: 'collapsed', 646 closed: 'collapsed',
566 readwrite: '!readonly', 647 readwrite: '!readonly',
567 }; 648 };
568 649
569 states.State.prototype = { 650 states.State.prototype = {
570
571 /** 651 /**
572 * @memberof Drupal.states.State# 652 * @memberof Drupal.states.State#
573 */ 653 */
574 invert: false, 654 invert: false,
575 655
592 * bubble up to these handlers. We use this system so that themes and modules 672 * bubble up to these handlers. We use this system so that themes and modules
593 * can override these state change handlers for particular parts of a page. 673 * can override these state change handlers for particular parts of a page.
594 */ 674 */
595 675
596 const $document = $(document); 676 const $document = $(document);
597 $document.on('state:disabled', (e) => { 677 $document.on('state:disabled', e => {
598 // Only act when this change was triggered by a dependency and not by the 678 // Only act when this change was triggered by a dependency and not by the
599 // element monitoring itself. 679 // element monitoring itself.
600 if (e.trigger) { 680 if (e.trigger) {
601 $(e.target) 681 $(e.target)
602 .prop('disabled', e.value) 682 .prop('disabled', e.value)
608 // Note: WebKit nightlies don't reflect that change correctly. 688 // Note: WebKit nightlies don't reflect that change correctly.
609 // See https://bugs.webkit.org/show_bug.cgi?id=23789 689 // See https://bugs.webkit.org/show_bug.cgi?id=23789
610 } 690 }
611 }); 691 });
612 692
613 $document.on('state:required', (e) => { 693 $document.on('state:required', e => {
614 if (e.trigger) { 694 if (e.trigger) {
615 if (e.value) { 695 if (e.value) {
616 const label = `label${e.target.id ? `[for=${e.target.id}]` : ''}`; 696 const label = `label${e.target.id ? `[for=${e.target.id}]` : ''}`;
617 const $label = $(e.target).attr({ required: 'required', 'aria-required': 'aria-required' }).closest('.js-form-item, .js-form-wrapper').find(label); 697 const $label = $(e.target)
698 .attr({ required: 'required', 'aria-required': 'aria-required' })
699 .closest('.js-form-item, .js-form-wrapper')
700 .find(label);
618 // Avoids duplicate required markers on initialization. 701 // Avoids duplicate required markers on initialization.
619 if (!$label.hasClass('js-form-required').length) { 702 if (!$label.hasClass('js-form-required').length) {
620 $label.addClass('js-form-required form-required'); 703 $label.addClass('js-form-required form-required');
621 } 704 }
622 } 705 } else {
623 else {
624 $(e.target) 706 $(e.target)
625 .removeAttr('required aria-required') 707 .removeAttr('required aria-required')
626 .closest('.js-form-item, .js-form-wrapper') 708 .closest('.js-form-item, .js-form-wrapper')
627 .find('label.js-form-required') 709 .find('label.js-form-required')
628 .removeClass('js-form-required form-required'); 710 .removeClass('js-form-required form-required');
629 } 711 }
630 } 712 }
631 }); 713 });
632 714
633 $document.on('state:visible', (e) => { 715 $document.on('state:visible', e => {
634 if (e.trigger) { 716 if (e.trigger) {
635 $(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggle(e.value); 717 $(e.target)
718 .closest('.js-form-item, .js-form-submit, .js-form-wrapper')
719 .toggle(e.value);
636 } 720 }
637 }); 721 });
638 722
639 $document.on('state:checked', (e) => { 723 $document.on('state:checked', e => {
640 if (e.trigger) { 724 if (e.trigger) {
641 $(e.target).prop('checked', e.value); 725 $(e.target).prop('checked', e.value);
642 } 726 }
643 }); 727 });
644 728
645 $document.on('state:collapsed', (e) => { 729 $document.on('state:collapsed', e => {
646 if (e.trigger) { 730 if (e.trigger) {
647 if ($(e.target).is('[open]') === e.value) { 731 if ($(e.target).is('[open]') === e.value) {
648 $(e.target).find('> summary').trigger('click'); 732 $(e.target)
733 .find('> summary')
734 .trigger('click');
649 } 735 }
650 } 736 }
651 }); 737 });
652 738 })(jQuery, Drupal);
653 /**
654 * These are helper functions implementing addition "operators" and don't
655 * implement any logic that is particular to states.
656 */
657
658 /**
659 * Bitwise AND with a third undefined state.
660 *
661 * @function Drupal.states~ternary
662 *
663 * @param {*} a
664 * Value a.
665 * @param {*} b
666 * Value b
667 *
668 * @return {bool}
669 * The result.
670 */
671 function ternary(a, b) {
672 if (typeof a === 'undefined') {
673 return b;
674 }
675 else if (typeof b === 'undefined') {
676 return a;
677 }
678
679 return a && b;
680 }
681
682 /**
683 * Inverts a (if it's not undefined) when invertState is true.
684 *
685 * @function Drupal.states~invert
686 *
687 * @param {*} a
688 * The value to maybe invert.
689 * @param {bool} invertState
690 * Whether to invert state or not.
691 *
692 * @return {bool}
693 * The result.
694 */
695 function invert(a, invertState) {
696 return (invertState && typeof a !== 'undefined') ? !a : a;
697 }
698
699 /**
700 * Compares two values while ignoring undefined values.
701 *
702 * @function Drupal.states~compare
703 *
704 * @param {*} a
705 * Value a.
706 * @param {*} b
707 * Value b.
708 *
709 * @return {bool}
710 * The comparison result.
711 */
712 function compare(a, b) {
713 if (a === b) {
714 return typeof a === 'undefined' ? a : true;
715 }
716
717 return typeof a === 'undefined' || typeof b === 'undefined';
718 }
719 }(jQuery, Drupal));