/**
* NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
* can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
* (to adjust the value in increments) to allow the user to enter a number.
*
* @example
* // A NumberInputWidget.
* const numberInput = new OO.ui.NumberInputWidget( {
* label: 'NumberInputWidget',
* input: { value: 5 },
* min: 1,
* max: 10
* } );
* $( document.body ).append( numberInput.$element );
*
* @class
* @extends OO.ui.TextInputWidget
*
* @constructor
* @param {Object} [config] Configuration options
* @param {Object} [config.minusButton] Configuration options to pass to the
* {@link OO.ui.ButtonWidget decrementing button widget}.
* @param {Object} [config.plusButton] Configuration options to pass to the
* {@link OO.ui.ButtonWidget incrementing button widget}.
* @param {number} [config.min=-Infinity] Minimum allowed value
* @param {number} [config.max=Infinity] Maximum allowed value
* @param {number|null} [config.step] If specified, the field only accepts values that are multiples of this.
* @param {number} [config.buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
* Defaults to `step` if specified, otherwise `1`.
* @param {number} [config.pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
* Defaults to 10 times `buttonStep`.
* @param {boolean} [config.showButtons=true] Whether to show the plus and minus buttons.
*/
OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
const $field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
// Configuration initialization
config = Object.assign( {
min: -Infinity,
max: Infinity,
showButtons: true
}, config );
// For backward compatibility
Object.assign( config, config.input );
this.input = this;
// Parent constructor
OO.ui.NumberInputWidget.super.call( this, Object.assign( config, {
type: 'number'
} ) );
if ( config.showButtons ) {
this.minusButton = new OO.ui.ButtonWidget( Object.assign(
{
disabled: this.isDisabled(),
tabIndex: -1,
classes: [ 'oo-ui-numberInputWidget-minusButton' ],
icon: 'subtract'
},
config.minusButton
) );
this.minusButton.$element.attr( 'aria-hidden', 'true' );
this.plusButton = new OO.ui.ButtonWidget( Object.assign(
{
disabled: this.isDisabled(),
tabIndex: -1,
classes: [ 'oo-ui-numberInputWidget-plusButton' ],
icon: 'add'
},
config.plusButton
) );
this.plusButton.$element.attr( 'aria-hidden', 'true' );
}
// Events
this.$input.on( {
keydown: this.onKeyDown.bind( this ),
'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
} );
if ( config.showButtons ) {
this.plusButton.connect( this, {
click: [ 'onButtonClick', +1 ]
} );
this.minusButton.connect( this, {
click: [ 'onButtonClick', -1 ]
} );
}
// Build the field
$field.append( this.$input );
if ( config.showButtons ) {
$field
.prepend( this.minusButton.$element )
.append( this.plusButton.$element );
}
// Initialization
if ( config.allowInteger || config.isInteger ) {
// Backward compatibility
config.step = 1;
}
this.setRange( config.min, config.max );
this.setStep( config.buttonStep, config.pageStep, config.step );
// Set the validation method after we set step and range
// so that it doesn't immediately call setValidityFlag
this.setValidation( this.validateNumber.bind( this ) );
this.$element
.addClass( 'oo-ui-numberInputWidget' )
.toggleClass( 'oo-ui-numberInputWidget-buttoned', config.showButtons )
.append( $field );
};
/* Setup */
OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.TextInputWidget );
/* Methods */
// Backward compatibility
OO.ui.NumberInputWidget.prototype.setAllowInteger = function ( flag ) {
this.setStep( flag ? 1 : null );
};
// Backward compatibility
OO.ui.NumberInputWidget.prototype.setIsInteger = OO.ui.NumberInputWidget.prototype.setAllowInteger;
// Backward compatibility
OO.ui.NumberInputWidget.prototype.getAllowInteger = function () {
return this.step === 1;
};
// Backward compatibility
OO.ui.NumberInputWidget.prototype.getIsInteger = OO.ui.NumberInputWidget.prototype.getAllowInteger;
/**
* Set the range of allowed values
*
* @param {number} min Minimum allowed value
* @param {number} max Maximum allowed value
*/
OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
if ( min > max ) {
throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
}
this.min = min;
this.max = max;
this.$input.attr( { min: this.min, max: this.max } );
this.setValidityFlag();
};
/**
* Get the current range
*
* @return {number[]} Minimum and maximum values
*/
OO.ui.NumberInputWidget.prototype.getRange = function () {
return [ this.min, this.max ];
};
/**
* Set the stepping deltas
*
* @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
* Defaults to `step` if specified, otherwise `1`.
* @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
* Defaults to 10 times `buttonStep`.
* @param {number|null} [step] If specified, the field only accepts values that are multiples
* of this.
*/
OO.ui.NumberInputWidget.prototype.setStep = function ( buttonStep, pageStep, step ) {
if ( buttonStep === undefined ) {
buttonStep = step || 1;
}
if ( pageStep === undefined ) {
pageStep = 10 * buttonStep;
}
if ( step !== null && step <= 0 ) {
throw new Error( 'Step value, if given, must be positive' );
}
if ( buttonStep <= 0 ) {
throw new Error( 'Button step value must be positive' );
}
if ( pageStep <= 0 ) {
throw new Error( 'Page step value must be positive' );
}
this.step = step;
this.buttonStep = buttonStep;
this.pageStep = pageStep;
this.$input.attr( 'step', this.step || 'any' );
this.setValidityFlag();
};
/**
* @inheritdoc
*/
OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
if ( value === '' ) {
// Some browsers allow a value in the input even if there isn't one reported by $input.val()
// so here we make sure an 'empty' value is actually displayed as such.
this.$input.val( '' );
}
return OO.ui.NumberInputWidget.super.prototype.setValue.call( this, value );
};
/**
* Get the current stepping values
*
* @return {number[]} Button step, page step, and validity step
*/
OO.ui.NumberInputWidget.prototype.getStep = function () {
return [ this.buttonStep, this.pageStep, this.step ];
};
/**
* Get the current value of the widget as a number
*
* @return {number} May be NaN, or an invalid number
*/
OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
return +this.getValue();
};
/**
* Adjust the value of the widget
*
* @param {number} delta Adjustment amount
*/
OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
const v = this.getNumericValue();
delta = +delta;
if ( isNaN( delta ) || !isFinite( delta ) ) {
throw new Error( 'Delta must be a finite number' );
}
let n;
if ( isNaN( v ) ) {
n = 0;
} else {
n = v + delta;
n = Math.max( Math.min( n, this.max ), this.min );
if ( this.step ) {
n = Math.round( n / this.step ) * this.step;
}
}
if ( n !== v ) {
this.setValue( n );
}
};
/**
* Validate input
*
* @private
* @param {string} value Field value
* @return {boolean}
*/
OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
const n = +value;
if ( value === '' ) {
return !this.isRequired();
}
if ( isNaN( n ) || !isFinite( n ) ) {
return false;
}
if ( this.step && Math.floor( n / this.step ) !== n / this.step ) {
return false;
}
if ( n < this.min || n > this.max ) {
return false;
}
return true;
};
/**
* Handle mouse click events.
*
* @private
* @param {number} dir +1 or -1
*/
OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
this.adjustValue( dir * this.buttonStep );
};
/**
* Handle mouse wheel events.
*
* @private
* @param {jQuery.Event} event
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
let delta = 0;
if ( this.isDisabled() || this.isReadOnly() ) {
return;
}
if ( this.$input.is( ':focus' ) ) {
// Standard 'wheel' event
if ( event.originalEvent.deltaMode !== undefined ) {
this.sawWheelEvent = true;
}
if ( event.originalEvent.deltaY ) {
delta = -event.originalEvent.deltaY;
} else if ( event.originalEvent.deltaX ) {
delta = event.originalEvent.deltaX;
}
// Non-standard events
if ( !this.sawWheelEvent ) {
if ( event.originalEvent.wheelDeltaX ) {
delta = -event.originalEvent.wheelDeltaX;
} else if ( event.originalEvent.wheelDeltaY ) {
delta = event.originalEvent.wheelDeltaY;
} else if ( event.originalEvent.wheelDelta ) {
delta = event.originalEvent.wheelDelta;
} else if ( event.originalEvent.detail ) {
delta = -event.originalEvent.detail;
}
}
if ( delta ) {
delta = delta < 0 ? -1 : 1;
this.adjustValue( delta * this.buttonStep );
}
return false;
}
};
/**
* Handle key down events.
*
* @private
* @param {jQuery.Event} e Key down event
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
if ( this.isDisabled() || this.isReadOnly() ) {
return;
}
switch ( e.which ) {
case OO.ui.Keys.UP:
this.adjustValue( this.buttonStep );
return false;
case OO.ui.Keys.DOWN:
this.adjustValue( -this.buttonStep );
return false;
case OO.ui.Keys.PAGEUP:
this.adjustValue( this.pageStep );
return false;
case OO.ui.Keys.PAGEDOWN:
this.adjustValue( -this.pageStep );
return false;
}
};
/**
* Update the disabled state of the controls
*
* @chainable
* @protected
* @return {OO.ui.NumberInputWidget} The widget, for chaining
*/
OO.ui.NumberInputWidget.prototype.updateControlsDisabled = function () {
const disabled = this.isDisabled() || this.isReadOnly();
if ( this.minusButton ) {
this.minusButton.setDisabled( disabled );
}
if ( this.plusButton ) {
this.plusButton.setDisabled( disabled );
}
return this;
};
/**
* @inheritdoc
*/
OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
// Parent method
OO.ui.NumberInputWidget.super.prototype.setDisabled.call( this, disabled );
this.updateControlsDisabled();
return this;
};
/**
* @inheritdoc
*/
OO.ui.NumberInputWidget.prototype.setReadOnly = function () {
// Parent method
OO.ui.NumberInputWidget.super.prototype.setReadOnly.apply( this, arguments );
this.updateControlsDisabled();
return this;
};