From: Brad Jorsch Date: Thu, 27 Feb 2014 17:14:38 +0000 (-0500) Subject: HTMLForm: Add hide-if X-Git-Tag: 1.31.0-rc.0~15920 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22calendrier%22%2C%22type=semaine%22%29%20.%20%22?a=commitdiff_plain;h=c310e7b40c3b11b6e162c5e84acd638728a885f8;p=lhc%2Fweb%2Fwiklou.git HTMLForm: Add hide-if SecurePoll will need a way to display a field only if another field has a particular value. We already have this for a limited case in HTMLSelectOrOtherField; this makes it possible to specify that any particular field should be hidden based on any other field. Change-Id: I5d2e6fb1efba0ad97647ac140e2b9a9ac0aee06e --- diff --git a/includes/htmlform/HTMLFormField.php b/includes/htmlform/HTMLFormField.php index fdb392452f..204020d780 100644 --- a/includes/htmlform/HTMLFormField.php +++ b/includes/htmlform/HTMLFormField.php @@ -16,6 +16,7 @@ abstract class HTMLFormField { protected $mDefault; protected $mOptions = false; protected $mOptionsLabelsNotFromMessage = false; + protected $mHideIf = null; /** * @var bool If true will generate an empty div element with no label @@ -62,6 +63,184 @@ abstract class HTMLFormField { return call_user_func_array( $callback, $args ); } + + /** + * Fetch a field value from $alldata for the closest field matching a given + * name. + * + * This is complex because it needs to handle array fields like the user + * would expect. The general algorithm is to look for $name as a sibling + * of $this, then a sibling of $this's parent, and so on. Keeping in mind + * that $name itself might be referencing an array. + * + * @param array $alldata + * @param string $name + * @return string + */ + protected function getNearestFieldByName( $alldata, $name ) { + $tmp = $this->mName; + $thisKeys = array(); + while ( preg_match( '/^(.+)\[([^\]]+)\]$/', $tmp, $m ) ) { + array_unshift( $thisKeys, $m[2] ); + $tmp = $m[1]; + } + if ( substr( $tmp, 0, 2 ) == 'wp' && + !isset( $alldata[$tmp] ) && + isset( $alldata[substr( $tmp, 2 )] ) + ) { + // Adjust for name mangling. + $tmp = substr( $tmp, 2 ); + } + array_unshift( $thisKeys, $tmp ); + + $tmp = $name; + $nameKeys = array(); + while ( preg_match( '/^(.+)\[([^\]]+)\]$/', $tmp, $m ) ) { + array_unshift( $nameKeys, $m[2] ); + $tmp = $m[1]; + } + array_unshift( $nameKeys, $tmp ); + + $testValue = ''; + for ( $i = count( $thisKeys ) - 1; $i >= 0; $i-- ) { + $keys = array_merge( array_slice( $thisKeys, 0, $i ), $nameKeys ); + $data = $alldata; + while ( $keys ) { + $key = array_shift( $keys ); + if ( !is_array( $data ) || !isset( $data[$key] ) ) { + continue 2; + } + $data = $data[$key]; + } + $testValue = $data; + break; + } + + return $testValue; + } + + /** + * Helper function for isHidden to handle recursive data structures. + * + * @param array $alldata + * @param array $params + * @return boolean + */ + protected function isHiddenRecurse( array $alldata, array $params ) { + $origParams = $params; + $op = array_shift( $params ); + + try { + switch ( $op ) { + case 'AND': + foreach ( $params as $i => $p ) { + if ( !is_array( $p ) ) { + throw new MWException( + "Expected array, found " . gettype( $p ) . " at index $i" + ); + } + if ( !$this->isHiddenRecurse( $alldata, $p ) ) { + return false; + } + } + return true; + + case 'OR': + foreach ( $params as $p ) { + if ( !is_array( $p ) ) { + throw new MWException( + "Expected array, found " . gettype( $p ) . " at index $i" + ); + } + if ( $this->isHiddenRecurse( $alldata, $p ) ) { + return true; + } + } + return false; + + case 'NAND': + foreach ( $params as $i => $p ) { + if ( !is_array( $p ) ) { + throw new MWException( + "Expected array, found " . gettype( $p ) . " at index $i" + ); + } + if ( !$this->isHiddenRecurse( $alldata, $p ) ) { + return true; + } + } + return false; + + case 'NOR': + foreach ( $params as $p ) { + if ( !is_array( $p ) ) { + throw new MWException( + "Expected array, found " . gettype( $p ) . " at index $i" + ); + } + if ( $this->isHiddenRecurse( $alldata, $p ) ) { + return false; + } + } + return true; + + case 'NOT': + if ( count( $params ) !== 1 ) { + throw new MWException( "NOT takes exactly one parameter" ); + } + $p = $params[0]; + if ( !is_array( $p ) ) { + throw new MWException( + "Expected array, found " . gettype( $p ) . " at index 0" + ); + } + return !$this->isHiddenRecurse( $alldata, $p ); + + case '===': + case '!==': + if ( count( $params ) !== 2 ) { + throw new MWException( "$op takes exactly two parameters" ); + } + list( $field, $value ) = $params; + if ( !is_string( $field ) || !is_string( $value ) ) { + throw new MWException( "Parameters for $op must be strings" ); + } + $testValue = $this->getNearestFieldByName( $alldata, $field ); + switch ( $op ) { + case '===': + return ( $value === $testValue ); + case '!==': + return ( $value !== $testValue ); + } + + default: + throw new MWException( "Unknown operation" ); + } + } catch ( MWException $ex ) { + throw new MWException( + "Invalid hide-if specification for $this->mName: " . + $ex->getMessage() . " in " . var_export( $origParams, true ), + 0, $ex + ); + } + } + + /** + * Test whether this field is supposed to be hidden, based on the values of + * the other form fields. + * + * @since 1.23 + * @param array $alldata The data collected from the form + * @return bool + */ + function isHidden( $alldata ) { + if ( !$this->mHideIf ) { + return false; + } + + return $this->isHiddenRecurse( $alldata, $this->mHideIf ); + } + /** * Override this function to add specific validation checks on the * field input. Don't forget to call parent::validate() to ensure @@ -73,6 +252,10 @@ abstract class HTMLFormField { * @return bool|string true on success, or String error to display. */ function validate( $value, $alldata ) { + if ( $this->isHidden( $alldata ) ) { + return true; + } + if ( isset( $this->mParams['required'] ) && $this->mParams['required'] !== false && $value === '' @@ -213,6 +396,10 @@ abstract class HTMLFormField { if ( isset( $params['hidelabel'] ) ) { $this->mShowEmptyLabels = false; } + + if ( isset( $params['hide-if'] ) ) { + $this->mHideIf = $params['hide-if']; + } } /** @@ -229,6 +416,8 @@ abstract class HTMLFormField { $fieldType = get_class( $this ); $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); $cellAttributes = array(); + $rowAttributes = array(); + $rowClasses = ''; if ( !empty( $this->mParams['vertical-label'] ) ) { $cellAttributes['colspan'] = 2; @@ -245,15 +434,25 @@ abstract class HTMLFormField { $inputHtml . "\n$errors" ); + if ( $this->mHideIf ) { + $rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); + $rowClasses .= ' mw-htmlform-hide-if'; + } + if ( $verticalLabel ) { - $html = Html::rawElement( 'tr', array( 'class' => 'mw-htmlform-vertical-label' ), $label ); + $html = Html::rawElement( 'tr', + $rowAttributes + array( 'class' => "mw-htmlform-vertical-label $rowClasses" ), $label ); $html .= Html::rawElement( 'tr', - array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), + $rowAttributes + array( + 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses" + ), $field ); } else { $html = Html::rawElement( 'tr', - array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), + $rowAttributes + array( + 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses" + ), $label . $field ); } @@ -291,7 +490,15 @@ abstract class HTMLFormField { if ( $this->mParent->isVForm() ) { $divCssClasses[] = 'mw-ui-vform-div'; } - $html = Html::rawElement( 'div', array( 'class' => $divCssClasses ), $label . $field ); + + $wrapperAttributes = array( + 'class' => $divCssClasses, + ); + if ( $this->mHideIf ) { + $wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); + $wrapperAttributes['class'][] = ' mw-htmlform-hide-if'; + } + $html = Html::rawElement( 'div', $wrapperAttributes, $label . $field ); $html .= $helptext; return $html; @@ -333,8 +540,14 @@ abstract class HTMLFormField { return ''; } + $rowAttributes = array(); + if ( $this->mHideIf ) { + $rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); + $rowAttributes['class'] = 'mw-htmlform-hide-if'; + } + $row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ), $helptext ); - $row = Html::rawElement( 'tr', array(), $row ); + $row = Html::rawElement( 'tr', $rowAttributes, $row ); return $row; } @@ -352,7 +565,14 @@ abstract class HTMLFormField { return ''; } - $div = Html::rawElement( 'div', array( 'class' => 'htmlform-tip' ), $helptext ); + $wrapperAttributes = array( + 'class' => 'htmlform-tip', + ); + if ( $this->mHideIf ) { + $wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf ); + $wrapperAttributes['class'] .= ' mw-htmlform-hide-if'; + } + $div = Html::rawElement( 'div', $wrapperAttributes, $helptext ); return $div; } diff --git a/resources/src/mediawiki/mediawiki.htmlform.js b/resources/src/mediawiki/mediawiki.htmlform.js index 5ba1a54dd6..def29fcc06 100644 --- a/resources/src/mediawiki/mediawiki.htmlform.js +++ b/resources/src/mediawiki/mediawiki.htmlform.js @@ -5,6 +5,175 @@ */ ( function ( mw, $ ) { + /** + * Helper function for hide-if 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 + * ending in array keys matching the given name (e.g. "baz" matches + * "foo[bar][baz]"). + * + * @param {jQuery} element + * @param {string} name + * @return {jQuery|null} + */ + function hideIfGetField( $el, name ) { + var sel, $found, $p; + + sel = '[name="' + name + '"],' + + '[name="wp' + name + '"],' + + '[name$="' + name.replace( /^([^\[]+)/, '[$1]' ) + '"]'; + for ( $p = $el.parent(); $p.length > 0; $p = $p.parent() ) { + $found = $p.find( sel ); + if ( $found.length > 0 ) { + return $found; + } + } + return null; + } + + /** + * Helper function for hide-if to return a test function and list of + * dependent fields for a hide-if specification. + * + * @param {jQuery} element + * @param {Array} hide-if spec + * @return {Array} 2 elements: jQuery of dependent fields, and test function + */ + function hideIfParse( $el, spec ) { + var op, i, l, v, $field, $fields, func, funcs, getVal; + + op = spec[0]; + l = spec.length; + switch ( op ) { + case 'AND': + case 'OR': + case 'NAND': + case 'NOR': + funcs = []; + $fields = $(); + for ( i = 1; i < l; i++ ) { + if ( !$.isArray( spec[i] ) ) { + throw new Error( op + ' parameters must be arrays' ); + } + v = hideIfParse( $el, spec[i] ); + $fields = $fields.add( v[0] ); + funcs.push( v[1] ); + } + + l = funcs.length; + switch ( op ) { + case 'AND': + func = function () { + var i; + for ( i = 0; i < l; i++ ) { + if ( !funcs[i]() ) { + return false; + } + } + return true; + }; + break; + + case 'OR': + func = function () { + var i; + for ( i = 0; i < l; i++ ) { + if ( funcs[i]() ) { + return true; + } + } + return false; + }; + break; + + case 'NAND': + func = function () { + var i; + for ( i = 0; i < l; i++ ) { + if ( !funcs[i]() ) { + return true; + } + } + return false; + }; + break; + + case 'NOR': + func = function () { + var i; + for ( i = 0; i < l; i++ ) { + if ( funcs[i]() ) { + return false; + } + } + return true; + }; + break; + } + + return [ $fields, func ]; + + case 'NOT': + if ( l !== 2 ) { + throw new Error( 'NOT takes exactly one parameter' ); + } + if ( !$.isArray( spec[1] ) ) { + throw new Error( 'NOT parameters must be arrays' ); + } + v = hideIfParse( $el, spec[1] ); + $fields = v[0]; + func = v[1]; + return [ $fields, function () { + return !func(); + } ]; + + case '===': + case '!==': + if ( l !== 3 ) { + throw new Error( op + ' takes exactly two parameters' ); + } + $field = hideIfGetField( $el, spec[1] ); + if ( !$field ) { + return [ $(), function () { + return false; + } ]; + } + v = spec[2]; + + if ( $field.first().attr( 'type' ) === 'radio' || + $field.first().attr( 'type' ) === 'checkbox' + ) { + getVal = function () { + var $selected = $field.filter( ':checked' ); + return $selected.length > 0 ? $selected.val() : ''; + }; + } else { + getVal = function () { + return $field.val(); + }; + } + + switch ( op ) { + case '===': + func = function () { + return getVal() === v; + }; + break; + case '!==': + func = function () { + return getVal() !== v; + }; + break; + } + + return [ $field, func ]; + + default: + throw new Error( 'Unrecognized operation \'' + op + '\'' ); + } + } + /** * jQuery plugin to fade or snap to visible state. * @@ -62,6 +231,30 @@ } } ); + // Set up hide-if elements + $( '.mw-htmlform-hide-if' ).each( function () { + var $el = $( this ), + spec = $el.data( 'hideIf' ), + v, $fields, test, func; + + if ( !spec ) { + return; + } + + v = hideIfParse( $el, spec ); + $fields = v[0]; + test = v[1]; + func = function () { + if ( test() ) { + $el.hide(); + } else { + $el.show(); + } + }; + $fields.change( func ); + func(); + } ); + } ); function addMulti( $oldContainer, $container ) {