Merge "Create an HTMLForm field for selecting a timezone"

This commit is contained in:
jenkins-bot 2022-09-23 18:16:06 +00:00 committed by Gerrit Code Review
commit 1e60c7337a
9 changed files with 241 additions and 161 deletions

View file

@ -601,6 +601,7 @@ $wgAutoloadLocalClasses = [
'HTMLTextAreaField' => __DIR__ . '/includes/htmlform/fields/HTMLTextAreaField.php',
'HTMLTextField' => __DIR__ . '/includes/htmlform/fields/HTMLTextField.php',
'HTMLTextFieldWithButton' => __DIR__ . '/includes/htmlform/fields/HTMLTextFieldWithButton.php',
'HTMLTimezoneField' => __DIR__ . '/includes/htmlform/fields/HTMLTimezoneField.php',
'HTMLTitleTextField' => __DIR__ . '/includes/htmlform/fields/HTMLTitleTextField.php',
'HTMLTitlesMultiselectField' => __DIR__ . '/includes/htmlform/fields/HTMLTitlesMultiselectField.php',
'HTMLUserTextField' => __DIR__ . '/includes/htmlform/fields/HTMLUserTextField.php',

View file

@ -184,6 +184,7 @@ class HTMLForm extends ContextSource {
'time' => HTMLDateTimeField::class,
'datetime' => HTMLDateTimeField::class,
'expiry' => HTMLExpiryField::class,
'timezone' => HTMLTimezoneField::class,
// HTMLTextField will output the correct type="" attribute automagically.
// There are about four zillion other HTML5 input types, like range, but
// we don't use those at the moment, so no point in adding all of them.

View file

@ -63,10 +63,10 @@ class HTMLSelectOrOtherField extends HTMLTextField {
$wrapperAttribs = [
'id' => $this->mID,
'class' => self::FIELD_CLASS
'class' => $this->getFieldClasses()
];
if ( $this->mClass !== '' ) {
$wrapperAttribs['class'] .= ' ' . $this->mClass;
$wrapperAttribs['class'][] = $this->mClass;
}
return Html::rawElement(
'div',
@ -139,7 +139,7 @@ class HTMLSelectOrOtherField extends HTMLTextField {
$disabled = true;
}
$inputClasses = [ self::FIELD_CLASS ];
$inputClasses = $this->getFieldClasses();
if ( $this->mClass !== '' ) {
$inputClasses = array_merge( $inputClasses, explode( ' ', $this->mClass ) );
}
@ -176,4 +176,16 @@ class HTMLSelectOrOtherField extends HTMLTextField {
return $this->getDefault();
}
}
/**
* Returns a list of classes that should be applied to the widget itself. Unfortunately, we can't use
* $this->mClass or the 'cssclass' config option, because they're also added to the outer field wrapper
* (which includes the label). This method exists a temporary workaround until HTMLFormField will have
* a stable way for subclasses to specify additional classes for the widget itself.
* @internal Should only be used in HTMLTimezoneField
* @return string[]
*/
protected function getFieldClasses(): array {
return [ self::FIELD_CLASS ];
}
}

View file

@ -0,0 +1,151 @@
<?php
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use Wikimedia\Message\ITextFormatter;
use Wikimedia\Message\MessageValue;
/**
* Dropdown widget that allows the user to select a timezone, either by choosing a geographic zone, by using the wiki
* default, or by manually specifying an offset. It also has an option to fill the value from the browser settings.
* The value of this field is in a format accepted by UserTimeCorrection.
*/
class HTMLTimezoneField extends HTMLSelectOrOtherField {
private const FIELD_CLASS = 'mw-htmlform-timezone-field';
/** @var ITextFormatter */
private $msgFormatter;
/**
* @stable to call
* @inheritDoc
* Note that no options should be specified.
*/
public function __construct( $params ) {
if ( isset( $params['options'] ) ) {
throw new InvalidArgumentException( "Options should not be provided to " . __CLASS__ );
}
$params['placeholder-message'] = $params['placeholder-message'] ?? 'timezone-useoffset-placeholder';
$params['options'] = [];
parent::__construct( $params );
$lang = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage();
$langCode = $lang->getCode();
$this->msgFormatter = MediaWikiServices::getInstance()->getMessageFormatterFactory()
->getTextFormatter( $langCode );
$this->mOptions = $this->getTimezoneOptions();
}
/**
* @return array<string|string[]>
*/
private function getTimezoneOptions(): array {
$opt = [];
$localTZoffset = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LocalTZoffset );
$timeZoneList = $this->getTimeZoneList();
$timestamp = MWTimestamp::getLocalInstance();
// Check that the LocalTZoffset is the same as the local time zone offset
if ( $localTZoffset === (int)$timestamp->format( 'Z' ) / 60 ) {
$timezoneName = $timestamp->getTimezone()->getName();
// Localize timezone
if ( isset( $timeZoneList[$timezoneName] ) ) {
$timezoneName = $timeZoneList[$timezoneName]['name'];
}
$server_tz_msg = $this->msgFormatter->format(
MessageValue::new( 'timezoneuseserverdefault', [ $timezoneName ] )
);
} else {
$tzstring = sprintf(
'%+03d:%02d',
floor( $localTZoffset / 60 ),
abs( $localTZoffset ) % 60
);
$server_tz_msg = $this->msgFormatter->format(
MessageValue::new( 'timezoneuseserverdefault', [ $tzstring ] )
);
}
$opt[$server_tz_msg] = "System|$localTZoffset";
$opt[$this->msgFormatter->format( MessageValue::new( 'timezoneuseoffset' ) )] = 'other';
$opt[$this->msgFormatter->format( MessageValue::new( 'guesstimezone' ) )] = 'guess';
foreach ( $timeZoneList as $timeZoneInfo ) {
$region = $timeZoneInfo['region'];
if ( !isset( $opt[$region] ) ) {
$opt[$region] = [];
}
$opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
}
return $opt;
}
/**
* Get a list of all time zones
* @return string[][] A list of all time zones. The system name of the time zone is used as key and
* the value is an array which contains localized name, the timecorrection value used for
* preferences and the region
*/
private function getTimeZoneList(): array {
$identifiers = DateTimeZone::listIdentifiers();
// @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3162
if ( $identifiers === false ) {
return [];
}
sort( $identifiers );
$tzRegions = [
'Africa' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-africa' ) ),
'America' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-america' ) ),
'Antarctica' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-antarctica' ) ),
'Arctic' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-arctic' ) ),
'Asia' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-asia' ) ),
'Atlantic' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-atlantic' ) ),
'Australia' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-australia' ) ),
'Europe' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-europe' ) ),
'Indian' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-indian' ) ),
'Pacific' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-pacific' ) ),
];
asort( $tzRegions );
$timeZoneList = [];
$now = new DateTime();
foreach ( $identifiers as $identifier ) {
$parts = explode( '/', $identifier, 2 );
// DateTimeZone::listIdentifiers() returns a number of
// backwards-compatibility entries. This filters them out of the
// list presented to the user.
if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
continue;
}
// Localize region
$parts[0] = $tzRegions[$parts[0]];
$dateTimeZone = new DateTimeZone( $identifier );
$minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
$display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
$value = "ZoneInfo|$minDiff|$identifier";
$timeZoneList[$identifier] = [
'name' => $display,
'timecorrection' => $value,
'region' => $parts[0],
];
}
return $timeZoneList;
}
/**
* @inheritDoc
*/
protected function getFieldClasses(): array {
$classes = parent::getFieldClasses();
$classes[] = self::FIELD_CLASS;
return $classes;
}
}

