/**
* ButtonElement is often mixed into other classes to generate a button, which is a clickable
* interface element that can be configured with access keys for keyboard interaction.
* See the [OOUI documentation on MediaWiki][1] for examples.
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
*
* @abstract
* @class
*
* @constructor
* @param {Object} [config] Configuration options
* @param {jQuery} [config.$button] The button element created by the class.
* If this configuration is omitted, the button element will use a generated `<a>`.
* @param {boolean} [config.framed=true] Render the button with a frame
*/
OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
// Configuration initialization
config = config || {};
// Properties
this.$button = null;
this.framed = null;
this.active = config.active !== undefined && config.active;
this.onDocumentMouseUpHandler = this.onDocumentMouseUp.bind( this );
this.onMouseDownHandler = this.onMouseDown.bind( this );
this.onDocumentKeyUpHandler = this.onDocumentKeyUp.bind( this );
this.onKeyDownHandler = this.onKeyDown.bind( this );
this.onClickHandler = this.onClick.bind( this );
this.onKeyPressHandler = this.onKeyPress.bind( this );
// Initialization
this.$element.addClass( 'oo-ui-buttonElement' );
this.toggleFramed( config.framed === undefined || config.framed );
this.setButtonElement( config.$button || $( '<a>' ) );
};
/* Setup */
OO.initClass( OO.ui.mixin.ButtonElement );
/* Static Properties */
/**
* Cancel mouse down events.
*
* This property is usually set to `true` to prevent the focus from changing when the button is
* clicked.
* Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and
* {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} use a value of `false` so that dragging
* behavior is possible and mousedown events can be handled by a parent widget.
*
* @static
* @property {boolean}
*/
OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
/* Events */
/**
* A 'click' event is emitted when the button element is clicked.
*
* @event OO.ui.mixin.ButtonElement#click
*/
/* Methods */
/**
* Set the button element.
*
* This method is used to retarget a button mixin so that its functionality applies to
* the specified button element instead of the one created by the class. If a button element
* is already set, the method will remove the mixin’s effect on that element.
*
* @param {jQuery} $button Element to use as button
*/
OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
if ( this.$button ) {
this.$button
.removeClass( 'oo-ui-buttonElement-button' )
.removeAttr( 'role accesskey' )
.off( {
mousedown: this.onMouseDownHandler,
keydown: this.onKeyDownHandler,
click: this.onClickHandler,
keypress: this.onKeyPressHandler
} );
}
this.$button = $button
.addClass( 'oo-ui-buttonElement-button' )
.on( {
mousedown: this.onMouseDownHandler,
keydown: this.onKeyDownHandler,
click: this.onClickHandler,
keypress: this.onKeyPressHandler
} );
// Add `role="button"` on `<a>` elements, where it's needed
// `toUpperCase()` is added for XHTML documents
if ( this.$button.prop( 'tagName' ).toUpperCase() === 'A' ) {
this.$button.attr( 'role', 'button' );
}
};
/**
* Handles mouse down events.
*
* @protected
* @param {jQuery.Event} e Mouse down event
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
return;
}
this.$element.addClass( 'oo-ui-buttonElement-pressed' );
// Run the mouseup handler no matter where the mouse is when the button is let go, so we can
// reliably remove the pressed class
this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );
// Prevent change of focus unless specifically configured otherwise
if ( this.constructor.static.cancelButtonMouseDownEvents ) {
return false;
}
};
/**
* Handles document mouse up events.
*
* @protected
* @param {MouseEvent} e Mouse up event
*/
OO.ui.mixin.ButtonElement.prototype.onDocumentMouseUp = function ( e ) {
if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
return;
}
this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
// Stop listening for mouseup, since we only needed this once
this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );
};
/**
* Handles mouse click events.
*
* @protected
* @param {jQuery.Event} e Mouse click event
* @fires OO.ui.mixin.ButtonElement#click
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
if ( this.emit( 'click' ) ) {
return false;
}
}
};
/**
* Handles key down events.
*
* @protected
* @param {jQuery.Event} e Key down event
*/
OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
return;
}
this.$element.addClass( 'oo-ui-buttonElement-pressed' );
// Run the keyup handler no matter where the key is when the button is let go, so we can
// reliably remove the pressed class
this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler, true );
};
/**
* Handles document key up events.
*
* @protected
* @param {KeyboardEvent} e Key up event
*/
OO.ui.mixin.ButtonElement.prototype.onDocumentKeyUp = function ( e ) {
if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
return;
}
this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
// Stop listening for keyup, since we only needed this once
this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler, true );
};
/**
* Handles key press events.
*
* @protected
* @param {jQuery.Event} e Key press event
* @fires OO.ui.mixin.ButtonElement#click
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
if ( this.emit( 'click' ) ) {
return false;
}
}
};
/**
* Check if button has a frame.
*
* @return {boolean} Button is framed
*/
OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
return this.framed;
};
/**
* Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame
* on and off.
*
* @param {boolean} [framed] Make button framed, omit to toggle
* @chainable
* @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
framed = framed === undefined ? !this.framed : !!framed;
if ( framed !== this.framed ) {
this.framed = framed;
this.$element
.toggleClass( 'oo-ui-buttonElement-frameless', !framed )
.toggleClass( 'oo-ui-buttonElement-framed', framed );
this.updateThemeClasses();
}
return this;
};
/**
* Set the button's active state.
*
* The active state can be set on:
*
* - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
* - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
* - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
*
* @protected
* @param {boolean} [value=false] Make button active
* @chainable
* @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
this.active = !!value;
this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
this.updateThemeClasses();
return this;
};
/**
* Check if the button is active
*
* @protected
* @return {boolean} The button is active
*/
OO.ui.mixin.ButtonElement.prototype.isActive = function () {
return this.active;
};