summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>2021-12-13 22:50:53 +0000
committerGerrit Code Review <gerrit@wikimedia.org>2021-12-13 22:50:53 +0000
commit5a46f9c44d112feb3e2593c03540361858dac1d2 (patch)
treee972441bc7e1cb8d9275c2ce338ab7a0c7b60a74
parente85d532aa244cce4c9a68b79985464d25e94672d (diff)
parent361954801e1bbb99a7d0e2a9b57bb8ba596b4f45 (diff)
Merge "Add support for conditional disable fields in HTMLForm"
-rw-r--r--RELEASE-NOTES-1.383
-rw-r--r--includes/htmlform/HTMLForm.php8
-rw-r--r--includes/htmlform/HTMLFormElement.php14
-rw-r--r--includes/htmlform/HTMLFormField.php65
-rw-r--r--includes/htmlform/fields/HTMLAutoCompleteSelectField.php7
-rw-r--r--includes/htmlform/fields/HTMLCheckMatrix.php16
-rw-r--r--includes/htmlform/fields/HTMLFormFieldCloner.php24
-rw-r--r--resources/Resources.php2
-rw-r--r--resources/src/mediawiki.htmlform.ooui/Element.js11
-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();
}