/*!
- * OOUI v0.25.1
+ * OOUI v0.26.0
* https://www.mediawiki.org/wiki/OOUI
*
* Copyright 2011–2018 OOUI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2018-01-17T01:47:15Z
+ * Date: 2018-03-21T00:00:53Z
*/
( function ( OO ) {
* It is left up to implementors to decide how to compute this
* so the default implementation always returns false.
*
- * @return {boolean} Use is on a mobile device
+ * @return {boolean} User is on a mobile device
*/
OO.ui.isMobile = function () {
return false;
// rebuild widget
// eslint-disable-next-line new-cap
obj = new cls( data );
+ // If anyone is holding a reference to the old DOM element,
+ // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
+ // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
+ $elem[ 0 ].oouiInfused = obj.$element;
// now replace old DOM with this new DOM.
if ( top ) {
// An efficient constructor might be able to reuse the entire DOM tree of the original element,
// so only mutate the DOM if we need to.
if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
$elem.replaceWith( obj.$element );
- // This element is now gone from the DOM, but if anyone is holding a reference to it,
- // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
- // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
- $elem[ 0 ].oouiInfused = obj.$element;
}
top.resolve();
}
* @param {Object} [config] Configuration options
* @cfg {jQuery} [$indicator] The indicator element created by the class. If this
* configuration is omitted, the indicator element will use a generated `<span>`.
- * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
+ * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
* See the [OOUI documentation on MediaWiki][2] for a list of indicators included
* in the library.
* [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
/* Static Properties */
/**
- * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
+ * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
* The static property will be overridden if the #indicator configuration is used.
*
* @static
};
/**
- * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
+ * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null` to remove the indicator.
*
* @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
* @chainable
};
/**
- * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
+ * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
*
* @return {string} Symbolic name of indicator
*/
/**
* IndicatorWidgets create indicators, which are small graphics that are generally used to draw
- * attention to the status of an item or to clarify the function of a control. For a list of
+ * attention to the status of an item or to clarify the function within a control. For a list of
* indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
*
* @example
* // Example of an indicator widget
* var indicator1 = new OO.ui.IndicatorWidget( {
- * indicator: 'alert'
+ * indicator: 'required'
* } );
*
* // Create a fieldset layout to add a label
* var fieldset = new OO.ui.FieldsetLayout();
* fieldset.addItems( [
- * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
+ * new OO.ui.FieldLayout( indicator1, { label: 'A required indicator:' } )
* ] );
* $( 'body' ).append( fieldset.$element );
*
* @return {boolean} Item is selectable
*/
OO.ui.OptionWidget.prototype.isSelectable = function () {
- return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
+ return this.constructor.static.selectable && !this.disabled && this.isVisible();
};
/**
* @return {boolean} Item is highlightable
*/
OO.ui.OptionWidget.prototype.isHighlightable = function () {
- return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
+ return this.constructor.static.highlightable && !this.disabled && this.isVisible();
};
/**
* @return {boolean} Item is pressable
*/
OO.ui.OptionWidget.prototype.isPressable = function () {
- return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
+ return this.constructor.static.pressable && !this.disabled && this.isVisible();
};
/**
this.value = value;
this.emit( 'change', this.value );
}
+ // The first time that the value is set (probably while constructing the widget),
+ // remember it in defaultValue. This property can be later used to check whether
+ // the value of the input has been changed since it was created.
+ if ( this.defaultValue === undefined ) {
+ this.defaultValue = this.value;
+ this.$input[ 0 ].defaultValue = this.defaultValue;
+ }
return this;
};
this.$input.prop( 'checked', this.selected );
this.emit( 'change', this.selected );
}
+ // The first time that the selection state is set (probably while constructing the widget),
+ // remember it in defaultSelected. This property can be later used to check whether
+ // the selection state of the input has been changed since it was created.
+ if ( this.defaultSelected === undefined ) {
+ this.defaultSelected = this.selected;
+ this.$input[ 0 ].defaultChecked = this.defaultSelected;
+ }
return this;
};
// Properties (must be done before parent constructor which calls #setDisabled)
this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
+ // Set up the options before parent constructor, which uses them to validate config.value.
+ // Use this instead of setOptions() because this.$input is not set up yet.
+ this.setOptionsData( config.options || [] );
// Parent constructor
OO.ui.DropdownInputWidget.parent.call( this, config );
this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
// Initialization
- this.setOptions( config.options || [] );
- // Set the value again, after we did setOptions(). The call from parent doesn't work because the
- // widget has no valid options when it happens.
- this.setValue( config.value );
this.$element
.addClass( 'oo-ui-dropdownInputWidget' )
.append( this.dropdownWidget.$element );
this.dropdownWidget.getMenu().selectItem( selected );
value = selected ? selected.getData() : '';
OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
+ if ( this.optionsDirty ) {
+ // We reached this from the constructor or from #setOptions.
+ // We have to update the <select> element.
+ this.updateOptionsInterface();
+ }
return this;
};
* @chainable
*/
OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
+ var value = this.getValue();
+
+ this.setOptionsData( options );
+
+ // Re-set the value to update the visible interface (DropdownWidget and <select>).
+ // In case the previous value is no longer an available option, select the first valid one.
+ this.setValue( value );
+
+ return this;
+};
+
+/**
+ * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
+ *
+ * This method may be called before the parent constructor, so various properties may not be
+ * intialized yet.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @private
+ */
+OO.ui.DropdownInputWidget.prototype.setOptionsData = function ( options ) {
+ var
+ optionWidgets,
+ widget = this;
+
+ this.optionsDirty = true;
+
+ optionWidgets = options.map( function ( opt ) {
+ var optValue;
+
+ if ( opt.optgroup !== undefined ) {
+ return widget.createMenuSectionOptionWidget( opt.optgroup );
+ }
+
+ optValue = widget.cleanUpValue( opt.data );
+ return widget.createMenuOptionWidget(
+ optValue,
+ opt.label !== undefined ? opt.label : optValue
+ );
+
+ } );
+
+ this.dropdownWidget.getMenu().clearItems().addItems( optionWidgets );
+};
+
+/**
+ * Create a menu option widget.
+ *
+ * @protected
+ * @param {string} data Item data
+ * @param {string} label Item label
+ * @return {OO.ui.MenuOptionWidget} Option widget
+ */
+OO.ui.DropdownInputWidget.prototype.createMenuOptionWidget = function ( data, label ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: data,
+ label: label
+ } );
+};
+
+/**
+ * Create a menu section option widget.
+ *
+ * @protected
+ * @param {string} label Section item label
+ * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
+ */
+OO.ui.DropdownInputWidget.prototype.createMenuSectionOptionWidget = function ( label ) {
+ return new OO.ui.MenuSectionOptionWidget( {
+ label: label
+ } );
+};
+
+/**
+ * Update the user-visible interface to match the internal list of options and value.
+ *
+ * This method must only be called after the parent constructor.
+ *
+ * @private
+ */
+OO.ui.DropdownInputWidget.prototype.updateOptionsInterface = function () {
var
- optionWidgets = [],
- value = this.getValue(),
$optionsContainer = this.$input,
+ defaultValue = this.defaultValue,
widget = this;
- this.dropdownWidget.getMenu().clearItems();
this.$input.empty();
- // Rebuild the dropdown menu: our visible one and the hidden `<select>`
- options.forEach( function ( opt ) {
- var optValue, $optionNode, optionWidget;
-
- if ( opt.optgroup === undefined ) {
- optValue = widget.cleanUpValue( opt.data );
+ this.dropdownWidget.getMenu().getItems().forEach( function ( optionWidget ) {
+ var $optionNode;
+ if ( !( optionWidget instanceof OO.ui.MenuSectionOptionWidget ) ) {
$optionNode = $( '<option>' )
- .attr( 'value', optValue )
- .text( opt.label !== undefined ? opt.label : optValue );
- optionWidget = new OO.ui.MenuOptionWidget( {
- data: optValue,
- label: opt.label !== undefined ? opt.label : optValue
- } );
+ .attr( 'value', optionWidget.getData() )
+ .text( optionWidget.getLabel() );
+
+ // Remember original selection state. This property can be later used to check whether
+ // the selection state of the input has been changed since it was created.
+ $optionNode[ 0 ].defaultSelected = ( optionWidget.getData() === defaultValue );
$optionsContainer.append( $optionNode );
- optionWidgets.push( optionWidget );
} else {
$optionNode = $( '<optgroup>' )
- .attr( 'label', opt.optgroup );
- optionWidget = new OO.ui.MenuSectionOptionWidget( {
- label: opt.optgroup
- } );
-
+ .attr( 'label', optionWidget.getLabel() );
widget.$input.append( $optionNode );
$optionsContainer = $optionNode;
- optionWidgets.push( optionWidget );
}
} );
- this.dropdownWidget.getMenu().addItems( optionWidgets );
-
- // Restore the previous value, or reset to something sensible
- if ( this.dropdownWidget.getMenu().findItemFromData( value ) ) {
- // Previous value is still available, ensure consistency with the dropdown
- this.setValue( value );
- } else {
- // No longer valid, reset
- if ( options.length ) {
- this.setValue( options[ 0 ].data );
- }
- }
- return this;
+ this.optionsDirty = false;
};
/**
OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
// RadioInputWidget doesn't track its state.
this.$input.prop( 'checked', state );
+ // The first time that the selection state is set (probably while constructing the widget),
+ // remember it in defaultSelected. This property can be later used to check whether
+ // the selection state of the input has been changed since it was created.
+ if ( this.defaultSelected === undefined ) {
+ this.defaultSelected = state;
+ this.$input[ 0 ].defaultChecked = this.defaultSelected;
+ }
return this;
};
// Properties (must be done before parent constructor which calls #setDisabled)
this.radioSelectWidget = new OO.ui.RadioSelectWidget();
+ // Set up the options before parent constructor, which uses them to validate config.value.
+ // Use this instead of setOptions() because this.$input is not set up yet
+ this.setOptionsData( config.options || [] );
// Parent constructor
OO.ui.RadioSelectInputWidget.parent.call( this, config );
this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
// Initialization
- this.setOptions( config.options || [] );
this.$element
.addClass( 'oo-ui-radioSelectInputWidget' )
.append( this.radioSelectWidget.$element );
* @protected
*/
OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
- return $( '<input>' ).attr( 'type', 'hidden' );
+ // Use this instead of <input type="hidden">, because hidden inputs do not have separate
+ // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
+ return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
};
/**
* @inheritdoc
*/
OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
+ var selected;
value = this.cleanUpValue( value );
- this.radioSelectWidget.selectItemByData( value );
+ // Only allow setting values that are actually present in the dropdown
+ selected = this.radioSelectWidget.findItemFromData( value ) ||
+ this.radioSelectWidget.findFirstSelectableItem();
+ this.radioSelectWidget.selectItem( selected );
+ value = selected ? selected.getData() : '';
OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
return this;
};
* @chainable
*/
OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
- var
- value = this.getValue(),
- widget = this;
+ var value = this.getValue();
+
+ this.setOptionsData( options );
+
+ // Re-set the value to update the visible interface (RadioSelectWidget).
+ // In case the previous value is no longer an available option, select the first valid one.
+ this.setValue( value );
+
+ return this;
+};
+
+/**
+ * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
+ *
+ * This method may be called before the parent constructor, so various properties may not be
+ * intialized yet.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @private
+ */
+OO.ui.RadioSelectInputWidget.prototype.setOptionsData = function ( options ) {
+ var widget = this;
- // Rebuild the radioSelect menu
this.radioSelectWidget
.clearItems()
.addItems( options.map( function ( opt ) {
label: opt.label !== undefined ? opt.label : optValue
} );
} ) );
-
- // Restore the previous value, or reset to something sensible
- if ( this.radioSelectWidget.findItemFromData( value ) ) {
- // Previous value is still available, ensure consistency with the radioSelect
- this.setValue( value );
- } else {
- // No longer valid, reset
- if ( options.length ) {
- this.setValue( options[ 0 ].data );
- }
- }
-
- return this;
};
/**
// Properties (must be done before parent constructor which calls #setDisabled)
this.checkboxMultiselectWidget = new OO.ui.CheckboxMultiselectWidget();
+ // Must be set before the #setOptionsData call below
+ this.inputName = config.name;
+ // Set up the options before parent constructor, which uses them to validate config.value.
+ // Use this instead of setOptions() because this.$input is not set up yet
+ this.setOptionsData( config.options || [] );
// Parent constructor
OO.ui.CheckboxMultiselectInputWidget.parent.call( this, config );
- // Properties
- this.inputName = config.name;
+ // Events
+ this.checkboxMultiselectWidget.connect( this, { select: 'onCheckboxesSelect' } );
// Initialization
this.$element
.append( this.checkboxMultiselectWidget.$element );
// We don't use this.$input, but rather the CheckboxInputWidgets inside each option
this.$input.detach();
- this.setOptions( config.options || [] );
- // Have to repeat this from parent, as we need options to be set up for this to make sense
- this.setValue( config.value );
-
- // setValue when checkboxMultiselectWidget changes
- this.checkboxMultiselectWidget.on( 'change', function () {
- this.setValue( this.checkboxMultiselectWidget.findSelectedItemsData() );
- }.bind( this ) );
};
/* Setup */
return $( '<unused>' );
};
+/**
+ * Handles CheckboxMultiselectWidget select events.
+ *
+ * @private
+ */
+OO.ui.CheckboxMultiselectInputWidget.prototype.onCheckboxesSelect = function () {
+ this.setValue( this.checkboxMultiselectWidget.findSelectedItemsData() );
+};
+
/**
* @inheritdoc
*/
value = this.cleanUpValue( value );
this.checkboxMultiselectWidget.selectItemsByData( value );
OO.ui.CheckboxMultiselectInputWidget.parent.prototype.setValue.call( this, value );
+ if ( this.optionsDirty ) {
+ // We reached this from the constructor or from #setOptions.
+ // We have to update the <select> element.
+ this.updateOptionsInterface();
+ }
return this;
};
* @chainable
*/
OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options ) {
+ var value = this.getValue();
+
+ this.setOptionsData( options );
+
+ // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
+ // This will also get rid of any stale options that we just removed.
+ this.setValue( value );
+
+ return this;
+};
+
+/**
+ * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
+ *
+ * This method may be called before the parent constructor, so various properties may not be
+ * intialized yet.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @private
+ */
+OO.ui.CheckboxMultiselectInputWidget.prototype.setOptionsData = function ( options ) {
var widget = this;
- // Rebuild the checkboxMultiselectWidget menu
+ this.optionsDirty = true;
+
this.checkboxMultiselectWidget
.clearItems()
.addItems( options.map( function ( opt ) {
item.checkbox.setValue( optValue );
return item;
} ) );
+};
- // Re-set the value, checking the checkboxes as needed.
- // This will also get rid of any stale options that we just removed.
- this.setValue( this.getValue() );
+/**
+ * Update the user-visible interface to match the internal list of options and value.
+ *
+ * This method must only be called after the parent constructor.
+ *
+ * @private
+ */
+OO.ui.CheckboxMultiselectInputWidget.prototype.updateOptionsInterface = function () {
+ var defaultValue = this.defaultValue;
- return this;
+ this.checkboxMultiselectWidget.getItems().forEach( function ( item ) {
+ // Remember original selection state. This property can be later used to check whether
+ // the selection state of the input has been changed since it was created.
+ var isDefault = defaultValue.indexOf( item.getData() ) !== -1;
+ item.checkbox.defaultSelected = isDefault;
+ item.checkbox.$input[ 0 ].defaultChecked = isDefault;
+ } );
+
+ this.optionsDirty = false;
};
/**
* instruct the browser to focus this widget.
* @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
* @cfg {number} [maxLength] Maximum number of characters allowed in the input.
+ *
+ * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
+ * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
+ * many emojis) count as 2 characters each.
* @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
* the value or placeholder text: `'before'` or `'after'`
* @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator: 'required'`.
integer: /^\d+$/
};
-/* Static Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
- var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
- return state;
-};
-
/* Events */
/**
return this;
};
-/**
- * @inheritdoc
- */
-OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
- OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
- if ( state.scrollTop !== undefined ) {
- this.$input.scrollTop( state.scrollTop );
- }
-};
-
/**
* @class
* @extends OO.ui.TextInputWidget
* @param {Object} [config] Configuration options
* @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
* specifies minimum number of rows to display.
- * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
* @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
* Use the #maxRows config to specify a maximum number of displayed rows.
* @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
};
/**
- * Override TextInputWidget so it doesn't emit the 'enter' event.
+ * @inheritdoc
*
- * @private
- * @param {jQuery.Event} e Key press event
+ * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
*/
-OO.ui.MultilineTextInputWidget.prototype.onKeyPress = function () {
- return;
+OO.ui.MultilineTextInputWidget.prototype.onKeyPress = function ( e ) {
+ if (
+ ( e.which === OO.ui.Keys.ENTER && ( e.ctrlKey || e.metaKey ) ) ||
+ // Some platforms emit keycode 10 for ctrl+enter in a textarea
+ e.which === 10
+ ) {
+ this.emit( 'enter', e );
+ }
};
/**
return !!this.autosize;
};
+/**
+ * @inheritdoc
+ */
+OO.ui.MultilineTextInputWidget.prototype.restorePreInfuseState = function ( state ) {
+ OO.ui.MultilineTextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
+ if ( state.scrollTop !== undefined ) {
+ this.$input.scrollTop( state.scrollTop );
+ }
+};
+
/**
* ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
* can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
$icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
$listItem.attr( 'role', 'alert' );
} else if ( kind === 'notice' ) {
- $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
+ $icon = new OO.ui.IconWidget( { icon: 'notice' } ).$element;
} else {
$icon = '';
}