From e85bd04bcd26f9a05406cedf6ee0dcac0b54c435 Mon Sep 17 00:00:00 2001 From: Mark Holmquist Date: Tue, 21 Apr 2015 16:03:49 -0500 Subject: [PATCH] Implement OOUI display format for HTMLForm Bug: T85291 Change-Id: I6ffe93c16d6b209a0ab08d714ad8ddaefb6acd52 --- autoload.php | 1 + .../htmlform/HTMLAutoCompleteSelectField.php | 14 +- includes/htmlform/HTMLButtonField.php | 15 ++ includes/htmlform/HTMLCheckField.php | 45 +++++- includes/htmlform/HTMLCheckMatrix.php | 23 ++- includes/htmlform/HTMLForm.php | 15 ++ includes/htmlform/HTMLFormField.php | 113 ++++++++++++++- includes/htmlform/HTMLInfoField.php | 10 ++ includes/htmlform/HTMLSelectField.php | 22 +++ includes/htmlform/HTMLTextAreaField.php | 29 ++++ includes/htmlform/HTMLTextField.php | 46 +++++- includes/htmlform/OOUIHTMLForm.php | 137 ++++++++++++++++++ resources/Resources.php | 7 + .../src/mediawiki/mediawiki.htmlform.ooui.css | 20 +++ .../src/mediawiki/mediawiki.htmlform.ooui.js | 9 ++ 15 files changed, 496 insertions(+), 10 deletions(-) create mode 100644 includes/htmlform/OOUIHTMLForm.php create mode 100644 resources/src/mediawiki/mediawiki.htmlform.ooui.css create mode 100644 resources/src/mediawiki/mediawiki.htmlform.ooui.js diff --git a/autoload.php b/autoload.php index 6c623a382b..0760a65af3 100644 --- a/autoload.php +++ b/autoload.php @@ -836,6 +836,7 @@ $wgAutoloadLocalClasses = array( 'ObjectFileCache' => __DIR__ . '/includes/cache/ObjectFileCache.php', 'OldChangesList' => __DIR__ . '/includes/changes/OldChangesList.php', 'OldLocalFile' => __DIR__ . '/includes/filerepo/file/OldLocalFile.php', + 'OOUIHTMLForm' => __DIR__ . '/includes/htmlform/OOUIHTMLForm.php', 'OracleInstaller' => __DIR__ . '/includes/installer/OracleInstaller.php', 'OracleUpdater' => __DIR__ . '/includes/installer/OracleUpdater.php', 'OrphanStats' => __DIR__ . '/maintenance/storage/orphanStats.php', diff --git a/includes/htmlform/HTMLAutoCompleteSelectField.php b/includes/htmlform/HTMLAutoCompleteSelectField.php index 49053628de..55cd5d0c8b 100644 --- a/includes/htmlform/HTMLAutoCompleteSelectField.php +++ b/includes/htmlform/HTMLAutoCompleteSelectField.php @@ -98,11 +98,12 @@ class HTMLAutoCompleteSelectField extends HTMLTextField { return true; } - function getAttributes( array $list ) { + // FIXME Ewww, this shouldn't be adding any attributes not requested in $list :( + public function getAttributes( array $list, array $mappings = null ) { $attribs = array( 'type' => 'text', 'data-autocomplete' => FormatJson::encode( array_keys( $this->autocomplete ) ), - ) + parent::getAttributes( $list ); + ) + parent::getAttributes( $list, $mappings ); if ( $this->getOptions() ) { $attribs['data-hide-if'] = FormatJson::encode( @@ -162,4 +163,13 @@ class HTMLAutoCompleteSelectField extends HTMLTextField { return $ret; } + /** + * Get the OOUI version of this input. + * @param string $value + * @return false + */ + function getInputOOUI( $value ) { + // To be implemented, for now override the function from HTMLTextField + return false; + } } diff --git a/includes/htmlform/HTMLButtonField.php b/includes/htmlform/HTMLButtonField.php index 09c0ad97e4..9f30ee3de1 100644 --- a/includes/htmlform/HTMLButtonField.php +++ b/includes/htmlform/HTMLButtonField.php @@ -24,6 +24,21 @@ class HTMLButtonField extends HTMLFormField { return Html::input( $this->mName, $value, $this->buttonType, $attr ); } + /** + * Get the OOUI widget for this field. + * @param string $value + * @return OOUI\ButtonInputWidget + */ + public function getInputOOUI( $value ) { + return new OOUI\ButtonInputWidget( array( + 'name' => $this->mName, + 'value' => $value, + 'type' => $this->buttonType, + 'classes' => array( 'mw-htmlform-submit', $this->mClass ), + 'id' => $this->mID, + ) + $this->getAttributes( array( 'disabled', 'tabindex' ), array( 'tabindex' => 'tabIndex' ) ) ); + } + protected function needsLabel() { return false; } diff --git a/includes/htmlform/HTMLCheckField.php b/includes/htmlform/HTMLCheckField.php index 4942327fc7..ede30dd37b 100644 --- a/includes/htmlform/HTMLCheckField.php +++ b/includes/htmlform/HTMLCheckField.php @@ -35,13 +35,56 @@ class HTMLCheckField extends HTMLFormField { return $chkLabel; } + /** + * Get the OOUI version of this field. + * @since 1.26 + * @param string $value + * @return OOUI\CheckboxInputWidget The checkbox widget. + */ + public function getInputOOUI( $value ) { + if ( !empty( $this->mParams['invert'] ) ) { + $value = !$value; + } + + $attr = $this->getTooltipAndAccessKey(); + $attr['id'] = $this->mID; + $attr['name'] = $this->mName; + + $attr += $this->getAttributes( array( 'disabled', 'tabindex' ), array( 'tabindex' => 'tabIndex' ) ); + + if ( $this->mClass !== '' ) { + $attr['classes'] = array( $this->mClass ); + } + + $attr['selected'] = $value; + $attr['value'] = '1'; // Nasty hack, but needed to make this work + + return new OOUI\CheckboxInputWidget( $attr ); + } + /** * For a checkbox, the label goes on the right hand side, and is * added in getInputHTML(), rather than HTMLFormField::getRow() + * + * ...unless OOUI is being used, in which case we actually return + * the label here. + * * @return string */ function getLabel() { - return ' '; + if ( $this->mParent instanceof OOUIHTMLForm ) { + return $this->mLabel; + } else { + return ' '; + } + } + + /** + * Get label alignment when generating field for OOUI. + * @return string 'left', 'right', 'top' or 'inline' + */ + protected function getLabelAlignOOUI() { + return 'inline'; } /** diff --git a/includes/htmlform/HTMLCheckMatrix.php b/includes/htmlform/HTMLCheckMatrix.php index 83f126657f..7ccb60e04e 100644 --- a/includes/htmlform/HTMLCheckMatrix.php +++ b/includes/htmlform/HTMLCheckMatrix.php @@ -85,7 +85,13 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { $rows = $this->mParams['rows']; $columns = $this->mParams['columns']; - $attribs = $this->getAttributes( array( 'disabled', 'tabindex' ) ); + $mappings = array(); + + if ( $this->mParent instanceof OOUIHTMLForm ) { + $mappings['tabindex'] = 'tabIndex'; + } + + $attribs = $this->getAttributes( array( 'disabled', 'tabindex' ), $mappings ); // Build the column headers $headerContents = Html::rawElement( 'td', array(), ' ' ); @@ -126,7 +132,8 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { $checked = true; $thisAttribs['disabled'] = 1; } - $chkBox = Xml::check( "{$this->mName}[]", $checked, $attribs + $thisAttribs ); + $chkBox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs ); + if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { $chkBox = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . $chkBox . @@ -150,6 +157,18 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { return $html; } + protected function getOneCheckbox( $checked, $attribs ) { + if ( $this->mParent instanceof OOUIHTMLForm ) { + return new OOUI\CheckboxInputWidget( array( + 'name' => "{$this->mName}[]", + 'selected' => $checked, + 'value' => '1', + ) + $attribs ); + } + + return Xml::check( "{$this->mName}[]", $checked, $attribs ); + } + protected function isTagForcedOff( $tag ) { return isset( $this->mParams['force-options-off'] ) && in_array( $tag, $this->mParams['force-options-off'] ); diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index 738fec3b1e..adfa563b85 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -217,6 +217,7 @@ class HTMLForm extends ContextSource { */ protected $availableSubclassDisplayFormats = array( 'vform', + 'ooui', ); /** @@ -235,6 +236,9 @@ class HTMLForm extends ContextSource { case 'vform': $reflector = new ReflectionClass( 'VFormHTMLForm' ); return $reflector->newInstanceArgs( $arguments ); + case 'ooui': + $reflector = new ReflectionClass( 'OOUIHTMLForm' ); + return $reflector->newInstanceArgs( $arguments ); default: $reflector = new ReflectionClass( 'HTMLForm' ); $form = $reflector->newInstanceArgs( $arguments ); @@ -1371,6 +1375,17 @@ class HTMLForm extends ContextSource { Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n"; } elseif ( $displayFormat === 'inline' ) { $html = Html::rawElement( 'span', $attribs, "\n$html\n" ); + } elseif ( $displayFormat === 'ooui' ) { + $config = array( + 'classes' => $classes, + ); + if ( $sectionName ) { + $config['id'] = Sanitizer::escapeId( $sectionName ); + } + $fieldset = new OOUI\FieldsetLayout( $config ); + // Ewww. We should pass this as $config['items'], but there might be string snippets. + $fieldset->group->appendContent( new OOUI\HtmlSnippet( $html ) ); + $html = $fieldset->toString(); } else { $html = Html::rawElement( 'div', $attribs, "\n$html\n" ); } diff --git a/includes/htmlform/HTMLFormField.php b/includes/htmlform/HTMLFormField.php index 0c3fe4450f..49478fbf1c 100644 --- a/includes/htmlform/HTMLFormField.php +++ b/includes/htmlform/HTMLFormField.php @@ -44,6 +44,17 @@ abstract class HTMLFormField { */ abstract function getInputHTML( $value ); + /** + * Same as getInputHTML, but returns an OOUI object. + * Defaults to false, which getOOUI will interpret as "use the HTML version" + * + * @param string $value + * @return OOUI\Widget|false + */ + function getInputOOUI( $value ) { + return false; + } + /** * Get a translated interface message * @@ -533,6 +544,54 @@ abstract class HTMLFormField { return $html; } + /** + * Get the OOUI version of the div. Falls back to getDiv by default. + * @since 1.26 + * + * @param string $value The value to set the input to. + * + * @return string + */ + public function getOOUI( $value ) { + list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); + + $inputField = $this->getInputOOUI( $value ); + + if ( !$inputField ) { + // This field doesn't have an OOUI implementation yet at all. + // OK, use this trick: + return $this->getDiv( $value ); + } + + $infusable = true; + if ( is_string( $inputField ) ) { + // Mmm… We have an OOUI implementation, but it's not complete, and we got a load of HTML. + // Cheat a little and wrap it in a widget! It won't be infusable, though, since client-side + // JavaScript doesn't know how to rebuilt the contents. + $inputField = new OOUI\Widget( array( 'content' => new OOUI\HtmlSnippet( $inputField ) ) ); + $infusable = false; + } + + $fieldType = get_class( $this ); + $field = new OOUI\FieldLayout( $inputField, array( + 'classes' => array( "mw-htmlform-field-$fieldType", $this->mClass, $errorClass ), + 'align' => $this->getLabelAlignOOUI(), + 'label' => $this->getLabel(), + 'help' => $this->getHelpText(), + 'infusable' => $infusable, + ) ); + + return $field . $errors; + } + + /** + * Get label alignment when generating field for OOUI. + * @return string 'left', 'right', 'top' or 'inline' + */ + protected function getLabelAlignOOUI() { + return 'top'; + } + /** * Get the complete raw fields for the input, including help text, * labels, and whatever. @@ -713,6 +772,9 @@ abstract class HTMLFormField { return array( $errors, $errorClass ); } + /** + * @return string + */ function getLabel() { return is_null( $this->mLabel ) ? '' : $this->mLabel; } @@ -775,24 +837,44 @@ abstract class HTMLFormField { return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] ); } + /** + * Get a translated key if necessary. + * @param array|null $mappings Array of mappings, 'original' => 'translated' + * @param string $key + * @return string + */ + protected function getMappedKey( $mappings, $key ) { + if ( !is_array( $mappings ) ) { + return $key; + } + + if ( !empty( $mappings[$key] ) ) { + return $mappings[$key]; + } + + return $key; + } + /** * Returns the given attributes from the parameters * * @param array $list List of attributes to get + * @param array $mappings Optional - Key/value map of attribute names to use instead of the ones passed in * @return array Attributes */ - public function getAttributes( array $list ) { + public function getAttributes( array $list, array $mappings = null ) { static $boolAttribs = array( 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ); $ret = array(); - foreach ( $list as $key ) { + $mappedKey = $this->getMappedKey( $mappings, $key ); + if ( in_array( $key, $boolAttribs ) ) { if ( !empty( $this->mParams[$key] ) ) { - $ret[$key] = ''; + $ret[$mappedKey] = ''; } } elseif ( isset( $this->mParams[$key] ) ) { - $ret[$key] = $this->mParams[$key]; + $ret[$mappedKey] = $this->mParams[$key]; } } @@ -881,6 +963,29 @@ abstract class HTMLFormField { return $this->mOptions; } + /** + * Get options and make them into arrays suitable for OOUI. + * @return array Options for inclusion in a select or whatever. + */ + public function getOptionsOOUI() { + $oldoptions = $this->getOptions(); + + if ( $oldoptions === null ) { + return null; + } + + $options = array(); + + foreach ( $oldoptions as $text => $data ) { + $options[] = array( + 'data' => $data, + 'label' => $text, + ); + } + + return $options; + } + /** * flatten an array of options to a single array, for instance, * a set of "" inside "". diff --git a/includes/htmlform/HTMLInfoField.php b/includes/htmlform/HTMLInfoField.php index a422047ac4..a667653a19 100644 --- a/includes/htmlform/HTMLInfoField.php +++ b/includes/htmlform/HTMLInfoField.php @@ -14,6 +14,16 @@ class HTMLInfoField extends HTMLFormField { return !empty( $this->mParams['raw'] ) ? $value : htmlspecialchars( $value ); } + public function getInputOOUI( $value ) { + if ( !empty( $this->mParams['raw'] ) ) { + $value = new OOUI\HtmlSnippet( $value ); + } + + return new OOUI\LabelWidget( array( + 'label' => $value, + ) ); + } + public function getTableRow( $value ) { if ( !empty( $this->mParams['rawrow'] ) ) { return $value; diff --git a/includes/htmlform/HTMLSelectField.php b/includes/htmlform/HTMLSelectField.php index a198037a06..6ba69666e2 100644 --- a/includes/htmlform/HTMLSelectField.php +++ b/includes/htmlform/HTMLSelectField.php @@ -41,4 +41,26 @@ class HTMLSelectField extends HTMLFormField { return $select->getHTML(); } + + function getInputOOUI( $value ) { + $disabled = false; + $allowedParams = array( 'tabindex' ); + $attribs = $this->getAttributes( $allowedParams, array( 'tabindex' => 'tabIndex' ) ); + + if ( $this->mClass !== '' ) { + $attribs['classes'] = array( $this->mClass ); + } + + if ( !empty( $this->mParams['disabled'] ) ) { + $disabled = true; + } + + return new OOUI\DropdownInputWidget( array( + 'name' => $this->mName, + 'id' => $this->mID, + 'options' => $this->getOptionsOOUI(), + 'value' => strval( $value ), + 'disabled' => $disabled, + ) + $attribs ); + } } diff --git a/includes/htmlform/HTMLTextAreaField.php b/includes/htmlform/HTMLTextAreaField.php index 21173d2a63..22e96f614e 100644 --- a/includes/htmlform/HTMLTextAreaField.php +++ b/includes/htmlform/HTMLTextAreaField.php @@ -35,4 +35,33 @@ class HTMLTextAreaField extends HTMLFormField { $attribs += $this->getAttributes( $allowedParams ); return Html::textarea( $this->mName, $value, $attribs ); } + + function getInputOOUI( $value ) { + $attribs = $this->getTooltipAndAccessKey(); + + if ( $this->mClass !== '' ) { + $attribs['classes'] = array( $this->mClass ); + } + + $allowedParams = array( + 'placeholder', + 'tabindex', + 'disabled', + 'readonly', + 'required', + 'autofocus', + ); + + $attribs += $this->getAttributes( $allowedParams, array( + 'tabindex' => 'tabIndex', + 'readonly' => 'readOnly', + ) ); + + return new OOUI\TextInputWidget( array( + 'id' => $this->mID, + 'name' => $this->mName, + 'multiline' => true, + 'value' => $value, + ) + $attribs ); + } } diff --git a/includes/htmlform/HTMLTextField.php b/includes/htmlform/HTMLTextField.php index a67e52e636..2958274255 100644 --- a/includes/htmlform/HTMLTextField.php +++ b/includes/htmlform/HTMLTextField.php @@ -41,6 +41,11 @@ class HTMLTextField extends HTMLFormField { $attribs += $this->getAttributes( $allowedParams ); # Extract 'type' + $type = $this->getType( $attribs ); + return Html::input( $this->mName, $value, $type, $attribs ); + } + + protected function getType( &$attribs ) { $type = isset( $attribs['type'] ) ? $attribs['type'] : 'text'; unset( $attribs['type'] ); @@ -66,6 +71,45 @@ class HTMLTextField extends HTMLFormField { } } - return Html::input( $this->mName, $value, $type, $attribs ); + return $type; + } + + function getInputOOUI( $value ) { + $attribs = $this->getTooltipAndAccessKey(); + + if ( $this->mClass !== '' ) { + $attribs['classes'] = array( $this->mClass ); + } + + # @todo Enforce pattern, step, required, readonly on the server side as + # well + $allowedParams = array( + 'autofocus', + 'autosize', + 'disabled', + 'flags', + 'indicator', + 'maxlength', + 'placeholder', + 'readonly', + 'required', + 'tabindex', + 'type', + ); + + $attribs += $this->getAttributes( $allowedParams, array( + 'maxlength' => 'maxLength', + 'readonly' => 'readOnly', + 'tabindex' => 'tabIndex', + ) ); + + $type = $this->getType( $attribs ); + + return new OOUI\TextInputWidget( array( + 'id' => $this->mID, + 'name' => $this->mName, + 'value' => $value, + 'type' => $type, + ) + $attribs ); } } diff --git a/includes/htmlform/OOUIHTMLForm.php b/includes/htmlform/OOUIHTMLForm.php new file mode 100644 index 0000000000..60e6366004 --- /dev/null +++ b/includes/htmlform/OOUIHTMLForm.php @@ -0,0 +1,137 @@ +getOutput()->enableOOUI(); + $this->getOutput()->addModules( 'mediawiki.htmlform.ooui' ); + $this->getOutput()->addModuleStyles( 'mediawiki.htmlform.ooui.styles' ); + parent::__construct( $descriptor, $context, $messagePrefix ); + } + + /** + * Symbolic display format name. + * @var string + */ + protected $displayFormat = 'ooui'; + + public static function loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent = null ) { + $field = parent::loadInputFromParameters( $fieldname, $descriptor, $parent ); + $field->setShowEmptyLabel( false ); + return $field; + } + + function getButtons() { + $buttons = ''; + + if ( $this->mShowSubmit ) { + $attribs = array(); + + if ( isset( $this->mSubmitID ) ) { + $attribs['id'] = $this->mSubmitID; + } + + if ( isset( $this->mSubmitName ) ) { + $attribs['name'] = $this->mSubmitName; + } + + if ( isset( $this->mSubmitTooltip ) ) { + $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip ); + } + + $attribs['classes'] = array( + 'mw-htmlform-submit', + $this->mSubmitModifierClass, + ); + + $attribs['type'] = 'submit'; + $attribs['label'] = $this->getSubmitText(); + $attribs['value'] = $this->getSubmitText(); + $attribs['flags'] = array( 'primary', 'constructive' ); + + $buttons .= new OOUI\ButtonInputWidget( $attribs ); + } + + if ( $this->mShowReset ) { + $buttons .= new OOUI\ButtonInputWidget( array( + 'type' => 'reset', + 'label' => $this->msg( 'htmlform-reset' )->text(), + ) ); + } + + foreach ( $this->mButtons as $button ) { + $attrs = array(); + + if ( $button['attribs'] ) { + $attrs += $button['attribs']; + } + + if ( isset( $button['id'] ) ) { + $attrs['id'] = $button['id']; + } + + $attrs['classes'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : array(); + + $buttons .= new OOUI\ButtonInputWidget( array( + 'type' => 'submit', + 'name' => $button['name'], + 'value' => $button['value'], + 'label' => $button['value'], + ) + $attrs ); + } + + $html = Html::rawElement( 'div', + array( 'class' => 'mw-htmlform-submit-buttons' ), "\n$buttons" ) . "\n"; + + return $html; + } + + function getFormAttributes() { + $attribs = parent::getFormAttributes(); + if ( !isset( $attribs['class'] ) ) { + $attribs['class'] = ''; + } + + if ( is_string( $attribs['class'] ) ) { + $attribs['class'] = trim( $attribs['class'] . ' mw-htmlform-ooui' ); + } else { + $attribs['class'][] = 'mw-htmlform-ooui'; + } + + return $attribs; + } + + function wrapForm( $html ) { + // Always discard $this->mWrapperLegend + return Html::rawElement( 'form', $this->getFormAttributes(), $html ); + } +} diff --git a/resources/Resources.php b/resources/Resources.php index ae5b3f90d1..54f3c90b92 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -947,6 +947,13 @@ return array( 'colon-separator', ), ), + 'mediawiki.htmlform.ooui' => array( + 'scripts' => 'resources/src/mediawiki/mediawiki.htmlform.ooui.js', + 'dependencies' => 'oojs-ui', + ), + 'mediawiki.htmlform.ooui.styles' => array( + 'styles' => 'resources/src/mediawiki/mediawiki.htmlform.ooui.css', + ), 'mediawiki.icon' => array( 'styles' => 'resources/src/mediawiki/mediawiki.icon.less', ), diff --git a/resources/src/mediawiki/mediawiki.htmlform.ooui.css b/resources/src/mediawiki/mediawiki.htmlform.ooui.css new file mode 100644 index 0000000000..92294c94c5 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.htmlform.ooui.css @@ -0,0 +1,20 @@ +/* OOUIHTMLForm styles */ + +.mw-htmlform-ooui { + width: 50em; +} + +.mw-htmlform-ooui .mw-htmlform-submit-buttons { + margin-top: 1em; +} + +.mw-htmlform-ooui .mw-htmlform-field-HTMLCheckMatrix, +.mw-htmlform-ooui .mw-htmlform-matrix, +.mw-htmlform-ooui .mw-htmlform-matrix tr { + width: 100%; +} + +.mw-htmlform-ooui .mw-htmlform-matrix tr td.first { + margin-right: 5%; + width: 39%; +} diff --git a/resources/src/mediawiki/mediawiki.htmlform.ooui.js b/resources/src/mediawiki/mediawiki.htmlform.ooui.js new file mode 100644 index 0000000000..48b8a877f7 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.htmlform.ooui.js @@ -0,0 +1,9 @@ +/*global OO */ +jQuery( function ( $ ) { + + // Infuse everything with JavaScript widgets + $( '.mw-htmlform-ooui [data-ooui]' ).each( function () { + OO.ui.infuse( this.id ); + } ); + +} ); -- 2.20.1