summaryrefslogtreecommitdiff
path: root/vendor/oojs/oojs-ui/src/widgets/SelectWidget.js
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/oojs/oojs-ui/src/widgets/SelectWidget.js')
-rw-r--r--vendor/oojs/oojs-ui/src/widgets/SelectWidget.js630
1 files changed, 630 insertions, 0 deletions
diff --git a/vendor/oojs/oojs-ui/src/widgets/SelectWidget.js b/vendor/oojs/oojs-ui/src/widgets/SelectWidget.js
new file mode 100644
index 00000000..91172af0
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/widgets/SelectWidget.js
@@ -0,0 +1,630 @@
+/**
+ * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
+ * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
+ * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
+ * menu selects}.
+ *
+ * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
+ * information, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example of a select widget with three options
+ * var select = new OO.ui.SelectWidget( {
+ * items: [
+ * new OO.ui.OptionWidget( {
+ * data: 'a',
+ * label: 'Option One',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'b',
+ * label: 'Option Two',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'c',
+ * label: 'Option Three',
+ * } )
+ * ]
+ * } );
+ * $( 'body' ).append( select.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
+ * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
+ * the [OOjs UI documentation on MediaWiki] [2] for examples.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ */
+OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.SelectWidget.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ // Properties
+ this.pressed = false;
+ this.selecting = null;
+ this.onMouseUpHandler = this.onMouseUp.bind( this );
+ this.onMouseMoveHandler = this.onMouseMove.bind( this );
+ this.onKeyDownHandler = this.onKeyDown.bind( this );
+
+ // Events
+ this.$element.on( {
+ mousedown: this.onMouseDown.bind( this ),
+ mouseover: this.onMouseOver.bind( this ),
+ mouseleave: this.onMouseLeave.bind( this )
+ } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
+ .attr( 'role', 'listbox' );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
+
+// Need to mixin base class as well
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
+
+/* Events */
+
+/**
+ * @event highlight
+ *
+ * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
+ *
+ * @param {OO.ui.OptionWidget|null} item Highlighted item
+ */
+
+/**
+ * @event press
+ *
+ * A `press` event is emitted when the #pressItem method is used to programmatically modify the
+ * pressed state of an option.
+ *
+ * @param {OO.ui.OptionWidget|null} item Pressed item
+ */
+
+/**
+ * @event select
+ *
+ * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
+ *
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+
+/**
+ * @event choose
+ * A `choose` event is emitted when an item is chosen with the #chooseItem method.
+ * @param {OO.ui.OptionWidget} item Chosen item
+ */
+
+/**
+ * @event add
+ *
+ * An `add` event is emitted when options are added to the select with the #addItems method.
+ *
+ * @param {OO.ui.OptionWidget[]} items Added items
+ * @param {number} index Index of insertion point
+ */
+
+/**
+ * @event remove
+ *
+ * A `remove` event is emitted when options are removed from the select with the #clearItems
+ * or #removeItems methods.
+ *
+ * @param {OO.ui.OptionWidget[]} items Removed items
+ */
+
+/* Methods */
+
+/**
+ * Handle mouse down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
+ var item;
+
+ if ( !this.isDisabled() && e.which === 1 ) {
+ this.togglePressed( true );
+ item = this.getTargetItem( e );
+ if ( item && item.isSelectable() ) {
+ this.pressItem( item );
+ this.selecting = item;
+ this.getElementDocument().addEventListener(
+ 'mouseup',
+ this.onMouseUpHandler,
+ true
+ );
+ this.getElementDocument().addEventListener(
+ 'mousemove',
+ this.onMouseMoveHandler,
+ true
+ );
+ }
+ }
+ return false;
+};
+
+/**
+ * Handle mouse up events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
+ var item;
+
+ this.togglePressed( false );
+ if ( !this.selecting ) {
+ item = this.getTargetItem( e );
+ if ( item && item.isSelectable() ) {
+ this.selecting = item;
+ }
+ }
+ if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
+ this.pressItem( null );
+ this.chooseItem( this.selecting );
+ this.selecting = null;
+ }
+
+ this.getElementDocument().removeEventListener(
+ 'mouseup',
+ this.onMouseUpHandler,
+ true
+ );
+ this.getElementDocument().removeEventListener(
+ 'mousemove',
+ this.onMouseMoveHandler,
+ true
+ );
+
+ return false;
+};
+
+/**
+ * Handle mouse move events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse move event
+ */
+OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
+ var item;
+
+ if ( !this.isDisabled() && this.pressed ) {
+ item = this.getTargetItem( e );
+ if ( item && item !== this.selecting && item.isSelectable() ) {
+ this.pressItem( item );
+ this.selecting = item;
+ }
+ }
+ return false;
+};
+
+/**
+ * Handle mouse over events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse over event
+ */
+OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
+ var item;
+
+ if ( !this.isDisabled() ) {
+ item = this.getTargetItem( e );
+ this.highlightItem( item && item.isHighlightable() ? item : null );
+ }
+ return false;
+};
+
+/**
+ * Handle mouse leave events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse over event
+ */
+OO.ui.SelectWidget.prototype.onMouseLeave = function () {
+ if ( !this.isDisabled() ) {
+ this.highlightItem( null );
+ }
+ return false;
+};
+
+/**
+ * Handle key down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
+ var nextItem,
+ handled = false,
+ currentItem = this.getHighlightedItem() || this.getSelectedItem();
+
+ if ( !this.isDisabled() && this.isVisible() ) {
+ switch ( e.keyCode ) {
+ case OO.ui.Keys.ENTER:
+ if ( currentItem && currentItem.constructor.static.highlightable ) {
+ // Was only highlighted, now let's select it. No-op if already selected.
+ this.chooseItem( currentItem );
+ handled = true;
+ }
+ break;
+ case OO.ui.Keys.UP:
+ case OO.ui.Keys.LEFT:
+ nextItem = this.getRelativeSelectableItem( currentItem, -1 );
+ handled = true;
+ break;
+ case OO.ui.Keys.DOWN:
+ case OO.ui.Keys.RIGHT:
+ nextItem = this.getRelativeSelectableItem( currentItem, 1 );
+ handled = true;
+ break;
+ case OO.ui.Keys.ESCAPE:
+ case OO.ui.Keys.TAB:
+ if ( currentItem && currentItem.constructor.static.highlightable ) {
+ currentItem.setHighlighted( false );
+ }
+ this.unbindKeyDownListener();
+ // Don't prevent tabbing away / defocusing
+ handled = false;
+ break;
+ }
+
+ if ( nextItem ) {
+ if ( nextItem.constructor.static.highlightable ) {
+ this.highlightItem( nextItem );
+ } else {
+ this.chooseItem( nextItem );
+ }
+ nextItem.scrollElementIntoView();
+ }
+
+ if ( handled ) {
+ // Can't just return false, because e is not always a jQuery event
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+};
+
+/**
+ * Bind key down listener.
+ *
+ * @protected
+ */
+OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
+ this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+};
+
+/**
+ * Unbind key down listener.
+ *
+ * @protected
+ */
+OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
+ this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+};
+
+/**
+ * Get the closest item to a jQuery.Event.
+ *
+ * @private
+ * @param {jQuery.Event} e
+ * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
+ */
+OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
+ return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
+};
+
+/**
+ * Get selected item.
+ *
+ * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
+ */
+OO.ui.SelectWidget.prototype.getSelectedItem = function () {
+ var i, len;
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ if ( this.items[ i ].isSelected() ) {
+ return this.items[ i ];
+ }
+ }
+ return null;
+};
+
+/**
+ * Get highlighted item.
+ *
+ * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
+ */
+OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
+ var i, len;
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ if ( this.items[ i ].isHighlighted() ) {
+ return this.items[ i ];
+ }
+ }
+ return null;
+};
+
+/**
+ * Toggle pressed state.
+ *
+ * Press is a state that occurs when a user mouses down on an item, but
+ * has not yet let go of the mouse. The item may appear selected, but it will not be selected
+ * until the user releases the mouse.
+ *
+ * @param {boolean} pressed An option is being pressed
+ */
+OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
+ if ( pressed === undefined ) {
+ pressed = !this.pressed;
+ }
+ if ( pressed !== this.pressed ) {
+ this.$element
+ .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
+ .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
+ this.pressed = pressed;
+ }
+};
+
+/**
+ * Highlight an option. If the `item` param is omitted, no options will be highlighted
+ * and any existing highlight will be removed. The highlight is mutually exclusive.
+ *
+ * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
+ * @fires highlight
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
+ var i, len, highlighted,
+ changed = false;
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ highlighted = this.items[ i ] === item;
+ if ( this.items[ i ].isHighlighted() !== highlighted ) {
+ this.items[ i ].setHighlighted( highlighted );
+ changed = true;
+ }
+ }
+ if ( changed ) {
+ this.emit( 'highlight', item );
+ }
+
+ return this;
+};
+
+/**
+ * Programmatically select an option by its data. If the `data` parameter is omitted,
+ * or if the item does not exist, all options will be deselected.
+ *
+ * @param {Object|string} [data] Value of the item to select, omit to deselect all
+ * @fires select
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
+ var itemFromData = this.getItemFromData( data );
+ if ( data === undefined || !itemFromData ) {
+ return this.selectItem();
+ }
+ return this.selectItem( itemFromData );
+};
+
+/**
+ * Programmatically select an option by its reference. If the `item` parameter is omitted,
+ * all options will be deselected.
+ *
+ * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
+ * @fires select
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
+ var i, len, selected,
+ changed = false;
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ selected = this.items[ i ] === item;
+ if ( this.items[ i ].isSelected() !== selected ) {
+ this.items[ i ].setSelected( selected );
+ changed = true;
+ }
+ }
+ if ( changed ) {
+ this.emit( 'select', item );
+ }
+
+ return this;
+};
+
+/**
+ * Press an item.
+ *
+ * Press is a state that occurs when a user mouses down on an item, but has not
+ * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
+ * releases the mouse.
+ *
+ * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
+ * @fires press
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
+ var i, len, pressed,
+ changed = false;
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ pressed = this.items[ i ] === item;
+ if ( this.items[ i ].isPressed() !== pressed ) {
+ this.items[ i ].setPressed( pressed );
+ changed = true;
+ }
+ }
+ if ( changed ) {
+ this.emit( 'press', item );
+ }
+
+ return this;
+};
+
+/**
+ * Choose an item.
+ *
+ * Note that ‘choose’ should never be modified programmatically. A user can choose
+ * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
+ * use the #selectItem method.
+ *
+ * This method is identical to #selectItem, but may vary in subclasses that take additional action
+ * when users choose an item with the keyboard or mouse.
+ *
+ * @param {OO.ui.OptionWidget} item Item to choose
+ * @fires choose
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
+ this.selectItem( item );
+ this.emit( 'choose', item );
+
+ return this;
+};
+
+/**
+ * Get an option by its position relative to the specified item (or to the start of the option array,
+ * if item is `null`). The direction in which to search through the option array is specified with a
+ * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
+ * `null` if there are no options in the array.
+ *
+ * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
+ * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
+ * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
+ */
+OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
+ var currentIndex, nextIndex, i,
+ increase = direction > 0 ? 1 : -1,
+ len = this.items.length;
+
+ if ( item instanceof OO.ui.OptionWidget ) {
+ currentIndex = $.inArray( item, this.items );
+ nextIndex = ( currentIndex + increase + len ) % len;
+ } else {
+ // If no item is selected and moving forward, start at the beginning.
+ // If moving backward, start at the end.
+ nextIndex = direction > 0 ? 0 : len - 1;
+ }
+
+ for ( i = 0; i < len; i++ ) {
+ item = this.items[ nextIndex ];
+ if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
+ return item;
+ }
+ nextIndex = ( nextIndex + increase + len ) % len;
+ }
+ return null;
+};
+
+/**
+ * Get the next selectable item or `null` if there are no selectable items.
+ * Disabled options and menu-section markers and breaks are not selectable.
+ *
+ * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
+ */
+OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
+ var i, len, item;
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[ i ];
+ if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
+ return item;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Add an array of options to the select. Optionally, an index number can be used to
+ * specify an insertion point.
+ *
+ * @param {OO.ui.OptionWidget[]} items Items to add
+ * @param {number} [index] Index to insert items after
+ * @fires add
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
+ // Mixin method
+ OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
+
+ // Always provide an index, even if it was omitted
+ this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
+
+ return this;
+};
+
+/**
+ * Remove the specified array of options from the select. Options will be detached
+ * from the DOM, not removed, so they can be reused later. To remove all options from
+ * the select, you may wish to use the #clearItems method instead.
+ *
+ * @param {OO.ui.OptionWidget[]} items Items to remove
+ * @fires remove
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
+ var i, len, item;
+
+ // Deselect items being removed
+ for ( i = 0, len = items.length; i < len; i++ ) {
+ item = items[ i ];
+ if ( item.isSelected() ) {
+ this.selectItem( null );
+ }
+ }
+
+ // Mixin method
+ OO.ui.GroupWidget.prototype.removeItems.call( this, items );
+
+ this.emit( 'remove', items );
+
+ return this;
+};
+
+/**
+ * Clear all options from the select. Options will be detached from the DOM, not removed,
+ * so that they can be reused later. To remove a subset of options from the select, use
+ * the #removeItems method.
+ *
+ * @fires remove
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.clearItems = function () {
+ var items = this.items.slice();
+
+ // Mixin method
+ OO.ui.GroupWidget.prototype.clearItems.call( this );
+
+ // Clear selection
+ this.selectItem( null );
+
+ this.emit( 'remove', items );
+
+ return this;
+};