From 4633f4d46a8db46721474e9ffa6a130d8b7a2404 Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Tue, 1 May 2018 14:59:29 +0100 Subject: [PATCH] Special:Preferences: Create flag to enable OOjs UI This reverts commit 808e45d13d400256d36cfcd95e79a567197d9a8b. Bug: T117781 Change-Id: I152b82bcd647d97062eb82cd2d1064609124f9bc --- autoload.php | 2 + includes/DefaultSettings.php | 8 + .../preferences/DefaultPreferencesFactory.php | 158 +++++++++++++----- includes/specials/SpecialPreferences.php | 101 +++++++---- includes/specials/forms/PreferencesForm.php | 125 +------------- .../specials/forms/PreferencesFormLegacy.php | 143 ++++++++++++++++ .../specials/forms/PreferencesFormOOUI.php | 144 ++++++++++++++++ languages/i18n/en.json | 2 + languages/i18n/qqq.json | 2 + resources/Resources.php | 31 +++- resources/src/mediawiki.legacy/oldshared.css | 1 + ...iawiki.special.preferences.confirmClose.js | 42 ++++- .../mediawiki.special.preferences.editfont.js | 32 ++++ .../mediawiki.special.preferences.styles.css | 101 +++++++++-- ...wiki.special.preferences.styles.legacy.css | 47 ++++++ .../mediawiki.special.preferences.tabs.js | 129 +++++++------- ...diawiki.special.preferences.tabs.legacy.js | 143 ++++++++++++++++ .../mediawiki.special.preferences.timezone.js | 51 ++++-- .../DefaultPreferencesFactoryTest.php | 2 +- 19 files changed, 969 insertions(+), 295 deletions(-) create mode 100644 includes/specials/forms/PreferencesFormLegacy.php create mode 100644 includes/specials/forms/PreferencesFormOOUI.php create mode 100644 resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js create mode 100644 resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css create mode 100644 resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js diff --git a/autoload.php b/autoload.php index b832863564..489339eb8d 100644 --- a/autoload.php +++ b/autoload.php @@ -1182,6 +1182,8 @@ $wgAutoloadLocalClasses = [ 'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php', 'Preferences' => __DIR__ . '/includes/Preferences.php', 'PreferencesForm' => __DIR__ . '/includes/specials/forms/PreferencesForm.php', + 'PreferencesFormLegacy' => __DIR__ . '/includes/specials/forms/PreferencesFormLegacy.php', + 'PreferencesFormOOUI' => __DIR__ . '/includes/specials/forms/PreferencesFormOOUI.php', 'PrefixSearch' => __DIR__ . '/includes/PrefixSearch.php', 'PreprocessDump' => __DIR__ . '/maintenance/preprocessDump.php', 'Preprocessor' => __DIR__ . '/includes/parser/Preprocessor.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 4f4fa86e18..6d55c83fb8 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -3237,6 +3237,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.32 + */ +$wgOOUIPreferences = false; + /** * 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/preferences/DefaultPreferencesFactory.php b/includes/preferences/DefaultPreferencesFactory.php index 3bc21f726b..d94832429e 100644 --- a/includes/preferences/DefaultPreferencesFactory.php +++ b/includes/preferences/DefaultPreferencesFactory.php @@ -42,13 +42,16 @@ use MessageLocalizer; use MWException; use MWNamespace; use MWTimestamp; +use OutputPage; use Parser; use ParserOptions; use PreferencesForm; +use PreferencesFormOOUI; use Psr\Log\LoggerAwareTrait; use Psr\Log\NullLogger; use Skin; use SpecialPage; +use SpecialPreferences; use Status; use Title; use User; @@ -127,6 +130,13 @@ class DefaultPreferencesFactory implements PreferencesFactory { public function getFormDescriptor( User $user, IContextSource $context ) { $preferences = []; + if ( SpecialPreferences::isOouiEnabled( $context ) ) { + OutputPage::setupOOUI( + strtolower( $context->getSkin()->getSkinName() ), + $context->getLanguage()->getDir() + ); + } + $canIPUseHTTPS = wfCanIPUseHTTPS( $context->getRequest()->getIP() ); $this->profilePreferences( $user, $context, $preferences, $canIPUseHTTPS ); $this->skinPreferences( $user, $context, $preferences ); @@ -254,6 +264,8 @@ class DefaultPreferencesFactory implements PreferencesFactory { protected function profilePreferences( User $user, IContextSource $context, &$defaultPreferences, $canIPUseHTTPS ) { + $oouiEnabled = SpecialPreferences::isOouiEnabled( $context ); + // retrieving user name for GENDER and misc. $userName = $user->getName(); @@ -365,13 +377,23 @@ class DefaultPreferencesFactory implements PreferencesFactory { if ( $canEditPrivateInfo && $this->authManager->allowsAuthenticationDataChange( new PasswordAuthenticationRequest(), false )->isGood() ) { - $link = $this->linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ), - $context->msg( 'prefs-resetpass' )->text(), [], - [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] ); + if ( $oouiEnabled ) { + $link = new \OOUI\ButtonWidget( [ + 'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [ + 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() + ] ), + 'label' => $context->msg( 'prefs-resetpass' )->text(), + ] ); + } else { + $link = $this->linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ), + $context->msg( 'prefs-resetpass' )->text(), [], + [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] ); + } + $defaultPreferences['password'] = [ 'type' => 'info', 'raw' => true, - 'default' => $link, + 'default' => (string)$link, 'label-message' => 'yourpassword', 'section' => 'personal/info', ]; @@ -519,16 +541,28 @@ class DefaultPreferencesFactory implements PreferencesFactory { $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : ''; if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) { - $link = $this->linkRenderer->makeLink( - SpecialPage::getTitleFor( 'ChangeEmail' ), - $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(), - [], - [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] ); - - $emailAddress .= $emailAddress == '' ? $link : ( - $context->msg( 'word-separator' )->escaped() - . $context->msg( 'parentheses' )->rawParams( $link )->escaped() - ); + if ( $oouiEnabled ) { + $link = new \OOUI\ButtonWidget( [ + 'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [ + 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() + ] ), + 'label' => + $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(), + ] ); + + $emailAddress .= $emailAddress == '' ? $link : ( '
' . $link ); + } else { + $link = $this->linkRenderer->makeLink( + SpecialPage::getTitleFor( 'ChangeEmail' ), + $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(), + [], + [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] ); + + $emailAddress .= $emailAddress == '' ? $link : ( + $context->msg( 'word-separator' )->escaped() + . $context->msg( 'parentheses' )->rawParams( $link )->escaped() + ); + } } $defaultPreferences['emailaddress'] = [ @@ -562,11 +596,19 @@ class DefaultPreferencesFactory implements PreferencesFactory { $emailauthenticationclass = 'mw-email-authenticated'; } else { $disableEmailPrefs = true; - $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '
' . - $this->linkRenderer->makeKnownLink( - SpecialPage::getTitleFor( 'Confirmemail' ), - $context->msg( 'emailconfirmlink' )->text() - ) . '
'; + if ( $oouiEnabled ) { + $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '
' . + new \OOUI\ButtonWidget( [ + 'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(), + 'label' => $context->msg( 'emailconfirmlink' )->text(), + ] ); + } else { + $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '
' . + $this->linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Confirmemail' ), + $context->msg( 'emailconfirmlink' )->text() + ) . '
'; + } $emailauthenticationclass = "mw-email-not-authenticated"; } } else { @@ -810,6 +852,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { 'default' => $tzSetting, 'size' => 20, 'section' => 'rendering/timeoffset', + 'id' => 'wpTimeCorrection', ]; } @@ -1048,28 +1091,44 @@ class DefaultPreferencesFactory implements PreferencesFactory { protected function watchlistPreferences( User $user, IContextSource $context, &$defaultPreferences ) { + $oouiEnabled = SpecialPreferences::isOouiEnabled( $context ); + $watchlistdaysMax = ceil( $this->config->get( 'RCMaxAge' ) / ( 3600 * 24 ) ); # # Watchlist ##################################### if ( $user->isAllowed( 'editmywatchlist' ) ) { - $editWatchlistLinks = []; + $editWatchlistLinks = ''; + $editWatchlistLinksOld = []; $editWatchlistModes = [ - 'edit' => [ 'EditWatchlist', false ], - 'raw' => [ 'EditWatchlist', 'raw' ], - 'clear' => [ 'EditWatchlist', 'clear' ], + 'edit' => [ 'subpage' => false, 'flags' => [] ], + 'raw' => [ 'subpage' => 'raw', 'flags' => [] ], + 'clear' => [ 'subpage' => 'clear', 'flags' => [ 'destructive' ] ], ]; - foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) { + foreach ( $editWatchlistModes as $mode => $options ) { // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear - $editWatchlistLinks[] = $this->linkRenderer->makeKnownLink( - SpecialPage::getTitleFor( $mode[0], $mode[1] ), - new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() ) - ); + if ( $oouiEnabled ) { + $editWatchlistLinks .= + new \OOUI\ButtonWidget( [ + 'href' => SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] )->getLinkURL(), + 'flags' => $options[ 'flags' ], + 'label' => new \OOUI\HtmlSnippet( + $context->msg( "prefs-editwatchlist-{$mode}" )->parse() + ), + ] ); + } else { + $editWatchlistLinksOld[] = $this->linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] ), + new HtmlArmor( $context->msg( "prefs-editwatchlist-{$mode}" )->parse() ) + ); + } } $defaultPreferences['editwatchlist'] = [ 'type' => 'info', 'raw' => true, - 'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ), + 'default' => $oouiEnabled ? + $editWatchlistLinks : + $context->getLanguage()->pipeList( $editWatchlistLinksOld ), 'label-message' => 'prefs-editwatchlist-label', 'section' => 'watchlist/editwatchlist', ]; @@ -1191,13 +1250,31 @@ class DefaultPreferencesFactory implements PreferencesFactory { $defaultPreferences['watchlisttoken'] = [ 'type' => 'api', ]; - $defaultPreferences['watchlisttoken-info'] = [ - 'type' => 'info', - 'section' => 'watchlist/tokenwatchlist', - 'label-message' => 'prefs-watchlist-token', - 'default' => $user->getTokenFromOption( 'watchlisttoken' ), - 'help-message' => 'prefs-help-watchlist-token2', - ]; + + if ( $oouiEnabled ) { + $tokenButton = new \OOUI\ButtonWidget( [ + 'href' => SpecialPage::getTitleFor( 'ResetTokens' )->getLinkURL( [ + 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() + ] ), + 'label' => $context->msg( 'prefs-watchlist-managetokens' )->text(), + ] ); + $defaultPreferences['watchlisttoken-info'] = [ + 'type' => 'info', + 'section' => 'watchlist/tokenwatchlist', + 'label-message' => 'prefs-watchlist-token', + 'help-message' => 'prefs-help-tokenmanagement', + 'raw' => true, + 'default' => (string)$tokenButton, + ]; + } else { + $defaultPreferences['watchlisttoken-info'] = [ + 'type' => 'info', + 'section' => 'watchlist/tokenwatchlist', + 'label-message' => 'prefs-watchlist-token', + 'default' => $user->getTokenFromOption( 'watchlisttoken' ), + 'help-message' => 'prefs-help-watchlist-token2', + ]; + } } /** @@ -1406,14 +1483,19 @@ class DefaultPreferencesFactory implements PreferencesFactory { * @param IContextSource $context * @param string $formClass * @param array $remove Array of items to remove - * @return PreferencesForm|HTMLForm + * @return PreferencesForm */ public function getForm( User $user, IContextSource $context, - $formClass = PreferencesForm::class, + $formClass = PreferencesFormOOUI::class, array $remove = [] ) { + if ( SpecialPreferences::isOouiEnabled( $context ) ) { + // We use ButtonWidgets in some of the getPreferences() functions + $context->getOutput()->enableOOUI(); + } + $formDescriptor = $this->getFormDescriptor( $user, $context ); if ( count( $remove ) ) { $removeKeys = array_flip( $remove ); diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index a5c24e7b1e..0f6be06ba0 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -29,8 +29,26 @@ use MediaWiki\MediaWikiServices; * @ingroup SpecialPage */ class SpecialPreferences extends SpecialPage { + /** + * @var bool Whether OOUI should be enabled here + */ + private $oouiEnabled = false; + function __construct() { parent::__construct( 'Preferences' ); + + $this->oouiEnabled = self::isOouiEnabled( $this->getContext() ); + } + + /** + * Check if OOUI mode is enabled, by config or query string + * @param IContextSource $context The context. + * @return bool + */ + public static function isOouiEnabled( IContextSource $context ) { + return $context->getRequest()->getFuzzyBool( 'ooui', + $context->getConfig()->get( 'OOUIPreferences' ) + ); } public function doesWrites() { @@ -52,8 +70,13 @@ class SpecialPreferences extends SpecialPage { return; } - $out->addModules( 'mediawiki.special.preferences' ); - $out->addModuleStyles( 'mediawiki.special.preferences.styles' ); + if ( $this->oouiEnabled ) { + $out->addModules( 'mediawiki.special.preferences.ooui' ); + $out->addModuleStyles( 'mediawiki.special.preferences.styles.ooui' ); + } else { + $out->addModules( 'mediawiki.special.preferences' ); + $out->addModuleStyles( 'mediawiki.special.preferences.styles' ); + } $session = $this->getRequest()->getSession(); if ( $session->get( 'specialPreferencesSaveSuccess' ) ) { @@ -86,35 +109,51 @@ class SpecialPreferences extends SpecialPage { $htmlForm = $this->getFormObject( $user, $this->getContext() ); $sectionTitles = $htmlForm->getPreferenceSections(); - $prefTabs = ''; - foreach ( $sectionTitles as $key ) { - $prefTabs .= Html::rawElement( 'li', - [ - 'role' => 'presentation', - 'class' => ( $key === 'personal' ) ? 'selected' : null - ], - Html::rawElement( 'a', + if ( $this->oouiEnabled ) { + $prefTabs = []; + foreach ( $sectionTitles as $key ) { + $prefTabs[] = [ + 'name' => $key, + 'label' => $htmlForm->getLegend( $key ), + ]; + } + $out->addJsConfigVars( 'wgPreferencesTabs', $prefTabs ); + + // TODO: Render fake tabs here to avoid FOUC. + // $out->addHTML( $fakeTabs ); + } else { + + $prefTabs = ''; + foreach ( $sectionTitles as $key ) { + $prefTabs .= Html::rawElement( 'li', [ - 'id' => 'preftab-' . $key, - 'role' => 'tab', - 'href' => '#mw-prefsection-' . $key, - 'aria-controls' => 'mw-prefsection-' . $key, - 'aria-selected' => ( $key === 'personal' ) ? 'true' : 'false', - 'tabIndex' => ( $key === 'personal' ) ? 0 : -1, + 'role' => 'presentation', + 'class' => ( $key === 'personal' ) ? 'selected' : null ], - $htmlForm->getLegend( $key ) - ) + Html::rawElement( 'a', + [ + 'id' => 'preftab-' . $key, + 'role' => 'tab', + 'href' => '#mw-prefsection-' . $key, + 'aria-controls' => 'mw-prefsection-' . $key, + 'aria-selected' => ( $key === 'personal' ) ? 'true' : 'false', + 'tabIndex' => ( $key === 'personal' ) ? 0 : -1, + ], + $htmlForm->getLegend( $key ) + ) + ); + } + + $out->addHTML( + Html::rawElement( 'ul', + [ + 'id' => 'preftoc', + 'role' => 'tablist' + ], + $prefTabs ) ); } - $out->addHTML( - Html::rawElement( 'ul', - [ - 'id' => 'preftoc', - 'role' => 'tablist' - ], - $prefTabs ) - ); $htmlForm->show(); } @@ -126,7 +165,11 @@ class SpecialPreferences extends SpecialPage { */ protected function getFormObject( $user, IContextSource $context ) { $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory(); - $form = $preferencesFactory->getForm( $user, $context ); + if ( $this->oouiEnabled ) { + $form = $preferencesFactory->getForm( $user, $context, PreferencesFormOOUI::class ); + } else { + $form = $preferencesFactory->getForm( $user, $context, PreferencesFormLegacy::class ); + } return $form; } @@ -139,7 +182,9 @@ class SpecialPreferences extends SpecialPage { $context = new DerivativeContext( $this->getContext() ); $context->setTitle( $this->getPageTitle( 'reset' ) ); // Reset subpage - $htmlForm = new HTMLForm( [], $context, 'prefs-restore' ); + $htmlForm = HTMLForm::factory( + $this->oouiEnabled ? 'ooui' : 'vform', [], $context, 'prefs-restore' + ); $htmlForm->setSubmitTextMsg( 'restoreprefs' ); $htmlForm->setSubmitDestructive(); diff --git a/includes/specials/forms/PreferencesForm.php b/includes/specials/forms/PreferencesForm.php index d4e5ef4fdd..a12441030a 100644 --- a/includes/specials/forms/PreferencesForm.php +++ b/includes/specials/forms/PreferencesForm.php @@ -18,126 +18,11 @@ * @file */ -use MediaWiki\MediaWikiServices; - /** - * Form to edit user preferences. + * Temporarily define PreferencesForm as an interface, so PreferencesFormOOUI + * and PreferencesFormLegacy can implement it. + * + * When PreferencesFormLegacy we can merge PreferencesFormOOUI with PreferencesForm. */ -class PreferencesForm extends HTMLForm { - // Override default value from HTMLForm - protected $mSubSectionBeforeFields = false; - - private $modifiedUser; - - /** - * @param User $user - */ - public function setModifiedUser( $user ) { - $this->modifiedUser = $user; - } - - /** - * @return User - */ - public function getModifiedUser() { - if ( $this->modifiedUser === null ) { - return $this->getUser(); - } else { - return $this->modifiedUser; - } - } - - /** - * Get extra parameters for the query string when redirecting after - * successful save. - * - * @return array - */ - public function getExtraSuccessRedirectParameters() { - return []; - } - - /** - * @param string $html - * @return string - */ - function wrapForm( $html ) { - $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html ); - - return parent::wrapForm( $html ); - } - - /** - * @return string - */ - function getButtons() { - $attrs = [ 'id' => 'mw-prefs-restoreprefs' ]; - - if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) { - return ''; - } - - $html = parent::getButtons(); - - if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) { - $t = $this->getTitle()->getSubpage( 'reset' ); - - $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); - $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(), - Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) ); - - $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html ); - } - - return $html; - } - - /** - * Separate multi-option preferences into multiple preferences, since we - * have to store them separately - * @param array $data - * @return array - */ - function filterDataForSubmit( $data ) { - foreach ( $this->mFlatFields as $fieldname => $field ) { - if ( $field instanceof HTMLNestedFilterable ) { - $info = $field->mParams; - $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; - foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) { - $data["$prefix$key"] = $value; - } - unset( $data[$fieldname] ); - } - } - - return $data; - } - - /** - * Get the whole body of the form. - * @return string - */ - function getBody() { - return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' ); - } - - /** - * Get the "" for a given section key. Normally this is the - * prefs-$key message but we'll allow extensions to override it. - * @param string $key - * @return string - */ - function getLegend( $key ) { - $legend = parent::getLegend( $key ); - Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] ); - return $legend; - } - - /** - * Get the keys of each top level preference section. - * @return array of section keys - */ - function getPreferenceSections() { - return array_keys( array_filter( $this->mFieldTree, 'is_array' ) ); - } +interface PreferencesForm { } diff --git a/includes/specials/forms/PreferencesFormLegacy.php b/includes/specials/forms/PreferencesFormLegacy.php new file mode 100644 index 0000000000..e6bc494904 --- /dev/null +++ b/includes/specials/forms/PreferencesFormLegacy.php @@ -0,0 +1,143 @@ +modifiedUser = $user; + } + + /** + * @return User + */ + public function getModifiedUser() { + if ( $this->modifiedUser === null ) { + return $this->getUser(); + } else { + return $this->modifiedUser; + } + } + + /** + * Get extra parameters for the query string when redirecting after + * successful save. + * + * @return array + */ + public function getExtraSuccessRedirectParameters() { + return []; + } + + /** + * @param string $html + * @return string + */ + function wrapForm( $html ) { + $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html ); + + return parent::wrapForm( $html ); + } + + /** + * @return string + */ + function getButtons() { + $attrs = [ 'id' => 'mw-prefs-restoreprefs' ]; + + if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) { + return ''; + } + + $html = parent::getButtons(); + + if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) { + $t = $this->getTitle()->getSubpage( 'reset' ); + + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(), + Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) ); + + $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html ); + } + + return $html; + } + + /** + * Separate multi-option preferences into multiple preferences, since we + * have to store them separately + * @param array $data + * @return array + */ + function filterDataForSubmit( $data ) { + foreach ( $this->mFlatFields as $fieldname => $field ) { + if ( $field instanceof HTMLNestedFilterable ) { + $info = $field->mParams; + $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; + foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) { + $data["$prefix$key"] = $value; + } + unset( $data[$fieldname] ); + } + } + + return $data; + } + + /** + * Get the whole body of the form. + * @return string + */ + function getBody() { + return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' ); + } + + /** + * Get the "" for a given section key. Normally this is the + * prefs-$key message but we'll allow extensions to override it. + * @param string $key + * @return string + */ + function getLegend( $key ) { + $legend = parent::getLegend( $key ); + Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] ); + return $legend; + } + + /** + * Get the keys of each top level preference section. + * @return array of section keys + */ + function getPreferenceSections() { + return array_keys( array_filter( $this->mFieldTree, 'is_array' ) ); + } +} diff --git a/includes/specials/forms/PreferencesFormOOUI.php b/includes/specials/forms/PreferencesFormOOUI.php new file mode 100644 index 0000000000..a781254352 --- /dev/null +++ b/includes/specials/forms/PreferencesFormOOUI.php @@ -0,0 +1,144 @@ +modifiedUser = $user; + } + + /** + * @return User + */ + public function getModifiedUser() { + if ( $this->modifiedUser === null ) { + return $this->getUser(); + } else { + return $this->modifiedUser; + } + } + + /** + * Get extra parameters for the query string when redirecting after + * successful save. + * + * @return array + */ + public function getExtraSuccessRedirectParameters() { + return []; + } + + /** + * @param string $html + * @return string + */ + function wrapForm( $html ) { + $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html ); + + return parent::wrapForm( $html ); + } + + /** + * @return string + */ + function getButtons() { + if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) { + return ''; + } + + $html = parent::getButtons(); + + if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) { + $t = $this->getTitle()->getSubpage( 'reset' ); + + $html .= new OOUI\ButtonWidget( [ + 'infusable' => true, + 'id' => 'mw-prefs-restoreprefs', + 'label' => $this->msg( 'restoreprefs' )->text(), + 'href' => $t->getLinkURL(), + 'flags' => [ 'destructive' ], + 'framed' => false, + ] ); + + $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html ); + } + + return $html; + } + + /** + * Separate multi-option preferences into multiple preferences, since we + * have to store them separately + * @param array $data + * @return array + */ + function filterDataForSubmit( $data ) { + foreach ( $this->mFlatFields as $fieldname => $field ) { + if ( $field instanceof HTMLNestedFilterable ) { + $info = $field->mParams; + $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; + foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) { + $data["$prefix$key"] = $value; + } + unset( $data[$fieldname] ); + } + } + + return $data; + } + + /** + * Get the whole body of the form. + * @return string + */ + function getBody() { + return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' ); + } + + /** + * Get the "" for a given section key. Normally this is the + * prefs-$key message but we'll allow extensions to override it. + * @param string $key + * @return string + */ + function getLegend( $key ) { + $legend = parent::getLegend( $key ); + Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] ); + return $legend; + } + + /** + * Get the keys of each top level preference section. + * @return array of section keys + */ + function getPreferenceSections() { + return array_keys( array_filter( $this->mFieldTree, 'is_array' ) ); + } +} diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 741738aad5..75ebdc179f 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -1040,6 +1040,7 @@ "prefs-watchlist-edits": "Maximum number of changes to show in watchlist:", "prefs-watchlist-edits-max": "Maximum number: 1000", "prefs-watchlist-token": "Watchlist token:", + "prefs-watchlist-managetokens": "Manage tokens", "prefs-misc": "Misc", "prefs-resetpass": "Change password", "prefs-changeemail": "Change or remove email address", @@ -1058,6 +1059,7 @@ "recentchangescount": "Number of edits to show in recent changes, page histories, and in logs, by default:", "prefs-help-recentchangescount": "Maximum number: 1000", "prefs-help-watchlist-token2": "This is the secret key to the web feed of your watchlist.\nAnyone who knows it will be able to read your watchlist, so do not share it.\nIf you need to, [[Special:ResetTokens|you can reset it]].", + "prefs-help-tokenmanagement": "You can see and reset the secret key for your account that can access the Web feed of your watchlist. Anyone who knows the key will be able to read your watchlist, so do not share it.", "savedprefs": "Your preferences have been saved.", "savedrights": "The user groups of {{GENDER:$1|$1}} have been saved.", "timezonelegend": "Time zone:", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 29869a146d..a82e259424 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1237,6 +1237,7 @@ "prefs-watchlist-edits": "Used in [[Special:Preferences]], tab \"Watchlist\".", "prefs-watchlist-edits-max": "Shown as hint in [[Special:Preferences]], tab \"Watchlist\"", "prefs-watchlist-token": "Used in [[Special:Preferences]], tab Watchlist.", + "prefs-watchlist-managetokens": "Label for the button to see and reset the user's private tokens", "prefs-misc": "Tab used on the [[Special:Preferences|user preferences]] special page.", "prefs-resetpass": "Button on user data tab in user preferences. When you click the button you go to the special page [[Special:ResetPass]].\n\n{{Identical|Change password}}", "prefs-changeemail": "Link on [[Special:Preferences]] to [[Special:ChangeEmail]]. [[Special:ChangeEmail]] also allows removing email address. \n\nSee also:\n* {{msg-mw|prefs-help-email-required|help}}\n* {{msg-mw|prefs-help-email|help}}\n* {{msg-mw|prefs-help-email-others|help}}\n* {{msg-mw|prefs-setemail|link title}}", @@ -1255,6 +1256,7 @@ "recentchangescount": "Used in [[Special:Preferences]], tab \"Recent changes\".", "prefs-help-recentchangescount": "Used in [[Special:Preferences]], tab \"Recent changes\".", "prefs-help-watchlist-token2": "Used in [[Special:Preferences]], tab Watchlist. (Formerly in {{msg-mw|prefs-help-watchlist-token}}.)", + "prefs-help-tokenmanagement": "Used in [[Special:Preferences]], Watchlist tab.", "savedprefs": "This message appears after saving changes to your user preferences.", "savedrights": "This message appears after saving the user groups on [[Special:UserRights]].\n* $1 - The user name of the user which groups was saved.", "timezonelegend": "{{Identical|Time zone}}", diff --git a/resources/Resources.php b/resources/Resources.php index d3e1b6557b..873df4c3ce 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -2150,7 +2150,7 @@ return [ 'scripts' => [ 'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js', 'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js', + 'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js', 'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js', 'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js', ], @@ -2167,6 +2167,35 @@ return [ ], ], 'mediawiki.special.preferences.styles' => [ + 'targets' => [ 'desktop', 'mobile' ], + 'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css', + ], + 'mediawiki.special.preferences.ooui' => [ + 'targets' => [ 'desktop', 'mobile' ], + 'scripts' => [ + 'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js', + 'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js', + 'resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js', + 'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js', + 'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js', + 'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js', + ], + 'messages' => [ + 'prefs-tabs-navigation-hint', + 'prefswarning-warning', + 'saveprefs', + 'savedprefs', + ], + 'dependencies' => [ + 'mediawiki.language', + 'mediawiki.confirmCloseWindow', + 'mediawiki.notification.convertmessagebox', + 'oojs-ui-widgets', + 'mediawiki.widgets.SelectWithInputWidget', + 'mediawiki.editfont.styles', + ], + ], + 'mediawiki.special.preferences.styles.ooui' => [ 'targets' => [ 'desktop', 'mobile' ], 'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css', ], diff --git a/resources/src/mediawiki.legacy/oldshared.css b/resources/src/mediawiki.legacy/oldshared.css index 2572b5200d..b95a4363f0 100644 --- a/resources/src/mediawiki.legacy/oldshared.css +++ b/resources/src/mediawiki.legacy/oldshared.css @@ -215,6 +215,7 @@ table.toc td { } /* preference page with js-genrated toc */ +/* TODO: Delete #preftoc when Special:Preference's non-OOUI mode is disabled */ #preftoc { float: left; margin: 1em 1em 1em 1em; diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js b/resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js index 1476241e21..244154b38a 100644 --- a/resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js +++ b/resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js @@ -4,9 +4,12 @@ */ ( function ( mw, $ ) { $( function () { - var allowCloseWindow; + var allowCloseWindow, saveButton, restoreButton, + oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' ); - // Check if all of the form values are unchanged + // Check if all of the form values are unchanged. + // (This function could be changed to infuse and check OOUI widgets, but that would only make it + // slower and more complicated. It works fine to treat them as HTML elements.) function isPrefsChanged() { var inputs = $( '#mw-prefs-form :input[name]' ), input, $input, inputType, @@ -42,12 +45,24 @@ return false; } - // Disable the button to save preferences unless preferences have changed - // Check if preferences have been changed before JS has finished loading - $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() ); - $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () { + if ( oouiEnabled ) { + saveButton = OO.ui.infuse( $( '#prefcontrol' ) ); + restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) ); + + // Disable the button to save preferences unless preferences have changed + // Check if preferences have been changed before JS has finished loading + saveButton.setDisabled( !isPrefsChanged() ); + $( '#preferences .oo-ui-fieldsetLayout' ).on( 'change keyup mouseup', function () { + saveButton.setDisabled( !isPrefsChanged() ); + } ); + } else { + // Disable the button to save preferences unless preferences have changed + // Check if preferences have been changed before JS has finished loading $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() ); - } ); + $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () { + $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() ); + } ); + } // Set up a message to notify users if they try to leave the page without // saving. @@ -56,7 +71,16 @@ message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ), namespace: 'prefswarning' } ); - $( '#mw-prefs-form' ).submit( $.proxy( allowCloseWindow, 'release' ) ); - $( '#mw-prefs-restoreprefs' ).click( $.proxy( allowCloseWindow, 'release' ) ); + $( '#mw-prefs-form' ).on( 'submit', $.proxy( allowCloseWindow, 'release' ) ); + if ( oouiEnabled ) { + restoreButton.on( 'click', function () { + allowCloseWindow.release(); + // The default behavior of events in OOUI is always prevented. Follow the link manually. + // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event. + location.href = restoreButton.getHref(); + } ); + } else { + $( '#mw-prefs-restoreprefs' ).on( 'click', $.proxy( allowCloseWindow, 'release' ) ); + } } ); }( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js b/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js new file mode 100644 index 0000000000..fe48886027 --- /dev/null +++ b/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js @@ -0,0 +1,32 @@ +/*! + * JavaScript for Special:Preferences: editfont field enhancements. + */ +( function ( mw, $ ) { + $( function () { + var widget, lastValue; + + try { + widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) ); + } catch ( err ) { + // This preference could theoretically be disabled ($wgHiddenPrefs) + return; + } + + // Style options + widget.dropdownWidget.menu.items.forEach( function ( item ) { + item.$label.addClass( 'mw-editfont-' + item.getData() ); + } ); + + function updateLabel( value ) { + // Style selected item label + widget.dropdownWidget.$label + .removeClass( 'mw-editfont-' + lastValue ) + .addClass( 'mw-editfont-' + value ); + lastValue = value; + } + + widget.on( 'change', updateLabel ); + updateLabel( widget.getValue() ); + + } ); +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.styles.css b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.css index 33b630a948..2310377a62 100644 --- a/resources/src/mediawiki.special/mediawiki.special.preferences.styles.css +++ b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.css @@ -1,27 +1,29 @@ /* Reuses colors from mediawiki.legacy/shared.css */ -.mw-email-not-authenticated .mw-input, -.mw-email-none .mw-input { +.mw-email-not-authenticated .oo-ui-labelWidget, +.mw-email-none .oo-ui-labelWidget { border: 1px solid #fde29b; background-color: #fdf1d1; color: #000; + padding: 0.5em; } /* Authenticated email field has its own class too. Unstyled by default */ /* -.mw-email-authenticated .mw-input { } +.mw-email-authenticated .oo-ui-labelWidget { } */ -/* This breaks due to nolabel styling */ -#preferences > fieldset td.mw-label { - width: 20%; -} -#preferences > fieldset table { - width: 100%; +/* This is needed because add extra buttons in a weird way */ +.mw-prefs-buttons .mw-htmlform-submit-buttons { + margin: 0; + display: inline; } -#preferences > fieldset table.mw-htmlform-matrix { - width: auto; + +.mw-prefs-buttons { + margin-top: 1em; } -/* The CSS below is also for JS enabled version, because we want to prevent FOUC */ +#prefcontrol { + margin-right: 0.5em; +} /* * Hide, but keep accessible for screen-readers. @@ -33,15 +35,78 @@ zoom: 1; } -.client-nojs #preftoc { - display: none; +/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped, + * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be + * better solved by setting overlays for the widgets, but we can't do it from PHP... */ +#preferences .oo-ui-panelLayout { + position: static; + overflow: visible; + -webkit-transform: none; + transform: none; +} + +#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed { + border-color: #c8ccd1; + border-width: 1px 0 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; + box-shadow: none; +} + +/* Tweak the margins to reduce the shifting of form contents + * after JS code loads and rearranges the page */ +.client-js #preferences > .oo-ui-panelLayout { + margin: 1em 0; +} + +.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed { + margin-left: 0.25em; +} + +.client-js #preferences .oo-ui-tabPanelLayout { + padding-top: 0.5em; + padding-bottom: 0.5em; } -.client-js #preferences > fieldset { - display: none; +.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed { + margin-left: 0; + margin-bottom: 0; + border: 0; + padding-top: 0; } -/* Only the 1st tab is shown by default in JS mode */ -.client-js #preferences #mw-prefsection-personal { +.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header { + margin-bottom: 1em; +} + +/* Make the "Basic information" section more compact */ +/* OOUI's `align: 'left'` for FieldLayouts sucks, so we do our own */ +#mw-htmlform-info > .oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header { + width: 20%; + display: inline-block; + vertical-align: middle; + padding: 0; +} + +#mw-htmlform-info > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help { + margin-right: 0; +} + +#mw-htmlform-info > .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field { + width: 80%; + display: inline-block; + vertical-align: middle; +} + +/* Expand the dropdown and textfield of "Time zone" field to the */ +/* usual maximum width and display them on separate lines. */ +#wpTimeCorrection .oo-ui-dropdownInputWidget, +#wpTimeCorrection .oo-ui-textInputWidget { display: block; + max-width: 50em; +} + +#wpTimeCorrection .oo-ui-textInputWidget { + margin-top: 0.5em; } diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css new file mode 100644 index 0000000000..33b630a948 --- /dev/null +++ b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css @@ -0,0 +1,47 @@ +/* Reuses colors from mediawiki.legacy/shared.css */ +.mw-email-not-authenticated .mw-input, +.mw-email-none .mw-input { + border: 1px solid #fde29b; + background-color: #fdf1d1; + color: #000; +} +/* Authenticated email field has its own class too. Unstyled by default */ +/* +.mw-email-authenticated .mw-input { } +*/ +/* This breaks due to nolabel styling */ +#preferences > fieldset td.mw-label { + width: 20%; +} + +#preferences > fieldset table { + width: 100%; +} +#preferences > fieldset table.mw-htmlform-matrix { + width: auto; +} + +/* The CSS below is also for JS enabled version, because we want to prevent FOUC */ + +/* + * Hide, but keep accessible for screen-readers. + * Like .mw-jump, #jump-to-nav from shared.css + */ +.client-js .mw-navigation-hint { + overflow: hidden; + height: 0; + zoom: 1; +} + +.client-nojs #preftoc { + display: none; +} + +.client-js #preferences > fieldset { + display: none; +} + +/* Only the 1st tab is shown by default in JS mode */ +.client-js #preferences #mw-prefsection-personal { + display: block; +} diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js index 0d97d68e84..c948ff09f2 100644 --- a/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js +++ b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js @@ -3,29 +3,10 @@ */ ( function ( mw, $ ) { $( function () { - var $preftoc, $preferences, $fieldsets, labelFunc, previousTab; + var $preferences, tabs, wrapper, previousTab; - labelFunc = function () { - return this.id.replace( /^mw-prefsection/g, 'preftab' ); - }; - - $preftoc = $( '#preftoc' ); $preferences = $( '#preferences' ); - $fieldsets = $preferences.children( 'fieldset' ) - .attr( { - role: 'tabpanel', - 'aria-labelledby': labelFunc - } ); - $fieldsets.not( '#mw-prefsection-personal' ) - .hide() - .attr( 'aria-hidden', 'true' ); - - // T115692: The following is kept for backwards compatibility with older skins - $preferences.addClass( 'jsprefs' ); - $fieldsets.addClass( 'prefsection' ); - $fieldsets.children( 'legend' ).addClass( 'mainLegend' ); - // Make sure the accessibility tip is selectable so that screen reader users take notice, // but hide it per default to reduce interface clutter. Also make sure it becomes visible // when selected. Similar to jquery.mw-jump @@ -38,62 +19,76 @@ } else { $( this ).css( 'height', 'auto' ); } - } ).insertBefore( $preftoc ); + } ).prependTo( '#mw-content-text' ); - /** - * It uses document.getElementById for security reasons (HTML injections in $()). - * - * @ignore - * @param {string} name the name of a tab without the prefix ("mw-prefsection-") - * @param {string} [mode] A hash will be set according to the current - * open section. Set mode 'noHash' to surpress this. - */ - function switchPrefTab( name, mode ) { - var $tab, scrollTop; + tabs = new OO.ui.IndexLayout( { + expanded: false, + // Do not remove focus from the tabs menu after choosing a tab + autoFocus: false + } ); + + mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) { + var panel, $panelContents; + + panel = new OO.ui.TabPanelLayout( tabConfig.name, { + expanded: false, + label: tabConfig.label + } ); + $panelContents = $( '#mw-prefsection-' + tabConfig.name ); + + // Hide the unnecessary PHP PanelLayouts + // (Do not use .remove(), as that would remove event handlers for everything inside them) + $panelContents.parent().detach(); + + panel.$element.append( $panelContents ); + tabs.addTabPanels( [ panel ] ); + + // Remove duplicate labels + // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet) + $panelContents.children( 'legend' ).remove(); + $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() ); + } ); + + wrapper = new OO.ui.PanelLayout( { + expanded: false, + padded: false, + framed: true + } ); + wrapper.$element.append( tabs.$element ); + $preferences.prepend( wrapper.$element ); + + function updateHash( panel ) { + var scrollTop, active; // Handle hash manually to prevent jumping, // therefore save and restore scrollTop to prevent jumping. scrollTop = $( window ).scrollTop(); - if ( mode !== 'noHash' ) { - location.hash = '#mw-prefsection-' + name; + // Changing the hash apparently causes keyboard focus to be lost? + // Save and restore it. This makes no sense though. + active = document.activeElement; + location.hash = '#mw-prefsection-' + panel.getName(); + if ( active ) { + active.focus(); } $( window ).scrollTop( scrollTop ); - - $preftoc.find( 'li' ).removeClass( 'selected' ) - .find( 'a' ).attr( { - tabIndex: -1, - 'aria-selected': 'false' - } ); - - $tab = $( document.getElementById( 'preftab-' + name ) ); - if ( $tab.length ) { - $tab.attr( { - tabIndex: 0, - 'aria-selected': 'true' - } ).focus() - .parent().addClass( 'selected' ); - - $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' ); - $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' ); - } } - // Enable keyboard users to use left and right keys to switch tabs - $preftoc.on( 'keydown', function ( event ) { - var keyLeft = 37, - keyRight = 39, - $el; - - if ( event.keyCode === keyLeft ) { - $el = $( '#preftoc li.selected' ).prev().find( 'a' ); - } else if ( event.keyCode === keyRight ) { - $el = $( '#preftoc li.selected' ).next().find( 'a' ); - } else { - return; + tabs.on( 'set', updateHash ); + + /** + * @ignore + * @param {string} name the name of a tab without the prefix ("mw-prefsection-") + * @param {string} [mode] A hash will be set according to the current + * open section. Set mode 'noHash' to supress this. + */ + function switchPrefTab( name, mode ) { + if ( mode === 'noHash' ) { + tabs.off( 'set', updateHash ); } - if ( $el.length > 0 ) { - switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) ); + tabs.setTabPanel( name ); + if ( mode === 'noHash' ) { + tabs.on( 'set', updateHash ); } - } ); + } // Jump to correct section as indicated by the hash. // This function is called onload and onhashchange. @@ -135,7 +130,7 @@ } $( '#mw-prefs-form' ).on( 'submit', function () { - var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' ); + var value = tabs.getCurrentTabPanelName(); mw.storage.session.set( 'mwpreferences-prevTab', value ); } ); diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js new file mode 100644 index 0000000000..0d97d68e84 --- /dev/null +++ b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js @@ -0,0 +1,143 @@ +/*! + * JavaScript for Special:Preferences: Tab navigation. + */ +( function ( mw, $ ) { + $( function () { + var $preftoc, $preferences, $fieldsets, labelFunc, previousTab; + + labelFunc = function () { + return this.id.replace( /^mw-prefsection/g, 'preftab' ); + }; + + $preftoc = $( '#preftoc' ); + $preferences = $( '#preferences' ); + + $fieldsets = $preferences.children( 'fieldset' ) + .attr( { + role: 'tabpanel', + 'aria-labelledby': labelFunc + } ); + $fieldsets.not( '#mw-prefsection-personal' ) + .hide() + .attr( 'aria-hidden', 'true' ); + + // T115692: The following is kept for backwards compatibility with older skins + $preferences.addClass( 'jsprefs' ); + $fieldsets.addClass( 'prefsection' ); + $fieldsets.children( 'legend' ).addClass( 'mainLegend' ); + + // Make sure the accessibility tip is selectable so that screen reader users take notice, + // but hide it per default to reduce interface clutter. Also make sure it becomes visible + // when selected. Similar to jquery.mw-jump + $( '
' ).addClass( 'mw-navigation-hint' ) + .text( mw.msg( 'prefs-tabs-navigation-hint' ) ) + .attr( 'tabIndex', 0 ) + .on( 'focus blur', function ( e ) { + if ( e.type === 'blur' || e.type === 'focusout' ) { + $( this ).css( 'height', '0' ); + } else { + $( this ).css( 'height', 'auto' ); + } + } ).insertBefore( $preftoc ); + + /** + * It uses document.getElementById for security reasons (HTML injections in $()). + * + * @ignore + * @param {string} name the name of a tab without the prefix ("mw-prefsection-") + * @param {string} [mode] A hash will be set according to the current + * open section. Set mode 'noHash' to surpress this. + */ + function switchPrefTab( name, mode ) { + var $tab, scrollTop; + // Handle hash manually to prevent jumping, + // therefore save and restore scrollTop to prevent jumping. + scrollTop = $( window ).scrollTop(); + if ( mode !== 'noHash' ) { + location.hash = '#mw-prefsection-' + name; + } + $( window ).scrollTop( scrollTop ); + + $preftoc.find( 'li' ).removeClass( 'selected' ) + .find( 'a' ).attr( { + tabIndex: -1, + 'aria-selected': 'false' + } ); + + $tab = $( document.getElementById( 'preftab-' + name ) ); + if ( $tab.length ) { + $tab.attr( { + tabIndex: 0, + 'aria-selected': 'true' + } ).focus() + .parent().addClass( 'selected' ); + + $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' ); + $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' ); + } + } + + // Enable keyboard users to use left and right keys to switch tabs + $preftoc.on( 'keydown', function ( event ) { + var keyLeft = 37, + keyRight = 39, + $el; + + if ( event.keyCode === keyLeft ) { + $el = $( '#preftoc li.selected' ).prev().find( 'a' ); + } else if ( event.keyCode === keyRight ) { + $el = $( '#preftoc li.selected' ).next().find( 'a' ); + } else { + return; + } + if ( $el.length > 0 ) { + switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) ); + } + } ); + + // Jump to correct section as indicated by the hash. + // This function is called onload and onhashchange. + function detectHash() { + var hash = location.hash, + matchedElement, parentSection; + if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) { + mw.storage.session.remove( 'mwpreferences-prevTab' ); + switchPrefTab( hash.replace( '#mw-prefsection-', '' ) ); + } else if ( hash.match( /^#mw-[\w-]+$/ ) ) { + matchedElement = document.getElementById( hash.slice( 1 ) ); + parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' ); + if ( parentSection.length ) { + mw.storage.session.remove( 'mwpreferences-prevTab' ); + // Switch to proper tab and scroll to selected item. + switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' ); + matchedElement.scrollIntoView(); + } + } + } + + $( window ).on( 'hashchange', function () { + var hash = location.hash; + if ( hash.match( /^#mw-[\w-]+/ ) ) { + detectHash(); + } else if ( hash === '' ) { + switchPrefTab( 'personal', 'noHash' ); + } + } ) + // Run the function immediately to select the proper tab on startup. + .trigger( 'hashchange' ); + + // Restore the active tab after saving the preferences + previousTab = mw.storage.session.get( 'mwpreferences-prevTab' ); + if ( previousTab ) { + switchPrefTab( previousTab, 'noHash' ); + // Deleting the key, the tab states should be reset until we press Save + mw.storage.session.remove( 'mwpreferences-prevTab' ); + } + + $( '#mw-prefs-form' ).on( 'submit', function () { + var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' ); + mw.storage.session.set( 'mwpreferences-prevTab', value ); + } ); + + } ); +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js b/resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js index 03656eedea..a6ffae9739 100644 --- a/resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js +++ b/resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js @@ -3,14 +3,25 @@ */ ( function ( mw, $ ) { $( function () { - var - $tzSelect, $tzTextbox, $localtimeHolder, servertime; + var $tzSelect, $tzTextbox, timezoneWidget, $localtimeHolder, servertime, + oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' ); // Timezone functions. // Guesses Timezone from browser and updates fields onchange. - $tzSelect = $( '#mw-input-wptimecorrection' ); - $tzTextbox = $( '#mw-input-wptimecorrection-other' ); + if ( oouiEnabled ) { + // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known. + try { + timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) ); + } catch ( err ) { + // This preference could theoretically be disabled ($wgHiddenPrefs) + timezoneWidget = null; + } + } else { + $tzSelect = $( '#mw-input-wptimecorrection' ); + $tzTextbox = $( '#mw-input-wptimecorrection-other' ); + } + $localtimeHolder = $( '#wpLocalTime' ); servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 ); @@ -48,21 +59,27 @@ function updateTimezoneSelection() { var minuteDiff, localTime, - type = $tzSelect.val(); + type = oouiEnabled ? timezoneWidget.dropdowninput.getValue() : $tzSelect.val(), + val = oouiEnabled ? timezoneWidget.textinput.getValue() : $tzTextbox.val(); if ( type === 'other' ) { // User specified time zone manually in // Grab data from the textbox, parse it. - minuteDiff = hoursToMinutes( $tzTextbox.val() ); + minuteDiff = hoursToMinutes( val ); } else { // Time zone not manually specified by user if ( type === 'guess' ) { // Get browser timezone & fill it in minuteDiff = -( new Date().getTimezoneOffset() ); - $tzTextbox.val( minutesToHours( minuteDiff ) ); - $tzSelect.val( 'other' ); + if ( oouiEnabled ) { + timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) ); + timezoneWidget.dropdowninput.setValue( 'other' ); + } else { + $tzTextbox.val( minutesToHours( minuteDiff ) ); + $tzSelect.val( 'other' ); + } } else { - // Grab data from the $tzSelect value + // Grab data from the dropdown value minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0; } } @@ -76,10 +93,18 @@ $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) ); } - if ( $tzSelect.length && $tzTextbox.length ) { - $tzSelect.change( updateTimezoneSelection ); - $tzTextbox.blur( updateTimezoneSelection ); - updateTimezoneSelection(); + if ( oouiEnabled ) { + if ( timezoneWidget ) { + timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection ); + timezoneWidget.textinput.on( 'change', updateTimezoneSelection ); + updateTimezoneSelection(); + } + } else { + if ( $tzSelect.length && $tzTextbox.length ) { + $tzSelect.change( updateTimezoneSelection ); + $tzTextbox.blur( updateTimezoneSelection ); + updateTimezoneSelection(); + } } } ); diff --git a/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php b/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php index c10152344b..68a0453627 100644 --- a/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php +++ b/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php @@ -117,7 +117,7 @@ class DefaultPreferencesFactoryTest extends MediaWikiTestCase { $configMock = new HashConfig( [ 'HiddenPrefs' => [] ] ); - $form = $this->getMockBuilder( PreferencesForm::class ) + $form = $this->getMockBuilder( PreferencesFormLegacy::class ) ->disableOriginalConstructor() ->getMock(); -- 2.20.1