View file

@ -20,11 +20,10 @@
namespace MediaWiki\Preferences;
use DateTime;
use DateTimeZone;
use Html;
use HTMLForm;
use HTMLFormField;
use HTMLTimezoneField;
use IContextSource;
use ILanguageConverter;
use Language;
@ -48,7 +47,6 @@ use MediaWiki\User\UserTimeCorrection;
use Message;
use MessageLocalizer;
use MWException;
use MWTimestamp;
use NamespaceInfo;
use OutputPage;
use Parser;
@ -63,8 +61,6 @@ use Title;
use UnexpectedValueException;
use User;
use UserGroupMembership;
use Wikimedia\Message\ITextFormatter;
use Wikimedia\Message\MessageValue;
use Xml;
/**
@ -1041,20 +1037,14 @@ class DefaultPreferencesFactory implements PreferencesFactory {
$tzDefault = $userTimeCorrectionObj->toString();
}
$msgFormatter = MediaWikiServices::getInstance()->getMessageFormatterFactory()
->getTextFormatter( $context->getLanguage()->getCode() );
$tzOptions = $this->getTimezoneOptions( $msgFormatter );
$defaultPreferences['timecorrection'] = [
'class' => \HTMLSelectOrOtherField::class,
'class' => HTMLTimezoneField::class,
'label-message' => 'timezonelegend',
'options' => $tzOptions,
'default' => $tzDefault,
'size' => 20,
'section' => 'rendering/timeoffset',
'id' => 'wpTimeCorrection',
'filter' => TimezoneFilter::class,
'placeholder-message' => 'timezone-useoffset-placeholder',
];
}
@ -1836,49 +1826,6 @@ class DefaultPreferencesFactory implements PreferencesFactory {
return $htmlForm;
}
/**
* @param ITextFormatter $msgFormatter
* @return array
*/
protected function getTimezoneOptions( ITextFormatter $msgFormatter ) {
$opt = [];
$localTZoffset = $this->options->get( MainConfigNames::LocalTZoffset );
$timeZoneList = $this->getTimeZoneList( $msgFormatter );
$timestamp = MWTimestamp::getLocalInstance();
// Check that the LocalTZoffset is the same as the local time zone offset
if ( $localTZoffset === (int)$timestamp->format( 'Z' ) / 60 ) {
$timezoneName = $timestamp->getTimezone()->getName();
// Localize timezone
if ( isset( $timeZoneList[$timezoneName] ) ) {
$timezoneName = $timeZoneList[$timezoneName]['name'];
}
$server_tz_msg = $msgFormatter->format(
MessageValue::new( 'timezoneuseserverdefault', [ $timezoneName ] )
);
} else {
$tzstring = sprintf(
'%+03d:%02d',
floor( $localTZoffset / 60 ),
abs( $localTZoffset ) % 60
);
$server_tz_msg = $msgFormatter->format( MessageValue::new( 'timezoneuseserverdefault', [ $tzstring ] ) );
}
$opt[$server_tz_msg] = "System|$localTZoffset";
$opt[$msgFormatter->format( MessageValue::new( 'timezoneuseoffset' ) )] = 'other';
$opt[$msgFormatter->format( MessageValue::new( 'guesstimezone' ) )] = 'guess';
foreach ( $timeZoneList as $timeZoneInfo ) {
$region = $timeZoneInfo['region'];
if ( !isset( $opt[$region] ) ) {
$opt[$region] = [];
}
$opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
}
return $opt;
}
/**
* Handle the form submission if everything validated properly
*
@ -2011,67 +1958,4 @@ class DefaultPreferencesFactory implements PreferencesFactory {
return ( $res === true ? Status::newGood() : $res );
}
/**
* Get a list of all time zones
* @param ITextFormatter $msgFormatter
* @return array[] A list of all time zones. The system name of the time zone is used as key and
* the value is an array which contains localized name, the timecorrection value used for
* preferences and the region
* @since 1.26
*/
protected function getTimeZoneList( ITextFormatter $msgFormatter ) {
$identifiers = DateTimeZone::listIdentifiers();
// @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3162
if ( $identifiers === false ) {
return [];
}
sort( $identifiers );
$tzRegions = [
'Africa' => $msgFormatter->format( MessageValue::new( 'timezoneregion-africa' ) ),
'America' => $msgFormatter->format( MessageValue::new( 'timezoneregion-america' ) ),
'Antarctica' => $msgFormatter->format( MessageValue::new( 'timezoneregion-antarctica' ) ),
'Arctic' => $msgFormatter->format( MessageValue::new( 'timezoneregion-arctic' ) ),
'Asia' => $msgFormatter->format( MessageValue::new( 'timezoneregion-asia' ) ),
'Atlantic' => $msgFormatter->format( MessageValue::new( 'timezoneregion-atlantic' ) ),
'Australia' => $msgFormatter->format( MessageValue::new( 'timezoneregion-australia' ) ),
'Europe' => $msgFormatter->format( MessageValue::new( 'timezoneregion-europe' ) ),
'Indian' => $msgFormatter->format( MessageValue::new( 'timezoneregion-indian' ) ),
'Pacific' => $msgFormatter->format( MessageValue::new( 'timezoneregion-pacific' ) ),
];
asort( $tzRegions );
$timeZoneList = [];
$now = new DateTime();
foreach ( $identifiers as $identifier ) {
$parts = explode( '/', $identifier, 2 );
// DateTimeZone::listIdentifiers() returns a number of
// backwards-compatibility entries. This filters them out of the
// list presented to the user.
if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
continue;
}
// Localize region
$parts[0] = $tzRegions[$parts[0]];
$dateTimeZone = new DateTimeZone( $identifier );
$minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
$display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
$value = "ZoneInfo|$minDiff|$identifier";
$timeZoneList[$identifier] = [
'name' => $display,
'timecorrection' => $value,
'region' => $parts[0],
];
}
return $timeZoneList;
}
}

