'HTMLComboboxField' => __DIR__ . '/includes/htmlform/fields/HTMLComboboxField.php',
'HTMLDateTimeField' => __DIR__ . '/includes/htmlform/fields/HTMLDateTimeField.php',
'HTMLEditTools' => __DIR__ . '/includes/htmlform/fields/HTMLEditTools.php',
+ 'HTMLExpiryField' => __DIR__ . '/includes/htmlform/fields/HTMLExpiryField.php',
'HTMLFileCache' => __DIR__ . '/includes/cache/HTMLFileCache.php',
'HTMLFloatField' => __DIR__ . '/includes/htmlform/fields/HTMLFloatField.php',
'HTMLForm' => __DIR__ . '/includes/htmlform/HTMLForm.php',
'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
'MediaWiki\\Widget\\DateInputWidget' => __DIR__ . '/includes/widget/DateInputWidget.php',
'MediaWiki\\Widget\\DateTimeInputWidget' => __DIR__ . '/includes/widget/DateTimeInputWidget.php',
+ 'MediaWiki\\Widget\\ExpiryInputWidget' => __DIR__ . '/includes/widget/ExpiryInputWidget.php',
'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php',
'MediaWiki\\Widget\\SearchInputWidget' => __DIR__ . '/includes/widget/SearchInputWidget.php',
'MediaWiki\\Widget\\Search\\BasicSearchResultSetWidget' => __DIR__ . '/includes/widget/search/BasicSearchResultSetWidget.php',
*/
$wgActorTableSchemaMigrationStage = MIGRATION_OLD;
+/**
+ * Temporary option to disable the date picker from the Expiry Widget.
+ *
+ * @since 1.32
+ * @deprecated 1.32
+ * @var bool
+ */
+$wgExpiryWidgetNoDatePicker = false;
+
/**
* For really cool vim folding this needs to be at the end:
* vim: foldmarker=@{,@} foldmethod=marker
'date' => HTMLDateTimeField::class,
'time' => HTMLDateTimeField::class,
'datetime' => HTMLDateTimeField::class,
+ 'expiry' => HTMLExpiryField::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.
--- /dev/null
+<?php
+
+use MediaWiki\Widget\ExpiryInputWidget;
+
+/**
+ * Expiry Field that allows the user to specify a precise date or a
+ * relative date string.
+ */
+class HTMLExpiryField extends HTMLFormField {
+
+ /**
+ * @var HTMLFormField
+ */
+ protected $relativeField;
+
+ /**
+ * Relative Date Time Field.
+ */
+ public function __construct( array $params = [] ) {
+ parent::__construct( $params );
+
+ $type = !empty( $params['options'] ) ? 'selectorother' : 'text';
+ $this->relativeField = $this->getFieldByType( $type );
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Use whatever the relative field is as the standard HTML input.
+ */
+ public function getInputHTML( $value ) {
+ return $this->relativeField->getInputHtml( $value );
+ }
+
+ protected function shouldInfuseOOUI() {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getOOUIModules() {
+ return array_merge(
+ [
+ 'mediawiki.widgets.expiry',
+ ],
+ $this->relativeField->getOOUIModules()
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInputOOUI( $value ) {
+ return new ExpiryInputWidget(
+ $this->relativeField->getInputOOUI( $value ),
+ [
+ 'id' => $this->mID,
+ 'required' => isset( $this->mParams['required'] ) ? $this->mParams['required'] : false,
+ ]
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadDataFromRequest( $request ) {
+ return $this->relativeField->loadDataFromRequest( $request );
+ }
+
+ /**
+ * Get the HTMLForm field by the type string.
+ *
+ * @param string $type
+ * @return \HTMLFormField
+ */
+ protected function getFieldByType( $type ) {
+ $class = HTMLForm::$typeMappings[$type];
+ $params = $this->mParams;
+ $params['type'] = $type;
+ $params['class'] = $class;
+
+ // Remove Parameters that are being used on the parent.
+ unset( $params['label-message'] );
+ return new $class( $params );
+ }
+
+}
'validation-callback' => [ __CLASS__, 'validateTargetField' ],
],
'Expiry' => [
- 'type' => !count( $suggestedDurations ) ? 'text' : 'selectorother',
+ 'type' => 'expiry',
'label-message' => 'ipbexpiry',
'required' => true,
'options' => $suggestedDurations,
- 'other' => $this->msg( 'ipbother' )->text(),
'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(),
],
'Reason' => [
$a[$show] = $value;
}
+ if ( $a ) {
+ // if options exist, add other to the end instead of the begining (which
+ // is what happens by default).
+ $a[ wfMessage( 'ipbother' )->text() ] = 'other';
+ }
+
return $a;
}
/**
* Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute
* ("24 May 2034", etc), into an absolute timestamp we can put into the database.
+ *
+ * @todo strtotime() only accepts English strings. This means the expiry input
+ * can only be specified in English.
+ * @see https://secure.php.net/manual/en/function.strtotime.php
+ *
* @param string $expiry Whatever was typed into the form
- * @return string Timestamp or 'infinity'
+ * @return string|bool Timestamp or 'infinity' or false on error.
*/
public static function parseExpiryInput( $expiry ) {
if ( wfIsInfinity( $expiry ) ) {
- $expiry = 'infinity';
- } else {
- $expiry = strtotime( $expiry );
+ return 'infinity';
+ }
- if ( $expiry < 0 || $expiry === false ) {
- return false;
- }
+ $expiry = strtotime( $expiry );
- $expiry = wfTimestamp( TS_MW, $expiry );
+ if ( $expiry < 0 || $expiry === false ) {
+ return false;
}
- return $expiry;
+ return wfTimestamp( TS_MW, $expiry );
}
/**
--- /dev/null
+<?php
+
+namespace MediaWiki\Widget;
+
+use OOUI\Widget;
+
+/**
+ * Expiry widget.
+ *
+ * Allows the user to toggle between a precise time or enter a relative time,
+ * regardless, the value comes in as a relative time.
+ *
+ * @copyright 2018 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class ExpiryInputWidget extends Widget {
+
+ /**
+ * @var Widget
+ */
+ protected $relativeInput;
+
+ /**
+ * @var bool
+ */
+ protected $noDatePicker;
+
+ /**
+ * @var bool
+ */
+ protected $required;
+
+ /**
+ * @param Widget $relativeInput
+ * @param array $options Configuration options
+ */
+ public function __construct( Widget $relativeInput, array $options = [] ) {
+ $config = \RequestContext::getMain()->getConfig();
+
+ $options['noDatePicker'] = $config->get( 'ExpiryWidgetNoDatePicker' );
+
+ parent::__construct( $options );
+
+ $this->noDatePicker = $options['noDatePicker'];
+ $this->required = isset( $options['required'] ) ? $options['required'] : false;
+
+ // Properties
+ $this->relativeInput = $relativeInput;
+ $this->relativeInput->addClasses( [ 'mw-widget-ExpiryWidget-relative' ] );
+
+ // Initialization
+ $classes = [
+ 'mw-widget-ExpiryWidget',
+ ];
+ if ( $options['noDatePicker'] === false ) {
+ $classes[] = 'mw-widget-ExpiryWidget-hasDatePicker';
+ }
+ $this
+ ->addClasses( $classes )
+ ->appendContent( $this->relativeInput );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.ExpiryWidget';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfig( &$config ) {
+ $config['noDatePicker'] = $this->noDatePicker;
+ $config['required'] = $this->required;
+ $config['relativeInput'] = [];
+ $this->relativeInput->getConfig( $config['relativeInput'] );
+ return parent::getConfig( $config );
+ }
+}
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.block.js',
'dependencies' => [
'oojs-ui-core',
+ 'oojs-ui.styles.icons-editing-core',
+ 'oojs-ui.styles.icons-editing-advanced',
'mediawiki.widgets.SelectWithInputWidget',
+ 'mediawiki.widgets.DateInputWidget',
'mediawiki.util',
'mediawiki.htmlform',
+ 'moment',
],
],
'mediawiki.special.changecredentials.js' => [
],
'targets' => [ 'desktop', 'mobile' ],
],
+ 'mediawiki.widgets.expiry' => [
+ 'scripts' => [
+ 'resources/src/mediawiki.widgets/mw.widgets.ExpiryInputWidget.js',
+ ],
+ 'dependencies' => [
+ 'oojs-ui-core',
+ 'oojs-ui-widgets',
+ 'moment',
+ 'mediawiki.widgets.datetime'
+ ],
+ 'skinStyles' => [
+ 'default' => 'resources/src/mediawiki.widgets/mw.widgets.ExpiryInputWidget.less',
+ ],
+ 'targets' => [ 'desktop', 'mobile' ],
+ ],
'mediawiki.widgets.CategoryMultiselectWidget' => [
'scripts' => [
'resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js',
enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
- // mw.widgets.SelectWithInputWidget
expiryWidget = infuseOrNull( 'mw-input-wpExpiry' );
function updateBlockOptions() {
isIp = mw.util.isIPAddress( blocktarget, true ),
isIpRange = isIp && blocktarget.match( /\/\d+$/ ),
isNonEmptyIp = isIp && !isEmpty,
- expiryValue = expiryWidget.dropdowninput.getValue(),
+ expiryValue = expiryWidget.getValue(),
// infinityValues are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
- isIndefinite = infinityValues.indexOf( expiryValue ) !== -1 ||
- ( expiryValue === 'other' && infinityValues.indexOf( expiryWidget.textinput.getValue() ) !== -1 );
+ isIndefinite = infinityValues.indexOf( expiryValue ) !== -1;
if ( enableAutoblockField ) {
enableAutoblockField.toggle( !( isNonEmptyIp ) );
if ( blockTargetWidget ) {
// Bind functions so they're checked whenever stuff changes
blockTargetWidget.on( 'change', updateBlockOptions );
- expiryWidget.dropdowninput.on( 'change', updateBlockOptions );
- expiryWidget.textinput.on( 'change', updateBlockOptions );
+ expiryWidget.on( 'change', updateBlockOptions );
// Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
updateBlockOptions();
--- /dev/null
+/*!
+ * MediaWiki Widgets - ExpiryWidget class.
+ *
+ * @copyright 2018 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+/* global moment */
+( function ( $, mw ) {
+
+ /**
+ * Creates a mw.widgets.ExpiryWidget object.
+ *
+ * @class mw.widgets.ExpiryWidget
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+ mw.widgets.ExpiryWidget = function ( config ) {
+ var RFC2822 = 'ddd, DD MMM YYYY HH:mm:ss [GMT]';
+
+ // Config initialization
+ config = $.extend( {}, config );
+
+ this.relativeField = new config.RelativeInputClass( config.relativeInput );
+ this.relativeField.$element.addClass( 'mw-widget-ExpiryWidget-relative' );
+
+ // Parent constructor
+ mw.widgets.ExpiryWidget.parent.call( this, config );
+
+ // If the wiki does not want the date picker, then initialize the relative
+ // field and exit.
+ if ( config.noDatePicker ) {
+ this.relativeField.on( 'change', function ( event ) {
+ // Emit a change event for this widget.
+ this.emit( 'change', event );
+ }.bind( this ) );
+
+ // Initialization
+ this.$element
+ .addClass( 'mw-widget-ExpiryWidget' )
+ .append(
+ this.relativeField.$element
+ );
+
+ return;
+ }
+
+ // Properties
+ this.inputSwitch = new OO.ui.ButtonSelectWidget( {
+ tabIndex: -1,
+ items: [
+ new OO.ui.ButtonOptionWidget( {
+ data: 'relative',
+ icon: 'edit'
+ } ),
+ new OO.ui.ButtonOptionWidget( {
+ data: 'date',
+ icon: 'calendar'
+ } )
+ ]
+ } );
+ this.dateTimeField = new mw.widgets.datetime.DateTimeInputWidget( {
+ min: new Date(), // The selected date must at least be now.
+ required: config.required
+ } );
+
+ // Initially hide the dateTime field.
+ this.dateTimeField.toggle( false );
+ // Initially set the relative input.
+ this.inputSwitch.selectItemByData( 'relative' );
+
+ // Events
+
+ // Toggle the visible inputs.
+ this.inputSwitch.on( 'choose', function ( event ) {
+ switch ( event.getData() ) {
+ case 'date':
+ this.dateTimeField.toggle( true );
+ this.relativeField.toggle( false );
+ break;
+ case 'relative':
+ this.dateTimeField.toggle( false );
+ this.relativeField.toggle( true );
+ break;
+ }
+ }.bind( this ) );
+
+ // When the date time field update, update the relative
+ // field.
+ this.dateTimeField.on( 'change', function ( value ) {
+ var datetime;
+
+ // Do not alter the visible input.
+ if ( this.relativeField.isVisible() ) {
+ return;
+ }
+
+ // If the value was cleared, do not attempt to parse it.
+ if ( !value ) {
+ this.relativeField.setValue( value );
+ return;
+ }
+
+ datetime = moment( value );
+
+ // If the datetime is invlaid for some reason, reset the relative field.
+ if ( !datetime.isValid() ) {
+ this.relativeField.setValue( undefined );
+ }
+
+ // Set the relative field value. The field only accepts English strings.
+ this.relativeField.setValue( datetime.utc().locale( 'en' ).format( RFC2822 ) );
+ }.bind( this ) );
+
+ // When the relative field update, update the date time field if it's a
+ // value that moment understands.
+ this.relativeField.on( 'change', function ( event ) {
+ var datetime;
+
+ // Emit a change event for this widget.
+ this.emit( 'change', event );
+
+ // Do not alter the visible input.
+ if ( this.dateTimeField.isVisible() ) {
+ return;
+ }
+
+ // Parsing of free text field may fail, so always check if the date is
+ // valid.
+ datetime = moment( event );
+
+ if ( datetime.isValid() ) {
+ this.dateTimeField.setValue( datetime.utc().toISOString() );
+ } else {
+ this.dateTimeField.setValue( undefined );
+ }
+ }.bind( this ) );
+
+ // Initialization
+ this.$element
+ .addClass( 'mw-widget-ExpiryWidget' )
+ .addClass( 'mw-widget-ExpiryWidget-hasDatePicker' )
+ .append(
+ this.inputSwitch.$element,
+ this.dateTimeField.$element,
+ this.relativeField.$element
+ );
+
+ // Trigger an initial onChange.
+ this.relativeField.emit( 'change', this.relativeField.getValue() );
+ };
+
+ /* Inheritance */
+
+ OO.inheritClass( mw.widgets.ExpiryWidget, OO.ui.Widget );
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.ExpiryWidget.static.reusePreInfuseDOM = function ( node, config ) {
+ var relativeElement = $( node ).find( '.mw-widget-ExpiryWidget-relative' );
+
+ config = mw.widgets.ExpiryWidget.parent.static.reusePreInfuseDOM( node, config );
+
+ if ( relativeElement.hasClass( 'oo-ui-textInputWidget' ) ) {
+ config.RelativeInputClass = OO.ui.TextInputWidget;
+ } else if ( relativeElement.hasClass( 'mw-widget-selectWithInputWidget' ) ) {
+ config.RelativeInputClass = mw.widgets.SelectWithInputWidget;
+ }
+
+ config.relativeInput = config.RelativeInputClass.static.reusePreInfuseDOM(
+ relativeElement,
+ config.relativeInput
+ );
+
+ return config;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.ExpiryWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = mw.widgets.ExpiryWidget.parent.static.gatherPreInfuseState( node, config );
+
+ state.relativeInput = config.RelativeInputClass.static.gatherPreInfuseState(
+ $( node ).find( '.mw-widget-ExpiryWidget-relative' ),
+ config.relativeInput
+ );
+
+ return state;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.ExpiryWidget.prototype.restorePreInfuseState = function ( state ) {
+ mw.widgets.ExpiryWidget.parent.prototype.restorePreInfuseState.call( this, state );
+ this.relativeField.restorePreInfuseState( state.relativeInput );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.ExpiryWidget.prototype.setDisabled = function ( disabled ) {
+ mw.widgets.ExpiryWidget.parent.prototype.setDisabled.call( this, disabled );
+ this.relativeField.setDisabled( disabled );
+
+ if ( this.inputSwitch ) {
+ this.inputSwitch.setDisabled( disabled );
+ }
+
+ if ( this.dateTimeField ) {
+ this.dateTimeField.setDisabled( disabled );
+ }
+ };
+
+ /**
+ * Gets the value of the widget.
+ *
+ * @return {string}
+ */
+ mw.widgets.ExpiryWidget.prototype.getValue = function () {
+ return this.relativeField.getValue();
+ };
+
+}( jQuery, mediaWiki ) );
--- /dev/null
+@wm-expirywidget-text-width: 43.3em;
+
+.mw-widget-ExpiryWidget.mw-widget-ExpiryWidget-hasDatePicker {
+ .oo-ui-buttonSelectWidget {
+ float: left;
+ }
+
+ .oo-ui-textInputWidget.mw-widget-ExpiryWidget-relative {
+ display: inline-block;
+ max-width: @wm-expirywidget-text-width;
+ }
+
+ .mw-widget-selectWithInputWidget.mw-widget-ExpiryWidget-relative .oo-ui-textInputWidget {
+ max-width: 22.8em;
+ }
+
+ .mw-widgets-datetime-dateTimeInputWidget {
+ margin-top: 0;
+ margin-bottom: 0;
+ max-width: @wm-expirywidget-text-width;
+
+ .mw-widgets-datetime-dateTimeInputWidget-handle {
+ max-height: 2.286em;
+ }
+ }
+}
// Events
this.dropdowninput.on( 'change', this.onChange.bind( this ) );
+ this.textinput.on( 'change', function () {
+ this.emit( 'change', this.getValue() );
+ }.bind( this ) );
// Parent constructor
mw.widgets.SelectWithInputWidget.parent.call( this, config );
this.textinput.setDisabled( textinputIsHidden || disabled );
};
+ /**
+ * Set the value from outside.
+ *
+ * @param {string|undefined} value
+ */
+ mw.widgets.SelectWithInputWidget.prototype.setValue = function ( value ) {
+ var selectable = false;
+
+ if ( this.or ) {
+ if ( value !== 'other' ) {
+ selectable = !!this.dropdowninput.dropdownWidget.getMenu().findItemFromData( value );
+ }
+
+ if ( selectable ) {
+ this.dropdowninput.setValue( value );
+ this.textinput.setValue( undefined );
+ } else {
+ this.dropdowninput.setValue( 'other' );
+ this.textinput.setValue( value );
+ }
+
+ this.emit( 'change', value );
+ }
+ };
+
+ /**
+ * Get the value from outside.
+ *
+ * @return {string}
+ */
+ mw.widgets.SelectWithInputWidget.prototype.getValue = function () {
+ if ( this.or ) {
+ if ( this.dropdowninput.getValue() !== 'other' ) {
+ return this.dropdowninput.getValue();
+ }
+
+ return this.textinput.getValue();
+ } else {
+ return '';
+ }
+ };
+
/**
* Handle change events on the DropdownInput
*
// submitted with the form. So we should also disable fields when hiding them.
this.textinput.setDisabled( value !== 'other' || this.isDisabled() );
}
+
+ this.emit( 'change', this.getValue() );
};
}( jQuery, mediaWiki ) );