diff options
Diffstat (limited to 'vendor/oojs/oojs-ui/src/elements')
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/ButtonElement.js | 263 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/ClippableElement.js | 205 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/DraggableElement.js | 142 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/DraggableGroupElement.js | 261 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/FlaggedElement.js | 209 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/GroupElement.js | 290 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/IconElement.js | 187 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/IndicatorElement.js | 168 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/LabelElement.js | 152 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/LookupElement.js | 352 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/PendingElement.js | 84 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/PopupElement.js | 36 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/TabIndexedElement.js | 138 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/elements/TitledElement.js | 106 |
14 files changed, 2593 insertions, 0 deletions
diff --git a/vendor/oojs/oojs-ui/src/elements/ButtonElement.js b/vendor/oojs/oojs-ui/src/elements/ButtonElement.js new file mode 100644 index 00000000..6d338ed7 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/ButtonElement.js @@ -0,0 +1,263 @@ +/** + * 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 accessibility. + * See the [OOjs UI documentation on MediaWiki] [1] for examples. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$button] The button element created by the class. + * If this configuration is omitted, the button element will use a generated `<a>`. + * @cfg {boolean} [framed=true] Render the button with a frame + * @cfg {string} [accessKey] Button's access key + */ +OO.ui.ButtonElement = function OoUiButtonElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$button = null; + this.framed = null; + this.accessKey = null; + this.active = false; + this.onMouseUpHandler = this.onMouseUp.bind( this ); + this.onMouseDownHandler = this.onMouseDown.bind( this ); + this.onKeyDownHandler = this.onKeyDown.bind( this ); + this.onKeyUpHandler = this.onKeyUp.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.setAccessKey( config.accessKey ); + this.setButtonElement( config.$button || $( '<a>' ) ); +}; + +/* Setup */ + +OO.initClass( OO.ui.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.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 + * @inheritable + * @property {boolean} + */ +OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true; + +/* Events */ + +/** + * A 'click' event is emitted when the button element is clicked. + * + * @event 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.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' ) + .attr( { role: 'button', accesskey: this.accessKey } ) + .on( { + mousedown: this.onMouseDownHandler, + keydown: this.onKeyDownHandler, + click: this.onClickHandler, + keypress: this.onKeyPressHandler + } ); +}; + +/** + * Handles mouse down events. + * + * @protected + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) { + if ( this.isDisabled() || e.which !== 1 ) { + 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.onMouseUpHandler, true ); + // Prevent change of focus unless specifically configured otherwise + if ( this.constructor.static.cancelButtonMouseDownEvents ) { + return false; + } +}; + +/** + * Handles mouse up events. + * + * @protected + * @param {jQuery.Event} e Mouse up event + */ +OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) { + if ( this.isDisabled() || e.which !== 1 ) { + return; + } + this.$element.removeClass( 'oo-ui-buttonElement-pressed' ); + // Stop listening for mouseup, since we only needed this once + this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true ); +}; + +/** + * Handles mouse click events. + * + * @protected + * @param {jQuery.Event} e Mouse click event + * @fires click + */ +OO.ui.ButtonElement.prototype.onClick = function ( e ) { + if ( !this.isDisabled() && e.which === 1 ) { + if ( this.emit( 'click' ) ) { + return false; + } + } +}; + +/** + * Handles key down events. + * + * @protected + * @param {jQuery.Event} e Key down event + */ +OO.ui.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.onKeyUpHandler, true ); +}; + +/** + * Handles key up events. + * + * @protected + * @param {jQuery.Event} e Key up event + */ +OO.ui.ButtonElement.prototype.onKeyUp = 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.onKeyUpHandler, true ); +}; + +/** + * Handles key press events. + * + * @protected + * @param {jQuery.Event} e Key press event + * @fires click + */ +OO.ui.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.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 + */ +OO.ui.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 access key. + * + * @param {string} accessKey Button's access key, use empty string to remove + * @chainable + */ +OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) { + accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null; + + if ( this.accessKey !== accessKey ) { + if ( this.$button ) { + if ( accessKey !== null ) { + this.$button.attr( 'accesskey', accessKey ); + } else { + this.$button.removeAttr( 'accesskey' ); + } + } + this.accessKey = accessKey; + } + + return this; +}; + +/** + * Set the button to its 'active' state. + * + * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or + * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing + * for other button types. + * + * @param {boolean} [value] Make button active + * @chainable + */ +OO.ui.ButtonElement.prototype.setActive = function ( value ) { + this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value ); + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/ClippableElement.js b/vendor/oojs/oojs-ui/src/elements/ClippableElement.js new file mode 100644 index 00000000..33b0b234 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/ClippableElement.js @@ -0,0 +1,205 @@ +/** + * Element that can be automatically clipped to visible boundaries. + * + * Whenever the element's natural height changes, you have to call + * #clip to make sure it's still clipping correctly. + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element + */ +OO.ui.ClippableElement = function OoUiClippableElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$clippable = null; + this.clipping = false; + this.clippedHorizontally = false; + this.clippedVertically = false; + this.$clippableContainer = null; + this.$clippableScroller = null; + this.$clippableWindow = null; + this.idealWidth = null; + this.idealHeight = null; + this.onClippableContainerScrollHandler = this.clip.bind( this ); + this.onClippableWindowResizeHandler = this.clip.bind( this ); + + // Initialization + this.setClippableElement( config.$clippable || this.$element ); +}; + +/* Methods */ + +/** + * Set clippable element. + * + * If an element is already set, it will be cleaned up before setting up the new element. + * + * @param {jQuery} $clippable Element to make clippable + */ +OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) { + if ( this.$clippable ) { + this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' ); + this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } ); + OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] ); + } + + this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' ); + this.clip(); +}; + +/** + * Toggle clipping. + * + * Do not turn clipping on until after the element is attached to the DOM and visible. + * + * @param {boolean} [clipping] Enable clipping, omit to toggle + * @chainable + */ +OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) { + clipping = clipping === undefined ? !this.clipping : !!clipping; + + if ( this.clipping !== clipping ) { + this.clipping = clipping; + if ( clipping ) { + this.$clippableContainer = $( this.getClosestScrollableElementContainer() ); + // If the clippable container is the root, we have to listen to scroll events and check + // jQuery.scrollTop on the window because of browser inconsistencies + this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ? + $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) : + this.$clippableContainer; + this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler ); + this.$clippableWindow = $( this.getElementWindow() ) + .on( 'resize', this.onClippableWindowResizeHandler ); + // Initial clip after visible + this.clip(); + } else { + this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } ); + OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] ); + + this.$clippableContainer = null; + this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler ); + this.$clippableScroller = null; + this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler ); + this.$clippableWindow = null; + } + } + + return this; +}; + +/** + * Check if the element will be clipped to fit the visible area of the nearest scrollable container. + * + * @return {boolean} Element will be clipped to the visible area + */ +OO.ui.ClippableElement.prototype.isClipping = function () { + return this.clipping; +}; + +/** + * Check if the bottom or right of the element is being clipped by the nearest scrollable container. + * + * @return {boolean} Part of the element is being clipped + */ +OO.ui.ClippableElement.prototype.isClipped = function () { + return this.clippedHorizontally || this.clippedVertically; +}; + +/** + * Check if the right of the element is being clipped by the nearest scrollable container. + * + * @return {boolean} Part of the element is being clipped + */ +OO.ui.ClippableElement.prototype.isClippedHorizontally = function () { + return this.clippedHorizontally; +}; + +/** + * Check if the bottom of the element is being clipped by the nearest scrollable container. + * + * @return {boolean} Part of the element is being clipped + */ +OO.ui.ClippableElement.prototype.isClippedVertically = function () { + return this.clippedVertically; +}; + +/** + * Set the ideal size. These are the dimensions the element will have when it's not being clipped. + * + * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix + * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix + */ +OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) { + this.idealWidth = width; + this.idealHeight = height; + + if ( !this.clipping ) { + // Update dimensions + this.$clippable.css( { width: width, height: height } ); + } + // While clipping, idealWidth and idealHeight are not considered +}; + +/** + * Clip element to visible boundaries and allow scrolling when needed. Call this method when + * the element's natural height changes. + * + * Element will be clipped the bottom or right of the element is within 10px of the edge of, or + * overlapped by, the visible area of the nearest scrollable container. + * + * @chainable + */ +OO.ui.ClippableElement.prototype.clip = function () { + if ( !this.clipping ) { + // this.$clippableContainer and this.$clippableWindow are null, so the below will fail + return this; + } + + var buffer = 7, // Chosen by fair dice roll + cOffset = this.$clippable.offset(), + $container = this.$clippableContainer.is( 'html, body' ) ? + this.$clippableWindow : this.$clippableContainer, + ccOffset = $container.offset() || { top: 0, left: 0 }, + ccHeight = $container.innerHeight() - buffer, + ccWidth = $container.innerWidth() - buffer, + cHeight = this.$clippable.outerHeight() + buffer, + cWidth = this.$clippable.outerWidth() + buffer, + scrollTop = this.$clippableScroller.scrollTop(), + scrollLeft = this.$clippableScroller.scrollLeft(), + desiredWidth = cOffset.left < 0 ? + cWidth + cOffset.left : + ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left, + desiredHeight = cOffset.top < 0 ? + cHeight + cOffset.top : + ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top, + naturalWidth = this.$clippable.prop( 'scrollWidth' ), + naturalHeight = this.$clippable.prop( 'scrollHeight' ), + clipWidth = desiredWidth < naturalWidth, + clipHeight = desiredHeight < naturalHeight; + + if ( clipWidth ) { + this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } ); + } else { + this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } ); + } + if ( clipHeight ) { + this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } ); + } else { + this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } ); + } + + // If we stopped clipping in at least one of the dimensions + if ( !clipWidth || !clipHeight ) { + OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] ); + } + + this.clippedHorizontally = clipWidth; + this.clippedVertically = clipHeight; + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/DraggableElement.js b/vendor/oojs/oojs-ui/src/elements/DraggableElement.js new file mode 100644 index 00000000..9ae4d5bb --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/DraggableElement.js @@ -0,0 +1,142 @@ +/** + * DraggableElement is a mixin class used to create elements that can be clicked + * and dragged by a mouse to a new position within a group. This class must be used + * in conjunction with OO.ui.DraggableGroupElement, which provides a container for + * the draggable elements. + * + * @abstract + * @class + * + * @constructor + */ +OO.ui.DraggableElement = function OoUiDraggableElement() { + // Properties + this.index = null; + + // Initialize and events + this.$element + .attr( 'draggable', true ) + .addClass( 'oo-ui-draggableElement' ) + .on( { + dragstart: this.onDragStart.bind( this ), + dragover: this.onDragOver.bind( this ), + dragend: this.onDragEnd.bind( this ), + drop: this.onDrop.bind( this ) + } ); +}; + +OO.initClass( OO.ui.DraggableElement ); + +/* Events */ + +/** + * @event dragstart + * + * A dragstart event is emitted when the user clicks and begins dragging an item. + * @param {OO.ui.DraggableElement} item The item the user has clicked and is dragging with the mouse. + */ + +/** + * @event dragend + * A dragend event is emitted when the user drags an item and releases the mouse, + * thus terminating the drag operation. + */ + +/** + * @event drop + * A drop event is emitted when the user drags an item and then releases the mouse button + * over a valid target. + */ + +/* Static Properties */ + +/** + * @inheritdoc OO.ui.ButtonElement + */ +OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false; + +/* Methods */ + +/** + * Respond to dragstart event. + * + * @private + * @param {jQuery.Event} event jQuery event + * @fires dragstart + */ +OO.ui.DraggableElement.prototype.onDragStart = function ( e ) { + var dataTransfer = e.originalEvent.dataTransfer; + // Define drop effect + dataTransfer.dropEffect = 'none'; + dataTransfer.effectAllowed = 'move'; + // We must set up a dataTransfer data property or Firefox seems to + // ignore the fact the element is draggable. + try { + dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() ); + } catch ( err ) { + // The above is only for firefox. No need to set a catch clause + // if it fails, move on. + } + // Add dragging class + this.$element.addClass( 'oo-ui-draggableElement-dragging' ); + // Emit event + this.emit( 'dragstart', this ); + return true; +}; + +/** + * Respond to dragend event. + * + * @private + * @fires dragend + */ +OO.ui.DraggableElement.prototype.onDragEnd = function () { + this.$element.removeClass( 'oo-ui-draggableElement-dragging' ); + this.emit( 'dragend' ); +}; + +/** + * Handle drop event. + * + * @private + * @param {jQuery.Event} event jQuery event + * @fires drop + */ +OO.ui.DraggableElement.prototype.onDrop = function ( e ) { + e.preventDefault(); + this.emit( 'drop', e ); +}; + +/** + * In order for drag/drop to work, the dragover event must + * return false and stop propogation. + * + * @private + */ +OO.ui.DraggableElement.prototype.onDragOver = function ( e ) { + e.preventDefault(); +}; + +/** + * Set item index. + * Store it in the DOM so we can access from the widget drag event + * + * @private + * @param {number} Item index + */ +OO.ui.DraggableElement.prototype.setIndex = function ( index ) { + if ( this.index !== index ) { + this.index = index; + this.$element.data( 'index', index ); + } +}; + +/** + * Get item index + * + * @private + * @return {number} Item index + */ +OO.ui.DraggableElement.prototype.getIndex = function () { + return this.index; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/DraggableGroupElement.js b/vendor/oojs/oojs-ui/src/elements/DraggableGroupElement.js new file mode 100644 index 00000000..134e2953 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/DraggableGroupElement.js @@ -0,0 +1,261 @@ +/** + * DraggableGroupElement is a mixin class used to create a group element to + * contain draggable elements, which are items that can be clicked and dragged by a mouse. + * The class is used with OO.ui.DraggableElement. + * + * @abstract + * @class + * @mixins OO.ui.GroupElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation + * should match the layout of the items. Items displayed in a single row + * or in several rows should use horizontal orientation. The vertical orientation should only be + * used when the items are displayed in a single column. Defaults to 'vertical' + */ +OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.GroupElement.call( this, config ); + + // Properties + this.orientation = config.orientation || 'vertical'; + this.dragItem = null; + this.itemDragOver = null; + this.itemKeys = {}; + this.sideInsertion = ''; + + // Events + this.aggregate( { + dragstart: 'itemDragStart', + dragend: 'itemDragEnd', + drop: 'itemDrop' + } ); + this.connect( this, { + itemDragStart: 'onItemDragStart', + itemDrop: 'onItemDrop', + itemDragEnd: 'onItemDragEnd' + } ); + this.$element.on( { + dragover: $.proxy( this.onDragOver, this ), + dragleave: $.proxy( this.onDragLeave, this ) + } ); + + // Initialize + if ( Array.isArray( config.items ) ) { + this.addItems( config.items ); + } + this.$placeholder = $( '<div>' ) + .addClass( 'oo-ui-draggableGroupElement-placeholder' ); + this.$element + .addClass( 'oo-ui-draggableGroupElement' ) + .append( this.$status ) + .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' ) + .prepend( this.$placeholder ); +}; + +/* Setup */ +OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement ); + +/* Events */ + +/** + * A 'reorder' event is emitted when the order of items in the group changes. + * + * @event reorder + * @param {OO.ui.DraggableElement} item Reordered item + * @param {number} [newIndex] New index for the item + */ + +/* Methods */ + +/** + * Respond to item drag start event + * + * @private + * @param {OO.ui.DraggableElement} item Dragged item + */ +OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) { + var i, len; + + // Map the index of each object + for ( i = 0, len = this.items.length; i < len; i++ ) { + this.items[ i ].setIndex( i ); + } + + if ( this.orientation === 'horizontal' ) { + // Set the height of the indicator + this.$placeholder.css( { + height: item.$element.outerHeight(), + width: 2 + } ); + } else { + // Set the width of the indicator + this.$placeholder.css( { + height: 2, + width: item.$element.outerWidth() + } ); + } + this.setDragItem( item ); +}; + +/** + * Respond to item drag end event + * + * @private + */ +OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () { + this.unsetDragItem(); + return false; +}; + +/** + * Handle drop event and switch the order of the items accordingly + * + * @private + * @param {OO.ui.DraggableElement} item Dropped item + * @fires reorder + */ +OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) { + var toIndex = item.getIndex(); + // Check if the dropped item is from the current group + // TODO: Figure out a way to configure a list of legally droppable + // elements even if they are not yet in the list + if ( this.getDragItem() ) { + // If the insertion point is 'after', the insertion index + // is shifted to the right (or to the left in RTL, hence 'after') + if ( this.sideInsertion === 'after' ) { + toIndex++; + } + // Emit change event + this.emit( 'reorder', this.getDragItem(), toIndex ); + } + this.unsetDragItem(); + // Return false to prevent propogation + return false; +}; + +/** + * Handle dragleave event. + * + * @private + */ +OO.ui.DraggableGroupElement.prototype.onDragLeave = function () { + // This means the item was dragged outside the widget + this.$placeholder + .css( 'left', 0 ) + .addClass( 'oo-ui-element-hidden' ); +}; + +/** + * Respond to dragover event + * + * @private + * @param {jQuery.Event} event Event details + */ +OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) { + var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect, + itemSize, cssOutput, dragPosition, itemIndex, itemPosition, + clientX = e.originalEvent.clientX, + clientY = e.originalEvent.clientY; + + // Get the OptionWidget item we are dragging over + dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY ); + $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' ); + if ( $optionWidget[ 0 ] ) { + itemOffset = $optionWidget.offset(); + itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect(); + itemPosition = $optionWidget.position(); + itemIndex = $optionWidget.data( 'index' ); + } + + if ( + itemOffset && + this.isDragging() && + itemIndex !== this.getDragItem().getIndex() + ) { + if ( this.orientation === 'horizontal' ) { + // Calculate where the mouse is relative to the item width + itemSize = itemBoundingRect.width; + itemMidpoint = itemBoundingRect.left + itemSize / 2; + dragPosition = clientX; + // Which side of the item we hover over will dictate + // where the placeholder will appear, on the left or + // on the right + cssOutput = { + left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize, + top: itemPosition.top + }; + } else { + // Calculate where the mouse is relative to the item height + itemSize = itemBoundingRect.height; + itemMidpoint = itemBoundingRect.top + itemSize / 2; + dragPosition = clientY; + // Which side of the item we hover over will dictate + // where the placeholder will appear, on the top or + // on the bottom + cssOutput = { + top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize, + left: itemPosition.left + }; + } + // Store whether we are before or after an item to rearrange + // For horizontal layout, we need to account for RTL, as this is flipped + if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) { + this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before'; + } else { + this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after'; + } + // Add drop indicator between objects + this.$placeholder + .css( cssOutput ) + .removeClass( 'oo-ui-element-hidden' ); + } else { + // This means the item was dragged outside the widget + this.$placeholder + .css( 'left', 0 ) + .addClass( 'oo-ui-element-hidden' ); + } + // Prevent default + e.preventDefault(); +}; + +/** + * Set a dragged item + * + * @param {OO.ui.DraggableElement} item Dragged item + */ +OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) { + this.dragItem = item; +}; + +/** + * Unset the current dragged item + */ +OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () { + this.dragItem = null; + this.itemDragOver = null; + this.$placeholder.addClass( 'oo-ui-element-hidden' ); + this.sideInsertion = ''; +}; + +/** + * Get the item that is currently being dragged. + * + * @return {OO.ui.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged + */ +OO.ui.DraggableGroupElement.prototype.getDragItem = function () { + return this.dragItem; +}; + +/** + * Check if an item in the group is currently being dragged. + * + * @return {Boolean} Item is being dragged + */ +OO.ui.DraggableGroupElement.prototype.isDragging = function () { + return this.getDragItem() !== null; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/FlaggedElement.js b/vendor/oojs/oojs-ui/src/elements/FlaggedElement.js new file mode 100644 index 00000000..7050f696 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/FlaggedElement.js @@ -0,0 +1,209 @@ +/** + * The FlaggedElement class is an attribute mixin, meaning that it is used to add + * additional functionality to an element created by another class. The class provides + * a ‘flags’ property assigned the name (or an array of names) of styling flags, + * which are used to customize the look and feel of a widget to better describe its + * importance and functionality. + * + * The library currently contains the following styling flags for general use: + * + * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process. + * - **destructive**: Destructive styling is applied to convey that the widget will remove something. + * - **constructive**: Constructive styling is applied to convey that the widget will create something. + * + * The flags affect the appearance of the buttons: + * + * @example + * // FlaggedElement is mixed into ButtonWidget to provide styling flags + * var button1 = new OO.ui.ButtonWidget( { + * label: 'Constructive', + * flags: 'constructive' + * } ); + * var button2 = new OO.ui.ButtonWidget( { + * label: 'Destructive', + * flags: 'destructive' + * } ); + * var button3 = new OO.ui.ButtonWidget( { + * label: 'Progressive', + * flags: 'progressive' + * } ); + * $( 'body' ).append( button1.$element, button2.$element, button3.$element ); + * + * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**. + * Please see the [OOjs UI documentation on MediaWiki] [1] for more information. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply. + * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged + * @cfg {jQuery} [$flagged] The flagged element. By default, + * the flagged functionality is applied to the element created by the class ($element). + * If a different element is specified, the flagged functionality will be applied to it instead. + */ +OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.flags = {}; + this.$flagged = null; + + // Initialization + this.setFlags( config.flags ); + this.setFlaggedElement( config.$flagged || this.$element ); +}; + +/* Events */ + +/** + * @event flag + * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes` + * parameter contains the name of each modified flag and indicates whether it was + * added or removed. + * + * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates + * that the flag was added, `false` that the flag was removed. + */ + +/* Methods */ + +/** + * Set the flagged element. + * + * This method is used to retarget a flagged mixin so that its functionality applies to the specified element. + * If an element is already set, the method will remove the mixin’s effect on that element. + * + * @param {jQuery} $flagged Element that should be flagged + */ +OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) { + var classNames = Object.keys( this.flags ).map( function ( flag ) { + return 'oo-ui-flaggedElement-' + flag; + } ).join( ' ' ); + + if ( this.$flagged ) { + this.$flagged.removeClass( classNames ); + } + + this.$flagged = $flagged.addClass( classNames ); +}; + +/** + * Check if the specified flag is set. + * + * @param {string} flag Name of flag + * @return {boolean} The flag is set + */ +OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) { + return flag in this.flags; +}; + +/** + * Get the names of all flags set. + * + * @return {string[]} Flag names + */ +OO.ui.FlaggedElement.prototype.getFlags = function () { + return Object.keys( this.flags ); +}; + +/** + * Clear all flags. + * + * @chainable + * @fires flag + */ +OO.ui.FlaggedElement.prototype.clearFlags = function () { + var flag, className, + changes = {}, + remove = [], + classPrefix = 'oo-ui-flaggedElement-'; + + for ( flag in this.flags ) { + className = classPrefix + flag; + changes[ flag ] = false; + delete this.flags[ flag ]; + remove.push( className ); + } + + if ( this.$flagged ) { + this.$flagged.removeClass( remove.join( ' ' ) ); + } + + this.updateThemeClasses(); + this.emit( 'flag', changes ); + + return this; +}; + +/** + * Add one or more flags. + * + * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names, + * or an object keyed by flag name with a boolean value that indicates whether the flag should + * be added (`true`) or removed (`false`). + * @chainable + * @fires flag + */ +OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) { + var i, len, flag, className, + changes = {}, + add = [], + remove = [], + classPrefix = 'oo-ui-flaggedElement-'; + + if ( typeof flags === 'string' ) { + className = classPrefix + flags; + // Set + if ( !this.flags[ flags ] ) { + this.flags[ flags ] = true; + add.push( className ); + } + } else if ( Array.isArray( flags ) ) { + for ( i = 0, len = flags.length; i < len; i++ ) { + flag = flags[ i ]; + className = classPrefix + flag; + // Set + if ( !this.flags[ flag ] ) { + changes[ flag ] = true; + this.flags[ flag ] = true; + add.push( className ); + } + } + } else if ( OO.isPlainObject( flags ) ) { + for ( flag in flags ) { + className = classPrefix + flag; + if ( flags[ flag ] ) { + // Set + if ( !this.flags[ flag ] ) { + changes[ flag ] = true; + this.flags[ flag ] = true; + add.push( className ); + } + } else { + // Remove + if ( this.flags[ flag ] ) { + changes[ flag ] = false; + delete this.flags[ flag ]; + remove.push( className ); + } + } + } + } + + if ( this.$flagged ) { + this.$flagged + .addClass( add.join( ' ' ) ) + .removeClass( remove.join( ' ' ) ); + } + + this.updateThemeClasses(); + this.emit( 'flag', changes ); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/GroupElement.js b/vendor/oojs/oojs-ui/src/elements/GroupElement.js new file mode 100644 index 00000000..51cf5d25 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/GroupElement.js @@ -0,0 +1,290 @@ +/** + * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or + * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing + * items from the group is done through the interface the class provides. + * For more information, please see the [OOjs UI documentation on MediaWiki] [1]. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$group] The container element created by the class. If this configuration + * is omitted, the group element will use a generated `<div>`. + */ +OO.ui.GroupElement = function OoUiGroupElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$group = null; + this.items = []; + this.aggregateItemEvents = {}; + + // Initialization + this.setGroupElement( config.$group || $( '<div>' ) ); +}; + +/* Methods */ + +/** + * Set the group element. + * + * If an element is already set, items will be moved to the new element. + * + * @param {jQuery} $group Element to use as group + */ +OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) { + var i, len; + + this.$group = $group; + for ( i = 0, len = this.items.length; i < len; i++ ) { + this.$group.append( this.items[ i ].$element ); + } +}; + +/** + * Check if a group contains no items. + * + * @return {boolean} Group is empty + */ +OO.ui.GroupElement.prototype.isEmpty = function () { + return !this.items.length; +}; + +/** + * Get all items in the group. + * + * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful + * when synchronizing groups of items, or whenever the references are required (e.g., when removing items + * from a group). + * + * @return {OO.ui.Element[]} An array of items. + */ +OO.ui.GroupElement.prototype.getItems = function () { + return this.items.slice( 0 ); +}; + +/** + * Get an item by its data. + * + * Only the first item with matching data will be returned. To return all matching items, + * use the #getItemsFromData method. + * + * @param {Object} data Item data to search for + * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists + */ +OO.ui.GroupElement.prototype.getItemFromData = function ( data ) { + var i, len, item, + hash = OO.getHash( data ); + + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[ i ]; + if ( hash === OO.getHash( item.getData() ) ) { + return item; + } + } + + return null; +}; + +/** + * Get items by their data. + * + * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead. + * + * @param {Object} data Item data to search for + * @return {OO.ui.Element[]} Items with equivalent data + */ +OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) { + var i, len, item, + hash = OO.getHash( data ), + items = []; + + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[ i ]; + if ( hash === OO.getHash( item.getData() ) ) { + items.push( item ); + } + } + + return items; +}; + +/** + * Aggregate the events emitted by the group. + * + * When events are aggregated, the group will listen to all contained items for the event, + * and then emit the event under a new name. The new event will contain an additional leading + * parameter containing the item that emitted the original event. Other arguments emitted from + * the original event are passed through. + * + * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be + * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’). + * A `null` value will remove aggregated events. + + * @throws {Error} An error is thrown if aggregation already exists. + */ +OO.ui.GroupElement.prototype.aggregate = function ( events ) { + var i, len, item, add, remove, itemEvent, groupEvent; + + for ( itemEvent in events ) { + groupEvent = events[ itemEvent ]; + + // Remove existing aggregated event + if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) { + // Don't allow duplicate aggregations + if ( groupEvent ) { + throw new Error( 'Duplicate item event aggregation for ' + itemEvent ); + } + // Remove event aggregation from existing items + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[ i ]; + if ( item.connect && item.disconnect ) { + remove = {}; + remove[ itemEvent ] = [ 'emit', groupEvent, item ]; + item.disconnect( this, remove ); + } + } + // Prevent future items from aggregating event + delete this.aggregateItemEvents[ itemEvent ]; + } + + // Add new aggregate event + if ( groupEvent ) { + // Make future items aggregate event + this.aggregateItemEvents[ itemEvent ] = groupEvent; + // Add event aggregation to existing items + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[ i ]; + if ( item.connect && item.disconnect ) { + add = {}; + add[ itemEvent ] = [ 'emit', groupEvent, item ]; + item.connect( this, add ); + } + } + } + } +}; + +/** + * Add items to the group. + * + * Items will be added to the end of the group array unless the optional `index` parameter specifies + * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`. + * + * @param {OO.ui.Element[]} items An array of items to add to the group + * @param {number} [index] Index of the insertion point + * @chainable + */ +OO.ui.GroupElement.prototype.addItems = function ( items, index ) { + var i, len, item, event, events, currentIndex, + itemElements = []; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + // Check if item exists then remove it first, effectively "moving" it + currentIndex = $.inArray( item, this.items ); + if ( currentIndex >= 0 ) { + this.removeItems( [ item ] ); + // Adjust index to compensate for removal + if ( currentIndex < index ) { + index--; + } + } + // Add the item + if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) { + events = {}; + for ( event in this.aggregateItemEvents ) { + events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ]; + } + item.connect( this, events ); + } + item.setElementGroup( this ); + itemElements.push( item.$element.get( 0 ) ); + } + + if ( index === undefined || index < 0 || index >= this.items.length ) { + this.$group.append( itemElements ); + this.items.push.apply( this.items, items ); + } else if ( index === 0 ) { + this.$group.prepend( itemElements ); + this.items.unshift.apply( this.items, items ); + } else { + this.items[ index ].$element.before( itemElements ); + this.items.splice.apply( this.items, [ index, 0 ].concat( items ) ); + } + + return this; +}; + +/** + * Remove the specified items from a group. + * + * Removed items are detached (not removed) from the DOM so that they may be reused. + * To remove all items from a group, you may wish to use the #clearItems method instead. + * + * @param {OO.ui.Element[]} items An array of items to remove + * @chainable + */ +OO.ui.GroupElement.prototype.removeItems = function ( items ) { + var i, len, item, index, remove, itemEvent; + + // Remove specific items + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + index = $.inArray( item, this.items ); + if ( index !== -1 ) { + if ( + item.connect && item.disconnect && + !$.isEmptyObject( this.aggregateItemEvents ) + ) { + remove = {}; + if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) { + remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ]; + } + item.disconnect( this, remove ); + } + item.setElementGroup( null ); + this.items.splice( index, 1 ); + item.$element.detach(); + } + } + + return this; +}; + +/** + * Clear all items from the group. + * + * Cleared items are detached from the DOM, not removed, so that they may be reused. + * To remove only a subset of items from a group, use the #removeItems method. + * + * @chainable + */ +OO.ui.GroupElement.prototype.clearItems = function () { + var i, len, item, remove, itemEvent; + + // Remove all items + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[ i ]; + if ( + item.connect && item.disconnect && + !$.isEmptyObject( this.aggregateItemEvents ) + ) { + remove = {}; + if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) { + remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ]; + } + item.disconnect( this, remove ); + } + item.setElementGroup( null ); + item.$element.detach(); + } + + this.items = []; + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/IconElement.js b/vendor/oojs/oojs-ui/src/elements/IconElement.js new file mode 100644 index 00000000..e3cf2f2a --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/IconElement.js @@ -0,0 +1,187 @@ +/** + * IconElement is often mixed into other classes to generate an icon. + * Icons are graphics, about the size of normal text. They are used to aid the user + * in locating a control or to convey information in a space-efficient way. See the + * [OOjs UI documentation on MediaWiki] [1] for a list of icons + * included in the library. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted, + * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that + * the icon element be set to an existing icon instead of the one generated by this class, set a + * value using a jQuery selection. For example: + * + * // Use a <div> tag instead of a <span> + * $icon: $("<div>") + * // Use an existing icon element instead of the one generated by the class + * $icon: this.$element + * // Use an icon element from a child widget + * $icon: this.childwidget.$element + * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of + * symbolic names. A map is used for i18n purposes and contains a `default` icon + * name and additional names keyed by language code. The `default` name is used when no icon is keyed + * by the user's language. + * + * Example of an i18n map: + * + * { default: 'bold-a', en: 'bold-b', de: 'bold-f' } + * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons + * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title + * text. The icon title is displayed when users move the mouse over the icon. + */ +OO.ui.IconElement = function OoUiIconElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$icon = null; + this.icon = null; + this.iconTitle = null; + + // Initialization + this.setIcon( config.icon || this.constructor.static.icon ); + this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle ); + this.setIconElement( config.$icon || $( '<span>' ) ); +}; + +/* Setup */ + +OO.initClass( OO.ui.IconElement ); + +/* Static Properties */ + +/** + * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used + * for i18n purposes and contains a `default` icon name and additional names keyed by + * language code. The `default` name is used when no icon is keyed by the user's language. + * + * Example of an i18n map: + * + * { default: 'bold-a', en: 'bold-b', de: 'bold-f' } + * + * Note: the static property will be overridden if the #icon configuration is used. + * + * @static + * @inheritable + * @property {Object|string} + */ +OO.ui.IconElement.static.icon = null; + +/** + * The icon title, displayed when users move the mouse over the icon. The value can be text, a + * function that returns title text, or `null` for no title. + * + * The static property will be overridden if the #iconTitle configuration is used. + * + * @static + * @inheritable + * @property {string|Function|null} + */ +OO.ui.IconElement.static.iconTitle = null; + +/* Methods */ + +/** + * Set the icon element. This method is used to retarget an icon mixin so that its functionality + * applies to the specified icon element instead of the one created by the class. If an icon + * element is already set, the mixin’s effect on that element is removed. Generated CSS classes + * and mixin methods will no longer affect the element. + * + * @param {jQuery} $icon Element to use as icon + */ +OO.ui.IconElement.prototype.setIconElement = function ( $icon ) { + if ( this.$icon ) { + this.$icon + .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon ) + .removeAttr( 'title' ); + } + + this.$icon = $icon + .addClass( 'oo-ui-iconElement-icon' ) + .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon ); + if ( this.iconTitle !== null ) { + this.$icon.attr( 'title', this.iconTitle ); + } +}; + +/** + * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon. + * The icon parameter can also be set to a map of icon names. See the #icon config setting + * for an example. + * + * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed + * by language code, or `null` to remove the icon. + * @chainable + */ +OO.ui.IconElement.prototype.setIcon = function ( icon ) { + icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon; + icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null; + + if ( this.icon !== icon ) { + if ( this.$icon ) { + if ( this.icon !== null ) { + this.$icon.removeClass( 'oo-ui-icon-' + this.icon ); + } + if ( icon !== null ) { + this.$icon.addClass( 'oo-ui-icon-' + icon ); + } + } + this.icon = icon; + } + + this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon ); + this.updateThemeClasses(); + + return this; +}; + +/** + * Set the icon title. Use `null` to remove the title. + * + * @param {string|Function|null} iconTitle A text string used as the icon title, + * a function that returns title text, or `null` for no title. + * @chainable + */ +OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) { + iconTitle = typeof iconTitle === 'function' || + ( typeof iconTitle === 'string' && iconTitle.length ) ? + OO.ui.resolveMsg( iconTitle ) : null; + + if ( this.iconTitle !== iconTitle ) { + this.iconTitle = iconTitle; + if ( this.$icon ) { + if ( this.iconTitle !== null ) { + this.$icon.attr( 'title', iconTitle ); + } else { + this.$icon.removeAttr( 'title' ); + } + } + } + + return this; +}; + +/** + * Get the symbolic name of the icon. + * + * @return {string} Icon name + */ +OO.ui.IconElement.prototype.getIcon = function () { + return this.icon; +}; + +/** + * Get the icon title. The title text is displayed when a user moves the mouse over the icon. + * + * @return {string} Icon title text + */ +OO.ui.IconElement.prototype.getIconTitle = function () { + return this.iconTitle; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/IndicatorElement.js b/vendor/oojs/oojs-ui/src/elements/IndicatorElement.js new file mode 100644 index 00000000..5c6294d2 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/IndicatorElement.js @@ -0,0 +1,168 @@ +/** + * IndicatorElement is often mixed into other classes to generate an indicator. + * Indicators are small graphics that are generally used in two ways: + * + * - To draw attention to the status of an item. For example, an indicator might be + * used to show that an item in a list has errors that need to be resolved. + * - To clarify the function of a control that acts in an exceptional way (a button + * that opens a menu instead of performing an action directly, for example). + * + * For a list of indicators included in the library, please see the + * [OOjs UI documentation on MediaWiki] [1]. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators + * + * @abstract + * @class + * + * @constructor + * @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’). + * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included + * in the library. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators + * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title, + * or a function that returns title text. The indicator title is displayed when users move + * the mouse over the indicator. + */ +OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$indicator = null; + this.indicator = null; + this.indicatorTitle = null; + + // Initialization + this.setIndicator( config.indicator || this.constructor.static.indicator ); + this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle ); + this.setIndicatorElement( config.$indicator || $( '<span>' ) ); +}; + +/* Setup */ + +OO.initClass( OO.ui.IndicatorElement ); + +/* Static Properties */ + +/** + * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’). + * The static property will be overridden if the #indicator configuration is used. + * + * @static + * @inheritable + * @property {string|null} + */ +OO.ui.IndicatorElement.static.indicator = null; + +/** + * A text string used as the indicator title, a function that returns title text, or `null` + * for no title. The static property will be overridden if the #indicatorTitle configuration is used. + * + * @static + * @inheritable + * @property {string|Function|null} + */ +OO.ui.IndicatorElement.static.indicatorTitle = null; + +/* Methods */ + +/** + * Set the indicator element. + * + * If an element is already set, it will be cleaned up before setting up the new element. + * + * @param {jQuery} $indicator Element to use as indicator + */ +OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) { + if ( this.$indicator ) { + this.$indicator + .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator ) + .removeAttr( 'title' ); + } + + this.$indicator = $indicator + .addClass( 'oo-ui-indicatorElement-indicator' ) + .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator ); + if ( this.indicatorTitle !== null ) { + this.$indicator.attr( 'title', this.indicatorTitle ); + } +}; + +/** + * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator. + * + * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator + * @chainable + */ +OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) { + indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null; + + if ( this.indicator !== indicator ) { + if ( this.$indicator ) { + if ( this.indicator !== null ) { + this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator ); + } + if ( indicator !== null ) { + this.$indicator.addClass( 'oo-ui-indicator-' + indicator ); + } + } + this.indicator = indicator; + } + + this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator ); + this.updateThemeClasses(); + + return this; +}; + +/** + * Set the indicator title. + * + * The title is displayed when a user moves the mouse over the indicator. + * + * @param {string|Function|null} indicator Indicator title text, a function that returns text, or + * `null` for no indicator title + * @chainable + */ +OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) { + indicatorTitle = typeof indicatorTitle === 'function' || + ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ? + OO.ui.resolveMsg( indicatorTitle ) : null; + + if ( this.indicatorTitle !== indicatorTitle ) { + this.indicatorTitle = indicatorTitle; + if ( this.$indicator ) { + if ( this.indicatorTitle !== null ) { + this.$indicator.attr( 'title', indicatorTitle ); + } else { + this.$indicator.removeAttr( 'title' ); + } + } + } + + return this; +}; + +/** + * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’). + * + * @return {string} Symbolic name of indicator + */ +OO.ui.IndicatorElement.prototype.getIndicator = function () { + return this.indicator; +}; + +/** + * Get the indicator title. + * + * The title is displayed when a user moves the mouse over the indicator. + * + * @return {string} Indicator title text + */ +OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () { + return this.indicatorTitle; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/LabelElement.js b/vendor/oojs/oojs-ui/src/elements/LabelElement.js new file mode 100644 index 00000000..674fa73a --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/LabelElement.js @@ -0,0 +1,152 @@ +/** + * LabelElement is often mixed into other classes to generate a label, which + * helps identify the function of an interface element. + * See the [OOjs UI documentation on MediaWiki] [1] for more information. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$label] The label element created by the class. If this + * configuration is omitted, the label element will use a generated `<span>`. + * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified + * as a plaintext string, a jQuery selection of elements, or a function that will produce a string + * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels + * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element. + * The label will be truncated to fit if necessary. + */ +OO.ui.LabelElement = function OoUiLabelElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$label = null; + this.label = null; + this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel; + + // Initialization + this.setLabel( config.label || this.constructor.static.label ); + this.setLabelElement( config.$label || $( '<span>' ) ); +}; + +/* Setup */ + +OO.initClass( OO.ui.LabelElement ); + +/* Events */ + +/** + * @event labelChange + * @param {string} value + */ + +/* Static Properties */ + +/** + * The label text. The label can be specified as a plaintext string, a function that will + * produce a string in the future, or `null` for no label. The static value will + * be overridden if a label is specified with the #label config option. + * + * @static + * @inheritable + * @property {string|Function|null} + */ +OO.ui.LabelElement.static.label = null; + +/* Methods */ + +/** + * Set the label element. + * + * If an element is already set, it will be cleaned up before setting up the new element. + * + * @param {jQuery} $label Element to use as label + */ +OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) { + if ( this.$label ) { + this.$label.removeClass( 'oo-ui-labelElement-label' ).empty(); + } + + this.$label = $label.addClass( 'oo-ui-labelElement-label' ); + this.setLabelContent( this.label ); +}; + +/** + * Set the label. + * + * An empty string will result in the label being hidden. A string containing only whitespace will + * be converted to a single ` `. + * + * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or + * text; or null for no label + * @chainable + */ +OO.ui.LabelElement.prototype.setLabel = function ( label ) { + label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label; + label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null; + + this.$element.toggleClass( 'oo-ui-labelElement', !!label ); + + if ( this.label !== label ) { + if ( this.$label ) { + this.setLabelContent( label ); + } + this.label = label; + this.emit( 'labelChange' ); + } + + return this; +}; + +/** + * Get the label. + * + * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or + * text; or null for no label + */ +OO.ui.LabelElement.prototype.getLabel = function () { + return this.label; +}; + +/** + * Fit the label. + * + * @chainable + */ +OO.ui.LabelElement.prototype.fitLabel = function () { + if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) { + this.$label.autoEllipsis( { hasSpan: false, tooltip: true } ); + } + + return this; +}; + +/** + * Set the content of the label. + * + * Do not call this method until after the label element has been set by #setLabelElement. + * + * @private + * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or + * text; or null for no label + */ +OO.ui.LabelElement.prototype.setLabelContent = function ( label ) { + if ( typeof label === 'string' ) { + if ( label.match( /^\s*$/ ) ) { + // Convert whitespace only string to a single non-breaking space + this.$label.html( ' ' ); + } else { + this.$label.text( label ); + } + } else if ( label instanceof OO.ui.HtmlSnippet ) { + this.$label.html( label.toString() ); + } else if ( label instanceof jQuery ) { + this.$label.empty().append( label ); + } else { + this.$label.empty(); + } +}; diff --git a/vendor/oojs/oojs-ui/src/elements/LookupElement.js b/vendor/oojs/oojs-ui/src/elements/LookupElement.js new file mode 100644 index 00000000..b79f02a9 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/LookupElement.js @@ -0,0 +1,352 @@ +/** + * LookupElement is a mixin that creates a {@link OO.ui.TextInputMenuSelectWidget menu} of suggested values for + * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types + * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen + * from the lookup menu, that value becomes the value of the input field. + * + * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is + * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then + * re-enable lookups. + * + * See the [OOjs UI demos][1] for an example. + * + * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr + * + * @class + * @abstract + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning + * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element. + * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty. + * By default, the lookup menu is not generated and displayed until the user begins to type. + */ +OO.ui.LookupElement = function OoUiLookupElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$overlay = config.$overlay || this.$element; + this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, { + widget: this, + input: this, + $container: config.$container + } ); + + this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false; + + this.lookupCache = {}; + this.lookupQuery = null; + this.lookupRequest = null; + this.lookupsDisabled = false; + this.lookupInputFocused = false; + + // Events + this.$input.on( { + focus: this.onLookupInputFocus.bind( this ), + blur: this.onLookupInputBlur.bind( this ), + mousedown: this.onLookupInputMouseDown.bind( this ) + } ); + this.connect( this, { change: 'onLookupInputChange' } ); + this.lookupMenu.connect( this, { + toggle: 'onLookupMenuToggle', + choose: 'onLookupMenuItemChoose' + } ); + + // Initialization + this.$element.addClass( 'oo-ui-lookupElement' ); + this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' ); + this.$overlay.append( this.lookupMenu.$element ); +}; + +/* Methods */ + +/** + * Handle input focus event. + * + * @protected + * @param {jQuery.Event} e Input focus event + */ +OO.ui.LookupElement.prototype.onLookupInputFocus = function () { + this.lookupInputFocused = true; + this.populateLookupMenu(); +}; + +/** + * Handle input blur event. + * + * @protected + * @param {jQuery.Event} e Input blur event + */ +OO.ui.LookupElement.prototype.onLookupInputBlur = function () { + this.closeLookupMenu(); + this.lookupInputFocused = false; +}; + +/** + * Handle input mouse down event. + * + * @protected + * @param {jQuery.Event} e Input mouse down event + */ +OO.ui.LookupElement.prototype.onLookupInputMouseDown = function () { + // Only open the menu if the input was already focused. + // This way we allow the user to open the menu again after closing it with Esc + // by clicking in the input. Opening (and populating) the menu when initially + // clicking into the input is handled by the focus handler. + if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) { + this.populateLookupMenu(); + } +}; + +/** + * Handle input change event. + * + * @protected + * @param {string} value New input value + */ +OO.ui.LookupElement.prototype.onLookupInputChange = function () { + if ( this.lookupInputFocused ) { + this.populateLookupMenu(); + } +}; + +/** + * Handle the lookup menu being shown/hidden. + * + * @protected + * @param {boolean} visible Whether the lookup menu is now visible. + */ +OO.ui.LookupElement.prototype.onLookupMenuToggle = function ( visible ) { + if ( !visible ) { + // When the menu is hidden, abort any active request and clear the menu. + // This has to be done here in addition to closeLookupMenu(), because + // MenuSelectWidget will close itself when the user presses Esc. + this.abortLookupRequest(); + this.lookupMenu.clearItems(); + } +}; + +/** + * Handle menu item 'choose' event, updating the text input value to the value of the clicked item. + * + * @protected + * @param {OO.ui.MenuOptionWidget} item Selected item + */ +OO.ui.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) { + this.setValue( item.getData() ); +}; + +/** + * Get lookup menu. + * + * @private + * @return {OO.ui.TextInputMenuSelectWidget} + */ +OO.ui.LookupElement.prototype.getLookupMenu = function () { + return this.lookupMenu; +}; + +/** + * Disable or re-enable lookups. + * + * When lookups are disabled, calls to #populateLookupMenu will be ignored. + * + * @param {boolean} disabled Disable lookups + */ +OO.ui.LookupElement.prototype.setLookupsDisabled = function ( disabled ) { + this.lookupsDisabled = !!disabled; +}; + +/** + * Open the menu. If there are no entries in the menu, this does nothing. + * + * @private + * @chainable + */ +OO.ui.LookupElement.prototype.openLookupMenu = function () { + if ( !this.lookupMenu.isEmpty() ) { + this.lookupMenu.toggle( true ); + } + return this; +}; + +/** + * Close the menu, empty it, and abort any pending request. + * + * @private + * @chainable + */ +OO.ui.LookupElement.prototype.closeLookupMenu = function () { + this.lookupMenu.toggle( false ); + this.abortLookupRequest(); + this.lookupMenu.clearItems(); + return this; +}; + +/** + * Request menu items based on the input's current value, and when they arrive, + * populate the menu with these items and show the menu. + * + * If lookups have been disabled with #setLookupsDisabled, this function does nothing. + * + * @private + * @chainable + */ +OO.ui.LookupElement.prototype.populateLookupMenu = function () { + var widget = this, + value = this.getValue(); + + if ( this.lookupsDisabled ) { + return; + } + + // If the input is empty, clear the menu, unless suggestions when empty are allowed. + if ( !this.allowSuggestionsWhenEmpty && value === '' ) { + this.closeLookupMenu(); + // Skip population if there is already a request pending for the current value + } else if ( value !== this.lookupQuery ) { + this.getLookupMenuItems() + .done( function ( items ) { + widget.lookupMenu.clearItems(); + if ( items.length ) { + widget.lookupMenu + .addItems( items ) + .toggle( true ); + widget.initializeLookupMenuSelection(); + } else { + widget.lookupMenu.toggle( false ); + } + } ) + .fail( function () { + widget.lookupMenu.clearItems(); + } ); + } + + return this; +}; + +/** + * Highlight the first selectable item in the menu. + * + * @private + * @chainable + */ +OO.ui.LookupElement.prototype.initializeLookupMenuSelection = function () { + if ( !this.lookupMenu.getSelectedItem() ) { + this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() ); + } +}; + +/** + * Get lookup menu items for the current query. + * + * @private + * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of + * the done event. If the request was aborted to make way for a subsequent request, this promise + * will not be rejected: it will remain pending forever. + */ +OO.ui.LookupElement.prototype.getLookupMenuItems = function () { + var widget = this, + value = this.getValue(), + deferred = $.Deferred(), + ourRequest; + + this.abortLookupRequest(); + if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) { + deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) ); + } else { + this.pushPending(); + this.lookupQuery = value; + ourRequest = this.lookupRequest = this.getLookupRequest(); + ourRequest + .always( function () { + // We need to pop pending even if this is an old request, otherwise + // the widget will remain pending forever. + // TODO: this assumes that an aborted request will fail or succeed soon after + // being aborted, or at least eventually. It would be nice if we could popPending() + // at abort time, but only if we knew that we hadn't already called popPending() + // for that request. + widget.popPending(); + } ) + .done( function ( response ) { + // If this is an old request (and aborting it somehow caused it to still succeed), + // ignore its success completely + if ( ourRequest === widget.lookupRequest ) { + widget.lookupQuery = null; + widget.lookupRequest = null; + widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( response ); + deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) ); + } + } ) + .fail( function () { + // If this is an old request (or a request failing because it's being aborted), + // ignore its failure completely + if ( ourRequest === widget.lookupRequest ) { + widget.lookupQuery = null; + widget.lookupRequest = null; + deferred.reject(); + } + } ); + } + return deferred.promise(); +}; + +/** + * Abort the currently pending lookup request, if any. + * + * @private + */ +OO.ui.LookupElement.prototype.abortLookupRequest = function () { + var oldRequest = this.lookupRequest; + if ( oldRequest ) { + // First unset this.lookupRequest to the fail handler will notice + // that the request is no longer current + this.lookupRequest = null; + this.lookupQuery = null; + oldRequest.abort(); + } +}; + +/** + * Get a new request object of the current lookup query value. + * + * @protected + * @abstract + * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method + */ +OO.ui.LookupElement.prototype.getLookupRequest = function () { + // Stub, implemented in subclass + return null; +}; + +/** + * Pre-process data returned by the request from #getLookupRequest. + * + * The return value of this function will be cached, and any further queries for the given value + * will use the cache rather than doing API requests. + * + * @protected + * @abstract + * @param {Mixed} response Response from server + * @return {Mixed} Cached result data + */ +OO.ui.LookupElement.prototype.getLookupCacheDataFromResponse = function () { + // Stub, implemented in subclass + return []; +}; + +/** + * Get a list of menu option widgets from the (possibly cached) data returned by + * #getLookupCacheDataFromResponse. + * + * @protected + * @abstract + * @param {Mixed} data Cached result data, usually an array + * @return {OO.ui.MenuOptionWidget[]} Menu items + */ +OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () { + // Stub, implemented in subclass + return []; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/PendingElement.js b/vendor/oojs/oojs-ui/src/elements/PendingElement.js new file mode 100644 index 00000000..c5f71d54 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/PendingElement.js @@ -0,0 +1,84 @@ +/** + * Element that can be marked as pending. + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element + */ +OO.ui.PendingElement = function OoUiPendingElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.pending = 0; + this.$pending = null; + + // Initialisation + this.setPendingElement( config.$pending || this.$element ); +}; + +/* Setup */ + +OO.initClass( OO.ui.PendingElement ); + +/* Methods */ + +/** + * Set the pending element (and clean up any existing one). + * + * @param {jQuery} $pending The element to set to pending. + */ +OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) { + if ( this.$pending ) { + this.$pending.removeClass( 'oo-ui-pendingElement-pending' ); + } + + this.$pending = $pending; + if ( this.pending > 0 ) { + this.$pending.addClass( 'oo-ui-pendingElement-pending' ); + } +}; + +/** + * Check if input is pending. + * + * @return {boolean} + */ +OO.ui.PendingElement.prototype.isPending = function () { + return !!this.pending; +}; + +/** + * Increase the pending stack. + * + * @chainable + */ +OO.ui.PendingElement.prototype.pushPending = function () { + if ( this.pending === 0 ) { + this.$pending.addClass( 'oo-ui-pendingElement-pending' ); + this.updateThemeClasses(); + } + this.pending++; + + return this; +}; + +/** + * Reduce the pending stack. + * + * Clamped at zero. + * + * @chainable + */ +OO.ui.PendingElement.prototype.popPending = function () { + if ( this.pending === 1 ) { + this.$pending.removeClass( 'oo-ui-pendingElement-pending' ); + this.updateThemeClasses(); + } + this.pending = Math.max( 0, this.pending - 1 ); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/PopupElement.js b/vendor/oojs/oojs-ui/src/elements/PopupElement.js new file mode 100644 index 00000000..099e94b7 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/PopupElement.js @@ -0,0 +1,36 @@ +/** + * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}. + * A popup is a container for content. It is overlaid and positioned absolutely. By default, each + * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin. + * See {@link OO.ui.PopupWidget PopupWidget} for an example. + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {Object} [popup] Configuration to pass to popup + * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus + */ +OO.ui.PopupElement = function OoUiPopupElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.popup = new OO.ui.PopupWidget( $.extend( + { autoClose: true }, + config.popup, + { $autoCloseIgnore: this.$element } + ) ); +}; + +/* Methods */ + +/** + * Get popup. + * + * @return {OO.ui.PopupWidget} Popup widget + */ +OO.ui.PopupElement.prototype.getPopup = function () { + return this.popup; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/TabIndexedElement.js b/vendor/oojs/oojs-ui/src/elements/TabIndexedElement.js new file mode 100644 index 00000000..5c2151a0 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/TabIndexedElement.js @@ -0,0 +1,138 @@ +/** + * The TabIndexedElement class is an attribute mixin used to add additional functionality to an + * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the + * order in which users will navigate through the focusable elements via the "tab" key. + * + * @example + * // TabIndexedElement is mixed into the ButtonWidget class + * // to provide a tabIndex property. + * var button1 = new OO.ui.ButtonWidget( { + * label: 'fourth', + * tabIndex: 4 + * } ); + * var button2 = new OO.ui.ButtonWidget( { + * label: 'second', + * tabIndex: 2 + * } ); + * var button3 = new OO.ui.ButtonWidget( { + * label: 'third', + * tabIndex: 3 + * } ); + * var button4 = new OO.ui.ButtonWidget( { + * label: 'first', + * tabIndex: 1 + * } ); + * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element ); + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default, + * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex + * functionality will be applied to it instead. + * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation + * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1 + * to remove the element from the tab-navigation flow. + */ +OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) { + // Configuration initialization + config = $.extend( { tabIndex: 0 }, config ); + + // Properties + this.$tabIndexed = null; + this.tabIndex = null; + + // Events + this.connect( this, { disable: 'onDisable' } ); + + // Initialization + this.setTabIndex( config.tabIndex ); + this.setTabIndexedElement( config.$tabIndexed || this.$element ); +}; + +/* Setup */ + +OO.initClass( OO.ui.TabIndexedElement ); + +/* Methods */ + +/** + * Set the element that should use the tabindex functionality. + * + * This method is used to retarget a tabindex mixin so that its functionality applies + * to the specified element. If an element is currently using the functionality, the mixin’s + * effect on that element is removed before the new element is set up. + * + * @param {jQuery} $tabIndexed Element that should use the tabindex functionality + * @chainable + */ +OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) { + var tabIndex = this.tabIndex; + // Remove attributes from old $tabIndexed + this.setTabIndex( null ); + // Force update of new $tabIndexed + this.$tabIndexed = $tabIndexed; + this.tabIndex = tabIndex; + return this.updateTabIndex(); +}; + +/** + * Set the value of the tabindex. + * + * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex + * @chainable + */ +OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) { + tabIndex = typeof tabIndex === 'number' ? tabIndex : null; + + if ( this.tabIndex !== tabIndex ) { + this.tabIndex = tabIndex; + this.updateTabIndex(); + } + + return this; +}; + +/** + * Update the `tabindex` attribute, in case of changes to tab index or + * disabled state. + * + * @private + * @chainable + */ +OO.ui.TabIndexedElement.prototype.updateTabIndex = function () { + if ( this.$tabIndexed ) { + if ( this.tabIndex !== null ) { + // Do not index over disabled elements + this.$tabIndexed.attr( { + tabindex: this.isDisabled() ? -1 : this.tabIndex, + // ChromeVox and NVDA do not seem to inherit this from parent elements + 'aria-disabled': this.isDisabled().toString() + } ); + } else { + this.$tabIndexed.removeAttr( 'tabindex aria-disabled' ); + } + } + return this; +}; + +/** + * Handle disable events. + * + * @private + * @param {boolean} disabled Element is disabled + */ +OO.ui.TabIndexedElement.prototype.onDisable = function () { + this.updateTabIndex(); +}; + +/** + * Get the value of the tabindex. + * + * @return {number|null} Tabindex value + */ +OO.ui.TabIndexedElement.prototype.getTabIndex = function () { + return this.tabIndex; +}; diff --git a/vendor/oojs/oojs-ui/src/elements/TitledElement.js b/vendor/oojs/oojs-ui/src/elements/TitledElement.js new file mode 100644 index 00000000..905ec019 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/elements/TitledElement.js @@ -0,0 +1,106 @@ +/** + * TitledElement is mixed into other classes to provide a `title` attribute. + * Titles are rendered by the browser and are made visible when the user moves + * the mouse over the element. Titles are not visible on touch devices. + * + * @example + * // TitledElement provides a 'title' attribute to the + * // ButtonWidget class + * var button = new OO.ui.ButtonWidget( { + * label: 'Button with Title', + * title: 'I am a button' + * } ); + * $( 'body' ).append( button.$element ); + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied. + * If this config is omitted, the title functionality is applied to $element, the + * element created by the class. + * @cfg {string|Function} [title] The title text or a function that returns text. If + * this config is omitted, the value of the {@link #static-title static title} property is used. + */ +OO.ui.TitledElement = function OoUiTitledElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$titled = null; + this.title = null; + + // Initialization + this.setTitle( config.title || this.constructor.static.title ); + this.setTitledElement( config.$titled || this.$element ); +}; + +/* Setup */ + +OO.initClass( OO.ui.TitledElement ); + +/* Static Properties */ + +/** + * The title text, a function that returns text, or `null` for no title. The value of the static property + * is overridden if the #title config option is used. + * + * @static + * @inheritable + * @property {string|Function|null} + */ +OO.ui.TitledElement.static.title = null; + +/* Methods */ + +/** + * Set the titled element. + * + * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element. + * If an element is already set, the mixin’s effect on that element is removed before the new element is set up. + * + * @param {jQuery} $titled Element that should use the 'titled' functionality + */ +OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) { + if ( this.$titled ) { + this.$titled.removeAttr( 'title' ); + } + + this.$titled = $titled; + if ( this.title ) { + this.$titled.attr( 'title', this.title ); + } +}; + +/** + * Set title. + * + * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title + * @chainable + */ +OO.ui.TitledElement.prototype.setTitle = function ( title ) { + title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null; + + if ( this.title !== title ) { + if ( this.$titled ) { + if ( title !== null ) { + this.$titled.attr( 'title', title ); + } else { + this.$titled.removeAttr( 'title' ); + } + } + this.title = title; + } + + return this; +}; + +/** + * Get title. + * + * @return {string} Title string + */ +OO.ui.TitledElement.prototype.getTitle = function () { + return this.title; +}; |