View file

@ -1358,21 +1358,21 @@
"savedrights": "This message appears after saving the user groups on [[Special:UserRights]].\n* $1 - The username of the user which groups was saved.",
"timezonelegend": "{{Identical|Time zone}}",
"localtime": "Used as label in [[Special:Preferences#mw-prefsection-datetime|preferences]].",
"timezoneuseserverdefault": "[[Special:Preferences]] > Date and time > Time zone\n\nThis option lets your time zone setting use the one that is used on the wiki (often UTC).\n\nParameters:\n* $1 - timezone name, or timezone offset (in \"%+03d:%02d\" format)",
"timezoneuseoffset": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.",
"timezone-useoffset-placeholder": "Used in \"Time zone\" text input field as placeholder in [[Special:Preferences#mw-prefsection-datetime|preferences]]",
"timezoneuseserverdefault": "Option in timezone selector form fields.\n\nThis option lets your time zone setting use the one that is used on the wiki (often UTC).\n\nParameters:\n* $1 - timezone name, or timezone offset (in \"%+03d:%02d\" format)",
"timezoneuseoffset": "Option in timezone selector form fields that lets the user manually enter an offset from UTC.",
"timezone-useoffset-placeholder": "Placeholder of the text input in timezone selector form fields.",
"servertime": "Used as label in [[Special:Preferences#mw-prefsection-datetime|preferences]].",
"guesstimezone": "Option to fill in the timezone from the browser setting",
"timezoneregion-africa": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-america": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-antarctica": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-arctic": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-asia": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-atlantic": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-australia": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}\n{{Identical|Australia}}",
"timezoneregion-europe": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-indian": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-pacific": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"guesstimezone": "Option in timezone selector form fields to fill in the timezone from the browser setting",
"timezoneregion-africa": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"timezoneregion-america": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"timezoneregion-antarctica": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"timezoneregion-arctic": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"timezoneregion-asia": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"timezoneregion-atlantic": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"timezoneregion-australia": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}\n{{Identical|Australia}}",
"timezoneregion-europe": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"timezoneregion-indian": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"timezoneregion-pacific": "Used in timezone selector form fields.\n{{Related|Timezoneregion}}",
"allowemail": "Used in [[Special:Preferences]] > {{int:prefs-personal}} > {{int:email}}.",
"email-allow-new-users-label": "Used in [[Special:Preferences]] > {{int:prefs-prohibit}} > {{int:email}}.",
"email-mutelist-label": "Used in [[Special:Preferences]] > {{int:prefs-prohibit}} > {{int:email}}.",

