From d32891d99f185e5cf2b0e25bf947de221d4c3295 Mon Sep 17 00:00:00 2001 From: S Page Date: Tue, 25 Jun 2013 23:03:17 -0700 Subject: [PATCH] Add vform format to HTMLForm, use in PasswordReset This adds a new 'vform' display format to HTMLForm which styles forms using it in the new compact stacked vertical format: * Applies class mw-ui-vform to the form. * Gives Submit button 'mw-ui-{button,big,block,primary}' classes and spaced block styling. * Styles the error class. * HTMLForm divs are too nested to get styling "for free", so add .mw-ui-vform-div selector to Agora CSS and apply this class to form divs (maybe not ideal fix). * HTMLCheckField nests the checkbox inside label. * Add method to setShowEmptyLabel(); vform format sets this false as it doesn't want generated HTML for empty labels adding vertical space. In FormSpecialPage, don't set the wrapper legend with its old-school line around the form if in vform style. Build new mediawiki.ui CSS with the .mw-ui-vform-div styling Special:PasswordReset requests the new styling with setDisplayFormat( 'vform' ) and turns off the wrapper legend. (This version of the patch doesn't switch the default format of HTMLForm, individual forms have to choose it.) Notes are at https://www.mediawiki.org/wiki/Agora/Engineering#HTMLForm_Issues Change-Id: Id03d185bbee990595bfc469a61163cc598fc3441 --- includes/HTMLForm.php | 107 ++++++++++++++++-- includes/SpecialPage.php | 8 +- includes/specials/SpecialPasswordReset.php | 7 ++ .../mediawiki.ui/mediawiki.ui.default.css | 39 +++++-- .../mediawiki.ui/mediawiki.ui.vector.css | 39 +++++-- .../scss/components/default/_forms.scss | 29 +++++ 6 files changed, 203 insertions(+), 26 deletions(-) diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 70dc7c72db..d260862c0c 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -187,6 +187,7 @@ class HTMLForm extends ContextSource { 'table', 'div', 'raw', + 'vform', ); /** @@ -223,8 +224,15 @@ class HTMLForm extends ContextSource { } $field = self::loadInputFromParameters( $fieldname, $info ); + // FIXME During field's construct, the parent form isn't available! + // could add a 'parent' name-value to $info, could add a third parameter. $field->mParent = $this; + // vform gets too much space if empty labels generate HTML. + if ( $this->isVForm() ) { + $field->setShowEmptyLabel( false ); + } + $setSection =& $loadedDescriptor; if ( $section ) { $sectionParts = explode( '/', $section ); @@ -272,6 +280,15 @@ class HTMLForm extends ContextSource { return $this->displayFormat; } + /** + * Test if displayFormat is 'vform' + * @since 1.22 + * @return Bool + */ + public function isVForm() { + return $this->displayFormat === 'vform'; + } + /** * Add the HTMLForm-specific JavaScript, if it hasn't been * done already. @@ -626,6 +643,11 @@ class HTMLForm extends ContextSource { # For good measure (it is the default) $this->getOutput()->preventClickjacking(); $this->getOutput()->addModules( 'mediawiki.htmlform' ); + if ( $this->isVForm() ) { + $this->getOutput()->addModuleStyles( 'mediawiki.ui' ); + // TODO should vertical form set setWrapperLegend( false ) + // to hide ugly fieldsets? + } $html = '' . $this->getErrors( $submitResult ) @@ -660,13 +682,16 @@ class HTMLForm extends ContextSource { $attribs = array( 'action' => $this->getAction(), 'method' => $this->getMethod(), - 'class' => 'visualClear', + 'class' => array( 'visualClear' ), 'enctype' => $encType, ); if ( !empty( $this->mId ) ) { $attribs['id'] = $this->mId; } + if ( $this->isVForm() ) { + array_push( $attribs['class'], 'mw-ui-vform', 'mw-ui-container' ); + } return Html::rawElement( 'form', $attribs, $html ); } @@ -717,9 +742,22 @@ class HTMLForm extends ContextSource { $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip ); } - $attribs['class'] = 'mw-htmlform-submit'; + $attribs['class'] = array( 'mw-htmlform-submit' ); + + if ( $this->isVForm() ) { + // mw-ui-block is necessary because the buttons aren't necessarily in an + // immediate child div of the vform. + array_push( $attribs['class'], 'mw-ui-button', 'mw-ui-big', 'mw-ui-primary', 'mw-ui-block' ); + } $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n"; + + // Buttons are top-level form elements in table and div layouts, + // but vform wants all elements inside divs to get spaced-out block + // styling. + if ( $this->isVForm() ) { + $html = Html::rawElement( 'div', null, "\n$html\n" ); + } } if ( $this->mShowReset ) { @@ -913,7 +951,8 @@ class HTMLForm extends ContextSource { /** * Prompt the whole form to be wrapped in a "
", with * this text as its "" element. - * @param string $legend HTML to go inside the "" element. + * @param string|false $legend HTML to go inside the "" element, or + * false for no * Will be escaped * @return HTMLForm $this for chaining calls (since 1.20) */ @@ -982,7 +1021,7 @@ class HTMLForm extends ContextSource { /** * @todo Document - * @param $fields array[]|HTMLFormField[] array of fields (either arrays or objects) + * @param array[]|HTMLFormField[] $fields array of fields (either arrays or objects) * @param string $sectionName ID attribute of the "" tag for this section, ignored if empty * @param string $fieldsetIDPrefix ID prefix for the "
" tag of each subsection, ignored if empty * @param boolean &$hasUserVisibleFields Whether the section had user-visible fields @@ -995,7 +1034,17 @@ class HTMLForm extends ContextSource { $subsectionHtml = ''; $hasLabel = false; - $getFieldHtmlMethod = ( $displayFormat == 'table' ) ? 'getTableRow' : 'get' . ucfirst( $displayFormat ); + switch( $displayFormat ) { + case 'table': + $getFieldHtmlMethod = 'getTableRow'; + break; + case 'vform': + // Close enough to a div. + $getFieldHtmlMethod = 'getDiv'; + break; + default: + $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat ); + } foreach ( $fields as $key => $value ) { if ( $value instanceof HTMLFormField ) { @@ -1061,7 +1110,7 @@ class HTMLForm extends ContextSource { if ( $displayFormat === 'table' ) { $html = Html::rawElement( 'table', $attribs, Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n"; - } elseif ( $displayFormat === 'div' ) { + } elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) { $html = Html::rawElement( 'div', $attribs, "\n$html\n" ); } } @@ -1268,6 +1317,18 @@ abstract class HTMLFormField { return true; } + /** + * Tell the field whether to generate a separate label element if its label + * is blank. + * + * @since 1.22 + * @param bool $show Set to false to not generate a label. + * @return void + */ + public function setShowEmptyLabel( $show ) { + $this->mShowEmptyLabels = $show; + } + /** * Get the value that this input has been set to from a posted form, * or the input's default value if it has not been set. @@ -1431,8 +1492,12 @@ abstract class HTMLFormField { array( 'class' => $outerDivClass ) + $cellAttributes, $inputHtml . "\n$errors" ); + $divCssClasses = array( "mw-htmlform-field-$fieldType", $this->mClass, $errorClass ); + if ( $this->mParent->isVForm() ) { + $divCssClasses[] = 'mw-ui-vform-div'; + } $html = Html::rawElement( 'div', - array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), + array( 'class' => $divCssClasses ), $label . $field ); $html .= $helptext; return $html; @@ -1876,8 +1941,25 @@ class HTMLCheckField extends HTMLFormField { $attr['class'] = $this->mClass; } - return Xml::check( $this->mName, $value, $attr ) . ' ' . - Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel ); + if ( $this->mParent->isVForm() ) { + // Nest checkbox inside label. + return Html::rawElement( + 'label', + array( + 'class' => 'mw-ui-checkbox-label' + ), + Xml::check( + $this->mName, + $value, + $attr + ) . + // Html:rawElement doesn't escape contents. + htmlspecialchars( $this->mLabel ) + ); + } else { + return Xml::check( $this->mName, $value, $attr ) . ' ' . + Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel ); + } } /** @@ -1889,6 +1971,13 @@ class HTMLCheckField extends HTMLFormField { return ' '; } + /** + * checkboxes don't need a label. + */ + protected function needsLabel() { + return false; + } + /** * @param $request WebRequest * @return String diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index 61630a9bab..00f06cf808 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -982,7 +982,13 @@ abstract class FormSpecialPage extends SpecialPage { $form = new HTMLForm( $this->fields, $this->getContext(), $this->getMessagePrefix() ); $form->setSubmitCallback( array( $this, 'onSubmit' ) ); - $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' ); + // If the form is a compact vertical form, then don't output this ugly + // fieldset surrounding it. + // XXX Special pages can setDisplayFormat to 'vform' in alterForm(), but that + // is called after this. + if ( !$form->isVForm() ) { + $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' ); + } $headerMsg = $this->msg( $this->getMessagePrefix() . '-text' ); if ( !$headerMsg->isDisabled() ) { diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php index 69c40569d0..c486ba0171 100644 --- a/includes/specials/SpecialPasswordReset.php +++ b/includes/specials/SpecialPasswordReset.php @@ -105,6 +105,13 @@ class SpecialPasswordReset extends FormSpecialPage { public function alterForm( HTMLForm $form ) { global $wgPasswordResetRoutes; + $form->setDisplayFormat( 'vform' ); + // Turn the old-school line around the form off. + // XXX This wouldn't be necessary here if we could set the format of + // the HTMLForm to 'vform' at its creation, but there's no way to do so + // from a FormSpecialPage class. + $form->setWrapperLegend( false ); + $i = 0; if ( isset( $wgPasswordResetRoutes['username'] ) && $wgPasswordResetRoutes['username'] ) { $i++; diff --git a/resources/mediawiki.ui/mediawiki.ui.default.css b/resources/mediawiki.ui/mediawiki.ui.default.css index 498d13487f..b0726165dc 100644 --- a/resources/mediawiki.ui/mediawiki.ui.default.css +++ b/resources/mediawiki.ui/mediawiki.ui.default.css @@ -1,3 +1,4 @@ +@charset "UTF-8"; /** * Provide Agora appearance for mw-ui-* classes when using a skin other than * Vector. @@ -140,14 +141,14 @@ a.mw-ui-button { box-sizing: border-box; width: 290px; } -/* line 19, sourcefiles/scss/components/default/_forms.scss */ +/* line 20, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div { display: block; margin: 0 0 15px 0; padding: 0; width: 100%; } -/* line 27, sourcefiles/scss/components/default/_forms.scss */ +/* line 28, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div input, .mw-ui-vform > div .mw-ui-button { display: block; @@ -157,7 +158,7 @@ a.mw-ui-button { margin: 0; width: 100%; } -/* line 36, sourcefiles/scss/components/default/_forms.scss */ +/* line 37, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]) { border-style: solid; border-width: 1px; @@ -174,7 +175,7 @@ a.mw-ui-button { .mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus:not([type=checkbox]):not([type=radio]) { outline: 0; } -/* line 40, sourcefiles/scss/components/default/_forms.scss */ +/* line 41, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div label { display: block; -webkit-box-sizing: border-box; @@ -190,7 +191,7 @@ a.mw-ui-button { .mw-ui-vform > div label * { font-weight: normal; } -/* line 51, sourcefiles/scss/components/default/_forms.scss */ +/* line 52, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div input[type="checkbox"], .mw-ui-vform > div input[type="radio"] { display: inline; @@ -199,8 +200,30 @@ a.mw-ui-button { box-sizing: content-box; width: auto; } +/* line 63, sourcefiles/scss/components/default/_forms.scss */ +.mw-ui-vform .error { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + font-size: 0.9em; + margin: 0 0 1em 0; + padding: 0.5em; + color: #cc0000; + border: 1px solid #fac5c5; + background-color: #fae3e3; + text-shadow: 0 1px #fae3e3; + word-wrap: break-word; +} + +/* line 86, sourcefiles/scss/components/default/_forms.scss */ +.mw-ui-vform-div { + display: block; + margin: 0 0 15px 0; + padding: 0; + width: 100%; +} -/* line 67, sourcefiles/scss/components/default/_forms.scss */ +/* line 96, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-input { border-style: solid; border-width: 1px; @@ -218,7 +241,7 @@ a.mw-ui-button { outline: 0; } -/* line 74, sourcefiles/scss/components/default/_forms.scss */ +/* line 103, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-label { font-size: 0.9em; color: #4a4a4a; @@ -228,7 +251,7 @@ a.mw-ui-button { font-weight: normal; } -/* line 83, sourcefiles/scss/components/default/_forms.scss */ +/* line 112, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-checkbox-label, .mw-ui-radio-label { margin-bottom: 0.5em; cursor: pointer; diff --git a/resources/mediawiki.ui/mediawiki.ui.vector.css b/resources/mediawiki.ui/mediawiki.ui.vector.css index 6aa7f89904..fd9e091589 100644 --- a/resources/mediawiki.ui/mediawiki.ui.vector.css +++ b/resources/mediawiki.ui/mediawiki.ui.vector.css @@ -1,3 +1,4 @@ +@charset "UTF-8"; /** * Provide Agora appearance for mw-ui-* classes when using the Vector skin. * Compass builds these Agora styles from source Sass files in @@ -268,14 +269,14 @@ a.mw-ui-button { box-sizing: border-box; width: 290px; } -/* line 19, sourcefiles/scss/components/default/_forms.scss */ +/* line 20, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div { display: block; margin: 0 0 15px 0; padding: 0; width: 100%; } -/* line 27, sourcefiles/scss/components/default/_forms.scss */ +/* line 28, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div input, .mw-ui-vform > div .mw-ui-button { display: block; @@ -285,7 +286,7 @@ a.mw-ui-button { margin: 0; width: 100%; } -/* line 36, sourcefiles/scss/components/default/_forms.scss */ +/* line 37, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]) { border-style: solid; border-width: 1px; @@ -302,7 +303,7 @@ a.mw-ui-button { .mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus:not([type=checkbox]):not([type=radio]) { outline: 0; } -/* line 40, sourcefiles/scss/components/default/_forms.scss */ +/* line 41, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div label { display: block; -webkit-box-sizing: border-box; @@ -318,7 +319,7 @@ a.mw-ui-button { .mw-ui-vform > div label * { font-weight: normal; } -/* line 51, sourcefiles/scss/components/default/_forms.scss */ +/* line 52, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-vform > div input[type="checkbox"], .mw-ui-vform > div input[type="radio"] { display: inline; @@ -327,8 +328,30 @@ a.mw-ui-button { box-sizing: content-box; width: auto; } +/* line 63, sourcefiles/scss/components/default/_forms.scss */ +.mw-ui-vform .error { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + font-size: 0.9em; + margin: 0 0 1em 0; + padding: 0.5em; + color: #cc0000; + border: 1px solid #fac5c5; + background-color: #fae3e3; + text-shadow: 0 1px #fae3e3; + word-wrap: break-word; +} + +/* line 86, sourcefiles/scss/components/default/_forms.scss */ +.mw-ui-vform-div { + display: block; + margin: 0 0 15px 0; + padding: 0; + width: 100%; +} -/* line 67, sourcefiles/scss/components/default/_forms.scss */ +/* line 96, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-input { border-style: solid; border-width: 1px; @@ -346,7 +369,7 @@ a.mw-ui-button { outline: 0; } -/* line 74, sourcefiles/scss/components/default/_forms.scss */ +/* line 103, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-label { font-size: 0.9em; color: #4a4a4a; @@ -356,7 +379,7 @@ a.mw-ui-button { font-weight: normal; } -/* line 83, sourcefiles/scss/components/default/_forms.scss */ +/* line 112, sourcefiles/scss/components/default/_forms.scss */ .mw-ui-checkbox-label, .mw-ui-radio-label { margin-bottom: 0.5em; cursor: pointer; diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/default/_forms.scss b/resources/mediawiki.ui/sourcefiles/scss/components/default/_forms.scss index dfcd36fc7c..a9cec39a4b 100644 --- a/resources/mediawiki.ui/sourcefiles/scss/components/default/_forms.scss +++ b/resources/mediawiki.ui/sourcefiles/scss/components/default/_forms.scss @@ -16,6 +16,7 @@ $defaultFormWidth: $captchaContainerWidth; width: $defaultFormWidth; + // Immediate divs in a vform are block and spaced-out. & > div { display: block; margin: 0 0 15px 0; @@ -55,12 +56,40 @@ $defaultFormWidth: $captchaContainerWidth; } } + + // HTMLForm uses error, SpecialUserlogin (login and create account) uses + // errorbox. + // TODO move errorbox from mediawiki.special.vforms.css into here. + .error { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + font-size: 0.9em; + margin: 0 0 1em 0; + padding: 0.5em; + color: #cc0000; + border: 1px solid #fac5c5; + background-color: #fae3e3; + text-shadow: 0 1px #fae3e3; + word-wrap: break-word; + } } // -------------------------------------------------------------------------- // Elements // -------------------------------------------------------------------------- +// Apply this to individual elements to style them. +// You generally don't need to use this class on divs within an Agora +// form container such as mw-ui-vform +// XXX DRY: This repeats earlier styling, use an @include agora-div-styling ? +.mw-ui-vform-div { + display: block; + margin: 0 0 15px 0; + padding: 0; + width: 100%; +} + // Apply mw-ui-input to individual input fields to style them. // You generally don't need to use this class if is within an Agora // form container such as mw-ui-vform -- 2.20.1