diff options
author | jenkins-bot <jenkins-bot@gerrit.wikimedia.org> | 2021-12-13 22:50:53 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@wikimedia.org> | 2021-12-13 22:50:53 +0000 |
commit | 5a46f9c44d112feb3e2593c03540361858dac1d2 (patch) | |
tree | e972441bc7e1cb8d9275c2ce338ab7a0c7b60a74 | |
parent | e85d532aa244cce4c9a68b79985464d25e94672d (diff) | |
parent | 361954801e1bbb99a7d0e2a9b57bb8ba596b4f45 (diff) |
Merge "Add support for conditional disable fields in HTMLForm"
-rw-r--r-- | RELEASE-NOTES-1.38 | 3 | ||||
-rw-r--r-- | includes/htmlform/HTMLForm.php | 8 | ||||
-rw-r--r-- | includes/htmlform/HTMLFormElement.php | 14 | ||||
-rw-r--r-- | includes/htmlform/HTMLFormField.php | 65 | ||||
-rw-r--r-- | includes/htmlform/fields/HTMLAutoCompleteSelectField.php | 7 | ||||
-rw-r--r-- | includes/htmlform/fields/HTMLCheckMatrix.php | 16 | ||||
-rw-r--r-- | includes/htmlform/fields/HTMLFormFieldCloner.php | 24 | ||||
-rw-r--r-- | resources/Resources.php | 2 | ||||
-rw-r--r-- | resources/src/mediawiki.htmlform.ooui/Element.js | 11 | ||||
-rw-r--r-- | resources/src/mediawiki.htmlform/cond-state.js (renamed from resources/src/mediawiki.htmlform/hide-if.js) | 57 |
10 files changed, 121 insertions, 86 deletions
diff --git a/RELEASE-NOTES-1.38 b/RELEASE-NOTES-1.38 index c86dec229de4..b35d5af56efc 100644 --- a/RELEASE-NOTES-1.38 +++ b/RELEASE-NOTES-1.38 @@ -57,6 +57,9 @@ For notes on 1.36.x and older releases, see HISTORY. * Added a deleteUserEmail maintenance script - This file enables the deletion of a given user's associated email address. It can be helpful for privacy-preserving operations. +* Description array for constructing HTMLForm now can use 'disable-if' to + disable fields on condition easily, supported expressions are the same + as 'hide-if'. * … === External library changes in 1.38 === diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index 540383fa3f0c..a4ff89798249 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -118,7 +118,13 @@ use MediaWiki\Page\PageReference; * The expressions will be given to a JavaScript frontend * module which will continually update the field's * visibility. - * 'section' -- A string name for the section of the form to which the field + * 'disable-if' -- expression given as an array stating when the field + * should be disabled. See 'hide-if' for supported expressions. + * The 'hide-if' logic would also disable fields, you don't need + * to set this attribute with the same condiction manually. + * You can pass both 'disabled' and this attribute to omit extra + * ckeck, but this would function only for not 'disabled' fields. + * 'section' -- A string name for the section of the form to which the field * belongs. Subsections may be added using the separator '/', e.g.: * 'section' => 'section1/subsection1' * More levels may be added, e.g.: diff --git a/includes/htmlform/HTMLFormElement.php b/includes/htmlform/HTMLFormElement.php index d9e028e407f7..1a1228860b74 100644 --- a/includes/htmlform/HTMLFormElement.php +++ b/includes/htmlform/HTMLFormElement.php @@ -4,24 +4,24 @@ * Allows custom data specific to HTMLFormField to be set for OOUI forms. A matching JS widget * (defined in htmlform.Element.js) picks up the extra config when constructed using OO.ui.infuse(). * - * Currently only supports passing 'hide-if' data. + * Currently only supports passing 'hide-if' and 'disable-if' data. * @phan-file-suppress PhanUndeclaredMethod * * @stable to extend */ trait HTMLFormElement { - protected $hideIf = null; + protected $condState = null; protected $modules = null; public function initializeHTMLFormElement( array $config = [] ) { // Properties - $this->hideIf = $config['hideIf'] ?? null; + $this->condState = $config['condState'] ?? [ 'class' => [] ]; $this->modules = $config['modules'] ?? []; // Initialization - if ( $this->hideIf ) { - $this->addClasses( [ 'mw-htmlform-hide-if' ] ); + if ( $this->condState['class'] ) { + $this->addClasses( $this->condState['class'] ); } if ( $this->modules ) { // JS code must be able to read this before infusing (before OOUI is even loaded), @@ -30,8 +30,8 @@ trait HTMLFormElement { $this->setAttributes( [ 'data-mw-modules' => implode( ',', $this->modules ) ] ); } $this->registerConfigCallback( function ( &$config ) { - if ( $this->hideIf !== null ) { - $config['hideIf'] = $this->hideIf; + if ( $this->condState['class'] ) { + $config['condState'] = $this->condState; } } ); } diff --git a/includes/htmlform/HTMLFormField.php b/includes/htmlform/HTMLFormField.php index 6298e90b65f8..7321284bb40c 100644 --- a/includes/htmlform/HTMLFormField.php +++ b/includes/htmlform/HTMLFormField.php @@ -25,7 +25,10 @@ abstract class HTMLFormField { */ protected $mOptions = false; protected $mOptionsLabelsNotFromMessage = false; - protected $mHideIf = null; + /** + * @var array Array to hold params for 'hide-if' or 'disable-if' statements + */ + protected $mCondState = [ 'class' => [] ]; /** * @var bool If true will generate an empty div element with no label @@ -272,11 +275,11 @@ abstract class HTMLFormField { * @return bool */ public function isHidden( $alldata ) { - if ( !$this->mHideIf ) { + if ( $this->mCondState['class'] || !isset( $this->mCondState['hide'] ) ) { return false; } - return $this->isHiddenRecurse( $alldata, $this->mHideIf ); + return $this->isHiddenRecurse( $alldata, $this->mCondState['hide'] ); } /** @@ -464,8 +467,15 @@ abstract class HTMLFormField { $this->mShowEmptyLabels = false; } - if ( isset( $params['hide-if'] ) ) { - $this->mHideIf = $params['hide-if']; + if ( isset( $params['hide-if'] ) && $params['hide-if'] ) { + $this->mCondState['hide'] = $params['hide-if']; + $this->mCondState['class'][] = 'mw-htmlform-hide-if'; + } + if ( !( isset( $params['disabled'] ) && $params['disabled'] ) && + isset( $params['disable-if'] ) && $params['disable-if'] + ) { + $this->mCondState['disable'] = $params['disable-if']; + $this->mCondState['class'][] = 'mw-htmlform-disable-if'; } } @@ -502,9 +512,9 @@ abstract class HTMLFormField { $inputHtml . "\n$errors" ); - if ( $this->mHideIf ) { - $rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); - $rowClasses .= ' mw-htmlform-hide-if'; + if ( $this->mCondState['class'] ) { + $rowAttributes['data-cond-state'] = FormatJson::encode( $this->mCondState ); + $rowClasses = implode( ' ', $this->mCondState['class'] ); } if ( $verticalLabel ) { @@ -516,12 +526,11 @@ abstract class HTMLFormField { ], $field ); } else { - $html = - Html::rawElement( 'tr', - $rowAttributes + [ - 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses" - ], - $label . $field ); + $html = Html::rawElement( 'tr', + $rowAttributes + [ + 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses" + ], + $label . $field ); } return $html . $helptext; @@ -568,9 +577,9 @@ abstract class HTMLFormField { $wrapperAttributes = [ 'class' => $divCssClasses, ]; - if ( $this->mHideIf ) { - $wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); - $wrapperAttributes['class'][] = ' mw-htmlform-hide-if'; + if ( $this->mCondState['class'] ) { + $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->mCondState ); + $wrapperAttributes['class'] += $this->mCondState['class']; } $html = Html::rawElement( 'div', $wrapperAttributes, $label . $field ); $html .= $helptext; @@ -638,9 +647,9 @@ abstract class HTMLFormField { $config['label'] = new OOUI\HtmlSnippet( $label ); } - if ( $this->mHideIf ) { + if ( $this->mCondState['class'] ) { $preloadModules = true; - $config['hideIf'] = $this->mHideIf; + $config['condState'] = $this->mCondState; } $config['modules'] = $this->getOOUIModules(); @@ -788,9 +797,9 @@ abstract class HTMLFormField { } $rowAttributes = []; - if ( $this->mHideIf ) { - $rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); - $rowAttributes['class'] = 'mw-htmlform-hide-if'; + if ( $this->mCondState['class'] ) { + $rowAttributes['data-cond-state'] = FormatJson::encode( $this->mCondState ); + $rowAttributes['class'] = $this->mCondState['class']; } $tdClasses = [ 'htmlform-tip' ]; @@ -817,14 +826,14 @@ abstract class HTMLFormField { } $wrapperAttributes = [ - 'class' => 'htmlform-tip', + 'class' => [ 'htmlform-tip' ], ]; if ( $this->mHelpClass !== false ) { - $wrapperAttributes['class'] .= " {$this->mHelpClass}"; + $wrapperAttributes['class'][] = $this->mHelpClass; } - if ( $this->mHideIf ) { - $wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); - $wrapperAttributes['class'] .= ' mw-htmlform-hide-if'; + if ( $this->mCondState['class'] ) { + $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->mCondState ); + $wrapperAttributes['class'] += $this->mCondState['class']; } $div = Html::rawElement( 'div', $wrapperAttributes, $helptext ); @@ -1225,7 +1234,7 @@ abstract class HTMLFormField { * @since 1.29 */ public function needsJSForHtml5FormValidation() { - if ( $this->mHideIf ) { + if ( $this->mCondState['class'] ) { // This is probably more restrictive than it needs to be, but better safe than sorry return true; } diff --git a/includes/htmlform/fields/HTMLAutoCompleteSelectField.php b/includes/htmlform/fields/HTMLAutoCompleteSelectField.php index 278cfdc27fe1..5acb4b4e0520 100644 --- a/includes/htmlform/fields/HTMLAutoCompleteSelectField.php +++ b/includes/htmlform/fields/HTMLAutoCompleteSelectField.php @@ -134,9 +134,10 @@ class HTMLAutoCompleteSelectField extends HTMLTextField { ] + parent::getAttributes( $list ); if ( $this->getOptions() ) { - $attribs['data-hide-if'] = FormatJson::encode( - [ '!==', $this->mName . '-select', 'other' ] - ); + $attribs['data-cond-state'] = FormatJson::encode( [ + 'hide' => [ '!==', $this->mName . '-select', 'other' ], + 'class' => 'mw-htmlform-hide-if', + ] ); } return $attribs; diff --git a/includes/htmlform/fields/HTMLCheckMatrix.php b/includes/htmlform/fields/HTMLCheckMatrix.php index 45cf99f41c91..a9eb35e0cbb1 100644 --- a/includes/htmlform/fields/HTMLCheckMatrix.php +++ b/includes/htmlform/fields/HTMLCheckMatrix.php @@ -213,11 +213,11 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); $cellAttributes = [ 'colspan' => 2 ]; - $hideClass = ''; - $hideAttributes = []; - if ( $this->mHideIf ) { - $hideAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); - $hideClass = 'mw-htmlform-hide-if'; + $moreClass = ''; + $moreAttributes = []; + if ( $this->mCondState['class'] ) { + $moreAttributes['data-cond-state'] = FormatJson::encode( $this->mCondState ); + $moreClass = implode( ' ', $this->mCondState['class'] ); } $label = $this->getLabelHtml( $cellAttributes ); @@ -229,11 +229,11 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { ); $html = Html::rawElement( 'tr', - [ 'class' => "mw-htmlform-vertical-label $hideClass" ] + $hideAttributes, + [ 'class' => "mw-htmlform-vertical-label $moreClass" ] + $moreAttributes, $label ); $html .= Html::rawElement( 'tr', - [ 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $hideClass" ] + - $hideAttributes, + [ 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $moreClass" ] + + $moreAttributes, $field ); return $html . $helptext; diff --git a/includes/htmlform/fields/HTMLFormFieldCloner.php b/includes/htmlform/fields/HTMLFormFieldCloner.php index af34bd9429e9..655d97cf17bc 100644 --- a/includes/htmlform/fields/HTMLFormFieldCloner.php +++ b/includes/htmlform/fields/HTMLFormFieldCloner.php @@ -102,15 +102,21 @@ class HTMLFormFieldCloner extends HTMLFormField { } else { $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--$fieldname" ); } - // Copy the hide-if rules to "child" fields, so that the JavaScript code handling them - // (resources/src/mediawiki/htmlform/hide-if.js) doesn't have to handle nested fields. - if ( $this->mHideIf ) { - if ( isset( $info['hide-if'] ) ) { - // Hide child field if either its rules say it's hidden, or parent's rules say it's hidden - $info['hide-if'] = [ 'OR', $info['hide-if'], $this->mHideIf ]; - } else { - // Hide child field if parent's rules say it's hidden - $info['hide-if'] = $this->mHideIf; + // Copy the hide-if and disable-if rules to "child" fields, so that the JavaScript code handling them + // (resources/src/mediawiki.htmlform/cond-state.js) doesn't have to handle nested fields. + if ( $this->mCondState['class'] ) { + foreach ( [ 'hide', 'disable' ] as $type ) { + if ( !isset( $this->mCondState[$type] ) ) { + continue; + } + $field = $type . '-if'; + if ( isset( $info[$field] ) ) { + // Hide or disable child field if either its rules say so, or parent's rules say so. + $info[$field] = [ 'OR', $info[$field], $this->mCondState[$type] ]; + } else { + // Hide or disable child field if parent's rules say so. + $info[$field] = $this->mCondState[$type]; + } } } $field = HTMLForm::loadInputFromParameters( $name, $info, $this->mParent ); diff --git a/resources/Resources.php b/resources/Resources.php index be89ac51d904..0b0f1c7851fb 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -838,7 +838,7 @@ return [ 'resources/src/mediawiki.htmlform/autoinfuse.js', 'resources/src/mediawiki.htmlform/checkmatrix.js', 'resources/src/mediawiki.htmlform/cloner.js', - 'resources/src/mediawiki.htmlform/hide-if.js', + 'resources/src/mediawiki.htmlform/cond-state.js', 'resources/src/mediawiki.htmlform/multiselect.js', 'resources/src/mediawiki.htmlform/selectandother.js', 'resources/src/mediawiki.htmlform/selectorother.js', diff --git a/resources/src/mediawiki.htmlform.ooui/Element.js b/resources/src/mediawiki.htmlform.ooui/Element.js index 979651be788a..821217b74932 100644 --- a/resources/src/mediawiki.htmlform.ooui/Element.js +++ b/resources/src/mediawiki.htmlform.ooui/Element.js @@ -7,7 +7,7 @@ * extra config from a matching PHP widget (defined in HTMLFormElement.php) when constructed using * OO.ui.infuse(). * - * Currently only supports passing 'hide-if' data. + * Currently only supports passing 'cond-state' data. * * @ignore * @param {Object} [config] Configuration options @@ -17,11 +17,14 @@ config = config || {}; // Properties - this.hideIf = config.hideIf; + this.condState = config.condState; // Initialization - if ( this.hideIf ) { - this.$element.addClass( 'mw-htmlform-hide-if' ); + if ( this.condState && this.condState.class.length ) { + // The following classes are used here: + // * mw-htmlform-hide-if + // * mw-htmlform-disable-if + this.$element.addClass( this.condState.class ); } }; diff --git a/resources/src/mediawiki.htmlform/hide-if.js b/resources/src/mediawiki.htmlform/cond-state.js index 05345fca4b26..7ff7d97b3c53 100644 --- a/resources/src/mediawiki.htmlform/hide-if.js +++ b/resources/src/mediawiki.htmlform/cond-state.js @@ -1,11 +1,11 @@ /* * HTMLForm enhancements: - * Set up 'hide-if' behaviors for form fields that have them. + * Set up 'hide-if' and 'disable-if' behaviors for form fields that have them. */ ( function () { /** - * Helper function for hide-if to find the nearby form field. + * Helper function for conditional states to find the nearby form field. * * Find the closest match for the given name, "closest" being the minimum * level of parents to go to find a form field matching the given name or @@ -18,7 +18,7 @@ * @param {string} name * @return {jQuery|OO.ui.Widget|null} */ - function hideIfGetField( $el, name ) { + function conditionGetField( $el, name ) { var $found, $p, $widget, suffix = name.replace( /^([^[]+)/, '[$1]' ); @@ -42,8 +42,8 @@ } /** - * Helper function for hide-if to return a test function and list of - * dependent fields for a hide-if specification. + * Helper function for conditional states to return a test function and list of + * dependent fields for a conditional states specification. * * @ignore * @private @@ -53,7 +53,7 @@ * @return {Array} return.0 Dependent fields, array of jQuery objects or OO.ui.Widgets * @return {Function} return.1 Test function */ - function hideIfParse( $el, spec ) { + function conditionParse( $el, spec ) { var op, i, l, v, field, $field, fields, func, funcs, getVal; op = spec[ 0 ]; @@ -69,7 +69,7 @@ if ( !Array.isArray( spec[ i ] ) ) { throw new Error( op + ' parameters must be arrays' ); } - v = hideIfParse( $el, spec[ i ] ); + v = conditionParse( $el, spec[ i ] ); fields = fields.concat( v[ 0 ] ); funcs.push( v[ 1 ] ); } @@ -134,7 +134,7 @@ if ( !Array.isArray( spec[ 1 ] ) ) { throw new Error( 'NOT parameters must be arrays' ); } - v = hideIfParse( $el, spec[ 1 ] ); + v = conditionParse( $el, spec[ 1 ] ); fields = v[ 0 ]; func = v[ 1 ]; return [ fields, function () { @@ -146,7 +146,7 @@ if ( l !== 3 ) { throw new Error( op + ' takes exactly two parameters' ); } - field = hideIfGetField( $el, spec[ 1 ] ); + field = conditionGetField( $el, spec[ 1 ] ); if ( !field ) { return [ [], function () { return false; @@ -202,7 +202,7 @@ mw.hook( 'htmlform.enhance' ).add( function ( $root ) { var - $fields = $root.find( '.mw-htmlform-hide-if' ), + $fields = $root.find( '.mw-htmlform-hide-if, .mw-htmlform-disable-if' ), $oouiFields = $fields.filter( '[data-ooui]' ), modules = []; @@ -223,46 +223,53 @@ mw.loader.using( modules ).done( function () { $fields.each( function () { - var v, i, fields, test, func, spec, $elOrLayout, + var v, i, fields = [], test = [], func, spec, $elOrLayout, $el = $( this ); if ( $el.is( '[data-ooui]' ) ) { // $elOrLayout should be a FieldLayout that mixes in mw.htmlform.Element $elOrLayout = OO.ui.FieldLayout.static.infuse( $el ); - spec = $elOrLayout.hideIf; + spec = $elOrLayout.condState; // The original element has been replaced with infused one $el = $elOrLayout.$element; } else { $elOrLayout = $el; - spec = $el.data( 'hideIf' ); + spec = $el.data( 'condState' ); } if ( !spec ) { return; } - v = hideIfParse( $el, spec ); - fields = v[ 0 ]; - test = v[ 1 ]; - // The .toggle() method works mostly the same for jQuery objects and OO.ui.Widget + [ 'hide', 'disable' ].forEach( function ( type ) { + if ( spec[ type ] ) { + v = conditionParse( $el, spec[ type ] ); + fields = fields.concat( fields, v[ 0 ] ); + test[ type ] = v[ 1 ]; + } + } ); func = function () { - var shouldHide = test(); - $elOrLayout.toggle( !shouldHide ); + var shouldHide = spec.hide ? test.hide() : false; + var shouldDisable = shouldHide || ( spec.disable ? test.disable() : false ); + if ( spec.hide ) { + // The .toggle() method works mostly the same for jQuery objects and OO.ui.Widget + $elOrLayout.toggle( !shouldHide ); + } - // It is impossible to submit a form with hidden fields failing validation, e.g. one that - // is required. However, validity is not checked for disabled fields, as these are not - // submitted with the form. So we should also disable fields when hiding them. + // Disable fields with either 'disable-if' or 'hide-if' rules + // Hidden fields should be disabled to avoid users meet validation failure on these fields, + // because disabled fields will not be submitted with the form. if ( $elOrLayout instanceof $ ) { // This also finds elements inside any nested fields (in case of HTMLFormFieldCloner), // which is problematic. But it works because: - // * HTMLFormFieldCloner::createFieldsForKey() copies 'hide-if' rules to nested fields + // * HTMLFormFieldCloner::createFieldsForKey() copies '*-if' rules to nested fields // * jQuery collections like $fields are in document order, so we register event // handlers for parents first // * Event handlers are fired in the order they were registered, so even if the handler // for parent messed up the child, the handle for child will run next and fix it $elOrLayout.find( 'input, textarea, select' ).each( function () { var $this = $( this ); - if ( shouldHide ) { + if ( shouldDisable ) { if ( $this.data( 'was-disabled' ) === undefined ) { $this.data( 'was-disabled', $this.prop( 'disabled' ) ); } @@ -273,7 +280,7 @@ } ); } else { // $elOrLayout is a OO.ui.FieldLayout - if ( shouldHide ) { + if ( shouldDisable ) { if ( $elOrLayout.wasDisabled === undefined ) { $elOrLayout.wasDisabled = $elOrLayout.fieldWidget.isDisabled(); } |