/**
* Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute
* {@link OO.ui.Toolbar toolbars}.
* Each tool is configured with a static name, title, and icon and is customized with the command
* to carry out when the tool is selected. Tools must also be registered with a
* {@link OO.ui.ToolFactory tool factory}, which creates the tools on demand.
*
* Every Tool subclass must implement two methods:
*
* - {@link OO.ui.Tool#onUpdateState onUpdateState}
* - {@link OO.ui.Tool#onSelect onSelect}
*
* Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
* {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which
* determine how the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an
* example.
*
* For more information, please see the [OOUI documentation on MediaWiki][1].
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @abstract
* @class
* @extends OO.ui.Widget
* @mixes OO.ui.mixin.IconElement
* @mixes OO.ui.mixin.FlaggedElement
* @mixes OO.ui.mixin.TabIndexedElement
*
* @constructor
* @param {OO.ui.ToolGroup} toolGroup
* @param {Object} [config] Configuration options
* @param {string|Function} [config.title] Title text or a function that returns text. If this config is
* omitted, the value of the {@link OO.ui.Tool.static.title static title} property is used.
* @param {boolean} [config.displayBothIconAndLabel] See static.displayBothIconAndLabel
* @param {Object} [config.narrowConfig] See static.narrowConfig
*
* The title is used in different ways depending on the type of toolgroup that contains the tool.
* The title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar}
* toolgroup, or as the label text if the tool is part of a {@link OO.ui.ListToolGroup list} or
* {@link OO.ui.MenuToolGroup menu} toolgroup.
*
* For bar toolgroups, a description of the accelerator key is appended to the title if an
* accelerator key is associated with an action by the same name as the tool and accelerator
* functionality has been added to the application.
* To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the
* {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
*/
OO.ui.Tool = function OoUiTool( toolGroup, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
config = toolGroup;
toolGroup = config.toolGroup;
}
// Configuration initialization
config = config || {};
// Parent constructor
OO.ui.Tool.super.call( this, config );
// Properties
this.toolGroup = toolGroup;
this.toolbar = this.toolGroup.getToolbar();
this.active = false;
this.$title = $( '<span>' );
this.$accel = $( '<span>' );
this.$link = $( '<a>' );
this.title = null;
this.checkIcon = new OO.ui.IconWidget( {
icon: 'check',
classes: [ 'oo-ui-tool-checkIcon' ]
} );
this.displayBothIconAndLabel = config.displayBothIconAndLabel !== undefined ?
config.displayBothIconAndLabel : this.constructor.static.displayBothIconAndLabel;
this.narrowConfig = config.narrowConfig || this.constructor.static.narrowConfig;
// Mixin constructors
OO.ui.mixin.IconElement.call( this, config );
OO.ui.mixin.FlaggedElement.call( this, config );
OO.ui.mixin.TabIndexedElement.call( this, Object.assign( {
$tabIndexed: this.$link
}, config ) );
// Events
this.toolbar.connect( this, {
updateState: 'onUpdateState',
resize: 'onToolbarResize'
} );
// Initialization
this.$title.addClass( 'oo-ui-tool-title' );
this.$accel
.addClass( 'oo-ui-tool-accel' )
.prop( {
// This may need to be changed if the key names are ever localized,
// but for now they are essentially written in English
dir: 'ltr',
lang: 'en'
} );
this.$link
.addClass( 'oo-ui-tool-link' )
.append( this.checkIcon.$element, this.$icon, this.$title, this.$accel )
.attr( 'role', 'button' );
// Don't show keyboard shortcuts on mobile as users are unlikely to have
// a physical keyboard, and likely to have limited screen space.
if ( !OO.ui.isMobile() ) {
this.$link.append( this.$accel );
}
this.$element
.data( 'oo-ui-tool', this )
.addClass( 'oo-ui-tool' )
.addClass( 'oo-ui-tool-name-' +
this.constructor.static.name.replace( /^([^/]+)\/([^/]+).*$/, '$1-$2' ) )
.append( this.$link );
this.setTitle( config.title || this.constructor.static.title );
};
/* Setup */
OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
/* Static Properties */
/**
* @static
* @inheritdoc
*/
OO.ui.Tool.static.tagName = 'span';
/**
* Symbolic name of tool.
*
* The symbolic name is used internally to register the tool with a
* {@link OO.ui.ToolFactory ToolFactory}. It can also be used when adding tools to toolgroups.
*
* @abstract
* @static
* @property {string}
*/
OO.ui.Tool.static.name = '';
/**
* Symbolic name of the group.
*
* The group name is used to associate tools with each other so that they can be selected later by
* a {@link OO.ui.ToolGroup toolgroup}.
*
* @abstract
* @static
* @property {string}
*/
OO.ui.Tool.static.group = '';
/**
* Tool title text or a function that returns title text. The value of the static property is
* overridden if the #title config option is used.
*
* @abstract
* @static
* @property {string|Function}
*/
OO.ui.Tool.static.title = '';
/**
* Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
* Normally only the icon is displayed, or only the label if no icon is given.
*
* @static
* @property {boolean}
*/
OO.ui.Tool.static.displayBothIconAndLabel = false;
/**
* Add tool to catch-all groups automatically.
*
* A catch-all group, which contains all tools that do not currently belong to a toolgroup,
* can be included in a toolgroup using the wildcard selector, an asterisk (*).
*
* @static
* @property {boolean}
*/
OO.ui.Tool.static.autoAddToCatchall = true;
/**
* Add tool to named groups automatically.
*
* By default, tools that are configured with a static ‘group’ property are added
* to that group and will be selected when the symbolic name of the group is specified (e.g., when
* toolgroups include tools by group name).
*
* @static
* @property {boolean}
*/
OO.ui.Tool.static.autoAddToGroup = true;
/**
* Check if this tool is compatible with given data.
*
* This is a stub that can be overridden to provide support for filtering tools based on an
* arbitrary piece of information (e.g., where the cursor is in a document). The implementation
* must also call this method so that the compatibility check can be performed.
*
* @static
* @param {any} data Data to check
* @return {boolean} Tool can be used with data
*/
OO.ui.Tool.static.isCompatibleWith = function () {
return false;
};
/**
* Config options to change when toolbar is in narrow mode
*
* Supports `displayBothIconAndLabel`, `title` and `icon` properties.
*
* @static
* @property {Object|null}
*/
OO.ui.Tool.static.narrowConfig = null;
/* Methods */
/**
* Handle the toolbar state being updated. This method is called when the
* {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
* {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
* depending on application state (usually by calling #setDisabled to enable or disable the tool,
* or #setActive to mark is as currently in-use or not).
*
* This is an abstract method that must be overridden in a concrete subclass.
*
* @method
* @protected
* @abstract
*/
OO.ui.Tool.prototype.onUpdateState = null;
/**
* Handle the tool being selected. This method is called when the user triggers this tool,
* usually by clicking on its label/icon.
*
* This is an abstract method that must be overridden in a concrete subclass.
*
* @method
* @protected
* @abstract
*/
OO.ui.Tool.prototype.onSelect = null;
/**
* Check if the tool is active.
*
* Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
* with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
*
* @return {boolean} Tool is active
*/
OO.ui.Tool.prototype.isActive = function () {
return this.active;
};
/**
* Make the tool appear active or inactive.
*
* This method should be called within #onSelect or #onUpdateState event handlers to make the tool
* appear pressed or not.
*
* @param {boolean} [state=false] Make tool appear active
*/
OO.ui.Tool.prototype.setActive = function ( state ) {
this.active = !!state;
this.$element.toggleClass( 'oo-ui-tool-active', this.active );
this.updateThemeClasses();
};
/**
* Set the tool #title.
*
* @param {string|Function} title Title text or a function that returns text
* @chainable
* @return {OO.ui.Tool} The tool, for chaining
*/
OO.ui.Tool.prototype.setTitle = function ( title ) {
this.title = OO.ui.resolveMsg( title );
this.updateTitle();
// Update classes
this.setDisplayBothIconAndLabel( this.displayBothIconAndLabel );
return this;
};
/**
* Set the tool's displayBothIconAndLabel state.
*
* Update title classes if necessary
*
* @param {boolean} displayBothIconAndLabel
* @chainable
* @return {OO.ui.Tool} The tool, for chaining
*/
OO.ui.Tool.prototype.setDisplayBothIconAndLabel = function ( displayBothIconAndLabel ) {
this.displayBothIconAndLabel = displayBothIconAndLabel;
this.$element.toggleClass( 'oo-ui-tool-with-label', !!this.title && this.displayBothIconAndLabel );
return this;
};
/**
* Get the tool #title.
*
* @return {string} Title text
*/
OO.ui.Tool.prototype.getTitle = function () {
return this.title;
};
/**
* Get the tool's symbolic name.
*
* @return {string} Symbolic name of tool
*/
OO.ui.Tool.prototype.getName = function () {
return this.constructor.static.name;
};
/**
* Handle resize events from the toolbar
*/
OO.ui.Tool.prototype.onToolbarResize = function () {
if ( !this.narrowConfig ) {
return;
}
if ( this.toolbar.isNarrow() ) {
if ( this.narrowConfig.displayBothIconAndLabel !== undefined ) {
this.wideDisplayBothIconAndLabel = this.displayBothIconAndLabel;
this.setDisplayBothIconAndLabel( this.narrowConfig.displayBothIconAndLabel );
}
if ( this.narrowConfig.title !== undefined ) {
this.wideTitle = this.title;
this.setTitle( this.narrowConfig.title );
}
if ( this.narrowConfig.icon !== undefined ) {
this.wideIcon = this.icon;
this.setIcon( this.narrowConfig.icon );
}
} else {
if ( this.wideDisplayBothIconAndLabel !== undefined ) {
this.setDisplayBothIconAndLabel( this.wideDisplayBothIconAndLabel );
}
if ( this.wideTitle !== undefined ) {
this.setTitle( this.wideTitle );
}
if ( this.wideIcon !== undefined ) {
this.setIcon( this.wideIcon );
}
}
};
/**
* Update the title.
*/
OO.ui.Tool.prototype.updateTitle = function () {
const titleTooltips = this.toolGroup.constructor.static.titleTooltips,
accelTooltips = this.toolGroup.constructor.static.accelTooltips,
accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
tooltipParts = [];
this.$title.text( this.title );
this.$accel.text( accel );
if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
tooltipParts.push( this.title );
}
if ( accelTooltips && typeof accel === 'string' && accel.length ) {
tooltipParts.push( accel );
}
if ( tooltipParts.length ) {
this.$link.attr( 'title', tooltipParts.join( ' ' ) );
} else {
this.$link.removeAttr( 'title' );
}
};
/**
* @inheritdoc OO.ui.mixin.IconElement
*/
OO.ui.Tool.prototype.setIcon = function ( icon ) {
// Mixin method
OO.ui.mixin.IconElement.prototype.setIcon.call( this, icon );
this.$element.toggleClass( 'oo-ui-tool-with-icon', !!this.icon );
return this;
};
/**
* Destroy tool.
*
* Destroying the tool removes all event handlers and the tool’s DOM elements.
* Call this method whenever you are done using a tool.
*/
OO.ui.Tool.prototype.destroy = function () {
this.toolbar.disconnect( this );
this.$element.remove();
};