From 97d7de0b84ef65094108fbcf62ffcc1c55d75345 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 14 Aug 2015 20:07:35 +0200 Subject: [PATCH] Convert EditPage buttons, checkboxes and summary input to OOUI Several methods now have a new implementation using OOjs UI widgets (ButtonInputWidget/ButtonWidget, CheckboxInputWidget, TextInputWidget). The existing (public) methods are unchanged. The OOjs UI version is used by default. Because this change can cause problems for extensions and on-wiki scripts depending on the exact HTML, the old version is still available and can be used by setting $wgOOUIEditPage = false; in LocalSettings.php. This will be removed later and OOjs UI will become the only option. To make testing easier, users can also force either mode by adding &ooui=true or &ooui=false to the action=edit URL. * EditPage::getSummaryInput() and EditPage::getSummaryInputOOUI() * EditPage::getCheckboxes() and EditPage::getCheckboxesOOUI() * EditPage::getCancelLink() * EditPage::getEditButtons() Bug: T111088 Co-Authored-By: Amir Sarabadani Co-Authored-By: Florian Schmidt Change-Id: I25aa78ac59082789938ecfb5878eb16614392995 --- RELEASE-NOTES-1.29 | 7 + includes/DefaultSettings.php | 8 + includes/EditPage.php | 262 +++++++++++++++--- .../mediawiki.action/mediawiki.action.edit.js | 4 +- .../mediawiki.action.edit.styles.css | 74 +++-- .../src/mediawiki.skinning/interface.css | 2 +- 6 files changed, 295 insertions(+), 62 deletions(-) diff --git a/RELEASE-NOTES-1.29 b/RELEASE-NOTES-1.29 index e86ab1c381..2c194522c6 100644 --- a/RELEASE-NOTES-1.29 +++ b/RELEASE-NOTES-1.29 @@ -63,6 +63,13 @@ production. * Completely new user interface for the RecentChanges page, which structures filters into user-friendly groups. This has corresponding changes to how filters are registered by core and extensions. +* The edit form now uses pretty OOjs UI buttons, checkboxes and summary input. + Because this change can cause problems for extensions and on-wiki + scripts depending on the exact HTML, the old version is still available + and can be used by setting $wgOOUIEditPage = false; in LocalSettings.php. + This will be removed later and OOjs UI will become the only option. + To make testing easier, users can also force either mode by adding + &ooui=true or &ooui=false to the action=edit URL. === External library changes in 1.29 === diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 53a147b524..d3171a8719 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -3216,6 +3216,14 @@ $wgHTMLFormAllowTableFormat = true; */ $wgUseMediaWikiUIEverywhere = false; +/** + * Temporary variable that determines whether the EditPage class should use OOjs UI or not. + * This will be removed later and OOjs UI will become the only option. + * + * @since 1.29 + */ +$wgOOUIEditPage = true; + /** * Whether to label the store-to-database-and-show-to-others button in the editor * as "Save page"/"Save changes" if false (the default) or, if true, instead as diff --git a/includes/EditPage.php b/includes/EditPage.php index 7bdc3bc940..c19ed5e753 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -413,10 +413,17 @@ class EditPage { */ private $isOldRev = false; + /** + * @var bool Whether OOUI should be enabled here + */ + private $oouiEnabled = false; + /** * @param Article $article */ public function __construct( Article $article ) { + global $wgOOUIEditPage; + $this->mArticle = $article; $this->page = $article->getPage(); // model object $this->mTitle = $article->getTitle(); @@ -426,6 +433,8 @@ class EditPage { $handler = ContentHandler::getForModelID( $this->contentModel ); $this->contentFormat = $handler->getDefaultFormat(); + + $this->oouiEnabled = $wgOOUIEditPage; } /** @@ -476,6 +485,14 @@ class EditPage { } } + /** + * Check if the edit page is using OOUI controls + * @return bool + */ + public function isOouiEnabled() { + return $this->oouiEnabled; + } + /** * Returns if the given content model is editable. * @@ -843,6 +860,9 @@ class EditPage { public function importFormData( &$request ) { global $wgContLang, $wgUser; + # Allow users to change the mode for testing + $this->oouiEnabled = $request->getFuzzyBool( 'ooui', $this->oouiEnabled ); + # Section edit can come from either the form or a link $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); @@ -2637,6 +2657,7 @@ class EditPage { $wgOut->addHTML( Html::openElement( 'form', [ + 'class' => $this->oouiEnabled ? 'mw-editform-ooui' : 'mw-editform-legacy', 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID, 'method' => 'post', @@ -2736,6 +2757,11 @@ class EditPage { $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) ); $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) ); + // following functions will need OOUI, enable it only once; here. + if ( $this->oouiEnabled ) { + $wgOut->enableOOUI(); + } + if ( $this->section == 'new' ) { $this->showSummaryInput( true, $this->summary ); $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) ); @@ -3007,6 +3033,24 @@ class EditPage { $this->showHeaderCopyrightWarning(); } + /** + * Helper function for summary input functions, which returns the neccessary + * attributes for the input. + * + * @param array|null $inputAttrs Array of attrs to use on the input + * @return array + */ + private function getSummaryInputAttributes( array $inputAttrs = null ) { + // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters. + return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [ + 'id' => 'wpSummary', + 'maxlength' => '200', + 'tabindex' => '1', + 'size' => 60, + 'spellcheck' => 'true', + ] + Linker::tooltipAndAccesskeyAttribs( 'summary' ); + } + /** * Standard summary input and label (wgSummary), abstracted so EditPage * subclasses may reorganize the form. @@ -3024,14 +3068,7 @@ class EditPage { public function getSummaryInput( $summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null ) { - // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters. - $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [ - 'id' => 'wpSummary', - 'maxlength' => '200', - 'tabindex' => '1', - 'size' => 60, - 'spellcheck' => 'true', - ] + Linker::tooltipAndAccesskeyAttribs( 'summary' ); + $inputAttrs = $this->getSummaryInputAttributes( $inputAttrs ); $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [ 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary', @@ -3053,6 +3090,34 @@ class EditPage { return [ $label, $input ]; } + /** + * Same as self::getSummaryInput, but uses OOUI, instead of plain HTML. + * Builds a standard summary input with a label. + * + * @param string $summary The value of the summary input + * @param string $labelText The html to place inside the label + * @param array $inputAttrs Array of attrs to use on the input + * + * @return OOUI\FieldLayout OOUI FieldLayout with Label and Input + */ + function getSummaryInputOOUI( $summary = "", $labelText = null, $inputAttrs = null ) { + $inputAttrs = OOUI\Element::configFromHtmlAttributes( + $this->getSummaryInputAttributes( $inputAttrs ) + ); + + return new OOUI\FieldLayout( + new OOUI\TextInputWidget( [ + 'value' => $summary, + ] + $inputAttrs ), + [ + 'label' => new OOUI\HtmlSnippet( $labelText ), + 'align' => 'top', + 'id' => 'wpSummaryLabel', + 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ], + ] + ); + } + /** * @param bool $isSubjectPreview True if this is the section subject/title * up top, or false if this is the comment summary @@ -3073,14 +3138,23 @@ class EditPage { return; } } + $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse(); - list( $label, $input ) = $this->getSummaryInput( - $summary, - $labelText, - [ 'class' => $summaryClass ], - [] - ); - $wgOut->addHTML( "{$label} {$input}" ); + if ( $this->oouiEnabled ) { + $wgOut->addHTML( $this->getSummaryInputOOUI( + $summary, + $labelText, + [ 'class' => $summaryClass ] + ) ); + } else { + list( $label, $input ) = $this->getSummaryInput( + $summary, + $labelText, + [ 'class' => $summaryClass ] + ); + $wgOut->addHTML( "{$label} {$input}" ); + } + } /** @@ -3490,9 +3564,21 @@ HTML $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) ); } - $checkboxes = $this->getCheckboxes( $tabindex, - [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] ); - $wgOut->addHTML( "
" . implode( $checkboxes, "\n" ) . "
\n" ); + if ( $this->oouiEnabled ) { + $checkboxes = $this->getCheckboxesOOUI( + $tabindex, + [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] + ); + $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] ); + } else { + $checkboxes = $this->getCheckboxes( + $tabindex, + [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] + ); + $checkboxesHTML = implode( $checkboxes, "\n" ); + } + + $wgOut->addHTML( "
" . $checkboxesHTML . "
\n" ); // Show copyright warning. $wgOut->addWikiText( $this->getCopywarn() ); @@ -3580,13 +3666,22 @@ HTML } elseif ( $this->getContextTitle()->isRedirect() ) { $cancelParams['redirect'] = 'no'; } - - return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( - $this->getContextTitle(), - new HtmlArmor( $this->context->msg( 'cancel' )->parse() ), - Html::buttonAttributes( [ 'id' => 'mw-editform-cancel' ], [ 'mw-ui-quiet' ] ), - $cancelParams - ); + if ( $this->oouiEnabled ) { + return new OOUI\ButtonWidget( [ + 'id' => 'mw-editform-cancel', + 'href' => $this->getContextTitle()->getLinkUrl( $cancelParams ), + 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ), + 'framed' => false, + 'flags' => 'destructive', + ] ); + } else { + return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( + $this->getContextTitle(), + new HtmlArmor( $this->context->msg( 'cancel' )->parse() ), + Html::buttonAttributes( [ 'id' => 'mw-editform-cancel' ], [ 'mw-ui-quiet' ] ), + $cancelParams + ); + } } /** @@ -4062,7 +4157,7 @@ HTML } /** - * Returns an array of html code of the following checkboxes: + * Returns an array of html code of the following checkboxes old style: * minor and watch * * @param int $tabindex Current tabindex @@ -4119,6 +4214,68 @@ HTML return $checkboxes; } + /** + * Returns an array of html code of the following checkboxes: + * minor and watch + * + * @param int $tabindex Current tabindex + * @param array $checked Array of checkbox => bool, where bool indicates the checked + * status of the checkbox + * + * @return array + */ + public function getCheckboxesOOUI( &$tabindex, $checked ) { + $checkboxes = []; + $checkboxesDef = $this->getCheckboxesDefinition( $checked ); + + $origTabindex = $tabindex; + + foreach ( $checkboxesDef as $name => $options ) { + $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name; + + $title = null; + $accesskey = null; + if ( isset( $options['tooltip'] ) ) { + $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text(); + $title = Linker::titleAttrib( $options['tooltip'], 'withaccess' ); + } + if ( isset( $options['title-message'] ) ) { + $title = $this->context->msg( $options['title-message'] )->text(); + } + if ( isset( $options['label-id'] ) ) { + $labelAttribs['id'] = $options['label-id']; + } + + $checkboxes[ $legacyName ] = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( [ + 'tabIndex' => ++$tabindex, + 'accessKey' => $accesskey, + 'id' => $options['id'], + 'name' => $name, + 'selected' => $options['default'], + ] ), + [ + 'align' => 'inline', + 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ), + 'title' => $title, + 'id' => isset( $options['label-id'] ) ? $options['label-id'] : null, + ] + ); + } + + // Backwards-compatibility hack to run the EditPageBeforeEditChecks hook. It's important, + // people have used it for the weirdest things completely unrelated to checkboxes... + // And if we're gonna run it, might as well allow its legacy checkboxes to be shown. + $legacyCheckboxes = $this->getCheckboxes( $origTabindex, $checked ); + foreach ( $legacyCheckboxes as $name => $html ) { + if ( $html && !isset( $checkboxes[$name] ) ) { + $checkboxes[$name] = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $html ) ] ); + } + } + + return $checkboxes; + } + /** * Returns an array of html code of the following buttons: * save, diff and preview @@ -4144,31 +4301,56 @@ HTML 'name' => 'wpSave', 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'save' ); - $buttons['save'] = Html::submitButton( - $this->context->msg( $buttonLabelKey )->text(), - $attribs, - [ 'mw-ui-progressive' ] - ); + + if ( $this->oouiEnabled ) { + $saveConfig = OOUI\Element::configFromHtmlAttributes( $attribs ); + $buttons['save'] = new OOUI\ButtonInputWidget( [ + 'flags' => [ 'constructive', 'primary' ], + 'label' => $this->context->msg( $buttonLabelKey )->text(), + 'type' => 'submit', + ] + $saveConfig ); + } else { + $buttons['save'] = Html::submitButton( + $this->context->msg( $buttonLabelKey )->text(), + $attribs, + [ 'mw-ui-progressive' ] + ); + } $attribs = [ 'id' => 'wpPreview', 'name' => 'wpPreview', 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'preview' ); - $buttons['preview'] = Html::submitButton( - $this->context->msg( 'showpreview' )->text(), - $attribs - ); - + if ( $this->oouiEnabled ) { + $previewConfig = OOUI\Element::configFromHtmlAttributes( $attribs ); + $buttons['preview'] = new OOUI\ButtonInputWidget( [ + 'label' => $this->context->msg( 'showpreview' )->text(), + 'type' => 'submit' + ] + $previewConfig ); + } else { + $buttons['preview'] = Html::submitButton( + $this->context->msg( 'showpreview' )->text(), + $attribs + ); + } $attribs = [ 'id' => 'wpDiff', 'name' => 'wpDiff', 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'diff' ); - $buttons['diff'] = Html::submitButton( - $this->context->msg( 'showdiff' )->text(), - $attribs - ); + if ( $this->oouiEnabled ) { + $diffConfig = OOUI\Element::configFromHtmlAttributes( $attribs ); + $buttons['diff'] = new OOUI\ButtonInputWidget( [ + 'label' => $this->context->msg( 'showdiff' )->text(), + 'type' => 'submit', + ] + $diffConfig ); + } else { + $buttons['diff'] = Html::submitButton( + $this->context->msg( 'showdiff' )->text(), + $attribs + ); + } // Avoid PHP 7.1 warning of passing $this by reference $editPage = $this; diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.js b/resources/src/mediawiki.action/mediawiki.action.edit.js index c9834f0488..f6a9c54de7 100644 --- a/resources/src/mediawiki.action/mediawiki.action.edit.js +++ b/resources/src/mediawiki.action/mediawiki.action.edit.js @@ -20,7 +20,9 @@ var editBox, scrollTop, $editForm; // Make sure edit summary does not exceed byte limit - $( '#wpSummary' ).byteLimit( 255 ); + // TODO: Replace with this when $wgOOUIEditPage is removed: + // OO.ui.infuse( 'wpSummary' ).$input.byteLimit( 255 ); + $( 'input#wpSummary, #wpSummary > input' ).byteLimit( 255 ); // Restore the edit box scroll state following a preview operation, // and set up a form submission handler to remember this state. diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.styles.css b/resources/src/mediawiki.action/mediawiki.action.edit.styles.css index d22823609b..828726465b 100644 --- a/resources/src/mediawiki.action/mediawiki.action.edit.styles.css +++ b/resources/src/mediawiki.action/mediawiki.action.edit.styles.css @@ -12,30 +12,42 @@ min-height: 5em; } +/* + * Add a bit of margin space between the preview and the toolbar. + * This replaces the ugly


we used to insert into the page source + */ +#wikiPreview.ontop { + margin-bottom: 1em; +} + /* Adjustments to edit form elements */ -.editCheckboxes { +#editpage-copywarn { + font-size: 0.9em; +} + +#wpSummary { + display: block; + width: 80%; margin-bottom: 1em; } -.editCheckboxes input:first-child { - margin-left: 0; +/* Adjustments to edit form elements (only when $wgOOUIEditPage is false) */ +.mw-editform-legacy .editCheckboxes { + margin-bottom: 1em; } -.cancelLink { - margin-left: 0.5em; +.mw-editform-legacy .editCheckboxes input:first-child { + margin-left: 0; } -#editpage-copywarn { - font-size: 0.9em; +.mw-editform-legacy .cancelLink { + margin-left: 0.5em; } -input#wpSummary { - display: block; +.mw-editform-legacy input#wpSummary { background-color: #fff; color: #000; - width: 80%; margin-top: 0; - margin-bottom: 1em; padding: 0.625em 0.546875em 0.546875em; border: 1px solid #a2a9b1; border-radius: 2px; @@ -47,21 +59,43 @@ input#wpSummary { transition: border-color 200ms cubic-bezier( 0.39, 0.575, 0.565, 1 ), box-shadow 200ms cubic-bezier( 0.39, 0.575, 0.565, 1 ); } -input#wpSummary:focus, -input#wpSummary:active { +.mw-editform-legacy input#wpSummary:focus, +.mw-editform-legacy input#wpSummary:active { outline: 0; border-color: #36c; box-shadow: inset 0 0 0 1px #36c; } -.editButtons input:first-child { +.mw-editform-legacy .editButtons input:first-child { margin-left: 0.1em; } -/* - * Add a bit of margin space between the preview and the toolbar. - * This replaces the ugly


we used to insert into the page source - */ -#wikiPreview.ontop { - margin-bottom: 1em; +/* Adjustments to edit form elements (only when $wgOOUIEditPage is true) */ +.mw-editform-ooui #editpage-copywarn { + line-height: 1.26; +} + +.mw-editform-ooui #wpSummary { + max-width: none; +} + +.mw-editform-ooui #wpSummaryLabel { + margin: 0; +} + +.mw-editform-ooui .editCheckboxes .oo-ui-fieldLayout { + margin-right: 1em; +} + +.mw-editform-ooui .editHelp { + margin-left: 0.5em; + vertical-align: middle; +} + +.mw-editform-ooui .editHelp a { + font-weight: bold; +} + +.mw-editform-ooui .editOptions { + border-radius: 0 0 2px 2px; } diff --git a/resources/src/mediawiki.skinning/interface.css b/resources/src/mediawiki.skinning/interface.css index 2be3bb2ac8..c6d5082c99 100644 --- a/resources/src/mediawiki.skinning/interface.css +++ b/resources/src/mediawiki.skinning/interface.css @@ -21,7 +21,7 @@ textarea { } .editOptions { - background-color: #f8f9fa; + background-color: #eaecf0; border: 1px solid #c8ccd1; border-top: 0; padding: 1em 1em 1.5em 1em; -- 2.20.1