View file

@ -910,6 +910,7 @@ return [
'resources/src/mediawiki.htmlform/multiselect.js',
'resources/src/mediawiki.htmlform/selectandother.js',
'resources/src/mediawiki.htmlform/selectorother.js',
'resources/src/mediawiki.htmlform/timezone.js',
],
'dependencies' => [
'mediawiki.util',

View file

@ -0,0 +1,56 @@
/*
* HTMLForm enhancements:
* Enable the "Fill in from browser" option for the timezone selector
*/
( function () {
function minutesToHours( min ) {
var tzHour = Math.floor( Math.abs( min ) / 60 ),
tzMin = Math.abs( min ) % 60,
tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
return tzString;
}
mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
mw.loader.using( 'mediawiki.widgets.SelectWithInputWidget', function () {
$root.find( '.mw-htmlform-timezone-field' ).each( function () {
// This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
var timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( this ) );
function maybeGuessTimezone() {
if ( timezoneWidget.dropdowninput.getValue() !== 'guess' ) {
return;
}
// If available, get the named time zone from the browser.
// (We also support older browsers where this API is not available.)
var timeZone;
try {
// This may return undefined
// eslint-disable-next-line compat/compat
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch ( err ) {
timeZone = null;
}
// Get the time offset
var minuteDiff = -( new Date().getTimezoneOffset() );
var newValue;
if ( timeZone ) {
// Try to save both time zone and offset
newValue = 'ZoneInfo|' + minuteDiff + '|' + timeZone;
timezoneWidget.dropdowninput.setValue( newValue );
}
if ( !timeZone || timezoneWidget.dropdowninput.getValue() !== newValue ) {
// No time zone, or it's unknown to MediaWiki. Save only offset
timezoneWidget.dropdowninput.setValue( 'other' );
timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
}
}
timezoneWidget.dropdowninput.on( 'change', maybeGuessTimezone );
maybeGuessTimezone();
} );
} );
} );
}() );

View file

@ -13,9 +13,6 @@
return;
}
// Timezone functions.
// Guesses Timezone from browser and updates fields onchange.
// This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
var timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $target );
@ -65,31 +62,8 @@
} else {
// Time zone not manually specified by user
if ( type === 'guess' ) {
// If available, get the named time zone from the browser.
// (We also support older browsers where this API is not available.)
var timeZone;
try {
// This may return undefined
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch ( err ) {
timeZone = null;
}
// Get the time offset
minuteDiff = -( new Date().getTimezoneOffset() );
var newValue;
if ( timeZone ) {
// Try to save both time zone and offset
newValue = 'ZoneInfo|' + minuteDiff + '|' + timeZone;
timezoneWidget.dropdowninput.setValue( newValue );
}
if ( !timeZone || timezoneWidget.dropdowninput.getValue() !== newValue ) {
// No time zone, or it's unknown to MediaWiki. Save only offset
timezoneWidget.dropdowninput.setValue( 'other' );
timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
}
} else {
// Grab data from the dropdown value
minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;