diff --git a/core/misc/states.es6.js b/core/misc/states.es6.js index dd2c315539703d5c2c5cb358ca007c91eba34527..811a5112d1746cbc3bd65608cf54e56f12dc0e2d 100644 --- a/core/misc/states.es6.js +++ b/core/misc/states.es6.js @@ -59,6 +59,30 @@ return typeof a === 'undefined' || typeof b === 'undefined'; } + /** + * Bitwise AND with a third undefined state. + * + * @function Drupal.states~ternary + * + * @param {*} a + * Value a. + * @param {*} b + * Value b + * + * @return {bool} + * The result. + */ + function ternary(a, b) { + if (typeof a === 'undefined') { + return b; + } + if (typeof b === 'undefined') { + return a; + } + + return a && b; + } + /** * Attaches the states. * @@ -305,18 +329,20 @@ // bogus, we don't want to end up with an infinite loop. else if ($.isPlainObject(constraints)) { // This constraint is an object (AND). - result = Object.keys(constraints).every(constraint => { - const check = this.checkConstraints( - constraints[constraint], - selector, - constraint, - ); - /** - * The checkConstraints() function's return value can be undefined. If - * this so, consider it to have returned true. - */ - return typeof check === 'undefined' ? true : check; - }); + // eslint-disable-next-line no-restricted-syntax + for (const n in constraints) { + if (constraints.hasOwnProperty(n)) { + result = ternary( + result, + this.checkConstraints(constraints[n], selector, n), + ); + // False and anything else will evaluate to false, so return when + // any false condition is found. + if (result === false) { + return false; + } + } + } } return result; }, diff --git a/core/misc/states.js b/core/misc/states.js index fcdc37accd751c14d4f41b39c1b4379719fac995..7b451b07dec9c55526461602ac7860ae7738dee0 100644 --- a/core/misc/states.js +++ b/core/misc/states.js @@ -24,6 +24,17 @@ return typeof a === 'undefined' || typeof b === 'undefined'; } + function ternary(a, b) { + if (typeof a === 'undefined') { + return b; + } + if (typeof b === 'undefined') { + return a; + } + + return a && b; + } + Drupal.behaviors.states = { attach: function attach(context, settings) { var $states = $(context).find('[data-drupal-states]'); @@ -127,8 +138,6 @@ } }, verifyConstraints: function verifyConstraints(constraints, selector) { - var _this3 = this; - var result = void 0; if ($.isArray(constraints)) { var hasXor = $.inArray('xor', constraints) === -1; @@ -144,11 +153,15 @@ } } } else if ($.isPlainObject(constraints)) { - result = Object.keys(constraints).every(function (constraint) { - var check = _this3.checkConstraints(constraints[constraint], selector, constraint); + for (var n in constraints) { + if (constraints.hasOwnProperty(n)) { + result = ternary(result, this.checkConstraints(constraints[n], selector, n)); - return typeof check === 'undefined' ? true : check; - }); + if (result === false) { + return false; + } + } + } } return result; }, @@ -197,7 +210,7 @@ states.Trigger.prototype = { initialize: function initialize() { - var _this4 = this; + var _this3 = this; var trigger = states.Trigger.states[this.state]; @@ -205,7 +218,7 @@ trigger.call(window, this.element); } else { Object.keys(trigger || {}).forEach(function (event) { - _this4.defaultTrigger(event, trigger[event]); + _this3.defaultTrigger(event, trigger[event]); }); } diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index 0662853339d412b5517c3255977c6bf598d2b235..ba8e798ca1536532aed80516aad10466852e7dd0 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -169,3 +169,12 @@ function system_post_update_extra_fields(&$sandbox = NULL) { $config_entity_updater->update($sandbox, 'entity_form_display', $callback); $config_entity_updater->update($sandbox, 'entity_view_display', $callback); } + +/** + * Force cache clear to ensure aggregated JavaScript files are regenerated. + * + * @see https://www.drupal.org/project/drupal/issues/2995570 + */ +function system_post_update_states_clear_cache() { + // Empty post-update hook. +} diff --git a/core/modules/system/tests/modules/form_test/form_test.routing.yml b/core/modules/system/tests/modules/form_test/form_test.routing.yml index f2de1264743415bfcefb0ed06aa5cd16fb2b8d04..ce4dfa98fb3ef928644c2e99adcee89bdc4c4157 100644 --- a/core/modules/system/tests/modules/form_test/form_test.routing.yml +++ b/core/modules/system/tests/modules/form_test/form_test.routing.yml @@ -513,3 +513,10 @@ form_test.optional_container: _title: 'Optional container testing' requirements: _access: 'TRUE' + +form_test.javascript_states_form: + path: '/form-test/javascript-states-form' + defaults: + _form: '\Drupal\form_test\Form\JavascriptStatesForm' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php new file mode 100644 index 0000000000000000000000000000000000000000..5debb5bb7e8c181e91435cf1f11e318bf9183424 --- /dev/null +++ b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php @@ -0,0 +1,55 @@ + 'select', + '#title' => 'select 1', + '#options' => [0 => 0, 1 => 1, 2 => 2], + ]; + $form['number'] = [ + '#type' => 'number', + '#title' => 'enter 1', + ]; + $form['textfield'] = [ + '#type' => 'textfield', + '#title' => 'textfield', + '#states' => [ + 'visible' => [ + [':input[name="select"]' => ['value' => '1']], + 'or', + [':input[name="number"]' => ['value' => '1']], + ], + ], + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + +} diff --git a/core/tests/Drupal/Nightwatch/Tests/statesTest.js b/core/tests/Drupal/Nightwatch/Tests/statesTest.js new file mode 100644 index 0000000000000000000000000000000000000000..f7ee1ba52219d1219b201e7eaa529c0c0211db2f --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Tests/statesTest.js @@ -0,0 +1,23 @@ +module.exports = { + '@tags': ['core'], + before(browser) { + browser.drupalInstall().drupalLoginAsAdmin(() => { + browser + .drupalRelativeURL('/admin/modules') + .setValue('input[type="search"]', 'FormAPI') + .waitForElementVisible('input[name="modules[form_test][enable]"]', 1000) + .click('input[name="modules[form_test][enable]"]') + .click('input[type="submit"]') // Submit module form. + .click('input[type="submit"]'); // Confirm installation of dependencies. + }); + }, + after(browser) { + browser.drupalUninstall(); + }, + 'Test form with state API': browser => { + browser + .drupalRelativeURL('/form-test/javascript-states-form') + .waitForElementVisible('body', 1000) + .waitForElementNotVisible('input[name="textfield"]', 1000); + }, +};