2016-02-01 22:28:13 +00:00
/ * !
2020-01-07 23:13:07 +00:00
* OOUI v0 . 36.2
2018-01-17 05:47:34 +00:00
* https : //www.mediawiki.org/wiki/OOUI
2016-02-01 22:28:13 +00:00
*
2020-01-07 23:13:07 +00:00
* Copyright 2011 – 2020 OOUI Team and other contributors .
2016-02-01 22:28:13 +00:00
* Released under the MIT license
* http : //oojs.mit-license.org
*
2020-01-07 23:13:07 +00:00
* Date : 2020 - 01 - 07 T23 : 06 : 58 Z
2016-02-01 22:28:13 +00:00
* /
( function ( OO ) {
'use strict' ;
/ * *
* 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 . mixin . DraggableGroupElement , which provides a container for
* the draggable elements .
*
* @ abstract
* @ class
*
* @ constructor
2016-02-22 22:36:25 +00:00
* @ param { Object } [ config ] Configuration options
2019-03-07 09:22:27 +00:00
* @ cfg { jQuery } [ $handle ] The part of the element which can be used for dragging , defaults to
* the whole element
2017-03-28 23:25:58 +00:00
* @ cfg { boolean } [ draggable ] The items are draggable . This can change with # toggleDraggable
* but the draggable state should be called from the DraggableGroupElement , which updates
* the whole group
2016-02-01 22:28:13 +00:00
* /
2016-02-22 22:36:25 +00:00
OO . ui . mixin . DraggableElement = function OoUiMixinDraggableElement ( config ) {
config = config || { } ;
2016-02-01 22:28:13 +00:00
// Properties
this . index = null ;
2016-02-22 22:36:25 +00:00
this . $handle = config . $handle || this . $element ;
this . wasHandleUsed = null ;
2016-02-01 22:28:13 +00:00
// Initialize and events
2017-08-22 22:50:49 +00:00
this . $element
. addClass ( 'oo-ui-draggableElement' )
2016-02-01 22:28:13 +00:00
. on ( {
2016-02-22 22:36:25 +00:00
mousedown : this . onDragMouseDown . bind ( this ) ,
2016-02-01 22:28:13 +00:00
dragstart : this . onDragStart . bind ( this ) ,
dragover : this . onDragOver . bind ( this ) ,
dragend : this . onDragEnd . bind ( this ) ,
drop : this . onDrop . bind ( this )
} ) ;
2016-02-22 22:36:25 +00:00
this . $handle . addClass ( 'oo-ui-draggableElement-handle' ) ;
2017-08-22 22:50:49 +00:00
this . toggleDraggable ( config . draggable === undefined ? true : ! ! config . draggable ) ;
2016-02-01 22:28:13 +00:00
} ;
OO . initClass ( OO . ui . mixin . DraggableElement ) ;
/* Events */
/ * *
* @ event dragstart
*
* A dragstart event is emitted when the user clicks and begins dragging an item .
2019-03-07 09:22:27 +00:00
* @ param { OO . ui . mixin . DraggableElement } item The item the user has clicked and is dragging with
* the mouse .
2016-02-01 22:28:13 +00:00
* /
/ * *
* @ 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 . mixin . ButtonElement
* /
OO . ui . mixin . DraggableElement . static . cancelButtonMouseDownEvents = false ;
/* Methods */
2017-03-28 23:25:58 +00:00
/ * *
* Change the draggable state of this widget .
* This allows users to temporarily halt the dragging operations .
*
* @ param { boolean } isDraggable Widget supports draggable operations
* @ fires draggable
* /
OO . ui . mixin . DraggableElement . prototype . toggleDraggable = function ( isDraggable ) {
isDraggable = isDraggable !== undefined ? ! ! isDraggable : ! this . draggable ;
if ( this . draggable !== isDraggable ) {
this . draggable = isDraggable ;
2017-04-11 23:41:59 +00:00
this . $handle . toggleClass ( 'oo-ui-draggableElement-undraggable' , ! this . draggable ) ;
2017-08-22 22:50:49 +00:00
// We make the entire element draggable, not just the handle, so that
// the whole element appears to move. wasHandleUsed prevents drags from
// starting outside the handle
this . $element . prop ( 'draggable' , this . draggable ) ;
2017-03-28 23:25:58 +00:00
}
} ;
/ * *
2019-03-07 09:22:27 +00:00
* Check the draggable state of this widget .
2017-03-28 23:25:58 +00:00
*
* @ return { boolean } Widget supports draggable operations
* /
OO . ui . mixin . DraggableElement . prototype . isDraggable = function ( ) {
return this . draggable ;
} ;
2016-02-22 22:36:25 +00:00
/ * *
* Respond to mousedown event .
*
* @ private
2016-11-09 01:22:51 +00:00
* @ param { jQuery . Event } e Drag event
2016-02-22 22:36:25 +00:00
* /
OO . ui . mixin . DraggableElement . prototype . onDragMouseDown = function ( e ) {
2017-03-28 23:25:58 +00:00
if ( ! this . isDraggable ( ) ) {
return ;
}
2016-02-22 22:36:25 +00:00
this . wasHandleUsed =
// Optimization: if the handle is the whole element this is always true
this . $handle [ 0 ] === this . $element [ 0 ] ||
// Check the mousedown occurred inside the handle
OO . ui . contains ( this . $handle [ 0 ] , e . target , true ) ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
* Respond to dragstart event .
*
* @ private
2016-11-09 01:22:51 +00:00
* @ param { jQuery . Event } e Drag event
* @ return { boolean } False if the event is cancelled
2016-02-01 22:28:13 +00:00
* @ fires dragstart
* /
OO . ui . mixin . DraggableElement . prototype . onDragStart = function ( e ) {
2016-02-22 22:36:25 +00:00
var element = this ,
dataTransfer = e . originalEvent . dataTransfer ;
2017-03-28 23:25:58 +00:00
if ( ! this . wasHandleUsed || ! this . isDraggable ( ) ) {
2016-02-22 22:36:25 +00:00
return false ;
}
2016-02-01 22:28:13 +00:00
// Define drop effect
dataTransfer . dropEffect = 'none' ;
dataTransfer . effectAllowed = 'move' ;
// Support: Firefox
// We must set up a dataTransfer data property or Firefox seems to
// ignore the fact the element is draggable.
try {
2018-02-07 01:17:20 +00:00
dataTransfer . setData ( 'application-x/OOUI-draggable' , this . getIndex ( ) ) ;
2016-02-01 22:28:13 +00:00
} catch ( err ) {
// The above is only for Firefox. Move on if it fails.
}
2016-02-22 22:36:25 +00:00
// Briefly add a 'clone' class to style the browser's native drag image
this . $element . addClass ( 'oo-ui-draggableElement-clone' ) ;
// Add placeholder class after the browser has rendered the clone
setTimeout ( function ( ) {
element . $element
. removeClass ( 'oo-ui-draggableElement-clone' )
. addClass ( 'oo-ui-draggableElement-placeholder' ) ;
} ) ;
2016-02-01 22:28:13 +00:00
// Emit event
this . emit ( 'dragstart' , this ) ;
return true ;
} ;
/ * *
* Respond to dragend event .
*
* @ private
* @ fires dragend
* /
OO . ui . mixin . DraggableElement . prototype . onDragEnd = function ( ) {
2016-02-22 22:36:25 +00:00
this . $element . removeClass ( 'oo-ui-draggableElement-placeholder' ) ;
2016-02-01 22:28:13 +00:00
this . emit ( 'dragend' ) ;
} ;
/ * *
* Handle drop event .
*
* @ private
2016-11-09 01:22:51 +00:00
* @ param { jQuery . Event } e Drop event
2016-02-01 22:28:13 +00:00
* @ fires drop
* /
OO . ui . mixin . 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 .
*
2016-11-09 01:22:51 +00:00
* @ param { jQuery . Event } e Drag event
2016-02-01 22:28:13 +00:00
* @ private
* /
OO . ui . mixin . DraggableElement . prototype . onDragOver = function ( e ) {
e . preventDefault ( ) ;
} ;
/ * *
* Set item index .
2019-03-07 09:22:27 +00:00
* Store it in the DOM so we can access from the widget drag event .
2016-02-01 22:28:13 +00:00
*
* @ private
2016-03-01 22:00:31 +00:00
* @ param { number } index Item index
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . DraggableElement . prototype . setIndex = function ( index ) {
if ( this . index !== index ) {
this . index = index ;
this . $element . data ( 'index' , index ) ;
}
} ;
/ * *
2019-03-07 09:22:27 +00:00
* Get item index .
2016-02-01 22:28:13 +00:00
*
* @ private
* @ return { number } Item index
* /
OO . ui . mixin . DraggableElement . prototype . getIndex = function ( ) {
return this . index ;
} ;
/ * *
* 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 . mixin . DraggableElement .
*
* @ abstract
* @ class
* @ mixins OO . ui . mixin . 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'
2017-03-28 23:25:58 +00:00
* @ cfg { boolean } [ draggable ] The items are draggable . This can change with # toggleDraggable
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . DraggableGroupElement = function OoUiMixinDraggableGroupElement ( config ) {
// Configuration initialization
config = config || { } ;
// Parent constructor
OO . ui . mixin . GroupElement . call ( this , config ) ;
// Properties
this . orientation = config . orientation || 'vertical' ;
this . dragItem = null ;
this . itemKeys = { } ;
2016-02-22 22:36:25 +00:00
this . dir = null ;
this . itemsOrder = null ;
2017-03-28 23:25:58 +00:00
this . draggable = config . draggable === undefined ? true : ! ! config . draggable ;
2016-02-01 22:28:13 +00:00
// Events
this . aggregate ( {
dragstart : 'itemDragStart' ,
dragend : 'itemDragEnd' ,
drop : 'itemDrop'
} ) ;
this . connect ( this , {
itemDragStart : 'onItemDragStart' ,
2016-02-22 22:36:25 +00:00
itemDrop : 'onItemDropOrDragEnd' ,
itemDragEnd : 'onItemDropOrDragEnd'
2016-02-01 22:28:13 +00:00
} ) ;
// Initialize
if ( Array . isArray ( config . items ) ) {
this . addItems ( config . items ) ;
}
this . $element
. addClass ( 'oo-ui-draggableGroupElement' )
2016-02-22 22:36:25 +00:00
. toggleClass ( 'oo-ui-draggableGroupElement-horizontal' , this . orientation === 'horizontal' ) ;
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
OO . mixinClass ( OO . ui . mixin . DraggableGroupElement , OO . ui . mixin . GroupElement ) ;
/* Events */
/ * *
2016-02-22 22:36:25 +00:00
* An item has been dragged to a new position , but not yet dropped .
*
* @ event drag
* @ param { OO . ui . mixin . DraggableElement } item Dragged item
* @ param { number } [ newIndex ] New index for the item
* /
/ * *
2017-03-28 23:25:58 +00:00
* An item has been dropped at a new position .
2016-02-01 22:28:13 +00:00
*
* @ event reorder
* @ param { OO . ui . mixin . DraggableElement } item Reordered item
* @ param { number } [ newIndex ] New index for the item
* /
2017-03-28 23:25:58 +00:00
/ * *
* Draggable state of this widget has changed .
*
* @ event draggable
* @ param { boolean } [ draggable ] Widget is draggable
* /
2016-02-01 22:28:13 +00:00
/* Methods */
2017-03-28 23:25:58 +00:00
/ * *
* Change the draggable state of this widget .
* This allows users to temporarily halt the dragging operations .
*
* @ param { boolean } isDraggable Widget supports draggable operations
* @ fires draggable
* /
OO . ui . mixin . DraggableGroupElement . prototype . toggleDraggable = function ( isDraggable ) {
isDraggable = isDraggable !== undefined ? ! ! isDraggable : ! this . draggable ;
if ( this . draggable !== isDraggable ) {
this . draggable = isDraggable ;
// Tell the items their draggable state changed
this . getItems ( ) . forEach ( function ( item ) {
item . toggleDraggable ( this . draggable ) ;
} . bind ( this ) ) ;
// Emit event
this . emit ( 'draggable' , this . draggable ) ;
}
} ;
/ * *
* Check the draggable state of this widget
*
* @ return { boolean } Widget supports draggable operations
* /
OO . ui . mixin . DraggableGroupElement . prototype . isDraggable = function ( ) {
return this . draggable ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
* Respond to item drag start event
*
* @ private
* @ param { OO . ui . mixin . DraggableElement } item Dragged item
* /
OO . ui . mixin . DraggableGroupElement . prototype . onItemDragStart = function ( item ) {
2017-03-28 23:25:58 +00:00
if ( ! this . isDraggable ( ) ) {
return ;
}
2016-02-22 22:36:25 +00:00
// Make a shallow copy of this.items so we can re-order it during previews
// without affecting the original array.
this . itemsOrder = this . items . slice ( ) ;
this . updateIndexes ( ) ;
2016-02-01 22:28:13 +00:00
if ( this . orientation === 'horizontal' ) {
2016-02-22 22:36:25 +00:00
// Calculate and cache directionality on drag start - it's a little
// expensive and it shouldn't change while dragging.
this . dir = this . $element . css ( 'direction' ) ;
2016-02-01 22:28:13 +00:00
}
this . setDragItem ( item ) ;
} ;
/ * *
2016-02-22 22:36:25 +00:00
* Update the index properties of the items
2016-02-01 22:28:13 +00:00
* /
2016-02-22 22:36:25 +00:00
OO . ui . mixin . DraggableGroupElement . prototype . updateIndexes = function ( ) {
var i , len ;
// Map the index of each object
for ( i = 0 , len = this . itemsOrder . length ; i < len ; i ++ ) {
this . itemsOrder [ i ] . setIndex ( i ) ;
}
2016-02-01 22:28:13 +00:00
} ;
/ * *
2016-02-22 22:36:25 +00:00
* Handle drop or dragend event and switch the order of the items accordingly
2016-02-01 22:28:13 +00:00
*
* @ private
* @ param { OO . ui . mixin . DraggableElement } item Dropped item
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Element } The element , for chaining
2016-02-01 22:28:13 +00:00
* /
2016-02-22 22:36:25 +00:00
OO . ui . mixin . DraggableGroupElement . prototype . onItemDropOrDragEnd = function ( ) {
var targetIndex , originalIndex ,
item = this . getDragItem ( ) ;
2016-02-01 22:28:13 +00:00
// TODO: Figure out a way to configure a list of legally droppable
// elements even if they are not yet in the list
2016-02-22 22:36:25 +00:00
if ( item ) {
originalIndex = this . items . indexOf ( item ) ;
// If the item has moved forward, add one to the index to account for the left shift
targetIndex = item . getIndex ( ) + ( item . getIndex ( ) > originalIndex ? 1 : 0 ) ;
2016-03-01 22:00:31 +00:00
if ( targetIndex !== originalIndex ) {
this . reorder ( this . getDragItem ( ) , targetIndex ) ;
this . emit ( 'reorder' , this . getDragItem ( ) , targetIndex ) ;
}
2016-02-22 22:36:25 +00:00
this . updateIndexes ( ) ;
2016-02-01 22:28:13 +00:00
}
this . unsetDragItem ( ) ;
// Return false to prevent propogation
return false ;
} ;
/ * *
* Respond to dragover event
*
* @ private
2016-03-01 22:00:31 +00:00
* @ param { jQuery . Event } e Dragover event
2016-02-22 22:36:25 +00:00
* @ fires reorder
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . DraggableGroupElement . prototype . onDragOver = function ( e ) {
2016-05-03 23:09:20 +00:00
var overIndex , targetIndex ,
2016-02-22 22:36:25 +00:00
item = this . getDragItem ( ) ,
2016-05-03 23:09:20 +00:00
dragItemIndex = item . getIndex ( ) ;
2016-02-01 22:28:13 +00:00
// Get the OptionWidget item we are dragging over
2016-05-03 23:09:20 +00:00
overIndex = $ ( e . target ) . closest ( '.oo-ui-draggableElement' ) . data ( 'index' ) ;
if ( overIndex !== undefined && overIndex !== dragItemIndex ) {
targetIndex = overIndex + ( overIndex > dragItemIndex ? 1 : 0 ) ;
2016-02-01 22:28:13 +00:00
2016-02-22 22:36:25 +00:00
if ( targetIndex > 0 ) {
this . $group . children ( ) . eq ( targetIndex - 1 ) . after ( item . $element ) ;
} else {
this . $group . prepend ( item . $element ) ;
}
2016-05-03 23:09:20 +00:00
// Move item in itemsOrder array
this . itemsOrder . splice ( overIndex , 0 ,
2016-02-22 22:36:25 +00:00
this . itemsOrder . splice ( dragItemIndex , 1 ) [ 0 ]
) ;
this . updateIndexes ( ) ;
this . emit ( 'drag' , item , targetIndex ) ;
2016-02-01 22:28:13 +00:00
}
// Prevent default
e . preventDefault ( ) ;
} ;
2016-02-22 22:36:25 +00:00
/ * *
* Reorder the items in the group
*
* @ param { OO . ui . mixin . DraggableElement } item Reordered item
* @ param { number } newIndex New index
* /
OO . ui . mixin . DraggableGroupElement . prototype . reorder = function ( item , newIndex ) {
this . addItems ( [ item ] , newIndex ) ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
* Set a dragged item
*
* @ param { OO . ui . mixin . DraggableElement } item Dragged item
* /
OO . ui . mixin . DraggableGroupElement . prototype . setDragItem = function ( item ) {
2016-06-29 13:32:06 +00:00
if ( this . dragItem !== item ) {
this . dragItem = item ;
this . $element . on ( 'dragover' , this . onDragOver . bind ( this ) ) ;
this . $element . addClass ( 'oo-ui-draggableGroupElement-dragging' ) ;
}
2016-02-01 22:28:13 +00:00
} ;
/ * *
* Unset the current dragged item
* /
OO . ui . mixin . DraggableGroupElement . prototype . unsetDragItem = function ( ) {
2016-06-29 13:32:06 +00:00
if ( this . dragItem ) {
this . dragItem = null ;
this . $element . off ( 'dragover' ) ;
this . $element . removeClass ( 'oo-ui-draggableGroupElement-dragging' ) ;
}
2016-02-01 22:28:13 +00:00
} ;
/ * *
* Get the item that is currently being dragged .
*
2019-03-07 09:22:27 +00:00
* @ return { OO . ui . mixin . DraggableElement | null } The currently dragged item , or ` null ` if no item is
* being dragged
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . DraggableGroupElement . prototype . getDragItem = function ( ) {
return this . dragItem ;
} ;
/ * *
2019-03-07 09:22:27 +00:00
* RequestManager is a mixin that manages the lifecycle of a promise - backed request for a widget ,
* such as the { @ link OO . ui . mixin . LookupElement } .
2016-02-01 22:28:13 +00:00
*
* @ class
* @ abstract
*
* @ constructor
2019-04-16 23:22:32 +00:00
* @ param { Object } [ config ] Configuration options
* @ cfg { boolean } [ showPendingRequest = true ] Show pending state while request data is being fetched .
* Requires widget to have also mixed in { @ link OO . ui . mixin . PendingElement } .
2016-02-01 22:28:13 +00:00
* /
2019-04-16 23:22:32 +00:00
OO . ui . mixin . RequestManager = function OoUiMixinRequestManager ( config ) {
2016-02-01 22:28:13 +00:00
this . requestCache = { } ;
this . requestQuery = null ;
this . requestRequest = null ;
2019-04-16 23:22:32 +00:00
this . showPendingRequest = ! ! this . pushPending && config . showPendingRequest !== false ;
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
OO . initClass ( OO . ui . mixin . RequestManager ) ;
/ * *
* Get request results for the current query .
*
2019-03-07 09:22:27 +00:00
* @ return { jQuery . Promise } Promise object which will be passed response data as the first argument
* of the done event . If the request was aborted to make way for a subsequent request , this
* promise may not be rejected , depending on what jQuery feels like doing .
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . RequestManager . prototype . getRequestData = function ( ) {
var widget = this ,
value = this . getRequestQuery ( ) ,
deferred = $ . Deferred ( ) ,
ourRequest ;
this . abortRequest ( ) ;
if ( Object . prototype . hasOwnProperty . call ( this . requestCache , value ) ) {
deferred . resolve ( this . requestCache [ value ] ) ;
} else {
2019-04-16 23:22:32 +00:00
if ( this . showPendingRequest ) {
2016-02-01 22:28:13 +00:00
this . pushPending ( ) ;
}
this . requestQuery = value ;
ourRequest = this . requestRequest = this . getRequest ( ) ;
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.
2019-04-16 23:22:32 +00:00
if ( widget . showPendingRequest ) {
2016-02-01 22:28:13 +00:00
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 . requestRequest ) {
widget . requestQuery = null ;
widget . requestRequest = null ;
2019-03-07 09:22:27 +00:00
widget . requestCache [ value ] =
widget . getRequestCacheDataFromResponse ( response ) ;
2016-02-01 22:28:13 +00:00
deferred . resolve ( widget . requestCache [ 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 . requestRequest ) {
widget . requestQuery = null ;
widget . requestRequest = null ;
deferred . reject ( ) ;
}
} ) ;
}
return deferred . promise ( ) ;
} ;
/ * *
* Abort the currently pending request , if any .
*
* @ private
* /
OO . ui . mixin . RequestManager . prototype . abortRequest = function ( ) {
var oldRequest = this . requestRequest ;
if ( oldRequest ) {
// First unset this.requestRequest to the fail handler will notice
// that the request is no longer current
this . requestRequest = null ;
this . requestQuery = null ;
oldRequest . abort ( ) ;
}
} ;
/ * *
* Get the query to be made .
*
* @ protected
* @ method
* @ abstract
* @ return { string } query to be used
* /
OO . ui . mixin . RequestManager . prototype . getRequestQuery = null ;
/ * *
* Get a new request object of the current query value .
*
* @ protected
* @ method
* @ abstract
* @ return { jQuery . Promise } jQuery AJAX object , or promise object with an . abort ( ) method
* /
OO . ui . mixin . RequestManager . prototype . getRequest = null ;
/ * *
* Pre - process data returned by the request from # getRequest .
*
* 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
* @ method
* @ abstract
* @ param { Mixed } response Response from server
* @ return { Mixed } Cached result data
* /
OO . ui . mixin . RequestManager . prototype . getRequestCacheDataFromResponse = null ;
/ * *
2019-03-07 09:22:27 +00:00
* LookupElement is a mixin that creates a { @ link OO . ui . MenuSelectWidget 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 .
2016-02-01 22:28:13 +00:00
*
2019-03-07 09:22:27 +00:00
* 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 .
2016-02-01 22:28:13 +00:00
*
2018-01-17 05:47:34 +00:00
* See the [ OOUI demos ] [ 1 ] for an example .
2016-02-01 22:28:13 +00:00
*
2018-01-17 05:47:34 +00:00
* [ 1 ] : https : //doc.wikimedia.org/oojs-ui/master/demos/#LookupElement-try-inputting-an-integer
2016-02-01 22:28:13 +00:00
*
* @ class
* @ abstract
2016-05-31 21:55:25 +00:00
* @ mixins OO . ui . mixin . RequestManager
2016-02-01 22:28:13 +00:00
*
* @ constructor
* @ param { Object } [ config ] Configuration options
2017-04-26 01:28:38 +00:00
* @ cfg { jQuery } [ $overlay ] Overlay for the lookup menu ; defaults to relative positioning .
2018-01-17 05:47:34 +00:00
* See < https : //www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
2019-03-07 09:22:27 +00:00
* @ cfg { jQuery } [ $container = this . $element ] The container element . The lookup menu is rendered
* beneath the specified element .
* @ cfg { Object } [ menu ] Configuration options to pass to
* { @ link OO . ui . MenuSelectWidget menu select widget }
* @ cfg { boolean } [ allowSuggestionsWhenEmpty = false ] Request and display a lookup menu when the
* text input is empty .
2016-02-01 22:28:13 +00:00
* By default , the lookup menu is not generated and displayed until the user begins to type .
2019-03-07 09:22:27 +00:00
* @ cfg { boolean } [ highlightFirst = true ] Whether the first lookup result should be highlighted
* ( so , that the user can take it over into the input with simply pressing return ) automatically
* or not .
2019-05-29 00:57:59 +00:00
* @ cfg { boolean } [ showSuggestionsOnFocus = true ] Show suggestions when focusing the input . If this
* is set to false , suggestions will still be shown on a mousedown triggered focus . This matches
* browser autocomplete behavior .
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . LookupElement = function OoUiMixinLookupElement ( config ) {
// Configuration initialization
config = $ . extend ( { highlightFirst : true } , config ) ;
// Mixin constructors
OO . ui . mixin . RequestManager . call ( this , config ) ;
// Properties
2019-03-07 09:22:27 +00:00
this . $overlay = ( config . $overlay === true ?
OO . ui . getDefaultOverlay ( ) : config . $overlay ) || this . $element ;
2018-06-06 16:49:23 +00:00
this . lookupMenu = new OO . ui . MenuSelectWidget ( $ . extend ( {
2016-02-01 22:28:13 +00:00
widget : this ,
input : this ,
2017-05-10 01:21:26 +00:00
$floatableContainer : config . $container || this . $element
2018-06-06 16:49:23 +00:00
} , config . menu ) ) ;
2016-02-01 22:28:13 +00:00
this . allowSuggestionsWhenEmpty = config . allowSuggestionsWhenEmpty || false ;
this . lookupsDisabled = false ;
this . lookupInputFocused = false ;
this . lookupHighlightFirstItem = config . highlightFirst ;
2019-05-29 00:57:59 +00:00
this . showSuggestionsOnFocus = config . showSuggestionsOnFocus !== false ;
2016-02-01 22:28:13 +00:00
// Events
this . $input . on ( {
focus : this . onLookupInputFocus . bind ( this ) ,
blur : this . onLookupInputBlur . bind ( this ) ,
mousedown : this . onLookupInputMouseDown . bind ( this )
} ) ;
2019-03-07 09:22:27 +00:00
this . connect ( this , {
change : 'onLookupInputChange'
} ) ;
2016-02-01 22:28:13 +00:00
this . lookupMenu . connect ( this , {
toggle : 'onLookupMenuToggle' ,
2019-12-12 00:36:19 +00:00
choose : 'onLookupMenuChoose'
2016-02-01 22:28:13 +00:00
} ) ;
// Initialization
2017-04-26 01:28:38 +00:00
this . $input . attr ( {
role : 'combobox' ,
'aria-owns' : this . lookupMenu . getElementId ( ) ,
'aria-autocomplete' : 'list'
} ) ;
2016-02-01 22:28:13 +00:00
this . $element . addClass ( 'oo-ui-lookupElement' ) ;
this . lookupMenu . $element . addClass ( 'oo-ui-lookupElement-menu' ) ;
this . $overlay . append ( this . lookupMenu . $element ) ;
} ;
/* Setup */
OO . mixinClass ( OO . ui . mixin . LookupElement , OO . ui . mixin . RequestManager ) ;
/* Methods */
/ * *
* Handle input focus event .
*
* @ protected
* @ param { jQuery . Event } e Input focus event
* /
OO . ui . mixin . LookupElement . prototype . onLookupInputFocus = function ( ) {
this . lookupInputFocused = true ;
2019-05-29 00:57:59 +00:00
if ( this . showSuggestionsOnFocus ) {
this . populateLookupMenu ( ) ;
}
2016-02-01 22:28:13 +00:00
} ;
/ * *
* Handle input blur event .
*
* @ protected
* @ param { jQuery . Event } e Input blur event
* /
OO . ui . mixin . 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 . mixin . LookupElement . prototype . onLookupInputMouseDown = function ( ) {
2019-05-29 00:57:59 +00:00
if (
! this . lookupMenu . isVisible ( ) &&
(
// Open the menu if the input was already focused.
// This way we allow the user to open the menu again after closing it with Escape (esc)
// by clicking in the input.
this . lookupInputFocused ||
// If showSuggestionsOnFocus is disabled, still open the menu on mousedown.
! this . showSuggestionsOnFocus
)
) {
2016-02-01 22:28:13 +00:00
this . populateLookupMenu ( ) ;
}
} ;
/ * *
* Handle input change event .
*
* @ protected
* @ param { string } value New input value
* /
OO . ui . mixin . 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 . mixin . 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
2019-03-07 09:22:27 +00:00
// MenuSelectWidget will close itself when the user presses Escape (esc).
2016-02-01 22:28:13 +00:00
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
* /
2019-12-12 00:36:19 +00:00
OO . ui . mixin . LookupElement . prototype . onLookupMenuChoose = function ( item ) {
if ( this . onLookupMenuItemChoose ) {
// @since 0.35.2
OO . ui . warnDeprecation (
'onLookupMenuItemChoose is deprecated. ' +
'Use onLookupMenuChoose instead.'
) ;
this . onLookupMenuItemChoose ( item ) ;
} else {
this . setValue ( item . getData ( ) ) ;
}
2016-02-01 22:28:13 +00:00
} ;
/ * *
* Get lookup menu .
*
* @ private
2017-05-10 01:21:26 +00:00
* @ return { OO . ui . MenuSelectWidget }
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . 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 . mixin . LookupElement . prototype . setLookupsDisabled = function ( disabled ) {
this . lookupsDisabled = ! ! disabled ;
} ;
/ * *
* Open the menu . If there are no entries in the menu , this does nothing .
*
* @ private
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Element } The element , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . 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
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Element } The element , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . 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
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Element } The element , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . LookupElement . prototype . populateLookupMenu = function ( ) {
var widget = this ,
value = this . getValue ( ) ;
if ( this . lookupsDisabled || this . isReadOnly ( ) ) {
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 ( ) ;
2018-08-14 23:55:10 +00:00
widget . lookupMenu . toggle ( false ) ;
2016-02-01 22:28:13 +00:00
} ) ;
}
return this ;
} ;
/ * *
* Highlight the first selectable item in the menu , if configured .
*
* @ private
* @ chainable
* /
OO . ui . mixin . LookupElement . prototype . initializeLookupMenuSelection = function ( ) {
2018-01-17 05:47:34 +00:00
if ( this . lookupHighlightFirstItem && ! this . lookupMenu . findSelectedItem ( ) ) {
2017-09-20 00:58:44 +00:00
this . lookupMenu . highlightItem ( this . lookupMenu . findFirstSelectableItem ( ) ) ;
2016-02-01 22:28:13 +00:00
}
} ;
/ * *
* 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 . mixin . LookupElement . prototype . getLookupMenuItems = function ( ) {
return this . getRequestData ( ) . then ( function ( data ) {
return this . getLookupMenuOptionsFromData ( data ) ;
} . bind ( this ) ) ;
} ;
/ * *
* Abort the currently pending lookup request , if any .
*
* @ private
* /
OO . ui . mixin . LookupElement . prototype . abortLookupRequest = function ( ) {
this . abortRequest ( ) ;
} ;
/ * *
* Get a new request object of the current lookup query value .
*
* @ protected
* @ method
* @ abstract
* @ return { jQuery . Promise } jQuery AJAX object , or promise object with an . abort ( ) method
* /
OO . ui . mixin . LookupElement . prototype . getLookupRequest = 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
* @ method
* @ abstract
* @ param { Mixed } response Response from server
* @ return { Mixed } Cached result data
* /
OO . ui . mixin . LookupElement . prototype . getLookupCacheDataFromResponse = null ;
/ * *
* Get a list of menu option widgets from the ( possibly cached ) data returned by
* # getLookupCacheDataFromResponse .
*
* @ protected
* @ method
* @ abstract
* @ param { Mixed } data Cached result data , usually an array
* @ return { OO . ui . MenuOptionWidget [ ] } Menu items
* /
OO . ui . mixin . LookupElement . prototype . getLookupMenuOptionsFromData = null ;
/ * *
* Set the read - only state of the widget .
*
* This will also disable / enable the lookups functionality .
*
* @ param { boolean } readOnly Make input read - only
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Element } The element , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . mixin . LookupElement . prototype . setReadOnly = function ( readOnly ) {
// Parent method
// Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
OO . ui . TextInputWidget . prototype . setReadOnly . call ( this , readOnly ) ;
2019-03-07 09:22:27 +00:00
// During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor.
2016-02-01 22:28:13 +00:00
if ( this . isReadOnly ( ) && this . lookupMenu ) {
this . closeLookupMenu ( ) ;
}
return this ;
} ;
/ * *
* @ inheritdoc OO . ui . mixin . RequestManager
* /
OO . ui . mixin . LookupElement . prototype . getRequestQuery = function ( ) {
return this . getValue ( ) ;
} ;
/ * *
* @ inheritdoc OO . ui . mixin . RequestManager
* /
OO . ui . mixin . LookupElement . prototype . getRequest = function ( ) {
return this . getLookupRequest ( ) ;
} ;
/ * *
* @ inheritdoc OO . ui . mixin . RequestManager
* /
OO . ui . mixin . LookupElement . prototype . getRequestCacheDataFromResponse = function ( response ) {
return this . getLookupCacheDataFromResponse ( response ) ;
} ;
/ * *
2017-05-10 01:21:26 +00:00
* TabPanelLayouts are used within { @ link OO . ui . IndexLayout index layouts } to create tab panels that
* users can select and display from the index ' s optional { @ link OO . ui . TabSelectWidget tab }
* navigation . TabPanels are usually not instantiated directly , rather extended to include the
* required content and functionality .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* Each tab panel must have a unique symbolic name , which is passed to the constructor . In addition ,
* the tab panel ' s tab item is customized ( with a label ) using the # setupTabItem method . See
2016-02-01 22:28:13 +00:00
* { @ link OO . ui . IndexLayout IndexLayout } for an example .
*
* @ class
* @ extends OO . ui . PanelLayout
*
* @ constructor
2017-05-10 01:21:26 +00:00
* @ param { string } name Unique symbolic name of tab panel
2016-02-01 22:28:13 +00:00
* @ param { Object } [ config ] Configuration options
2017-05-10 01:21:26 +00:00
* @ cfg { jQuery | string | Function | OO . ui . HtmlSnippet } [ label ] Label for tab panel ' s tab
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . TabPanelLayout = function OoUiTabPanelLayout ( name , config ) {
2016-02-01 22:28:13 +00:00
// Allow passing positional parameters inside the config object
if ( OO . isPlainObject ( name ) && config === undefined ) {
config = name ;
name = config . name ;
}
// Configuration initialization
config = $ . extend ( { scrollable : true } , config ) ;
// Parent constructor
2017-05-10 01:21:26 +00:00
OO . ui . TabPanelLayout . parent . call ( this , config ) ;
2016-02-01 22:28:13 +00:00
// Properties
this . name = name ;
this . label = config . label ;
this . tabItem = null ;
this . active = false ;
// Initialization
2018-07-11 18:43:15 +00:00
this . $element
. addClass ( 'oo-ui-tabPanelLayout' )
. attr ( 'role' , 'tabpanel' ) ;
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
2017-05-10 01:21:26 +00:00
OO . inheritClass ( OO . ui . TabPanelLayout , OO . ui . PanelLayout ) ;
2016-02-01 22:28:13 +00:00
/* Events */
/ * *
2019-03-07 09:22:27 +00:00
* An 'active' event is emitted when the tab panel becomes active . Tab panels become active when
* they are shown in a index layout that is configured to display only one tab panel at a time .
2016-02-01 22:28:13 +00:00
*
* @ event active
2017-05-10 01:21:26 +00:00
* @ param { boolean } active Tab panel is active
2016-02-01 22:28:13 +00:00
* /
/* Methods */
/ * *
2017-05-10 01:21:26 +00:00
* Get the symbolic name of the tab panel .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ return { string } Symbolic name of tab panel
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . TabPanelLayout . prototype . getName = function ( ) {
2016-02-01 22:28:13 +00:00
return this . name ;
} ;
/ * *
2017-05-10 01:21:26 +00:00
* Check if tab panel is active .
2016-02-01 22:28:13 +00:00
*
2019-03-07 09:22:27 +00:00
* Tab panels become active when they are shown in a { @ link OO . ui . IndexLayout index layout } that is
* configured to display only one tab panel at a time . Additional CSS is applied to the tab panel ' s
* tab item to reflect the active state .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ return { boolean } Tab panel is active
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . TabPanelLayout . prototype . isActive = function ( ) {
2016-02-01 22:28:13 +00:00
return this . active ;
} ;
/ * *
* Get tab item .
*
2017-05-10 01:21:26 +00:00
* The tab item allows users to access the tab panel from the index ' s tab
2019-03-07 09:22:27 +00:00
* navigation . The tab item itself can be customized ( with a label , level , etc . ) using the
* # setupTabItem method .
2016-02-01 22:28:13 +00:00
*
* @ return { OO . ui . TabOptionWidget | null } Tab option widget
* /
2017-05-10 01:21:26 +00:00
OO . ui . TabPanelLayout . prototype . getTabItem = function ( ) {
2016-02-01 22:28:13 +00:00
return this . tabItem ;
} ;
/ * *
* Set or unset the tab item .
*
* Specify a { @ link OO . ui . TabOptionWidget tab option } to set it ,
* or ` null ` to clear the tab item . To customize the tab item itself ( e . g . , to set a label or tab
* level ) , use # setupTabItem instead of this method .
*
* @ param { OO . ui . TabOptionWidget | null } tabItem Tab option widget , null to clear
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . TabPanelLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . TabPanelLayout . prototype . setTabItem = function ( tabItem ) {
2016-02-01 22:28:13 +00:00
this . tabItem = tabItem || null ;
if ( tabItem ) {
this . setupTabItem ( ) ;
}
return this ;
} ;
/ * *
* Set up the tab item .
*
* Use this method to customize the tab item ( e . g . , to add a label or tab level ) . To set or unset
* the tab item itself ( with a { @ link OO . ui . TabOptionWidget tab option } or ` null ` ) , use
* the # setTabItem method instead .
*
* @ param { OO . ui . TabOptionWidget } tabItem Tab option widget to set up
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . TabPanelLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . TabPanelLayout . prototype . setupTabItem = function ( ) {
2018-07-11 18:43:15 +00:00
this . $element . attr ( 'aria-labelledby' , this . tabItem . getElementId ( ) ) ;
this . tabItem . $element . attr ( 'aria-controls' , this . getElementId ( ) ) ;
2016-02-01 22:28:13 +00:00
if ( this . label ) {
this . tabItem . setLabel ( this . label ) ;
}
return this ;
} ;
/ * *
2017-05-10 01:21:26 +00:00
* Set the tab panel to its 'active' state .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* Tab panels become active when they are shown in a index layout that is configured to display only
* one tab panel at a time . Additional CSS is applied to the tab item to reflect the tab panel ' s
* active state . Outside of the index context , setting the active state on a tab panel does nothing .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ param { boolean } active Tab panel is active
2016-02-01 22:28:13 +00:00
* @ fires active
* /
2017-05-10 01:21:26 +00:00
OO . ui . TabPanelLayout . prototype . setActive = function ( active ) {
2016-02-01 22:28:13 +00:00
active = ! ! active ;
if ( active !== this . active ) {
this . active = active ;
2017-05-10 01:21:26 +00:00
this . $element . toggleClass ( 'oo-ui-tabPanelLayout-active' , this . active ) ;
2016-02-01 22:28:13 +00:00
this . emit ( 'active' , this . active ) ;
}
} ;
/ * *
2019-03-07 09:22:27 +00:00
* PageLayouts are used within { @ link OO . ui . BookletLayout booklet layouts } to create pages that
* users can select and display from the booklet ' s optional
* { @ link OO . ui . OutlineSelectWidget outline } navigation . Pages are usually not instantiated
* directly , rather extended to include the required content and functionality .
2016-02-01 22:28:13 +00:00
*
2019-03-07 09:22:27 +00:00
* Each page must have a unique symbolic name , which is passed to the constructor . In addition , the
* page ' s outline item is customized ( with a label , outline level , etc . ) using the
* # setupOutlineItem method . See { @ link OO . ui . BookletLayout BookletLayout } for an example .
2016-02-01 22:28:13 +00:00
*
* @ class
* @ extends OO . ui . PanelLayout
*
* @ constructor
* @ param { string } name Unique symbolic name of page
* @ param { Object } [ config ] Configuration options
* /
OO . ui . PageLayout = function OoUiPageLayout ( name , config ) {
// Allow passing positional parameters inside the config object
if ( OO . isPlainObject ( name ) && config === undefined ) {
config = name ;
name = config . name ;
}
// Configuration initialization
config = $ . extend ( { scrollable : true } , config ) ;
// Parent constructor
OO . ui . PageLayout . parent . call ( this , config ) ;
// Properties
this . name = name ;
this . outlineItem = null ;
this . active = false ;
// Initialization
this . $element . addClass ( 'oo-ui-pageLayout' ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . PageLayout , OO . ui . PanelLayout ) ;
/* Events */
/ * *
* An 'active' event is emitted when the page becomes active . Pages become active when they are
* shown in a booklet layout that is configured to display only one page at a time .
*
* @ event active
* @ param { boolean } active Page is active
* /
/* Methods */
/ * *
* Get the symbolic name of the page .
*
* @ return { string } Symbolic name of page
* /
OO . ui . PageLayout . prototype . getName = function ( ) {
return this . name ;
} ;
/ * *
* Check if page is active .
*
2019-03-07 09:22:27 +00:00
* Pages become active when they are shown in a { @ link OO . ui . BookletLayout booklet layout } that is
* configured to display only one page at a time . Additional CSS is applied to the page ' s outline
* item to reflect the active state .
2016-02-01 22:28:13 +00:00
*
* @ return { boolean } Page is active
* /
OO . ui . PageLayout . prototype . isActive = function ( ) {
return this . active ;
} ;
/ * *
* Get outline item .
*
* The outline item allows users to access the page from the booklet ' s outline
2019-03-07 09:22:27 +00:00
* navigation . The outline item itself can be customized ( with a label , level , etc . ) using the
* # setupOutlineItem method .
2016-02-01 22:28:13 +00:00
*
* @ return { OO . ui . OutlineOptionWidget | null } Outline option widget
* /
OO . ui . PageLayout . prototype . getOutlineItem = function ( ) {
return this . outlineItem ;
} ;
/ * *
* Set or unset the outline item .
*
* Specify an { @ link OO . ui . OutlineOptionWidget outline option } to set it ,
2019-03-07 09:22:27 +00:00
* or ` null ` to clear the outline item . To customize the outline item itself ( e . g . , to set a label
* or outline level ) , use # setupOutlineItem instead of this method .
2016-02-01 22:28:13 +00:00
*
* @ param { OO . ui . OutlineOptionWidget | null } outlineItem Outline option widget , null to clear
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . PageLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . PageLayout . prototype . setOutlineItem = function ( outlineItem ) {
this . outlineItem = outlineItem || null ;
if ( outlineItem ) {
this . setupOutlineItem ( ) ;
}
return this ;
} ;
/ * *
* Set up the outline item .
*
2019-03-07 09:22:27 +00:00
* Use this method to customize the outline item ( e . g . , to add a label or outline level ) . To set or
* unset the outline item itself ( with an { @ link OO . ui . OutlineOptionWidget outline option } or
* ` null ` ) , use the # setOutlineItem method instead .
2016-02-01 22:28:13 +00:00
*
* @ param { OO . ui . OutlineOptionWidget } outlineItem Outline option widget to set up
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . PageLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . PageLayout . prototype . setupOutlineItem = function ( ) {
return this ;
} ;
/ * *
* Set the page to its 'active' state .
*
2019-03-07 09:22:27 +00:00
* Pages become active when they are shown in a booklet layout that is configured to display only
* one page at a time . Additional CSS is applied to the outline item to reflect the page ' s active
* state . Outside of the booklet context , setting the active state on a page does nothing .
2016-02-01 22:28:13 +00:00
*
2016-03-01 22:00:31 +00:00
* @ param { boolean } active Page is active
2016-02-01 22:28:13 +00:00
* @ fires active
* /
OO . ui . PageLayout . prototype . setActive = function ( active ) {
active = ! ! active ;
if ( active !== this . active ) {
this . active = active ;
this . $element . toggleClass ( 'oo-ui-pageLayout-active' , active ) ;
this . emit ( 'active' , this . active ) ;
}
} ;
/ * *
2019-03-07 09:22:27 +00:00
* StackLayouts contain a series of { @ link OO . ui . PanelLayout panel layouts } . By default , only one
* panel is displayed at a time , though the stack layout can also be configured to show all
* contained panels , one after another , by setting the # continuous option to 'true' .
2016-02-01 22:28:13 +00:00
*
* @ example
2018-11-01 02:57:41 +00:00
* // A stack layout with two panels, configured to be displayed continuously
2016-02-01 22:28:13 +00:00
* var myStack = new OO . ui . StackLayout ( {
* items : [
* new OO . ui . PanelLayout ( {
* $content : $ ( '<p>Panel One</p>' ) ,
* padded : true ,
* framed : true
* } ) ,
* new OO . ui . PanelLayout ( {
* $content : $ ( '<p>Panel Two</p>' ) ,
* padded : true ,
* framed : true
* } )
* ] ,
* continuous : true
* } ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( myStack . $element ) ;
2016-02-01 22:28:13 +00:00
*
* @ class
* @ extends OO . ui . PanelLayout
* @ mixins OO . ui . mixin . GroupElement
*
* @ constructor
* @ param { Object } [ config ] Configuration options
2019-03-07 09:22:27 +00:00
* @ cfg { boolean } [ continuous = false ] Show all panels , one after another . By default , only one panel
* is displayed at a time .
2016-02-01 22:28:13 +00:00
* @ cfg { OO . ui . Layout [ ] } [ items ] Panel layouts to add to the stack layout .
* /
OO . ui . StackLayout = function OoUiStackLayout ( config ) {
// Configuration initialization
2018-12-22 11:36:59 +00:00
// Make the layout scrollable in continuous mode, otherwise each
// panel is responsible for its own scrolling.
config = $ . extend ( { scrollable : ! ! ( config && config . continuous ) } , config ) ;
2016-02-01 22:28:13 +00:00
// Parent constructor
OO . ui . StackLayout . parent . call ( this , config ) ;
// Mixin constructors
2019-03-14 01:03:02 +00:00
OO . ui . mixin . GroupElement . call ( this , $ . extend ( { $group : this . $element } , config ) ) ;
2016-02-01 22:28:13 +00:00
// Properties
this . currentItem = null ;
this . continuous = ! ! config . continuous ;
// Initialization
this . $element . addClass ( 'oo-ui-stackLayout' ) ;
if ( this . continuous ) {
this . $element . addClass ( 'oo-ui-stackLayout-continuous' ) ;
this . $element . on ( 'scroll' , OO . ui . debounce ( this . onScroll . bind ( this ) , 250 ) ) ;
}
if ( Array . isArray ( config . items ) ) {
this . addItems ( config . items ) ;
}
} ;
/* Setup */
OO . inheritClass ( OO . ui . StackLayout , OO . ui . PanelLayout ) ;
OO . mixinClass ( OO . ui . StackLayout , OO . ui . mixin . GroupElement ) ;
/* Events */
/ * *
* A 'set' event is emitted when panels are { @ link # addItems added } , { @ link # removeItems removed } ,
* { @ link # clearItems cleared } or { @ link # setItem displayed } .
*
* @ event set
* @ param { OO . ui . Layout | null } item Current panel or ` null ` if no panel is shown
* /
/ * *
* When used in continuous mode , this event is emitted when the user scrolls down
* far enough such that currentItem is no longer visible .
*
* @ event visibleItemChange
* @ param { OO . ui . PanelLayout } panel The next visible item in the layout
* /
/* Methods */
/ * *
* Handle scroll events from the layout element
*
* @ param { jQuery . Event } e
* @ fires visibleItemChange
* /
OO . ui . StackLayout . prototype . onScroll = function ( ) {
var currentRect ,
len = this . items . length ,
currentIndex = this . items . indexOf ( this . currentItem ) ,
newIndex = currentIndex ,
containerRect = this . $element [ 0 ] . getBoundingClientRect ( ) ;
if ( ! containerRect || ( ! containerRect . top && ! containerRect . bottom ) ) {
// Can't get bounding rect, possibly not attached.
return ;
}
function getRect ( item ) {
return item . $element [ 0 ] . getBoundingClientRect ( ) ;
}
function isVisible ( item ) {
var rect = getRect ( item ) ;
return rect . bottom > containerRect . top && rect . top < containerRect . bottom ;
}
currentRect = getRect ( this . currentItem ) ;
if ( currentRect . bottom < containerRect . top ) {
// Scrolled down past current item
while ( ++ newIndex < len ) {
if ( isVisible ( this . items [ newIndex ] ) ) {
break ;
}
}
} else if ( currentRect . top > containerRect . bottom ) {
// Scrolled up past current item
while ( -- newIndex >= 0 ) {
if ( isVisible ( this . items [ newIndex ] ) ) {
break ;
}
}
}
if ( newIndex !== currentIndex ) {
this . emit ( 'visibleItemChange' , this . items [ newIndex ] ) ;
}
} ;
/ * *
* Get the current panel .
*
* @ return { OO . ui . Layout | null }
* /
OO . ui . StackLayout . prototype . getCurrentItem = function ( ) {
return this . currentItem ;
} ;
/ * *
* Unset the current item .
*
* @ private
* @ param { OO . ui . StackLayout } layout
* @ fires set
* /
OO . ui . StackLayout . prototype . unsetCurrentItem = function ( ) {
var prevItem = this . currentItem ;
if ( prevItem === null ) {
return ;
}
this . currentItem = null ;
this . emit ( 'set' , null ) ;
} ;
/ * *
* Add panel layouts to the stack layout .
*
2019-03-07 09:22:27 +00:00
* Panels will be added to the end of the stack layout array unless the optional index parameter
* specifies a different insertion point . Adding a panel that is already in the stack will move it
* to the end of the array or the point specified by the index .
2016-02-01 22:28:13 +00:00
*
* @ param { OO . ui . Layout [ ] } items Panels to add
* @ param { number } [ index ] Index of the insertion point
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . StackLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . StackLayout . prototype . addItems = function ( items , index ) {
// Update the visibility
this . updateHiddenState ( items , this . currentItem ) ;
// Mixin method
OO . ui . mixin . GroupElement . prototype . addItems . call ( this , items , index ) ;
if ( ! this . currentItem && items . length ) {
this . setItem ( items [ 0 ] ) ;
}
return this ;
} ;
/ * *
* Remove the specified panels from the stack layout .
*
2019-03-07 09:22:27 +00:00
* Removed panels are detached from the DOM , not removed , so that they may be reused . To remove all
* panels , you may wish to use the # clearItems method instead .
2016-02-01 22:28:13 +00:00
*
* @ param { OO . ui . Layout [ ] } items Panels to remove
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . StackLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* @ fires set
* /
OO . ui . StackLayout . prototype . removeItems = function ( items ) {
// Mixin method
OO . ui . mixin . GroupElement . prototype . removeItems . call ( this , items ) ;
if ( items . indexOf ( this . currentItem ) !== - 1 ) {
if ( this . items . length ) {
this . setItem ( this . items [ 0 ] ) ;
} else {
this . unsetCurrentItem ( ) ;
}
}
return this ;
} ;
/ * *
* Clear all panels from the stack layout .
*
* Cleared panels are detached from the DOM , not removed , so that they may be reused . To remove only
* a subset of panels , use the # removeItems method .
*
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . StackLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* @ fires set
* /
OO . ui . StackLayout . prototype . clearItems = function ( ) {
this . unsetCurrentItem ( ) ;
OO . ui . mixin . GroupElement . prototype . clearItems . call ( this ) ;
return this ;
} ;
/ * *
* Show the specified panel .
*
* If another panel is currently displayed , it will be hidden .
*
* @ param { OO . ui . Layout } item Panel to show
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . StackLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* @ fires set
* /
OO . ui . StackLayout . prototype . setItem = function ( item ) {
if ( item !== this . currentItem ) {
this . updateHiddenState ( this . items , item ) ;
if ( this . items . indexOf ( item ) !== - 1 ) {
this . currentItem = item ;
this . emit ( 'set' , item ) ;
} else {
this . unsetCurrentItem ( ) ;
}
}
return this ;
} ;
2018-11-01 02:57:41 +00:00
/ * *
* Reset the scroll offset of all panels , or the container if continuous
*
* @ inheritdoc
* /
OO . ui . StackLayout . prototype . resetScroll = function ( ) {
if ( this . continuous ) {
// Parent method
return OO . ui . StackLayout . parent . prototype . resetScroll . call ( this ) ;
}
// Reset each panel
this . getItems ( ) . forEach ( function ( panel ) {
2019-12-12 00:36:19 +00:00
// eslint-disable-next-line no-jquery/no-class-state
2018-11-01 02:57:41 +00:00
var hidden = panel . $element . hasClass ( 'oo-ui-element-hidden' ) ;
// Scroll can only be reset when panel is visible
panel . $element . removeClass ( 'oo-ui-element-hidden' ) ;
panel . resetScroll ( ) ;
if ( hidden ) {
panel . $element . addClass ( 'oo-ui-element-hidden' ) ;
}
} ) ;
return this ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
* Update the visibility of all items in case of non - continuous view .
*
* Ensure all items are hidden except for the selected one .
* This method does nothing when the stack is continuous .
*
* @ private
* @ param { OO . ui . Layout [ ] } items Item list iterate over
* @ param { OO . ui . Layout } [ selectedItem ] Selected item to show
* /
OO . ui . StackLayout . prototype . updateHiddenState = function ( items , selectedItem ) {
var i , len ;
if ( ! this . continuous ) {
for ( i = 0 , len = items . length ; i < len ; i ++ ) {
if ( ! selectedItem || selectedItem !== items [ i ] ) {
items [ i ] . $element . addClass ( 'oo-ui-element-hidden' ) ;
2016-07-12 20:30:06 +00:00
items [ i ] . $element . attr ( 'aria-hidden' , 'true' ) ;
2016-02-01 22:28:13 +00:00
}
}
if ( selectedItem ) {
selectedItem . $element . removeClass ( 'oo-ui-element-hidden' ) ;
2016-07-12 20:30:06 +00:00
selectedItem . $element . removeAttr ( 'aria-hidden' ) ;
2016-02-01 22:28:13 +00:00
}
}
} ;
/ * *
2019-03-07 09:22:27 +00:00
* MenuLayouts combine a menu and a content { @ link OO . ui . PanelLayout panel } . The menu is positioned
* relative to the content ( after , before , top , or bottom ) and its size is customized with the
* # menuSize config . The content area will fill all remaining space .
2016-02-01 22:28:13 +00:00
*
* @ example
2018-10-08 23:50:33 +00:00
* var menuLayout ,
2019-03-07 09:22:27 +00:00
* menuPanel = new OO . ui . PanelLayout ( {
* padded : true ,
* expanded : true ,
* scrollable : true
* } ) ,
* contentPanel = new OO . ui . PanelLayout ( {
* padded : true ,
* expanded : true ,
* scrollable : true
* } ) ,
2016-02-01 22:28:13 +00:00
* select = new OO . ui . SelectWidget ( {
* items : [
* new OO . ui . OptionWidget ( {
* data : 'before' ,
2018-10-08 23:50:33 +00:00
* label : 'Before'
2016-02-01 22:28:13 +00:00
* } ) ,
* new OO . ui . OptionWidget ( {
* data : 'after' ,
2018-10-08 23:50:33 +00:00
* label : 'After'
2016-02-01 22:28:13 +00:00
* } ) ,
* new OO . ui . OptionWidget ( {
* data : 'top' ,
2018-10-08 23:50:33 +00:00
* label : 'Top'
2016-02-01 22:28:13 +00:00
* } ) ,
* new OO . ui . OptionWidget ( {
* data : 'bottom' ,
2018-10-08 23:50:33 +00:00
* label : 'Bottom'
2016-02-01 22:28:13 +00:00
* } )
* ]
* } ) . on ( 'select' , function ( item ) {
* menuLayout . setMenuPosition ( item . getData ( ) ) ;
* } ) ;
*
2018-10-08 23:50:33 +00:00
* menuLayout = new OO . ui . MenuLayout ( {
* position : 'top' ,
* menuPanel : menuPanel ,
* contentPanel : contentPanel
2019-03-07 09:22:27 +00:00
* } ) ;
2016-02-01 22:28:13 +00:00
* menuLayout . $menu . append (
2019-04-04 21:29:43 +00:00
* menuPanel . $element . append ( '<b>Menu panel</b>' , select . $element )
2016-02-01 22:28:13 +00:00
* ) ;
* menuLayout . $content . append (
2019-03-07 09:22:27 +00:00
* contentPanel . $element . append (
* '<b>Content panel</b>' ,
* '<p>Note that the menu is positioned relative to the content panel: ' +
* 'top, bottom, after, before.</p>'
2019-04-04 21:29:43 +00:00
* )
2016-02-01 22:28:13 +00:00
* ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( menuLayout . $element ) ;
2016-02-01 22:28:13 +00:00
*
* If menu size needs to be overridden , it can be accomplished using CSS similar to the snippet
* below . MenuLayout 's CSS will override the appropriate values with ' auto ' or ' 0 ' to display the
* menu correctly . If ` menuPosition ` is known beforehand , CSS rules corresponding to other positions
* may be omitted .
*
* . oo - ui - menuLayout - menu {
* width : 200 px ;
2018-10-08 23:50:33 +00:00
* height : 200 px ;
2016-02-01 22:28:13 +00:00
* }
2018-10-08 23:50:33 +00:00
*
2016-02-01 22:28:13 +00:00
* . oo - ui - menuLayout - content {
* top : 200 px ;
* left : 200 px ;
* right : 200 px ;
* bottom : 200 px ;
* }
*
* @ class
* @ extends OO . ui . Layout
*
* @ constructor
* @ param { Object } [ config ] Configuration options
2018-10-08 23:50:33 +00:00
* @ cfg { OO . ui . PanelLayout } [ menuPanel ] Menu panel
* @ cfg { OO . ui . PanelLayout } [ contentPanel ] Content panel
2017-09-26 20:28:20 +00:00
* @ cfg { boolean } [ expanded = true ] Expand the layout to fill the entire parent element .
2016-02-01 22:28:13 +00:00
* @ cfg { boolean } [ showMenu = true ] Show menu
* @ cfg { string } [ menuPosition = 'before' ] Position of menu : ` top ` , ` after ` , ` bottom ` or ` before `
* /
OO . ui . MenuLayout = function OoUiMenuLayout ( config ) {
// Configuration initialization
config = $ . extend ( {
2017-09-26 20:28:20 +00:00
expanded : true ,
2016-02-01 22:28:13 +00:00
showMenu : true ,
menuPosition : 'before'
} , config ) ;
// Parent constructor
OO . ui . MenuLayout . parent . call ( this , config ) ;
2018-10-08 23:50:33 +00:00
this . menuPanel = null ;
this . contentPanel = null ;
2017-09-26 20:28:20 +00:00
this . expanded = ! ! config . expanded ;
2016-02-01 22:28:13 +00:00
/ * *
* Menu DOM node
*
* @ property { jQuery }
* /
this . $menu = $ ( '<div>' ) ;
/ * *
* Content DOM node
*
* @ property { jQuery }
* /
this . $content = $ ( '<div>' ) ;
// Initialization
2019-03-07 09:22:27 +00:00
this . $menu . addClass ( 'oo-ui-menuLayout-menu' ) ;
2016-02-01 22:28:13 +00:00
this . $content . addClass ( 'oo-ui-menuLayout-content' ) ;
2019-03-07 09:22:27 +00:00
this . $element . addClass ( 'oo-ui-menuLayout' ) ;
2017-09-26 20:28:20 +00:00
if ( config . expanded ) {
this . $element . addClass ( 'oo-ui-menuLayout-expanded' ) ;
} else {
this . $element . addClass ( 'oo-ui-menuLayout-static' ) ;
}
2018-10-08 23:50:33 +00:00
if ( config . menuPanel ) {
this . setMenuPanel ( config . menuPanel ) ;
}
if ( config . contentPanel ) {
this . setContentPanel ( config . contentPanel ) ;
}
2016-02-01 22:28:13 +00:00
this . setMenuPosition ( config . menuPosition ) ;
this . toggleMenu ( config . showMenu ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . MenuLayout , OO . ui . Layout ) ;
/* Methods */
/ * *
* Toggle menu .
*
* @ param { boolean } showMenu Show menu , omit to toggle
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . MenuLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . MenuLayout . prototype . toggleMenu = function ( showMenu ) {
showMenu = showMenu === undefined ? ! this . showMenu : ! ! showMenu ;
if ( this . showMenu !== showMenu ) {
this . showMenu = showMenu ;
this . $element
. toggleClass ( 'oo-ui-menuLayout-showMenu' , this . showMenu )
. toggleClass ( 'oo-ui-menuLayout-hideMenu' , ! this . showMenu ) ;
2016-07-12 20:30:06 +00:00
this . $menu . attr ( 'aria-hidden' , this . showMenu ? 'false' : 'true' ) ;
2016-02-01 22:28:13 +00:00
}
return this ;
} ;
/ * *
* Check if menu is visible
*
* @ return { boolean } Menu is visible
* /
OO . ui . MenuLayout . prototype . isMenuVisible = function ( ) {
return this . showMenu ;
} ;
/ * *
* Set menu position .
*
* @ param { string } position Position of menu , either ` top ` , ` after ` , ` bottom ` or ` before `
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . MenuLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . MenuLayout . prototype . setMenuPosition = function ( position ) {
2019-03-14 01:03:02 +00:00
if ( [ 'top' , 'bottom' , 'before' , 'after' ] . indexOf ( position ) === - 1 ) {
position = 'before' ;
}
2016-02-01 22:28:13 +00:00
this . $element . removeClass ( 'oo-ui-menuLayout-' + this . menuPosition ) ;
this . menuPosition = position ;
2017-09-26 20:28:20 +00:00
if ( this . menuPosition === 'top' || this . menuPosition === 'before' ) {
this . $element . append ( this . $menu , this . $content ) ;
} else {
this . $element . append ( this . $content , this . $menu ) ;
}
2016-02-01 22:28:13 +00:00
this . $element . addClass ( 'oo-ui-menuLayout-' + position ) ;
return this ;
} ;
/ * *
* Get menu position .
*
* @ return { string } Menu position
* /
OO . ui . MenuLayout . prototype . getMenuPosition = function ( ) {
return this . menuPosition ;
} ;
2018-10-08 23:50:33 +00:00
/ * *
* Set the menu panel .
*
* @ param { OO . ui . PanelLayout } menuPanel Menu panel
* /
OO . ui . MenuLayout . prototype . setMenuPanel = function ( menuPanel ) {
this . menuPanel = menuPanel ;
this . $menu . append ( this . menuPanel . $element ) ;
} ;
/ * *
* Set the content panel .
*
2018-11-07 01:01:56 +00:00
* @ param { OO . ui . PanelLayout } contentPanel Content panel
2018-10-08 23:50:33 +00:00
* /
OO . ui . MenuLayout . prototype . setContentPanel = function ( contentPanel ) {
this . contentPanel = contentPanel ;
this . $content . append ( this . contentPanel . $element ) ;
} ;
/ * *
* Clear the menu panel .
* /
OO . ui . MenuLayout . prototype . clearMenuPanel = function ( ) {
this . menuPanel = null ;
this . $menu . empty ( ) ;
} ;
/ * *
* Clear the content panel .
* /
OO . ui . MenuLayout . prototype . clearContentPanel = function ( ) {
this . contentPanel = null ;
this . $content . empty ( ) ;
} ;
2018-11-01 02:57:41 +00:00
/ * *
* Reset the scroll offset of all panels and the tab select widget
*
* @ inheritdoc
* /
OO . ui . MenuLayout . prototype . resetScroll = function ( ) {
if ( this . menuPanel ) {
this . menuPanel . resetScroll ( ) ;
}
if ( this . contentPanel ) {
this . contentPanel . resetScroll ( ) ;
}
return this ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
* BookletLayouts contain { @ link OO . ui . PageLayout page layouts } as well as
* an { @ link OO . ui . OutlineSelectWidget outline } that allows users to easily navigate
* through the pages and select which one to display . By default , only one page is
* displayed at a time and the outline is hidden . When a user navigates to a new page ,
* the booklet layout automatically focuses on the first focusable element , unless the
* default setting is changed . Optionally , booklets can be configured to show
* { @ link OO . ui . OutlineControlsWidget controls } for adding , moving , and removing items .
*
* @ example
* // Example of a BookletLayout that contains two PageLayouts.
*
* function PageOneLayout ( name , config ) {
* PageOneLayout . parent . call ( this , name , config ) ;
2019-03-07 09:22:27 +00:00
* this . $element . append ( '<p>First page</p><p>(This booklet has an outline, displayed on ' +
* 'the left)</p>' ) ;
2016-02-01 22:28:13 +00:00
* }
* OO . inheritClass ( PageOneLayout , OO . ui . PageLayout ) ;
* PageOneLayout . prototype . setupOutlineItem = function ( ) {
* this . outlineItem . setLabel ( 'Page One' ) ;
* } ;
*
* function PageTwoLayout ( name , config ) {
* PageTwoLayout . parent . call ( this , name , config ) ;
* this . $element . append ( '<p>Second page</p>' ) ;
* }
* OO . inheritClass ( PageTwoLayout , OO . ui . PageLayout ) ;
* PageTwoLayout . prototype . setupOutlineItem = function ( ) {
* this . outlineItem . setLabel ( 'Page Two' ) ;
* } ;
*
* var page1 = new PageOneLayout ( 'one' ) ,
* page2 = new PageTwoLayout ( 'two' ) ;
*
* var booklet = new OO . ui . BookletLayout ( {
* outlined : true
* } ) ;
*
2018-11-01 02:57:41 +00:00
* booklet . addPages ( [ page1 , page2 ] ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( booklet . $element ) ;
2016-02-01 22:28:13 +00:00
*
* @ class
* @ extends OO . ui . MenuLayout
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* @ cfg { boolean } [ continuous = false ] Show all pages , one after another
2019-03-07 09:22:27 +00:00
* @ cfg { boolean } [ autoFocus = true ] Focus on the first focusable element when a new page is
* displayed . Disabled on mobile .
* @ cfg { boolean } [ outlined = false ] Show the outline . The outline is used to navigate through the
* pages of the booklet .
* @ cfg { boolean } [ editable = false ] Show controls for adding , removing and reordering pages .
2016-02-01 22:28:13 +00:00
* /
OO . ui . BookletLayout = function OoUiBookletLayout ( config ) {
// Configuration initialization
config = config || { } ;
// Parent constructor
OO . ui . BookletLayout . parent . call ( this , config ) ;
// Properties
this . currentPageName = null ;
this . pages = { } ;
this . ignoreFocus = false ;
2017-09-26 20:28:20 +00:00
this . stackLayout = new OO . ui . StackLayout ( {
continuous : ! ! config . continuous ,
expanded : this . expanded
} ) ;
2018-10-08 23:50:33 +00:00
this . setContentPanel ( this . stackLayout ) ;
2016-02-01 22:28:13 +00:00
this . autoFocus = config . autoFocus === undefined || ! ! config . autoFocus ;
this . outlineVisible = false ;
this . outlined = ! ! config . outlined ;
if ( this . outlined ) {
this . editable = ! ! config . editable ;
this . outlineControlsWidget = null ;
this . outlineSelectWidget = new OO . ui . OutlineSelectWidget ( ) ;
2017-09-26 20:28:20 +00:00
this . outlinePanel = new OO . ui . PanelLayout ( {
expanded : this . expanded ,
scrollable : true
} ) ;
2018-10-08 23:50:33 +00:00
this . setMenuPanel ( this . outlinePanel ) ;
2016-02-01 22:28:13 +00:00
this . outlineVisible = true ;
if ( this . editable ) {
this . outlineControlsWidget = new OO . ui . OutlineControlsWidget (
this . outlineSelectWidget
) ;
}
}
this . toggleMenu ( this . outlined ) ;
// Events
2019-03-07 09:22:27 +00:00
this . stackLayout . connect ( this , {
set : 'onStackLayoutSet'
} ) ;
2016-02-01 22:28:13 +00:00
if ( this . outlined ) {
2019-03-07 09:22:27 +00:00
this . outlineSelectWidget . connect ( this , {
select : 'onOutlineSelectWidgetSelect'
} ) ;
2016-02-01 22:28:13 +00:00
this . scrolling = false ;
2019-03-07 09:22:27 +00:00
this . stackLayout . connect ( this , {
visibleItemChange : 'onStackLayoutVisibleItemChange'
} ) ;
2016-02-01 22:28:13 +00:00
}
if ( this . autoFocus ) {
// Event 'focus' does not bubble, but 'focusin' does
this . stackLayout . $element . on ( 'focusin' , this . onStackLayoutFocus . bind ( this ) ) ;
}
// Initialization
this . $element . addClass ( 'oo-ui-bookletLayout' ) ;
this . stackLayout . $element . addClass ( 'oo-ui-bookletLayout-stackLayout' ) ;
if ( this . outlined ) {
this . outlinePanel . $element
. addClass ( 'oo-ui-bookletLayout-outlinePanel' )
. append ( this . outlineSelectWidget . $element ) ;
if ( this . editable ) {
this . outlinePanel . $element
. addClass ( 'oo-ui-bookletLayout-outlinePanel-editable' )
. append ( this . outlineControlsWidget . $element ) ;
}
}
} ;
/* Setup */
OO . inheritClass ( OO . ui . BookletLayout , OO . ui . MenuLayout ) ;
/* Events */
/ * *
2019-03-07 09:22:27 +00:00
* A 'set' event is emitted when a page is { @ link # setPage set } to be displayed by the
* booklet layout .
2016-02-01 22:28:13 +00:00
* @ event set
* @ param { OO . ui . PageLayout } page Current page
* /
/ * *
* An 'add' event is emitted when pages are { @ link # addPages added } to the booklet layout .
*
* @ event add
* @ param { OO . ui . PageLayout [ ] } page Added pages
* @ param { number } index Index pages were added at
* /
/ * *
* A 'remove' event is emitted when pages are { @ link # clearPages cleared } or
* { @ link # removePages removed } from the booklet .
*
* @ event remove
* @ param { OO . ui . PageLayout [ ] } pages Removed pages
* /
/* Methods */
/ * *
* Handle stack layout focus .
*
* @ private
* @ param { jQuery . Event } e Focusin event
* /
OO . ui . BookletLayout . prototype . onStackLayoutFocus = function ( e ) {
var name , $target ;
// Find the page that an element was focused within
$target = $ ( e . target ) . closest ( '.oo-ui-pageLayout' ) ;
for ( name in this . pages ) {
// Check for page match, exclude current page to find only page changes
if ( this . pages [ name ] . $element [ 0 ] === $target [ 0 ] && name !== this . currentPageName ) {
this . setPage ( name ) ;
break ;
}
}
} ;
/ * *
* Handle visibleItemChange events from the stackLayout
*
* The next visible page is set as the current page by selecting it
* in the outline
*
* @ param { OO . ui . PageLayout } page The next visible page in the layout
* /
OO . ui . BookletLayout . prototype . onStackLayoutVisibleItemChange = function ( page ) {
// Set a flag to so that the resulting call to #onStackLayoutSet doesn't
// try and scroll the item into view again.
this . scrolling = true ;
this . outlineSelectWidget . selectItemByData ( page . getName ( ) ) ;
this . scrolling = false ;
} ;
/ * *
* Handle stack layout set events .
*
* @ private
* @ param { OO . ui . PanelLayout | null } page The page panel that is now the current panel
* /
OO . ui . BookletLayout . prototype . onStackLayoutSet = function ( page ) {
2017-10-04 01:41:52 +00:00
var promise , layout = this ;
// If everything is unselected, do nothing
if ( ! page ) {
return ;
}
// For continuous BookletLayouts, scroll the selected page into view first
if ( this . stackLayout . continuous && ! this . scrolling ) {
promise = page . scrollElementIntoView ( ) ;
} else {
promise = $ . Deferred ( ) . resolve ( ) ;
}
2018-05-29 23:31:25 +00:00
// Focus the first element on the newly selected panel.
// Don't focus if the page was set by scrolling.
if ( this . autoFocus && ! OO . ui . isMobile ( ) && ! this . scrolling ) {
2017-10-04 01:41:52 +00:00
promise . done ( function ( ) {
layout . focus ( ) ;
2016-06-29 13:32:06 +00:00
} ) ;
2016-02-01 22:28:13 +00:00
}
} ;
/ * *
* Focus the first input in the current page .
*
* If no page is selected , the first selectable page will be selected .
* If the focus is already in an element on the current page , nothing will happen .
2016-03-01 22:00:31 +00:00
*
2016-02-01 22:28:13 +00:00
* @ param { number } [ itemIndex ] A specific item to focus on
* /
OO . ui . BookletLayout . prototype . focus = function ( itemIndex ) {
var page ,
items = this . stackLayout . getItems ( ) ;
if ( itemIndex !== undefined && items [ itemIndex ] ) {
page = items [ itemIndex ] ;
} else {
page = this . stackLayout . getCurrentItem ( ) ;
}
if ( ! page && this . outlined ) {
this . selectFirstSelectablePage ( ) ;
page = this . stackLayout . getCurrentItem ( ) ;
}
if ( ! page ) {
return ;
}
// Only change the focus if is not already in the current page
if ( ! OO . ui . contains ( page . $element [ 0 ] , this . getElementDocument ( ) . activeElement , true ) ) {
page . focus ( ) ;
}
} ;
/ * *
* Find the first focusable input in the booklet layout and focus
* on it .
* /
OO . ui . BookletLayout . prototype . focusFirstFocusable = function ( ) {
OO . ui . findFocusable ( this . stackLayout . $element ) . focus ( ) ;
} ;
/ * *
* Handle outline widget select events .
*
* @ private
* @ param { OO . ui . OptionWidget | null } item Selected item
* /
OO . ui . BookletLayout . prototype . onOutlineSelectWidgetSelect = function ( item ) {
if ( item ) {
this . setPage ( item . getData ( ) ) ;
}
} ;
/ * *
* Check if booklet has an outline .
*
* @ return { boolean } Booklet has an outline
* /
OO . ui . BookletLayout . prototype . isOutlined = function ( ) {
return this . outlined ;
} ;
/ * *
* Check if booklet has editing controls .
*
* @ return { boolean } Booklet is editable
* /
OO . ui . BookletLayout . prototype . isEditable = function ( ) {
return this . editable ;
} ;
/ * *
* Check if booklet has a visible outline .
*
* @ return { boolean } Outline is visible
* /
OO . ui . BookletLayout . prototype . isOutlineVisible = function ( ) {
return this . outlined && this . outlineVisible ;
} ;
/ * *
* Hide or show the outline .
*
* @ param { boolean } [ show ] Show outline , omit to invert current state
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . BookletLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . BookletLayout . prototype . toggleOutline = function ( show ) {
2017-06-29 07:34:54 +00:00
var booklet = this ;
2016-02-01 22:28:13 +00:00
if ( this . outlined ) {
show = show === undefined ? ! this . outlineVisible : ! ! show ;
this . outlineVisible = show ;
this . toggleMenu ( show ) ;
2017-06-29 07:34:54 +00:00
if ( show && this . editable ) {
2019-03-07 09:22:27 +00:00
// HACK: Kill dumb scrollbars when the sidebar stops animating, see T161798.
// Only necessary when outline controls are present, delay matches transition on
// `.oo-ui-menuLayout-menu`.
2017-06-29 07:34:54 +00:00
setTimeout ( function ( ) {
OO . ui . Element . static . reconsiderScrollbars ( booklet . outlinePanel . $element [ 0 ] ) ;
2018-12-05 13:16:46 +00:00
} , OO . ui . theme . getDialogTransitionDuration ( ) ) ;
2017-06-29 07:34:54 +00:00
}
2016-02-01 22:28:13 +00:00
}
return this ;
} ;
/ * *
2017-09-05 22:07:32 +00:00
* Find the page closest to the specified page .
2016-02-01 22:28:13 +00:00
*
* @ param { OO . ui . PageLayout } page Page to use as a reference point
* @ return { OO . ui . PageLayout | null } Page closest to the specified page
* /
2017-09-05 22:07:32 +00:00
OO . ui . BookletLayout . prototype . findClosestPage = function ( page ) {
2016-02-01 22:28:13 +00:00
var next , prev , level ,
pages = this . stackLayout . getItems ( ) ,
index = pages . indexOf ( page ) ;
if ( index !== - 1 ) {
next = pages [ index + 1 ] ;
prev = pages [ index - 1 ] ;
// Prefer adjacent pages at the same level
if ( this . outlined ) {
2018-01-10 01:04:08 +00:00
level = this . outlineSelectWidget . findItemFromData ( page . getName ( ) ) . getLevel ( ) ;
2016-02-01 22:28:13 +00:00
if (
prev &&
2018-01-10 01:04:08 +00:00
level === this . outlineSelectWidget . findItemFromData ( prev . getName ( ) ) . getLevel ( )
2016-02-01 22:28:13 +00:00
) {
return prev ;
}
if (
next &&
2018-01-10 01:04:08 +00:00
level === this . outlineSelectWidget . findItemFromData ( next . getName ( ) ) . getLevel ( )
2016-02-01 22:28:13 +00:00
) {
return next ;
}
}
}
return prev || next || null ;
} ;
/ * *
* Get the outline widget .
*
* If the booklet is not outlined , the method will return ` null ` .
*
* @ return { OO . ui . OutlineSelectWidget | null } Outline widget , or null if the booklet is not outlined
* /
OO . ui . BookletLayout . prototype . getOutline = function ( ) {
return this . outlineSelectWidget ;
} ;
/ * *
* Get the outline controls widget .
*
* If the outline is not editable , the method will return ` null ` .
*
* @ return { OO . ui . OutlineControlsWidget | null } The outline controls widget .
* /
OO . ui . BookletLayout . prototype . getOutlineControls = function ( ) {
return this . outlineControlsWidget ;
} ;
/ * *
* Get a page by its symbolic name .
*
* @ param { string } name Symbolic name of page
* @ return { OO . ui . PageLayout | undefined } Page , if found
* /
OO . ui . BookletLayout . prototype . getPage = function ( name ) {
return this . pages [ name ] ;
} ;
/ * *
* Get the current page .
*
* @ return { OO . ui . PageLayout | undefined } Current page , if found
* /
OO . ui . BookletLayout . prototype . getCurrentPage = function ( ) {
var name = this . getCurrentPageName ( ) ;
return name ? this . getPage ( name ) : undefined ;
} ;
/ * *
* Get the symbolic name of the current page .
*
* @ return { string | null } Symbolic name of the current page
* /
OO . ui . BookletLayout . prototype . getCurrentPageName = function ( ) {
return this . currentPageName ;
} ;
/ * *
* Add pages to the booklet layout
*
* When pages are added with the same names as existing pages , the existing pages will be
* automatically removed before the new pages are added .
*
* @ param { OO . ui . PageLayout [ ] } pages Pages to add
* @ param { number } index Index of the insertion point
* @ fires add
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . BookletLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . BookletLayout . prototype . addPages = function ( pages , index ) {
var i , len , name , page , item , currentIndex ,
stackLayoutPages = this . stackLayout . getItems ( ) ,
remove = [ ] ,
items = [ ] ;
// Remove pages with same names
for ( i = 0 , len = pages . length ; i < len ; i ++ ) {
page = pages [ i ] ;
name = page . getName ( ) ;
if ( Object . prototype . hasOwnProperty . call ( this . pages , name ) ) {
// Correct the insertion index
currentIndex = stackLayoutPages . indexOf ( this . pages [ name ] ) ;
if ( currentIndex !== - 1 && currentIndex + 1 < index ) {
index -- ;
}
remove . push ( this . pages [ name ] ) ;
}
}
if ( remove . length ) {
this . removePages ( remove ) ;
}
// Add new pages
for ( i = 0 , len = pages . length ; i < len ; i ++ ) {
page = pages [ i ] ;
name = page . getName ( ) ;
this . pages [ page . getName ( ) ] = page ;
if ( this . outlined ) {
item = new OO . ui . OutlineOptionWidget ( { data : name } ) ;
page . setOutlineItem ( item ) ;
items . push ( item ) ;
}
}
if ( this . outlined && items . length ) {
this . outlineSelectWidget . addItems ( items , index ) ;
this . selectFirstSelectablePage ( ) ;
}
this . stackLayout . addItems ( pages , index ) ;
this . emit ( 'add' , pages , index ) ;
return this ;
} ;
/ * *
* Remove the specified pages from the booklet layout .
*
* To remove all pages from the booklet , you may wish to use the # clearPages method instead .
*
* @ param { OO . ui . PageLayout [ ] } pages An array of pages to remove
* @ fires remove
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . BookletLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . BookletLayout . prototype . removePages = function ( pages ) {
var i , len , name , page ,
items = [ ] ;
for ( i = 0 , len = pages . length ; i < len ; i ++ ) {
page = pages [ i ] ;
name = page . getName ( ) ;
delete this . pages [ name ] ;
if ( this . outlined ) {
2018-01-10 01:04:08 +00:00
items . push ( this . outlineSelectWidget . findItemFromData ( name ) ) ;
2016-02-01 22:28:13 +00:00
page . setOutlineItem ( null ) ;
}
}
if ( this . outlined && items . length ) {
this . outlineSelectWidget . removeItems ( items ) ;
this . selectFirstSelectablePage ( ) ;
}
this . stackLayout . removeItems ( pages ) ;
this . emit ( 'remove' , pages ) ;
return this ;
} ;
/ * *
* Clear all pages from the booklet layout .
*
* To remove only a subset of pages from the booklet , use the # removePages method .
*
* @ fires remove
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . BookletLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . BookletLayout . prototype . clearPages = function ( ) {
var i , len ,
pages = this . stackLayout . getItems ( ) ;
this . pages = { } ;
this . currentPageName = null ;
if ( this . outlined ) {
this . outlineSelectWidget . clearItems ( ) ;
for ( i = 0 , len = pages . length ; i < len ; i ++ ) {
pages [ i ] . setOutlineItem ( null ) ;
}
}
this . stackLayout . clearItems ( ) ;
this . emit ( 'remove' , pages ) ;
return this ;
} ;
/ * *
* Set the current page by symbolic name .
*
* @ fires set
* @ param { string } name Symbolic name of page
* /
OO . ui . BookletLayout . prototype . setPage = function ( name ) {
var selectedItem ,
$focused ,
page = this . pages [ name ] ,
previousPage = this . currentPageName && this . pages [ this . currentPageName ] ;
if ( name !== this . currentPageName ) {
if ( this . outlined ) {
2018-01-17 05:47:34 +00:00
selectedItem = this . outlineSelectWidget . findSelectedItem ( ) ;
2016-02-01 22:28:13 +00:00
if ( selectedItem && selectedItem . getData ( ) !== name ) {
this . outlineSelectWidget . selectItemByData ( name ) ;
}
}
if ( page ) {
if ( previousPage ) {
previousPage . setActive ( false ) ;
// Blur anything focused if the next page doesn't have anything focusable.
2019-03-07 09:22:27 +00:00
// This is not needed if the next page has something focusable (because once it is
// focused this blur happens automatically). If the layout is non-continuous, this
// check is meaningless because the next page is not visible yet and thus can't
// hold focus.
2016-02-01 22:28:13 +00:00
if (
this . autoFocus &&
2017-01-04 00:27:21 +00:00
! OO . ui . isMobile ( ) &&
2016-02-01 22:28:13 +00:00
this . stackLayout . continuous &&
OO . ui . findFocusable ( page . $element ) . length !== 0
) {
$focused = previousPage . $element . find ( ':focus' ) ;
if ( $focused . length ) {
$focused [ 0 ] . blur ( ) ;
}
}
}
this . currentPageName = name ;
page . setActive ( true ) ;
this . stackLayout . setItem ( page ) ;
if ( ! this . stackLayout . continuous && previousPage ) {
2019-03-07 09:22:27 +00:00
// This should not be necessary, since any inputs on the previous page should have
// been blurred when it was hidden, but browsers are not very consistent about
// this.
2016-02-01 22:28:13 +00:00
$focused = previousPage . $element . find ( ':focus' ) ;
if ( $focused . length ) {
$focused [ 0 ] . blur ( ) ;
}
}
this . emit ( 'set' , page ) ;
}
}
} ;
2018-11-01 02:57:41 +00:00
/ * *
* For outlined - continuous booklets , also reset the outlineSelectWidget to the first item .
*
* @ inheritdoc
* /
OO . ui . BookletLayout . prototype . resetScroll = function ( ) {
// Parent method
OO . ui . BookletLayout . parent . prototype . resetScroll . call ( this ) ;
2019-03-07 09:22:27 +00:00
if (
this . outlined &&
this . stackLayout . continuous &&
this . outlineSelectWidget . findFirstSelectableItem ( )
) {
2018-11-01 02:57:41 +00:00
this . scrolling = true ;
this . outlineSelectWidget . selectItem ( this . outlineSelectWidget . findFirstSelectableItem ( ) ) ;
this . scrolling = false ;
}
return this ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
* Select the first selectable page .
*
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . BookletLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . BookletLayout . prototype . selectFirstSelectablePage = function ( ) {
2018-01-17 05:47:34 +00:00
if ( ! this . outlineSelectWidget . findSelectedItem ( ) ) {
2017-09-20 00:58:44 +00:00
this . outlineSelectWidget . selectItem ( this . outlineSelectWidget . findFirstSelectableItem ( ) ) ;
2016-02-01 22:28:13 +00:00
}
return this ;
} ;
/ * *
2017-05-10 01:21:26 +00:00
* IndexLayouts contain { @ link OO . ui . TabPanelLayout tab panel layouts } as well as
2019-03-07 09:22:27 +00:00
* { @ link OO . ui . TabSelectWidget tabs } that allow users to easily navigate through the tab panels
* and select which one to display . By default , only one tab panel is displayed at a time . When a
* user navigates to a new tab panel , the index layout automatically focuses on the first focusable
* element , unless the default setting is changed .
2016-02-01 22:28:13 +00:00
*
* TODO : This class is similar to BookletLayout , we may want to refactor to reduce duplication
*
* @ example
2017-05-10 01:21:26 +00:00
* // Example of a IndexLayout that contains two TabPanelLayouts.
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* function TabPanelOneLayout ( name , config ) {
* TabPanelOneLayout . parent . call ( this , name , config ) ;
* this . $element . append ( '<p>First tab panel</p>' ) ;
2016-02-01 22:28:13 +00:00
* }
2017-05-10 01:21:26 +00:00
* OO . inheritClass ( TabPanelOneLayout , OO . ui . TabPanelLayout ) ;
* TabPanelOneLayout . prototype . setupTabItem = function ( ) {
* this . tabItem . setLabel ( 'Tab panel one' ) ;
2016-02-01 22:28:13 +00:00
* } ;
*
2017-05-10 01:21:26 +00:00
* var tabPanel1 = new TabPanelOneLayout ( 'one' ) ,
* tabPanel2 = new OO . ui . TabPanelLayout ( 'two' , { label : 'Tab panel two' } ) ;
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* tabPanel2 . $element . append ( '<p>Second tab panel</p>' ) ;
2016-02-01 22:28:13 +00:00
*
* var index = new OO . ui . IndexLayout ( ) ;
*
2018-10-08 23:50:33 +00:00
* index . addTabPanels ( [ tabPanel1 , tabPanel2 ] ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( index . $element ) ;
2016-02-01 22:28:13 +00:00
*
* @ class
* @ extends OO . ui . MenuLayout
*
* @ constructor
* @ param { Object } [ config ] Configuration options
2019-03-14 01:03:02 +00:00
* @ cfg { OO . ui . StackLayout } [ contentPanel ] Content stack ( see MenuLayout )
2017-05-10 01:21:26 +00:00
* @ cfg { boolean } [ continuous = false ] Show all tab panels , one after another
2019-03-07 09:22:27 +00:00
* @ cfg { boolean } [ autoFocus = true ] Focus on the first focusable element when a new tab panel is
* displayed . Disabled on mobile .
2019-05-29 00:57:59 +00:00
* @ cfg { boolean } [ framed = true ] Render the tabs with frames
2016-02-01 22:28:13 +00:00
* /
OO . ui . IndexLayout = function OoUiIndexLayout ( config ) {
// Configuration initialization
config = $ . extend ( { } , config , { menuPosition : 'top' } ) ;
// Parent constructor
OO . ui . IndexLayout . parent . call ( this , config ) ;
// Properties
2017-05-10 01:21:26 +00:00
this . currentTabPanelName = null ;
2019-03-14 01:03:02 +00:00
// Allow infused widgets to pass existing tabPanels
this . tabPanels = config . tabPanels || { } ;
2017-05-10 01:21:26 +00:00
2016-02-01 22:28:13 +00:00
this . ignoreFocus = false ;
2019-03-14 01:03:02 +00:00
this . stackLayout = this . contentPanel || new OO . ui . StackLayout ( {
2016-02-01 22:28:13 +00:00
continuous : ! ! config . continuous ,
2017-09-26 20:28:20 +00:00
expanded : this . expanded
2016-02-01 22:28:13 +00:00
} ) ;
2018-10-08 23:50:33 +00:00
this . setContentPanel ( this . stackLayout ) ;
2016-02-01 22:28:13 +00:00
this . autoFocus = config . autoFocus === undefined || ! ! config . autoFocus ;
2019-03-14 01:03:02 +00:00
// Allow infused widgets to pass an existing tabSelectWidget
2019-05-29 00:57:59 +00:00
this . tabSelectWidget = config . tabSelectWidget || new OO . ui . TabSelectWidget ( {
framed : config . framed === undefined || config . framed
} ) ;
2019-03-14 01:03:02 +00:00
this . tabPanel = this . menuPanel || new OO . ui . PanelLayout ( {
2017-09-26 20:28:20 +00:00
expanded : this . expanded
} ) ;
2018-10-08 23:50:33 +00:00
this . setMenuPanel ( this . tabPanel ) ;
2016-02-01 22:28:13 +00:00
this . toggleMenu ( true ) ;
// Events
2019-03-07 09:22:27 +00:00
this . stackLayout . connect ( this , {
set : 'onStackLayoutSet'
} ) ;
this . tabSelectWidget . connect ( this , {
select : 'onTabSelectWidgetSelect'
} ) ;
2016-02-01 22:28:13 +00:00
if ( this . autoFocus ) {
2019-03-07 09:22:27 +00:00
// Event 'focus' does not bubble, but 'focusin' does.
2016-02-01 22:28:13 +00:00
this . stackLayout . $element . on ( 'focusin' , this . onStackLayoutFocus . bind ( this ) ) ;
}
// Initialization
this . $element . addClass ( 'oo-ui-indexLayout' ) ;
this . stackLayout . $element . addClass ( 'oo-ui-indexLayout-stackLayout' ) ;
this . tabPanel . $element
. addClass ( 'oo-ui-indexLayout-tabPanel' )
. append ( this . tabSelectWidget . $element ) ;
2019-03-14 01:03:02 +00:00
this . selectFirstSelectableTabPanel ( ) ;
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
OO . inheritClass ( OO . ui . IndexLayout , OO . ui . MenuLayout ) ;
/* Events */
/ * *
2019-03-07 09:22:27 +00:00
* A 'set' event is emitted when a tab panel is { @ link # setTabPanel set } to be displayed by the
* index layout .
*
2016-02-01 22:28:13 +00:00
* @ event set
2017-05-10 01:21:26 +00:00
* @ param { OO . ui . TabPanelLayout } tabPanel Current tab panel
2016-02-01 22:28:13 +00:00
* /
/ * *
2017-05-10 01:21:26 +00:00
* An 'add' event is emitted when tab panels are { @ link # addTabPanels added } to the index layout .
2016-02-01 22:28:13 +00:00
*
* @ event add
2017-05-10 01:21:26 +00:00
* @ param { OO . ui . TabPanelLayout [ ] } tabPanel Added tab panels
* @ param { number } index Index tab panels were added at
2016-02-01 22:28:13 +00:00
* /
/ * *
2017-05-10 01:21:26 +00:00
* A 'remove' event is emitted when tab panels are { @ link # clearTabPanels cleared } or
* { @ link # removeTabPanels removed } from the index .
2016-02-01 22:28:13 +00:00
*
* @ event remove
2017-05-10 01:21:26 +00:00
* @ param { OO . ui . TabPanelLayout [ ] } tabPanel Removed tab panels
2016-02-01 22:28:13 +00:00
* /
/* Methods */
/ * *
* Handle stack layout focus .
*
* @ private
2017-05-10 01:21:26 +00:00
* @ param { jQuery . Event } e Focusing event
2016-02-01 22:28:13 +00:00
* /
OO . ui . IndexLayout . prototype . onStackLayoutFocus = function ( e ) {
var name , $target ;
2017-05-10 01:21:26 +00:00
// Find the tab panel that an element was focused within
$target = $ ( e . target ) . closest ( '.oo-ui-tabPanelLayout' ) ;
for ( name in this . tabPanels ) {
// Check for tab panel match, exclude current tab panel to find only tab panel changes
2019-03-07 09:22:27 +00:00
if ( this . tabPanels [ name ] . $element [ 0 ] === $target [ 0 ] &&
name !== this . currentTabPanelName ) {
2017-05-10 01:21:26 +00:00
this . setTabPanel ( name ) ;
2016-02-01 22:28:13 +00:00
break ;
}
}
} ;
/ * *
* Handle stack layout set events .
*
* @ private
2017-05-10 01:21:26 +00:00
* @ param { OO . ui . PanelLayout | null } tabPanel The tab panel that is now the current panel
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . IndexLayout . prototype . onStackLayoutSet = function ( tabPanel ) {
2017-10-04 01:41:52 +00:00
// If everything is unselected, do nothing
if ( ! tabPanel ) {
return ;
}
// Focus the first element on the newly selected panel
if ( this . autoFocus && ! OO . ui . isMobile ( ) ) {
this . focus ( ) ;
2016-02-01 22:28:13 +00:00
}
} ;
/ * *
2017-05-10 01:21:26 +00:00
* Focus the first input in the current tab panel .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* If no tab panel is selected , the first selectable tab panel will be selected .
* If the focus is already in an element on the current tab panel , nothing will happen .
2016-03-01 22:00:31 +00:00
*
2016-02-01 22:28:13 +00:00
* @ param { number } [ itemIndex ] A specific item to focus on
* /
OO . ui . IndexLayout . prototype . focus = function ( itemIndex ) {
2017-05-10 01:21:26 +00:00
var tabPanel ,
2016-02-01 22:28:13 +00:00
items = this . stackLayout . getItems ( ) ;
if ( itemIndex !== undefined && items [ itemIndex ] ) {
2017-05-10 01:21:26 +00:00
tabPanel = items [ itemIndex ] ;
2016-02-01 22:28:13 +00:00
} else {
2017-05-10 01:21:26 +00:00
tabPanel = this . stackLayout . getCurrentItem ( ) ;
2016-02-01 22:28:13 +00:00
}
2017-05-10 01:21:26 +00:00
if ( ! tabPanel ) {
this . selectFirstSelectableTabPanel ( ) ;
tabPanel = this . stackLayout . getCurrentItem ( ) ;
2016-02-01 22:28:13 +00:00
}
2017-05-10 01:21:26 +00:00
if ( ! tabPanel ) {
2016-02-01 22:28:13 +00:00
return ;
}
// Only change the focus if is not already in the current page
2019-03-07 09:22:27 +00:00
if ( ! OO . ui . contains (
tabPanel . $element [ 0 ] ,
this . getElementDocument ( ) . activeElement ,
true
) ) {
2017-05-10 01:21:26 +00:00
tabPanel . focus ( ) ;
2016-02-01 22:28:13 +00:00
}
} ;
/ * *
* Find the first focusable input in the index layout and focus
* on it .
* /
OO . ui . IndexLayout . prototype . focusFirstFocusable = function ( ) {
OO . ui . findFocusable ( this . stackLayout . $element ) . focus ( ) ;
} ;
/ * *
* Handle tab widget select events .
*
* @ private
* @ param { OO . ui . OptionWidget | null } item Selected item
* /
OO . ui . IndexLayout . prototype . onTabSelectWidgetSelect = function ( item ) {
if ( item ) {
2017-05-10 01:21:26 +00:00
this . setTabPanel ( item . getData ( ) ) ;
2016-02-01 22:28:13 +00:00
}
} ;
/ * *
2017-05-10 01:21:26 +00:00
* Get the tab panel closest to the specified tab panel .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ param { OO . ui . TabPanelLayout } tabPanel Tab panel to use as a reference point
* @ return { OO . ui . TabPanelLayout | null } Tab panel closest to the specified
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . IndexLayout . prototype . getClosestTabPanel = function ( tabPanel ) {
2016-02-01 22:28:13 +00:00
var next , prev , level ,
2017-05-10 01:21:26 +00:00
tabPanels = this . stackLayout . getItems ( ) ,
index = tabPanels . indexOf ( tabPanel ) ;
2016-02-01 22:28:13 +00:00
if ( index !== - 1 ) {
2017-05-10 01:21:26 +00:00
next = tabPanels [ index + 1 ] ;
prev = tabPanels [ index - 1 ] ;
// Prefer adjacent tab panels at the same level
2018-01-10 01:04:08 +00:00
level = this . tabSelectWidget . findItemFromData ( tabPanel . getName ( ) ) . getLevel ( ) ;
2016-02-01 22:28:13 +00:00
if (
prev &&
2018-01-10 01:04:08 +00:00
level === this . tabSelectWidget . findItemFromData ( prev . getName ( ) ) . getLevel ( )
2016-02-01 22:28:13 +00:00
) {
return prev ;
}
if (
next &&
2018-01-10 01:04:08 +00:00
level === this . tabSelectWidget . findItemFromData ( next . getName ( ) ) . getLevel ( )
2016-02-01 22:28:13 +00:00
) {
return next ;
}
}
return prev || next || null ;
} ;
/ * *
* Get the tabs widget .
*
* @ return { OO . ui . TabSelectWidget } Tabs widget
* /
OO . ui . IndexLayout . prototype . getTabs = function ( ) {
return this . tabSelectWidget ;
} ;
/ * *
2017-05-10 01:21:26 +00:00
* Get a tab panel by its symbolic name .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ param { string } name Symbolic name of tab panel
* @ return { OO . ui . TabPanelLayout | undefined } Tab panel , if found
* /
OO . ui . IndexLayout . prototype . getTabPanel = function ( name ) {
return this . tabPanels [ name ] ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
2017-05-10 01:21:26 +00:00
* Get the current tab panel .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ return { OO . ui . TabPanelLayout | undefined } Current tab panel , if found
* /
OO . ui . IndexLayout . prototype . getCurrentTabPanel = function ( ) {
var name = this . getCurrentTabPanelName ( ) ;
return name ? this . getTabPanel ( name ) : undefined ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
2017-05-10 01:21:26 +00:00
* Get the symbolic name of the current tab panel .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ return { string | null } Symbolic name of the current tab panel
* /
OO . ui . IndexLayout . prototype . getCurrentTabPanelName = function ( ) {
return this . currentTabPanelName ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
2019-03-07 09:22:27 +00:00
* Add tab panels to the index layout .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* When tab panels are added with the same names as existing tab panels , the existing tab panels
* will be automatically removed before the new tab panels are added .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ param { OO . ui . TabPanelLayout [ ] } tabPanels Tab panels to add
2016-02-01 22:28:13 +00:00
* @ param { number } index Index of the insertion point
* @ fires add
* @ chainable
2019-09-04 19:42:13 +00:00
* @ return { OO . ui . IndexLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . IndexLayout . prototype . addTabPanels = function ( tabPanels , index ) {
var i , len , name , tabPanel , item , currentIndex ,
stackLayoutTabPanels = this . stackLayout . getItems ( ) ,
2016-02-01 22:28:13 +00:00
remove = [ ] ,
items = [ ] ;
2017-05-10 01:21:26 +00:00
// Remove tab panels with same names
for ( i = 0 , len = tabPanels . length ; i < len ; i ++ ) {
tabPanel = tabPanels [ i ] ;
name = tabPanel . getName ( ) ;
2016-02-01 22:28:13 +00:00
2017-05-10 01:21:26 +00:00
if ( Object . prototype . hasOwnProperty . call ( this . tabPanels , name ) ) {
2016-02-01 22:28:13 +00:00
// Correct the insertion index
2017-05-10 01:21:26 +00:00
currentIndex = stackLayoutTabPanels . indexOf ( this . tabPanels [ name ] ) ;
2016-02-01 22:28:13 +00:00
if ( currentIndex !== - 1 && currentIndex + 1 < index ) {
index -- ;
}
2017-05-10 01:21:26 +00:00
remove . push ( this . tabPanels [ name ] ) ;
2016-02-01 22:28:13 +00:00
}
}
if ( remove . length ) {
2017-05-10 01:21:26 +00:00
this . removeTabPanels ( remove ) ;
2016-02-01 22:28:13 +00:00
}
2017-05-10 01:21:26 +00:00
// Add new tab panels
for ( i = 0 , len = tabPanels . length ; i < len ; i ++ ) {
tabPanel = tabPanels [ i ] ;
name = tabPanel . getName ( ) ;
this . tabPanels [ tabPanel . getName ( ) ] = tabPanel ;
2016-02-01 22:28:13 +00:00
item = new OO . ui . TabOptionWidget ( { data : name } ) ;
2017-05-10 01:21:26 +00:00
tabPanel . setTabItem ( item ) ;
2016-02-01 22:28:13 +00:00
items . push ( item ) ;
}
if ( items . length ) {
this . tabSelectWidget . addItems ( items , index ) ;
2017-05-10 01:21:26 +00:00
this . selectFirstSelectableTabPanel ( ) ;
2016-02-01 22:28:13 +00:00
}
2017-05-10 01:21:26 +00:00
this . stackLayout . addItems ( tabPanels , index ) ;
this . emit ( 'add' , tabPanels , index ) ;
2016-02-01 22:28:13 +00:00
return this ;
} ;
2017-05-10 01:21:26 +00:00
/ * *
* Remove the specified tab panels from the index layout .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* To remove all tab panels from the index , you may wish to use the # clearTabPanels method instead .
2016-02-01 22:28:13 +00:00
*
2017-05-10 01:21:26 +00:00
* @ param { OO . ui . TabPanelLayout [ ] } tabPanels An array of tab panels to remove
2016-02-01 22:28:13 +00:00
* @ fires remove
* @ chainable
2019-09-04 19:42:13 +00:00
* @ return { OO . ui . IndexLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . IndexLayout . prototype . removeTabPanels = function ( tabPanels ) {
var i , len , name , tabPanel ,
2016-02-01 22:28:13 +00:00
items = [ ] ;
2017-05-10 01:21:26 +00:00
for ( i = 0 , len = tabPanels . length ; i < len ; i ++ ) {
tabPanel = tabPanels [ i ] ;
name = tabPanel . getName ( ) ;
delete this . tabPanels [ name ] ;
2018-01-10 01:04:08 +00:00
items . push ( this . tabSelectWidget . findItemFromData ( name ) ) ;
2017-05-10 01:21:26 +00:00
tabPanel . setTabItem ( null ) ;
2016-02-01 22:28:13 +00:00
}
if ( items . length ) {
this . tabSelectWidget . removeItems ( items ) ;
2017-05-10 01:21:26 +00:00
this . selectFirstSelectableTabPanel ( ) ;
2016-02-01 22:28:13 +00:00
}
2017-05-10 01:21:26 +00:00
this . stackLayout . removeItems ( tabPanels ) ;
this . emit ( 'remove' , tabPanels ) ;
2016-02-01 22:28:13 +00:00
return this ;
} ;
2017-05-10 01:21:26 +00:00
/ * *
* Clear all tab panels from the index layout .
*
* To remove only a subset of tab panels from the index , use the # removeTabPanels method .
*
* @ fires remove
* @ chainable
2019-09-04 19:42:13 +00:00
* @ return { OO . ui . IndexLayout } The layout , for chaining
2017-05-10 01:21:26 +00:00
* /
OO . ui . IndexLayout . prototype . clearTabPanels = function ( ) {
2016-02-01 22:28:13 +00:00
var i , len ,
2017-05-10 01:21:26 +00:00
tabPanels = this . stackLayout . getItems ( ) ;
2016-02-01 22:28:13 +00:00
2017-05-10 01:21:26 +00:00
this . tabPanels = { } ;
this . currentTabPanelName = null ;
2016-02-01 22:28:13 +00:00
this . tabSelectWidget . clearItems ( ) ;
2017-05-10 01:21:26 +00:00
for ( i = 0 , len = tabPanels . length ; i < len ; i ++ ) {
tabPanels [ i ] . setTabItem ( null ) ;
2016-02-01 22:28:13 +00:00
}
this . stackLayout . clearItems ( ) ;
2017-05-10 01:21:26 +00:00
this . emit ( 'remove' , tabPanels ) ;
2016-02-01 22:28:13 +00:00
return this ;
} ;
2017-05-10 01:21:26 +00:00
/ * *
* Set the current tab panel by symbolic name .
2016-02-01 22:28:13 +00:00
*
* @ fires set
2017-05-10 01:21:26 +00:00
* @ param { string } name Symbolic name of tab panel
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . IndexLayout . prototype . setTabPanel = function ( name ) {
2016-02-01 22:28:13 +00:00
var selectedItem ,
$focused ,
2018-11-01 02:57:41 +00:00
previousTabPanel ,
tabPanel = this . tabPanels [ name ] ;
2016-02-01 22:28:13 +00:00
2017-05-10 01:21:26 +00:00
if ( name !== this . currentTabPanelName ) {
2018-11-01 02:57:41 +00:00
previousTabPanel = this . getCurrentTabPanel ( ) ;
2018-01-17 05:47:34 +00:00
selectedItem = this . tabSelectWidget . findSelectedItem ( ) ;
2016-02-01 22:28:13 +00:00
if ( selectedItem && selectedItem . getData ( ) !== name ) {
this . tabSelectWidget . selectItemByData ( name ) ;
}
2017-05-10 01:21:26 +00:00
if ( tabPanel ) {
if ( previousTabPanel ) {
previousTabPanel . setActive ( false ) ;
// Blur anything focused if the next tab panel doesn't have anything focusable.
2019-03-07 09:22:27 +00:00
// This is not needed if the next tab panel has something focusable (because once
// it is focused this blur happens automatically). If the layout is non-continuous,
// this check is meaningless because the next tab panel is not visible yet and thus
// can't hold focus.
2016-02-01 22:28:13 +00:00
if (
this . autoFocus &&
2017-01-04 00:27:21 +00:00
! OO . ui . isMobile ( ) &&
2016-02-01 22:28:13 +00:00
this . stackLayout . continuous &&
2017-05-10 01:21:26 +00:00
OO . ui . findFocusable ( tabPanel . $element ) . length !== 0
2016-02-01 22:28:13 +00:00
) {
2017-05-10 01:21:26 +00:00
$focused = previousTabPanel . $element . find ( ':focus' ) ;
2016-02-01 22:28:13 +00:00
if ( $focused . length ) {
$focused [ 0 ] . blur ( ) ;
}
}
}
2017-05-10 01:21:26 +00:00
this . currentTabPanelName = name ;
tabPanel . setActive ( true ) ;
this . stackLayout . setItem ( tabPanel ) ;
if ( ! this . stackLayout . continuous && previousTabPanel ) {
2019-03-07 09:22:27 +00:00
// This should not be necessary, since any inputs on the previous tab panel should
// have been blurred when it was hidden, but browsers are not very consistent about
// this.
2017-05-10 01:21:26 +00:00
$focused = previousTabPanel . $element . find ( ':focus' ) ;
2016-02-01 22:28:13 +00:00
if ( $focused . length ) {
$focused [ 0 ] . blur ( ) ;
}
}
2017-05-10 01:21:26 +00:00
this . emit ( 'set' , tabPanel ) ;
2016-02-01 22:28:13 +00:00
}
}
} ;
2017-05-10 01:21:26 +00:00
/ * *
* Select the first selectable tab panel .
2016-02-01 22:28:13 +00:00
*
* @ chainable
2019-09-04 19:42:13 +00:00
* @ return { OO . ui . IndexLayout } The layout , for chaining
2016-02-01 22:28:13 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . IndexLayout . prototype . selectFirstSelectableTabPanel = function ( ) {
2018-01-17 05:47:34 +00:00
if ( ! this . tabSelectWidget . findSelectedItem ( ) ) {
2017-09-20 00:58:44 +00:00
this . tabSelectWidget . selectItem ( this . tabSelectWidget . findFirstSelectableItem ( ) ) ;
2016-02-01 22:28:13 +00:00
}
return this ;
} ;
/ * *
* ToggleWidget implements basic behavior of widgets with an on / off state .
* Please see OO . ui . ToggleButtonWidget and OO . ui . ToggleSwitchWidget for examples .
*
* @ abstract
* @ class
* @ extends OO . ui . Widget
2019-01-11 16:19:16 +00:00
* @ mixins OO . ui . mixin . TitledElement
2016-02-01 22:28:13 +00:00
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* @ cfg { boolean } [ value = false ] The toggle ’ s initial on / off state .
* By default , the toggle is in the 'off' state .
* /
OO . ui . ToggleWidget = function OoUiToggleWidget ( config ) {
// Configuration initialization
config = config || { } ;
// Parent constructor
OO . ui . ToggleWidget . parent . call ( this , config ) ;
2019-01-11 16:19:16 +00:00
// Mixin constructor
OO . ui . mixin . TitledElement . call ( this , config ) ;
2016-02-01 22:28:13 +00:00
// Properties
this . value = null ;
// Initialization
this . $element . addClass ( 'oo-ui-toggleWidget' ) ;
this . setValue ( ! ! config . value ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . ToggleWidget , OO . ui . Widget ) ;
2019-01-11 16:19:16 +00:00
OO . mixinClass ( OO . ui . ToggleWidget , OO . ui . mixin . TitledElement ) ;
2016-02-01 22:28:13 +00:00
/* Events */
/ * *
* @ event change
*
* A change event is emitted when the on / off state of the toggle changes .
*
* @ param { boolean } value Value representing the new state of the toggle
* /
/* Methods */
/ * *
* Get the value representing the toggle ’ s state .
*
* @ return { boolean } The on / off state of the toggle
* /
OO . ui . ToggleWidget . prototype . getValue = function ( ) {
return this . value ;
} ;
/ * *
2017-05-10 01:21:26 +00:00
* Set the state of the toggle : ` true ` for 'on' , ` false ` for 'off' .
2016-02-01 22:28:13 +00:00
*
* @ param { boolean } value The state of the toggle
* @ fires change
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Widget } The widget , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . ToggleWidget . prototype . setValue = function ( value ) {
value = ! ! value ;
if ( this . value !== value ) {
this . value = value ;
this . emit ( 'change' , value ) ;
this . $element . toggleClass ( 'oo-ui-toggleWidget-on' , value ) ;
this . $element . toggleClass ( 'oo-ui-toggleWidget-off' , ! value ) ;
}
return this ;
} ;
/ * *
* ToggleButtons are buttons that have a state ( ‘ on ’ or ‘ off ’ ) that is represented by a
* Boolean value . Like other { @ link OO . ui . ButtonWidget buttons } , toggle buttons can be
2019-03-07 09:22:27 +00:00
* configured with { @ link OO . ui . mixin . IconElement icons } ,
* { @ link OO . ui . mixin . IndicatorElement indicators } ,
2016-02-01 22:28:13 +00:00
* { @ link OO . ui . mixin . TitledElement titles } , { @ link OO . ui . mixin . FlaggedElement styling flags } ,
* and { @ link OO . ui . mixin . LabelElement labels } . Please see
2018-01-17 05:47:34 +00:00
* the [ OOUI documentation ] [ 1 ] on MediaWiki for more information .
2016-02-01 22:28:13 +00:00
*
* @ example
* // Toggle buttons in the 'off' and 'on' state.
* var toggleButton1 = new OO . ui . ToggleButtonWidget ( {
2019-01-11 16:19:16 +00:00
* label : 'Toggle Button off'
* } ) ,
* toggleButton2 = new OO . ui . ToggleButtonWidget ( {
* label : 'Toggle Button on' ,
* value : true
* } ) ;
2016-02-01 22:28:13 +00:00
* // Append the buttons to the DOM.
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( toggleButton1 . $element , toggleButton2 . $element ) ;
2016-02-01 22:28:13 +00:00
*
2018-01-17 05:47:34 +00:00
* [ 1 ] : https : //www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Toggle_buttons
2016-02-01 22:28:13 +00:00
*
* @ class
* @ extends OO . ui . ToggleWidget
* @ mixins OO . ui . mixin . ButtonElement
* @ mixins OO . ui . mixin . IconElement
* @ mixins OO . ui . mixin . IndicatorElement
* @ mixins OO . ui . mixin . LabelElement
* @ mixins OO . ui . mixin . FlaggedElement
* @ mixins OO . ui . mixin . TabIndexedElement
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* @ cfg { boolean } [ value = false ] The toggle button ’ s initial on / off
* state . By default , the button is in the 'off' state .
* /
OO . ui . ToggleButtonWidget = function OoUiToggleButtonWidget ( config ) {
// Configuration initialization
config = config || { } ;
// Parent constructor
OO . ui . ToggleButtonWidget . parent . call ( this , config ) ;
// Mixin constructors
2019-03-14 01:03:02 +00:00
OO . ui . mixin . ButtonElement . call ( this , $ . extend ( {
2019-03-07 09:22:27 +00:00
active : this . active
2019-03-14 01:03:02 +00:00
} , config ) ) ;
2016-02-01 22:28:13 +00:00
OO . ui . mixin . IconElement . call ( this , config ) ;
OO . ui . mixin . IndicatorElement . call ( this , config ) ;
OO . ui . mixin . LabelElement . call ( this , config ) ;
OO . ui . mixin . FlaggedElement . call ( this , config ) ;
2019-03-14 01:03:02 +00:00
OO . ui . mixin . TabIndexedElement . call ( this , $ . extend ( {
2019-03-07 09:22:27 +00:00
$tabIndexed : this . $button
2019-03-14 01:03:02 +00:00
} , config ) ) ;
2016-02-01 22:28:13 +00:00
// Events
2019-03-07 09:22:27 +00:00
this . connect ( this , {
click : 'onAction'
} ) ;
2016-02-01 22:28:13 +00:00
// Initialization
this . $button . append ( this . $icon , this . $label , this . $indicator ) ;
this . $element
. addClass ( 'oo-ui-toggleButtonWidget' )
. append ( this . $button ) ;
2019-01-11 16:19:16 +00:00
this . setTitledElement ( this . $button ) ;
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
OO . inheritClass ( OO . ui . ToggleButtonWidget , OO . ui . ToggleWidget ) ;
OO . mixinClass ( OO . ui . ToggleButtonWidget , OO . ui . mixin . ButtonElement ) ;
OO . mixinClass ( OO . ui . ToggleButtonWidget , OO . ui . mixin . IconElement ) ;
OO . mixinClass ( OO . ui . ToggleButtonWidget , OO . ui . mixin . IndicatorElement ) ;
OO . mixinClass ( OO . ui . ToggleButtonWidget , OO . ui . mixin . LabelElement ) ;
OO . mixinClass ( OO . ui . ToggleButtonWidget , OO . ui . mixin . FlaggedElement ) ;
OO . mixinClass ( OO . ui . ToggleButtonWidget , OO . ui . mixin . TabIndexedElement ) ;
2017-03-15 17:33:46 +00:00
/* Static Properties */
/ * *
* @ static
* @ inheritdoc
* /
OO . ui . ToggleButtonWidget . static . tagName = 'span' ;
2016-02-01 22:28:13 +00:00
/* Methods */
/ * *
* Handle the button action being triggered .
*
* @ private
* /
OO . ui . ToggleButtonWidget . prototype . onAction = function ( ) {
this . setValue ( ! this . value ) ;
} ;
/ * *
* @ inheritdoc
* /
OO . ui . ToggleButtonWidget . prototype . setValue = function ( value ) {
value = ! ! value ;
if ( value !== this . value ) {
// Might be called from parent constructor before ButtonElement constructor
if ( this . $button ) {
this . $button . attr ( 'aria-pressed' , value . toString ( ) ) ;
}
this . setActive ( value ) ;
}
// Parent method
OO . ui . ToggleButtonWidget . parent . prototype . setValue . call ( this , value ) ;
return this ;
} ;
/ * *
* @ inheritdoc
* /
OO . ui . ToggleButtonWidget . prototype . setButtonElement = function ( $button ) {
if ( this . $button ) {
this . $button . removeAttr ( 'aria-pressed' ) ;
}
OO . ui . mixin . ButtonElement . prototype . setButtonElement . call ( this , $button ) ;
this . $button . attr ( 'aria-pressed' , this . value . toString ( ) ) ;
} ;
/ * *
* ToggleSwitches are switches that slide on and off . Their state is represented by a Boolean
* value ( ` true ` for ‘ on ’ , and ` false ` otherwise , the default ) . The ‘ off ’ state is represented
* visually by a slider in the leftmost position .
*
* @ example
* // Toggle switches in the 'off' and 'on' position.
2019-01-11 16:19:16 +00:00
* var toggleSwitch1 = new OO . ui . ToggleSwitchWidget ( ) ,
* toggleSwitch2 = new OO . ui . ToggleSwitchWidget ( {
* value : true
* } ) ;
* // Create a FieldsetLayout to layout and label switches.
* fieldset = new OO . ui . FieldsetLayout ( {
* label : 'Toggle switches'
* } ) ;
2016-02-01 22:28:13 +00:00
* fieldset . addItems ( [
2019-01-11 16:19:16 +00:00
* new OO . ui . FieldLayout ( toggleSwitch1 , {
* label : 'Off' ,
* align : 'top'
* } ) ,
* new OO . ui . FieldLayout ( toggleSwitch2 , {
* label : 'On' ,
* align : 'top'
* } )
2016-02-01 22:28:13 +00:00
* ] ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( fieldset . $element ) ;
2016-02-01 22:28:13 +00:00
*
* @ class
* @ extends OO . ui . ToggleWidget
* @ mixins OO . ui . mixin . TabIndexedElement
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* @ cfg { boolean } [ value = false ] The toggle switch ’ s initial on / off state .
* By default , the toggle switch is in the 'off' position .
* /
OO . ui . ToggleSwitchWidget = function OoUiToggleSwitchWidget ( config ) {
// Parent constructor
OO . ui . ToggleSwitchWidget . parent . call ( this , config ) ;
// Mixin constructors
OO . ui . mixin . TabIndexedElement . call ( this , config ) ;
// Properties
this . dragging = false ;
this . dragStart = null ;
this . sliding = false ;
this . $glow = $ ( '<span>' ) ;
this . $grip = $ ( '<span>' ) ;
// Events
this . $element . on ( {
click : this . onClick . bind ( this ) ,
keypress : this . onKeyPress . bind ( this )
} ) ;
// Initialization
this . $glow . addClass ( 'oo-ui-toggleSwitchWidget-glow' ) ;
this . $grip . addClass ( 'oo-ui-toggleSwitchWidget-grip' ) ;
this . $element
. addClass ( 'oo-ui-toggleSwitchWidget' )
. attr ( 'role' , 'checkbox' )
. append ( this . $glow , this . $grip ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . ToggleSwitchWidget , OO . ui . ToggleWidget ) ;
OO . mixinClass ( OO . ui . ToggleSwitchWidget , OO . ui . mixin . TabIndexedElement ) ;
/* Methods */
/ * *
* Handle mouse click events .
*
* @ private
* @ param { jQuery . Event } e Mouse click event
2019-04-24 18:36:00 +00:00
* @ return { undefined | boolean } False to prevent default if event is handled
2016-02-01 22:28:13 +00:00
* /
OO . ui . ToggleSwitchWidget . prototype . onClick = function ( e ) {
if ( ! this . isDisabled ( ) && e . which === OO . ui . MouseButtons . LEFT ) {
this . setValue ( ! this . value ) ;
}
return false ;
} ;
/ * *
* Handle key press events .
*
* @ private
* @ param { jQuery . Event } e Key press event
2019-04-24 18:36:00 +00:00
* @ return { undefined | boolean } False to prevent default if event is handled
2016-02-01 22:28:13 +00:00
* /
OO . ui . ToggleSwitchWidget . prototype . onKeyPress = function ( e ) {
if ( ! this . isDisabled ( ) && ( e . which === OO . ui . Keys . SPACE || e . which === OO . ui . Keys . ENTER ) ) {
this . setValue ( ! this . value ) ;
return false ;
}
} ;
2017-05-10 01:21:26 +00:00
/ * *
* @ inheritdoc
* /
OO . ui . ToggleSwitchWidget . prototype . setValue = function ( value ) {
OO . ui . ToggleSwitchWidget . parent . prototype . setValue . call ( this , value ) ;
this . $element . attr ( 'aria-checked' , this . value . toString ( ) ) ;
return this ;
} ;
2017-05-31 19:26:04 +00:00
/ * *
* @ inheritdoc
* /
OO . ui . ToggleSwitchWidget . prototype . simulateLabelClick = function ( ) {
if ( ! this . isDisabled ( ) ) {
this . setValue ( ! this . value ) ;
}
this . focus ( ) ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
2019-03-07 09:22:27 +00:00
* OutlineControlsWidget is a set of controls for an
* { @ link OO . ui . OutlineSelectWidget outline select widget } .
2016-02-01 22:28:13 +00:00
* Controls include moving items up and down , removing items , and adding different kinds of items .
*
* * * Currently , this class is only used by { @ link OO . ui . BookletLayout booklet layouts } . * *
*
* @ class
* @ extends OO . ui . Widget
* @ mixins OO . ui . mixin . GroupElement
*
* @ constructor
* @ param { OO . ui . OutlineSelectWidget } outline Outline to control
* @ param { Object } [ config ] Configuration options
* @ cfg { Object } [ abilities ] List of abilties
* @ cfg { boolean } [ abilities . move = true ] Allow moving movable items
* @ cfg { boolean } [ abilities . remove = true ] Allow removing removable items
* /
OO . ui . OutlineControlsWidget = function OoUiOutlineControlsWidget ( outline , config ) {
// Allow passing positional parameters inside the config object
if ( OO . isPlainObject ( outline ) && config === undefined ) {
config = outline ;
outline = config . outline ;
}
// Configuration initialization
2018-05-29 23:31:25 +00:00
config = config || { } ;
2016-02-01 22:28:13 +00:00
// Parent constructor
OO . ui . OutlineControlsWidget . parent . call ( this , config ) ;
// Mixin constructors
OO . ui . mixin . GroupElement . call ( this , config ) ;
// Properties
this . outline = outline ;
this . $movers = $ ( '<div>' ) ;
this . upButton = new OO . ui . ButtonWidget ( {
framed : false ,
icon : 'collapse' ,
title : OO . ui . msg ( 'ooui-outline-control-move-up' )
} ) ;
this . downButton = new OO . ui . ButtonWidget ( {
framed : false ,
icon : 'expand' ,
title : OO . ui . msg ( 'ooui-outline-control-move-down' )
} ) ;
this . removeButton = new OO . ui . ButtonWidget ( {
framed : false ,
2017-06-29 07:34:54 +00:00
icon : 'trash' ,
2016-02-01 22:28:13 +00:00
title : OO . ui . msg ( 'ooui-outline-control-remove' )
} ) ;
this . abilities = { move : true , remove : true } ;
// Events
outline . connect ( this , {
select : 'onOutlineChange' ,
add : 'onOutlineChange' ,
remove : 'onOutlineChange'
} ) ;
2019-03-07 09:22:27 +00:00
this . upButton . connect ( this , {
click : [ 'emit' , 'move' , - 1 ]
} ) ;
this . downButton . connect ( this , {
click : [ 'emit' , 'move' , 1 ]
} ) ;
this . removeButton . connect ( this , {
click : [ 'emit' , 'remove' ]
} ) ;
2016-02-01 22:28:13 +00:00
// Initialization
this . $element . addClass ( 'oo-ui-outlineControlsWidget' ) ;
this . $group . addClass ( 'oo-ui-outlineControlsWidget-items' ) ;
this . $movers
. addClass ( 'oo-ui-outlineControlsWidget-movers' )
. append ( this . removeButton . $element , this . upButton . $element , this . downButton . $element ) ;
this . $element . append ( this . $icon , this . $group , this . $movers ) ;
this . setAbilities ( config . abilities || { } ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . OutlineControlsWidget , OO . ui . Widget ) ;
OO . mixinClass ( OO . ui . OutlineControlsWidget , OO . ui . mixin . GroupElement ) ;
/* Events */
/ * *
* @ event move
* @ param { number } places Number of places to move
* /
/ * *
* @ event remove
* /
/* Methods */
/ * *
* Set abilities .
*
* @ param { Object } abilities List of abilties
* @ param { boolean } [ abilities . move ] Allow moving movable items
* @ param { boolean } [ abilities . remove ] Allow removing removable items
* /
OO . ui . OutlineControlsWidget . prototype . setAbilities = function ( abilities ) {
var ability ;
for ( ability in this . abilities ) {
if ( abilities [ ability ] !== undefined ) {
this . abilities [ ability ] = ! ! abilities [ ability ] ;
}
}
this . onOutlineChange ( ) ;
} ;
/ * *
* Handle outline change events .
2016-03-01 22:00:31 +00:00
*
* @ private
2016-02-01 22:28:13 +00:00
* /
OO . ui . OutlineControlsWidget . prototype . onOutlineChange = function ( ) {
var i , len , firstMovable , lastMovable ,
items = this . outline . getItems ( ) ,
2018-01-17 05:47:34 +00:00
selectedItem = this . outline . findSelectedItem ( ) ,
2016-02-01 22:28:13 +00:00
movable = this . abilities . move && selectedItem && selectedItem . isMovable ( ) ,
removable = this . abilities . remove && selectedItem && selectedItem . isRemovable ( ) ;
if ( movable ) {
i = - 1 ;
len = items . length ;
while ( ++ i < len ) {
if ( items [ i ] . isMovable ( ) ) {
firstMovable = items [ i ] ;
break ;
}
}
i = len ;
while ( i -- ) {
if ( items [ i ] . isMovable ( ) ) {
lastMovable = items [ i ] ;
break ;
}
}
}
this . upButton . setDisabled ( ! movable || selectedItem === firstMovable ) ;
this . downButton . setDisabled ( ! movable || selectedItem === lastMovable ) ;
this . removeButton . setDisabled ( ! removable ) ;
} ;
/ * *
* OutlineOptionWidget is an item in an { @ link OO . ui . OutlineSelectWidget OutlineSelectWidget } .
*
* Currently , this class is only used by { @ link OO . ui . BookletLayout booklet layouts } , which contain
* { @ link OO . ui . PageLayout page layouts } . See { @ link OO . ui . BookletLayout BookletLayout }
* for an example .
*
* @ class
* @ extends OO . ui . DecoratedOptionWidget
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* @ cfg { number } [ level ] Indentation level
2019-03-07 09:22:27 +00:00
* @ cfg { boolean } [ movable ] Allow modification from
* { @ link OO . ui . OutlineControlsWidget outline controls } .
2016-02-01 22:28:13 +00:00
* /
OO . ui . OutlineOptionWidget = function OoUiOutlineOptionWidget ( config ) {
// Configuration initialization
config = config || { } ;
// Parent constructor
OO . ui . OutlineOptionWidget . parent . call ( this , config ) ;
// Properties
this . level = 0 ;
this . movable = ! ! config . movable ;
this . removable = ! ! config . removable ;
// Initialization
this . $element . addClass ( 'oo-ui-outlineOptionWidget' ) ;
this . setLevel ( config . level ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . OutlineOptionWidget , OO . ui . DecoratedOptionWidget ) ;
/* Static Properties */
2017-02-08 00:50:58 +00:00
/ * *
* @ static
* @ inheritdoc
* /
2016-09-13 18:48:04 +00:00
OO . ui . OutlineOptionWidget . static . highlightable = true ;
2016-02-01 22:28:13 +00:00
2017-02-08 00:50:58 +00:00
/ * *
* @ static
* @ inheritdoc
* /
2016-02-01 22:28:13 +00:00
OO . ui . OutlineOptionWidget . static . scrollIntoViewOnSelect = true ;
2017-02-08 00:50:58 +00:00
/ * *
* @ static
* @ inheritable
* @ property { string }
* /
2016-02-01 22:28:13 +00:00
OO . ui . OutlineOptionWidget . static . levelClass = 'oo-ui-outlineOptionWidget-level-' ;
2017-02-08 00:50:58 +00:00
/ * *
* @ static
* @ inheritable
* @ property { number }
* /
2016-02-01 22:28:13 +00:00
OO . ui . OutlineOptionWidget . static . levels = 3 ;
/* Methods */
/ * *
* Check if item is movable .
*
* Movability is used by { @ link OO . ui . OutlineControlsWidget outline controls } .
*
* @ return { boolean } Item is movable
* /
OO . ui . OutlineOptionWidget . prototype . isMovable = function ( ) {
return this . movable ;
} ;
/ * *
* Check if item is removable .
*
* Removability is used by { @ link OO . ui . OutlineControlsWidget outline controls } .
*
* @ return { boolean } Item is removable
* /
OO . ui . OutlineOptionWidget . prototype . isRemovable = function ( ) {
return this . removable ;
} ;
/ * *
* Get indentation level .
*
* @ return { number } Indentation level
* /
OO . ui . OutlineOptionWidget . prototype . getLevel = function ( ) {
return this . level ;
} ;
2016-09-13 18:48:04 +00:00
/ * *
* @ inheritdoc
* /
OO . ui . OutlineOptionWidget . prototype . setPressed = function ( state ) {
OO . ui . OutlineOptionWidget . parent . prototype . setPressed . call ( this , state ) ;
return this ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
* Set movability .
*
* Movability is used by { @ link OO . ui . OutlineControlsWidget outline controls } .
*
* @ param { boolean } movable Item is movable
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Widget } The widget , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . OutlineOptionWidget . prototype . setMovable = function ( movable ) {
this . movable = ! ! movable ;
this . updateThemeClasses ( ) ;
return this ;
} ;
/ * *
* Set removability .
*
* Removability is used by { @ link OO . ui . OutlineControlsWidget outline controls } .
*
* @ param { boolean } removable Item is removable
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Widget } The widget , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . OutlineOptionWidget . prototype . setRemovable = function ( removable ) {
this . removable = ! ! removable ;
this . updateThemeClasses ( ) ;
return this ;
} ;
2016-09-13 18:48:04 +00:00
/ * *
* @ inheritdoc
* /
OO . ui . OutlineOptionWidget . prototype . setSelected = function ( state ) {
OO . ui . OutlineOptionWidget . parent . prototype . setSelected . call ( this , state ) ;
return this ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
* Set indentation level .
*
* @ param { number } [ level = 0 ] Indentation level , in the range of [ 0 , # maxLevel ]
* @ chainable
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Widget } The widget , for chaining
2016-02-01 22:28:13 +00:00
* /
OO . ui . OutlineOptionWidget . prototype . setLevel = function ( level ) {
var levels = this . constructor . static . levels ,
levelClass = this . constructor . static . levelClass ,
i = levels ;
this . level = level ? Math . max ( 0 , Math . min ( levels - 1 , level ) ) : 0 ;
while ( i -- ) {
if ( this . level === i ) {
this . $element . addClass ( levelClass + i ) ;
} else {
this . $element . removeClass ( levelClass + i ) ;
}
}
this . updateThemeClasses ( ) ;
return this ;
} ;
/ * *
2019-03-07 09:22:27 +00:00
* OutlineSelectWidget is a structured list that contains
* { @ link OO . ui . OutlineOptionWidget outline options }
* A set of controls can be provided with an { @ link OO . ui . OutlineControlsWidget outline controls }
* widget .
2016-02-01 22:28:13 +00:00
*
* * * Currently , this class is only used by { @ link OO . ui . BookletLayout booklet layouts } . * *
*
* @ class
* @ extends OO . ui . SelectWidget
* @ mixins OO . ui . mixin . TabIndexedElement
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* /
OO . ui . OutlineSelectWidget = function OoUiOutlineSelectWidget ( config ) {
// Parent constructor
OO . ui . OutlineSelectWidget . parent . call ( this , config ) ;
// Mixin constructors
OO . ui . mixin . TabIndexedElement . call ( this , config ) ;
// Events
this . $element . on ( {
2018-10-04 17:12:39 +00:00
focus : this . bindDocumentKeyDownListener . bind ( this ) ,
blur : this . unbindDocumentKeyDownListener . bind ( this )
2016-02-01 22:28:13 +00:00
} ) ;
// Initialization
this . $element . addClass ( 'oo-ui-outlineSelectWidget' ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . OutlineSelectWidget , OO . ui . SelectWidget ) ;
OO . mixinClass ( OO . ui . OutlineSelectWidget , OO . ui . mixin . TabIndexedElement ) ;
/ * *
* ButtonOptionWidget is a special type of { @ link OO . ui . mixin . ButtonElement button element } that
* can be selected and configured with data . The class is
* used with OO . ui . ButtonSelectWidget to create a selection of button options . Please see the
2018-01-17 05:47:34 +00:00
* [ OOUI documentation on MediaWiki ] [ 1 ] for more information .
2016-02-01 22:28:13 +00:00
*
2018-01-17 05:47:34 +00:00
* [ 1 ] : https : //www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_options
2016-02-01 22:28:13 +00:00
*
* @ class
2016-04-19 22:00:12 +00:00
* @ extends OO . ui . OptionWidget
2016-02-01 22:28:13 +00:00
* @ mixins OO . ui . mixin . ButtonElement
2016-04-19 22:00:12 +00:00
* @ mixins OO . ui . mixin . IconElement
* @ mixins OO . ui . mixin . IndicatorElement
2016-02-01 22:28:13 +00:00
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* /
OO . ui . ButtonOptionWidget = function OoUiButtonOptionWidget ( config ) {
// Configuration initialization
config = config || { } ;
// Parent constructor
OO . ui . ButtonOptionWidget . parent . call ( this , config ) ;
// Mixin constructors
OO . ui . mixin . ButtonElement . call ( this , config ) ;
2016-04-19 22:00:12 +00:00
OO . ui . mixin . IconElement . call ( this , config ) ;
OO . ui . mixin . IndicatorElement . call ( this , config ) ;
2016-02-01 22:28:13 +00:00
// Initialization
this . $element . addClass ( 'oo-ui-buttonOptionWidget' ) ;
2016-04-19 22:00:12 +00:00
this . $button . append ( this . $icon , this . $label , this . $indicator ) ;
2016-02-01 22:28:13 +00:00
this . $element . append ( this . $button ) ;
2019-01-11 16:19:16 +00:00
this . setTitledElement ( this . $button ) ;
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
2016-04-19 22:00:12 +00:00
OO . inheritClass ( OO . ui . ButtonOptionWidget , OO . ui . OptionWidget ) ;
2016-02-01 22:28:13 +00:00
OO . mixinClass ( OO . ui . ButtonOptionWidget , OO . ui . mixin . ButtonElement ) ;
2016-04-19 22:00:12 +00:00
OO . mixinClass ( OO . ui . ButtonOptionWidget , OO . ui . mixin . IconElement ) ;
OO . mixinClass ( OO . ui . ButtonOptionWidget , OO . ui . mixin . IndicatorElement ) ;
2016-02-01 22:28:13 +00:00
/* Static Properties */
2017-02-08 00:50:58 +00:00
/ * *
* Allow button mouse down events to pass through so they can be handled by the parent select widget
*
* @ static
* @ inheritdoc
* /
2016-02-01 22:28:13 +00:00
OO . ui . ButtonOptionWidget . static . cancelButtonMouseDownEvents = false ;
2017-02-08 00:50:58 +00:00
/ * *
* @ static
* @ inheritdoc
* /
2016-02-01 22:28:13 +00:00
OO . ui . ButtonOptionWidget . static . highlightable = false ;
/* Methods */
/ * *
* @ inheritdoc
* /
OO . ui . ButtonOptionWidget . prototype . setSelected = function ( state ) {
OO . ui . ButtonOptionWidget . parent . prototype . setSelected . call ( this , state ) ;
if ( this . constructor . static . selectable ) {
this . setActive ( state ) ;
}
return this ;
} ;
/ * *
* ButtonSelectWidget is a { @ link OO . ui . SelectWidget select widget } that contains
* button options and is used together with
* OO . ui . ButtonOptionWidget . The ButtonSelectWidget provides an interface for
* highlighting , choosing , and selecting mutually exclusive options . Please see
2018-01-17 05:47:34 +00:00
* the [ OOUI documentation on MediaWiki ] [ 1 ] for more information .
2016-02-01 22:28:13 +00:00
*
* @ example
2019-01-11 16:19:16 +00:00
* // A ButtonSelectWidget that contains three ButtonOptionWidgets.
2016-02-01 22:28:13 +00:00
* var option1 = new OO . ui . ButtonOptionWidget ( {
2019-01-11 16:19:16 +00:00
* data : 1 ,
* label : 'Option 1' ,
* title : 'Button option 1'
* } ) ,
* option2 = new OO . ui . ButtonOptionWidget ( {
* data : 2 ,
* label : 'Option 2' ,
* title : 'Button option 2'
* } ) ,
* option3 = new OO . ui . ButtonOptionWidget ( {
* data : 3 ,
* label : 'Option 3' ,
* title : 'Button option 3'
* } ) ,
* buttonSelect = new OO . ui . ButtonSelectWidget ( {
* items : [ option1 , option2 , option3 ]
* } ) ;
* $ ( document . body ) . append ( buttonSelect . $element ) ;
2016-02-01 22:28:13 +00:00
*
2018-01-17 05:47:34 +00:00
* [ 1 ] : https : //www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
2016-02-01 22:28:13 +00:00
*
* @ class
* @ extends OO . ui . SelectWidget
* @ mixins OO . ui . mixin . TabIndexedElement
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* /
OO . ui . ButtonSelectWidget = function OoUiButtonSelectWidget ( config ) {
// Parent constructor
OO . ui . ButtonSelectWidget . parent . call ( this , config ) ;
// Mixin constructors
OO . ui . mixin . TabIndexedElement . call ( this , config ) ;
// Events
this . $element . on ( {
2018-10-04 17:12:39 +00:00
focus : this . bindDocumentKeyDownListener . bind ( this ) ,
blur : this . unbindDocumentKeyDownListener . bind ( this )
2016-02-01 22:28:13 +00:00
} ) ;
// Initialization
this . $element . addClass ( 'oo-ui-buttonSelectWidget' ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . ButtonSelectWidget , OO . ui . SelectWidget ) ;
OO . mixinClass ( OO . ui . ButtonSelectWidget , OO . ui . mixin . TabIndexedElement ) ;
/ * *
* TabOptionWidget is an item in a { @ link OO . ui . TabSelectWidget TabSelectWidget } .
*
* Currently , this class is only used by { @ link OO . ui . IndexLayout index layouts } , which contain
2017-05-10 01:21:26 +00:00
* { @ link OO . ui . TabPanelLayout tab panel layouts } . See { @ link OO . ui . IndexLayout IndexLayout }
2016-02-01 22:28:13 +00:00
* for an example .
*
* @ class
* @ extends OO . ui . OptionWidget
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* /
OO . ui . TabOptionWidget = function OoUiTabOptionWidget ( config ) {
// Configuration initialization
config = config || { } ;
// Parent constructor
OO . ui . TabOptionWidget . parent . call ( this , config ) ;
// Initialization
2018-07-11 18:43:15 +00:00
this . $element
. addClass ( 'oo-ui-tabOptionWidget' )
. attr ( 'role' , 'tab' ) ;
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
OO . inheritClass ( OO . ui . TabOptionWidget , OO . ui . OptionWidget ) ;
/* Static Properties */
2017-02-08 00:50:58 +00:00
/ * *
* @ static
* @ inheritdoc
* /
2016-02-01 22:28:13 +00:00
OO . ui . TabOptionWidget . static . highlightable = false ;
2019-07-10 14:07:23 +00:00
/ * *
* @ static
* @ inheritdoc
* /
OO . ui . TabOptionWidget . static . scrollIntoViewOnSelect = true ;
/ * *
* Center tab horizontally after selecting on mobile
*
* @ param { Object } [ config ] Configuration options
* @ return { jQuery . Promise } Promise which resolves when the scroll is complete
* /
OO . ui . TabOptionWidget . prototype . scrollElementIntoView = function ( config ) {
var padding ;
if ( ! OO . ui . isMobile ( ) || ! this . getElementGroup ( ) ) {
// Parent method
return OO . ui . TabOptionWidget . super . prototype . scrollElementIntoView . call ( this ) ;
} else {
padding = Math . max ( (
this . getElementGroup ( ) . $element [ 0 ] . clientWidth - this . $element [ 0 ] . clientWidth
) / 2 , 0 ) ;
// Parent method
return OO . ui . TabOptionWidget . super . prototype . scrollElementIntoView . call ( this , $ . extend (
{
padding : {
left : padding ,
right : padding
}
} ,
config
) ) ;
}
} ;
2016-02-01 22:28:13 +00:00
/ * *
* TabSelectWidget is a list that contains { @ link OO . ui . TabOptionWidget tab options }
*
* * * Currently , this class is only used by { @ link OO . ui . IndexLayout index layouts } . * *
*
* @ class
* @ extends OO . ui . SelectWidget
* @ mixins OO . ui . mixin . TabIndexedElement
*
* @ constructor
* @ param { Object } [ config ] Configuration options
2019-05-29 00:57:59 +00:00
* @ cfg { boolean } [ framed = true ] Use framed tabs
2016-02-01 22:28:13 +00:00
* /
OO . ui . TabSelectWidget = function OoUiTabSelectWidget ( config ) {
// Parent constructor
OO . ui . TabSelectWidget . parent . call ( this , config ) ;
// Mixin constructors
OO . ui . mixin . TabIndexedElement . call ( this , config ) ;
// Events
this . $element . on ( {
2018-10-04 17:12:39 +00:00
focus : this . bindDocumentKeyDownListener . bind ( this ) ,
blur : this . unbindDocumentKeyDownListener . bind ( this )
2016-02-01 22:28:13 +00:00
} ) ;
// Initialization
2018-07-11 18:43:15 +00:00
this . $element
. addClass ( 'oo-ui-tabSelectWidget' )
. attr ( 'role' , 'tablist' ) ;
2019-05-29 00:57:59 +00:00
this . toggleFramed ( config . framed === undefined || config . framed ) ;
if ( OO . ui . isMobile ( ) ) {
this . $element . addClass ( 'oo-ui-tabSelectWidget-mobile' ) ;
}
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
OO . inheritClass ( OO . ui . TabSelectWidget , OO . ui . SelectWidget ) ;
OO . mixinClass ( OO . ui . TabSelectWidget , OO . ui . mixin . TabIndexedElement ) ;
2019-05-29 00:57:59 +00:00
/* Methods */
/ * *
* Check if tabs are framed .
*
* @ return { boolean } Tabs are framed
* /
OO . ui . TabSelectWidget . prototype . isFramed = function ( ) {
return this . framed ;
} ;
/ * *
* Render the tabs with or without frames .
*
* @ param { boolean } [ framed ] Make tabs framed , omit to toggle
* @ chainable
* @ return { OO . ui . Element } The element , for chaining
* /
OO . ui . TabSelectWidget . prototype . toggleFramed = function ( framed ) {
framed = framed === undefined ? ! this . framed : ! ! framed ;
if ( framed !== this . framed ) {
this . framed = framed ;
this . $element
. toggleClass ( 'oo-ui-tabSelectWidget-frameless' , ! framed )
. toggleClass ( 'oo-ui-tabSelectWidget-framed' , framed ) ;
}
return this ;
} ;
2017-04-11 23:41:59 +00:00
/ * *
* TagItemWidgets are used within a { @ link OO . ui . TagMultiselectWidget
* TagMultiselectWidget } to display the selected items .
*
* @ class
* @ extends OO . ui . Widget
* @ mixins OO . ui . mixin . ItemWidget
* @ mixins OO . ui . mixin . LabelElement
* @ mixins OO . ui . mixin . FlaggedElement
* @ mixins OO . ui . mixin . TabIndexedElement
* @ mixins OO . ui . mixin . DraggableElement
*
* @ constructor
* @ param { Object } [ config ] Configuration object
* @ cfg { boolean } [ valid = true ] Item is valid
2018-05-09 01:38:43 +00:00
* @ cfg { boolean } [ fixed ] Item is fixed . This means the item is
* always included in the values and cannot be removed .
2017-04-11 23:41:59 +00:00
* /
OO . ui . TagItemWidget = function OoUiTagItemWidget ( config ) {
config = config || { } ;
// Parent constructor
OO . ui . TagItemWidget . parent . call ( this , config ) ;
// Mixin constructors
OO . ui . mixin . ItemWidget . call ( this ) ;
OO . ui . mixin . LabelElement . call ( this , config ) ;
OO . ui . mixin . FlaggedElement . call ( this , config ) ;
OO . ui . mixin . TabIndexedElement . call ( this , config ) ;
OO . ui . mixin . DraggableElement . call ( this , config ) ;
this . valid = config . valid === undefined ? true : ! ! config . valid ;
2018-05-09 01:38:43 +00:00
this . fixed = ! ! config . fixed ;
2017-04-11 23:41:59 +00:00
this . closeButton = new OO . ui . ButtonWidget ( {
framed : false ,
2017-06-29 07:34:54 +00:00
icon : 'close' ,
2017-08-22 22:50:49 +00:00
tabIndex : - 1 ,
title : OO . ui . msg ( 'ooui-item-remove' )
2017-04-11 23:41:59 +00:00
} ) ;
this . closeButton . setDisabled ( this . isDisabled ( ) ) ;
// Events
2019-03-07 09:22:27 +00:00
this . closeButton . connect ( this , {
click : 'remove'
} ) ;
2017-04-11 23:41:59 +00:00
this . $element
. on ( 'click' , this . select . bind ( this ) )
. on ( 'keydown' , this . onKeyDown . bind ( this ) )
// Prevent propagation of mousedown; the tag item "lives" in the
// clickable area of the TagMultiselectWidget, which listens to
// mousedown to open the menu or popup. We want to prevent that
// for clicks specifically on the tag itself, so the actions taken
// are more deliberate. When the tag is clicked, it will emit the
// selection event (similar to how #OO.ui.MultioptionWidget emits 'change')
// and can be handled separately.
. on ( 'mousedown' , function ( e ) { e . stopPropagation ( ) ; } ) ;
// Initialization
this . $element
. addClass ( 'oo-ui-tagItemWidget' )
. append ( this . $label , this . closeButton . $element ) ;
} ;
/* Initialization */
OO . inheritClass ( OO . ui . TagItemWidget , OO . ui . Widget ) ;
OO . mixinClass ( OO . ui . TagItemWidget , OO . ui . mixin . ItemWidget ) ;
OO . mixinClass ( OO . ui . TagItemWidget , OO . ui . mixin . LabelElement ) ;
OO . mixinClass ( OO . ui . TagItemWidget , OO . ui . mixin . FlaggedElement ) ;
OO . mixinClass ( OO . ui . TagItemWidget , OO . ui . mixin . TabIndexedElement ) ;
OO . mixinClass ( OO . ui . TagItemWidget , OO . ui . mixin . DraggableElement ) ;
/* Events */
/ * *
* @ event remove
*
* A remove action was performed on the item
* /
/ * *
* @ event navigate
* @ param { string } direction Direction of the movement , forward or backwards
*
* A navigate action was performed on the item
* /
/ * *
* @ event select
*
* The tag widget was selected . This can occur when the widget
* is either clicked or enter was pressed on it .
* /
/ * *
* @ event valid
* @ param { boolean } isValid Item is valid
*
* Item validity has changed
* /
2018-03-21 00:36:37 +00:00
/ * *
2018-05-09 01:38:43 +00:00
* @ event fixed
* @ param { boolean } isFixed Item is fixed
2018-03-21 00:36:37 +00:00
*
2018-05-09 01:38:43 +00:00
* Item fixed state has changed
2018-03-21 00:36:37 +00:00
* /
2017-04-11 23:41:59 +00:00
/* Methods */
2018-05-09 01:38:43 +00:00
/ * *
* Set this item as fixed , meaning it cannot be removed
*
* @ param { string } [ state ] Item is fixed
* @ fires fixed
2018-11-07 01:01:56 +00:00
* @ return { OO . ui . Widget } The widget , for chaining
2018-05-09 01:38:43 +00:00
* /
OO . ui . TagItemWidget . prototype . setFixed = function ( state ) {
state = state === undefined ? ! this . fixed : ! ! state ;
if ( this . fixed !== state ) {
this . fixed = state ;
if ( this . closeButton ) {
this . closeButton . toggle ( ! this . fixed ) ;
}
if ( ! this . fixed && this . elementGroup && ! this . elementGroup . isDraggable ( ) ) {
// Only enable the state of the item if the
// entire group is draggable
this . toggleDraggable ( ! this . fixed ) ;
}
this . $element . toggleClass ( 'oo-ui-tagItemWidget-fixed' , this . fixed ) ;
this . emit ( 'fixed' , this . isFixed ( ) ) ;
}
return this ;
} ;
/ * *
* Check whether the item is fixed
2018-11-07 01:01:56 +00:00
* @ return { boolean }
2018-05-09 01:38:43 +00:00
* /
OO . ui . TagItemWidget . prototype . isFixed = function ( ) {
return this . fixed ;
} ;
2017-04-11 23:41:59 +00:00
/ * *
* @ inheritdoc
* /
OO . ui . TagItemWidget . prototype . setDisabled = function ( state ) {
2018-05-09 01:38:43 +00:00
if ( state && this . elementGroup && ! this . elementGroup . isDisabled ( ) ) {
OO . ui . warnDeprecation ( 'TagItemWidget#setDisabled: Disabling individual items is deprecated and will result in inconsistent behavior. Use #setFixed instead. See T193571.' ) ;
}
2017-04-11 23:41:59 +00:00
// Parent method
OO . ui . TagItemWidget . parent . prototype . setDisabled . call ( this , state ) ;
2018-05-09 01:38:43 +00:00
if (
! state &&
// Verify we have a group, and that the widget is ready
this . toggleDraggable && this . elementGroup &&
! this . isFixed ( ) &&
! this . elementGroup . isDraggable ( )
) {
// Only enable the draggable state of the item if the
// entire group is draggable to begin with, and if the
// item is not fixed
this . toggleDraggable ( ! state ) ;
}
2017-04-11 23:41:59 +00:00
if ( this . closeButton ) {
this . closeButton . setDisabled ( state ) ;
}
2018-03-21 00:36:37 +00:00
2017-04-11 23:41:59 +00:00
return this ;
} ;
/ * *
* Handle removal of the item
*
* This is mainly for extensibility concerns , so other children
* of this class can change the behavior if they need to . This
* is called by both clicking the 'remove' button but also
* on keypress , which is harder to override if needed .
*
* @ fires remove
* /
OO . ui . TagItemWidget . prototype . remove = function ( ) {
2018-06-06 16:49:23 +00:00
if ( ! this . isDisabled ( ) && ! this . isFixed ( ) ) {
2017-04-11 23:41:59 +00:00
this . emit ( 'remove' ) ;
}
} ;
/ * *
* Handle a keydown event on the widget
*
* @ fires navigate
* @ fires remove
2017-05-10 01:21:26 +00:00
* @ param { jQuery . Event } e Key down event
2017-04-11 23:41:59 +00:00
* @ return { boolean | undefined } false to stop the operation
* /
OO . ui . TagItemWidget . prototype . onKeyDown = function ( e ) {
var movement ;
2019-03-07 09:22:27 +00:00
if (
! this . isDisabled ( ) &&
! this . isFixed ( ) &&
( e . keyCode === OO . ui . Keys . BACKSPACE || e . keyCode === OO . ui . Keys . DELETE )
) {
2017-04-11 23:41:59 +00:00
this . remove ( ) ;
return false ;
} else if ( e . keyCode === OO . ui . Keys . ENTER ) {
this . select ( ) ;
return false ;
} else if (
e . keyCode === OO . ui . Keys . LEFT ||
e . keyCode === OO . ui . Keys . RIGHT
) {
if ( OO . ui . Element . static . getDir ( this . $element ) === 'rtl' ) {
movement = {
left : 'forwards' ,
right : 'backwards'
} ;
} else {
movement = {
left : 'backwards' ,
right : 'forwards'
} ;
}
this . emit (
'navigate' ,
e . keyCode === OO . ui . Keys . LEFT ?
movement . left : movement . right
) ;
2018-04-04 17:55:02 +00:00
return false ;
2017-04-11 23:41:59 +00:00
}
} ;
/ * *
* Select this item
*
* @ fires select
* /
OO . ui . TagItemWidget . prototype . select = function ( ) {
if ( ! this . isDisabled ( ) ) {
this . emit ( 'select' ) ;
}
} ;
/ * *
* Set the valid state of this item
*
* @ param { boolean } [ valid ] Item is valid
* @ fires valid
* /
OO . ui . TagItemWidget . prototype . toggleValid = function ( valid ) {
valid = valid === undefined ? ! this . valid : ! ! valid ;
if ( this . valid !== valid ) {
this . valid = valid ;
this . setFlags ( { invalid : ! this . valid } ) ;
this . emit ( 'valid' , this . valid ) ;
}
} ;
/ * *
* Check whether the item is valid
*
* @ return { boolean } Item is valid
* /
OO . ui . TagItemWidget . prototype . isValid = function ( ) {
return this . valid ;
} ;
/ * *
2019-03-07 09:22:27 +00:00
* A basic tag multiselect widget , similar in concept to
* { @ link OO . ui . ComboBoxInputWidget combo box widget } that allows the user to add multiple values
* that are displayed in a tag area .
2017-04-11 23:41:59 +00:00
*
2019-03-07 09:22:27 +00:00
* This widget is a base widget ; see { @ link OO . ui . MenuTagMultiselectWidget MenuTagMultiselectWidget }
* and { @ link OO . ui . PopupTagMultiselectWidget PopupTagMultiselectWidget } for the implementations
* that use a menu and a popup respectively .
2017-04-11 23:41:59 +00:00
*
* @ example
2019-01-11 16:19:16 +00:00
* // A TagMultiselectWidget.
2017-04-11 23:41:59 +00:00
* var widget = new OO . ui . TagMultiselectWidget ( {
* inputPosition : 'outline' ,
* allowedValues : [ 'Option 1' , 'Option 2' , 'Option 3' ] ,
* selected : [ 'Option 1' ]
* } ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( widget . $element ) ;
2017-04-11 23:41:59 +00:00
*
* @ class
* @ extends OO . ui . Widget
* @ mixins OO . ui . mixin . GroupWidget
* @ mixins OO . ui . mixin . DraggableGroupElement
* @ mixins OO . ui . mixin . IndicatorElement
* @ mixins OO . ui . mixin . IconElement
* @ mixins OO . ui . mixin . TabIndexedElement
* @ mixins OO . ui . mixin . FlaggedElement
2019-01-11 16:19:16 +00:00
* @ mixins OO . ui . mixin . TitledElement
2017-04-11 23:41:59 +00:00
*
* @ constructor
* @ param { Object } config Configuration object
* @ cfg { Object } [ input ] Configuration options for the input widget
2017-04-18 23:41:08 +00:00
* @ cfg { OO . ui . InputWidget } [ inputWidget ] An optional input widget . If given , it will
* replace the input widget used in the TagMultiselectWidget . If not given ,
* TagMultiselectWidget creates its own .
2017-04-11 23:41:59 +00:00
* @ cfg { boolean } [ inputPosition = 'inline' ] Position of the input . Options are :
2018-12-05 13:16:46 +00:00
* - inline : The input is invisible , but exists inside the tag list , so
* the user types into the tag groups to add tags .
* - outline : The input is underneath the tag area .
* - none : No input supplied
2017-04-11 23:41:59 +00:00
* @ cfg { boolean } [ allowEditTags = true ] Allow editing of the tags by clicking them
* @ cfg { boolean } [ allowArbitrary = false ] Allow data items to be added even if
* not present in the menu .
* @ cfg { Object [ ] } [ allowedValues ] An array representing the allowed items
* by their datas .
* @ cfg { boolean } [ allowDuplicates = false ] Allow duplicate items to be added
* @ cfg { boolean } [ allowDisplayInvalidTags = false ] Allow the display of
* invalid tags . These tags will display with an invalid state , and
* the widget as a whole will have an invalid state if any invalid tags
* are present .
2018-11-08 22:42:29 +00:00
* @ cfg { number } [ tagLimit ] An optional limit on the number of selected options .
* If 'tagLimit' is set and is reached , the input is disabled , not allowing any
* additions . If 'tagLimit' is unset or is 0 , an unlimited number of items can be
* added .
2017-04-11 23:41:59 +00:00
* @ cfg { boolean } [ allowReordering = true ] Allow reordering of the items
* @ cfg { Object [ ] | String [ ] } [ selected ] A set of selected tags . If given ,
* these will appear in the tag list on initialization , as long as they
* pass the validity tests .
* /
OO . ui . TagMultiselectWidget = function OoUiTagMultiselectWidget ( config ) {
var inputEvents ,
rAF = window . requestAnimationFrame || setTimeout ,
widget = this ,
2019-03-07 09:22:27 +00:00
$tabFocus = $ ( '<span>' ) . addClass ( 'oo-ui-tagMultiselectWidget-focusTrap' ) ;
2017-04-11 23:41:59 +00:00
config = config || { } ;
// Parent constructor
OO . ui . TagMultiselectWidget . parent . call ( this , config ) ;
// Mixin constructors
OO . ui . mixin . GroupWidget . call ( this , config ) ;
OO . ui . mixin . IndicatorElement . call ( this , config ) ;
OO . ui . mixin . IconElement . call ( this , config ) ;
OO . ui . mixin . TabIndexedElement . call ( this , config ) ;
OO . ui . mixin . FlaggedElement . call ( this , config ) ;
OO . ui . mixin . DraggableGroupElement . call ( this , config ) ;
2019-01-11 16:19:16 +00:00
OO . ui . mixin . TitledElement . call ( this , config ) ;
2017-04-11 23:41:59 +00:00
this . toggleDraggable (
config . allowReordering === undefined ?
true : ! ! config . allowReordering
) ;
2017-08-22 22:50:49 +00:00
this . inputPosition =
this . constructor . static . allowedInputPositions . indexOf ( config . inputPosition ) > - 1 ?
2017-04-11 23:41:59 +00:00
config . inputPosition : 'inline' ;
this . allowEditTags = config . allowEditTags === undefined ? true : ! ! config . allowEditTags ;
this . allowArbitrary = ! ! config . allowArbitrary ;
this . allowDuplicates = ! ! config . allowDuplicates ;
this . allowedValues = config . allowedValues || [ ] ;
this . allowDisplayInvalidTags = config . allowDisplayInvalidTags ;
this . hasInput = this . inputPosition !== 'none' ;
2018-11-08 22:42:29 +00:00
this . tagLimit = config . tagLimit ;
2017-04-11 23:41:59 +00:00
this . height = null ;
this . valid = true ;
2019-03-07 09:22:27 +00:00
this . $content = $ ( '<div>' ) . addClass ( 'oo-ui-tagMultiselectWidget-content' ) ;
2017-04-11 23:41:59 +00:00
this . $handle = $ ( '<div>' )
. addClass ( 'oo-ui-tagMultiselectWidget-handle' )
. append (
this . $indicator ,
this . $icon ,
this . $content
. append (
2019-03-07 09:22:27 +00:00
this . $group . addClass ( 'oo-ui-tagMultiselectWidget-group' )
2017-04-11 23:41:59 +00:00
)
) ;
// Events
this . aggregate ( {
remove : 'itemRemove' ,
navigate : 'itemNavigate' ,
2018-03-21 00:36:37 +00:00
select : 'itemSelect' ,
2018-05-09 01:38:43 +00:00
fixed : 'itemFixed'
2017-04-11 23:41:59 +00:00
} ) ;
this . connect ( this , {
itemRemove : 'onTagRemove' ,
itemSelect : 'onTagSelect' ,
2018-05-09 01:38:43 +00:00
itemFixed : 'onTagFixed' ,
2017-04-11 23:41:59 +00:00
itemNavigate : 'onTagNavigate' ,
change : 'onChangeTags'
} ) ;
this . $handle . on ( {
mousedown : this . onMouseDown . bind ( this )
} ) ;
// Initialize
this . $element
. addClass ( 'oo-ui-tagMultiselectWidget' )
. append ( this . $handle ) ;
if ( this . hasInput ) {
2017-04-18 23:41:08 +00:00
if ( config . inputWidget ) {
this . input = config . inputWidget ;
} else {
this . input = new OO . ui . TextInputWidget ( $ . extend ( {
placeholder : config . placeholder ,
classes : [ 'oo-ui-tagMultiselectWidget-input' ]
} , config . input ) ) ;
}
2017-04-11 23:41:59 +00:00
this . input . setDisabled ( this . isDisabled ( ) ) ;
inputEvents = {
focus : this . onInputFocus . bind ( this ) ,
blur : this . onInputBlur . bind ( this ) ,
'propertychange change click mouseup keydown keyup input cut paste select focus' :
OO . ui . debounce ( this . updateInputSize . bind ( this ) ) ,
keydown : this . onInputKeyDown . bind ( this ) ,
keypress : this . onInputKeyPress . bind ( this )
} ;
this . input . $input . on ( inputEvents ) ;
2018-11-07 01:01:56 +00:00
this . inputPlaceholder = this . input . $input . attr ( 'placeholder' ) ;
2017-04-11 23:41:59 +00:00
if ( this . inputPosition === 'outline' ) {
// Override max-height for the input widget
// in the case the widget is outline so it can
2018-11-01 02:57:41 +00:00
// stretch all the way if the widget is wide
2017-04-11 23:41:59 +00:00
this . input . $element . css ( 'max-width' , 'inherit' ) ;
2017-04-18 23:41:08 +00:00
this . $element
. addClass ( 'oo-ui-tagMultiselectWidget-outlined' )
. append ( this . input . $element ) ;
2017-04-11 23:41:59 +00:00
} else {
2017-04-18 23:41:08 +00:00
this . $element . addClass ( 'oo-ui-tagMultiselectWidget-inlined' ) ;
2017-04-11 23:41:59 +00:00
// HACK: When the widget is using 'inline' input, the
// behavior needs to only use the $input itself
// so we style and size it accordingly (otherwise
// the styling and sizing can get very convoluted
// when the wrapping divs and other elements)
// We are taking advantage of still being able to
// call the widget itself for operations like
// .getValue() and setDisabled() and .focus() but
// having only the $input attached to the DOM
this . $content . append ( this . input . $input ) ;
}
2017-05-10 01:21:26 +00:00
} else {
this . $content . append ( $tabFocus ) ;
2017-04-11 23:41:59 +00:00
}
this . setTabIndexedElement (
this . hasInput ?
this . input . $input :
$tabFocus
) ;
if ( config . selected ) {
this . setValue ( config . selected ) ;
}
// HACK: Input size needs to be calculated after everything
// else is rendered
rAF ( function ( ) {
if ( widget . hasInput ) {
widget . updateInputSize ( ) ;
}
} ) ;
} ;
/* Initialization */
OO . inheritClass ( OO . ui . TagMultiselectWidget , OO . ui . Widget ) ;
OO . mixinClass ( OO . ui . TagMultiselectWidget , OO . ui . mixin . GroupWidget ) ;
OO . mixinClass ( OO . ui . TagMultiselectWidget , OO . ui . mixin . DraggableGroupElement ) ;
OO . mixinClass ( OO . ui . TagMultiselectWidget , OO . ui . mixin . IndicatorElement ) ;
OO . mixinClass ( OO . ui . TagMultiselectWidget , OO . ui . mixin . IconElement ) ;
OO . mixinClass ( OO . ui . TagMultiselectWidget , OO . ui . mixin . TabIndexedElement ) ;
OO . mixinClass ( OO . ui . TagMultiselectWidget , OO . ui . mixin . FlaggedElement ) ;
2019-01-11 16:19:16 +00:00
OO . mixinClass ( OO . ui . TagMultiselectWidget , OO . ui . mixin . TitledElement ) ;
2017-04-11 23:41:59 +00:00
/* Static properties */
/ * *
* Allowed input positions .
* - inline : The input is inside the tag list
* - outline : The input is under the tag list
* - none : There is no input
*
* @ property { Array }
* /
OO . ui . TagMultiselectWidget . static . allowedInputPositions = [ 'inline' , 'outline' , 'none' ] ;
/* Methods */
/ * *
* Handle mouse down events .
*
* @ private
* @ param { jQuery . Event } e Mouse down event
* @ return { boolean } False to prevent defaults
* /
OO . ui . TagMultiselectWidget . prototype . onMouseDown = function ( e ) {
2018-03-07 07:13:15 +00:00
if (
! this . isDisabled ( ) &&
( ! this . hasInput || e . target !== this . input . $input [ 0 ] ) &&
e . which === OO . ui . MouseButtons . LEFT
) {
2017-04-11 23:41:59 +00:00
this . focus ( ) ;
return false ;
}
} ;
/ * *
* Handle key press events .
*
* @ private
* @ param { jQuery . Event } e Key press event
* @ return { boolean } Whether to prevent defaults
* /
OO . ui . TagMultiselectWidget . prototype . onInputKeyPress = function ( e ) {
var stopOrContinue ,
withMetaKey = e . metaKey || e . ctrlKey ;
if ( ! this . isDisabled ( ) ) {
if ( e . which === OO . ui . Keys . ENTER ) {
stopOrContinue = this . doInputEnter ( e , withMetaKey ) ;
}
// Make sure the input gets resized.
setTimeout ( this . updateInputSize . bind ( this ) , 0 ) ;
return stopOrContinue ;
}
} ;
/ * *
* Handle key down events .
*
* @ private
* @ param { jQuery . Event } e Key down event
* @ return { boolean }
* /
OO . ui . TagMultiselectWidget . prototype . onInputKeyDown = function ( e ) {
var movement , direction ,
2018-04-04 17:55:02 +00:00
widget = this ,
withMetaKey = e . metaKey || e . ctrlKey ,
isMovementInsideInput = function ( direction ) {
var inputRange = widget . input . getRange ( ) ,
inputValue = widget . hasInput && widget . input . getValue ( ) ;
if ( direction === 'forwards' && inputRange . to > inputValue . length - 1 ) {
return false ;
}
if ( direction === 'backwards' && inputRange . from <= 0 ) {
return false ;
}
return true ;
} ;
2017-04-11 23:41:59 +00:00
if ( ! this . isDisabled ( ) ) {
2019-03-07 09:22:27 +00:00
// 'keypress' event is not triggered for Backspace key
2017-04-11 23:41:59 +00:00
if ( e . keyCode === OO . ui . Keys . BACKSPACE ) {
return this . doInputBackspace ( e , withMetaKey ) ;
} else if ( e . keyCode === OO . ui . Keys . ESCAPE ) {
return this . doInputEscape ( e ) ;
} else if (
e . keyCode === OO . ui . Keys . LEFT ||
e . keyCode === OO . ui . Keys . RIGHT
) {
if ( OO . ui . Element . static . getDir ( this . $element ) === 'rtl' ) {
movement = {
left : 'forwards' ,
right : 'backwards'
} ;
} else {
movement = {
left : 'backwards' ,
right : 'forwards'
} ;
}
direction = e . keyCode === OO . ui . Keys . LEFT ?
movement . left : movement . right ;
2018-04-04 17:55:02 +00:00
if ( ! this . hasInput || ! isMovementInsideInput ( direction ) ) {
return this . doInputArrow ( e , direction , withMetaKey ) ;
}
2017-04-11 23:41:59 +00:00
}
}
} ;
/ * *
* Respond to input focus event
* /
2017-05-10 01:21:26 +00:00
OO . ui . TagMultiselectWidget . prototype . onInputFocus = function ( ) {
this . $element . addClass ( 'oo-ui-tagMultiselectWidget-focus' ) ;
2018-11-07 01:01:56 +00:00
// Reset validity
this . toggleValid ( true ) ;
2017-05-10 01:21:26 +00:00
} ;
2017-04-11 23:41:59 +00:00
/ * *
* Respond to input blur event
* /
2017-05-10 01:21:26 +00:00
OO . ui . TagMultiselectWidget . prototype . onInputBlur = function ( ) {
this . $element . removeClass ( 'oo-ui-tagMultiselectWidget-focus' ) ;
2018-11-07 01:01:56 +00:00
// Set the widget as invalid if there's text in the input
this . addTagFromInput ( ) ;
this . toggleValid ( this . checkValidity ( ) && ( ! this . hasInput || ! this . input . getValue ( ) ) ) ;
2017-05-10 01:21:26 +00:00
} ;
2017-04-11 23:41:59 +00:00
/ * *
2019-03-07 09:22:27 +00:00
* Perform an action after the Enter key on the input
2017-04-11 23:41:59 +00:00
*
* @ param { jQuery . Event } e Event data
* @ param { boolean } [ withMetaKey ] Whether this key was pressed with
2019-03-07 09:22:27 +00:00
* a meta key like Control
2017-04-11 23:41:59 +00:00
* @ return { boolean } Whether to prevent defaults
* /
OO . ui . TagMultiselectWidget . prototype . doInputEnter = function ( ) {
this . addTagFromInput ( ) ;
return false ;
} ;
/ * *
2019-07-16 21:59:51 +00:00
* Perform an action responding to the Backspace key on the input
2017-04-11 23:41:59 +00:00
*
* @ param { jQuery . Event } e Event data
* @ param { boolean } [ withMetaKey ] Whether this key was pressed with
2019-03-07 09:22:27 +00:00
* a meta key like Control
2017-04-11 23:41:59 +00:00
* @ return { boolean } Whether to prevent defaults
* /
2017-05-16 22:34:01 +00:00
OO . ui . TagMultiselectWidget . prototype . doInputBackspace = function ( e , withMetaKey ) {
2017-04-11 23:41:59 +00:00
var items , item ;
if (
this . inputPosition === 'inline' &&
this . input . getValue ( ) === '' &&
! this . isEmpty ( )
) {
// Delete the last item
items = this . getItems ( ) ;
item = items [ items . length - 1 ] ;
2018-03-21 00:36:37 +00:00
2018-06-06 16:49:23 +00:00
if ( ! item . isDisabled ( ) && ! item . isFixed ( ) ) {
2018-03-21 00:36:37 +00:00
this . removeItems ( [ item ] ) ;
// If Ctrl/Cmd was pressed, delete item entirely.
// Otherwise put it into the text field for editing.
if ( ! withMetaKey ) {
2019-02-21 11:08:08 +00:00
this . input . setValue ( item . getLabel ( ) ) ;
2018-03-21 00:36:37 +00:00
}
2017-05-16 22:34:01 +00:00
}
2017-04-11 23:41:59 +00:00
return false ;
}
} ;
/ * *
2019-03-07 09:22:27 +00:00
* Perform an action after the Escape key on the input
2017-04-11 23:41:59 +00:00
*
* @ param { jQuery . Event } e Event data
* /
OO . ui . TagMultiselectWidget . prototype . doInputEscape = function ( ) {
this . clearInput ( ) ;
} ;
/ * *
2019-03-07 09:22:27 +00:00
* Perform an action after the Left / Right arrow key on the input , select the previous
2018-04-04 17:55:02 +00:00
* item from the input .
* See # getPreviousItem
2017-04-11 23:41:59 +00:00
*
* @ param { jQuery . Event } e Event data
* @ param { string } direction Direction of the movement ; forwards or backwards
* @ param { boolean } [ withMetaKey ] Whether this key was pressed with
2019-03-07 09:22:27 +00:00
* a meta key like Control
2017-04-11 23:41:59 +00:00
* /
2017-05-10 01:21:26 +00:00
OO . ui . TagMultiselectWidget . prototype . doInputArrow = function ( e , direction ) {
2017-04-11 23:41:59 +00:00
if (
this . inputPosition === 'inline' &&
2018-04-04 17:55:02 +00:00
! this . isEmpty ( ) &&
direction === 'backwards'
2017-04-11 23:41:59 +00:00
) {
2018-04-04 17:55:02 +00:00
// Get previous item
this . getPreviousItem ( ) . focus ( ) ;
2017-04-11 23:41:59 +00:00
}
} ;
/ * *
* Respond to item select event
2017-05-10 01:21:26 +00:00
*
* @ param { OO . ui . TagItemWidget } item Selected item
2017-04-11 23:41:59 +00:00
* /
OO . ui . TagMultiselectWidget . prototype . onTagSelect = function ( item ) {
2018-05-09 01:38:43 +00:00
if ( this . hasInput && this . allowEditTags && ! item . isFixed ( ) ) {
2017-04-11 23:41:59 +00:00
if ( this . input . getValue ( ) ) {
this . addTagFromInput ( ) ;
}
// 1. Get the label of the tag into the input
2019-03-21 16:15:22 +00:00
this . input . setValue ( item . getLabel ( ) ) ;
2017-04-11 23:41:59 +00:00
// 2. Remove the tag
this . removeItems ( [ item ] ) ;
// 3. Focus the input
this . focus ( ) ;
}
} ;
2018-03-21 00:36:37 +00:00
/ * *
2018-05-09 01:38:43 +00:00
* Respond to item fixed state change
2018-03-21 00:36:37 +00:00
*
* @ param { OO . ui . TagItemWidget } item Selected item
* /
2018-05-09 01:38:43 +00:00
OO . ui . TagMultiselectWidget . prototype . onTagFixed = function ( item ) {
var i ,
items = this . getItems ( ) ;
// Move item to the end of the static items
for ( i = 0 ; i < items . length ; i ++ ) {
if ( items [ i ] !== item && ! items [ i ] . isFixed ( ) ) {
break ;
}
2018-03-21 00:36:37 +00:00
}
2018-05-09 01:38:43 +00:00
this . addItems ( item , i ) ;
2018-03-21 00:36:37 +00:00
} ;
2017-04-11 23:41:59 +00:00
/ * *
* Respond to change event , where items were added , removed , or cleared .
* /
OO . ui . TagMultiselectWidget . prototype . onChangeTags = function ( ) {
2018-11-07 01:01:56 +00:00
var isUnderLimit = this . isUnderLimit ( ) ;
// Reset validity
this . toggleValid (
this . checkValidity ( ) &&
2018-11-08 22:42:29 +00:00
! ( this . hasInput && this . input . getValue ( ) )
2018-11-07 01:01:56 +00:00
) ;
2017-04-11 23:41:59 +00:00
if ( this . hasInput ) {
this . updateInputSize ( ) ;
2018-11-07 01:01:56 +00:00
if ( ! isUnderLimit ) {
// Clear the input
this . input . setValue ( '' ) ;
}
if ( this . inputPosition === 'outline' ) {
// Show/clear the placeholder and enable/disable the input
// based on whether we are/aren't under the specified limit
this . input . $input . attr ( 'placeholder' , isUnderLimit ? this . inputPlaceholder : '' ) ;
this . input . setDisabled ( ! isUnderLimit ) ;
} else {
// Show/hide the input
this . input . $input . toggleClass ( 'oo-ui-element-hidden' , ! isUnderLimit ) ;
}
2017-04-11 23:41:59 +00:00
}
this . updateIfHeightChanged ( ) ;
} ;
/ * *
* @ inheritdoc
* /
OO . ui . TagMultiselectWidget . prototype . setDisabled = function ( isDisabled ) {
// Parent method
OO . ui . TagMultiselectWidget . parent . prototype . setDisabled . call ( this , isDisabled ) ;
if ( this . hasInput && this . input ) {
2018-12-22 11:36:59 +00:00
if ( ! isDisabled ) {
this . updateInputSize ( ) ;
}
2018-11-07 01:01:56 +00:00
this . input . setDisabled ( ! ! isDisabled && ! this . isUnderLimit ( ) ) ;
2017-04-11 23:41:59 +00:00
}
if ( this . items ) {
this . getItems ( ) . forEach ( function ( item ) {
item . setDisabled ( ! ! isDisabled ) ;
} ) ;
}
} ;
/ * *
* Respond to tag remove event
* @ param { OO . ui . TagItemWidget } item Removed tag
* /
OO . ui . TagMultiselectWidget . prototype . onTagRemove = function ( item ) {
this . removeTagByData ( item . getData ( ) ) ;
} ;
/ * *
* Respond to navigate event on the tag
*
* @ param { OO . ui . TagItemWidget } item Removed tag
* @ param { string } direction Direction of movement ; 'forwards' or 'backwards'
* /
OO . ui . TagMultiselectWidget . prototype . onTagNavigate = function ( item , direction ) {
2018-04-04 17:55:02 +00:00
var firstItem = this . getItems ( ) [ 0 ] ;
2017-04-11 23:41:59 +00:00
if ( direction === 'forwards' ) {
this . getNextItem ( item ) . focus ( ) ;
2018-04-04 17:55:02 +00:00
} else if ( ! this . inputPosition === 'inline' || item !== firstItem ) {
// If the widget has an inline input, we want to stop at the starting edge
// of the tags
2017-04-11 23:41:59 +00:00
this . getPreviousItem ( item ) . focus ( ) ;
}
} ;
/ * *
* Add tag from input value
* /
OO . ui . TagMultiselectWidget . prototype . addTagFromInput = function ( ) {
var val = this . input . getValue ( ) ,
isValid = this . isAllowedData ( val ) ;
if ( ! val ) {
return ;
}
if ( isValid || this . allowDisplayInvalidTags ) {
this . clearInput ( ) ;
2018-11-07 01:01:56 +00:00
this . addTag ( val ) ;
2017-04-11 23:41:59 +00:00
}
} ;
/ * *
* Clear the input
* /
OO . ui . TagMultiselectWidget . prototype . clearInput = function ( ) {
this . input . setValue ( '' ) ;
} ;
/ * *
* Check whether the given value is a duplicate of an existing
* tag already in the list .
*
* @ param { string | Object } data Requested value
* @ return { boolean } Value is duplicate
* /
OO . ui . TagMultiselectWidget . prototype . isDuplicateData = function ( data ) {
2018-01-10 01:04:08 +00:00
return ! ! this . findItemFromData ( data ) ;
2017-04-11 23:41:59 +00:00
} ;
/ * *
* Check whether a given value is allowed to be added
*
* @ param { string | Object } data Requested value
2017-04-18 23:41:08 +00:00
* @ return { boolean } Value is allowed
2017-04-11 23:41:59 +00:00
* /
OO . ui . TagMultiselectWidget . prototype . isAllowedData = function ( data ) {
if (
! this . allowDuplicates &&
this . isDuplicateData ( data )
) {
return false ;
}
2017-05-16 22:34:01 +00:00
if ( this . allowArbitrary ) {
return true ;
}
2017-04-11 23:41:59 +00:00
// Check with allowed values
if (
this . getAllowedValues ( ) . some ( function ( value ) {
2017-04-18 23:41:08 +00:00
return data === value ;
2017-04-11 23:41:59 +00:00
} )
) {
return true ;
}
return false ;
} ;
/ * *
* Get the allowed values list
*
* @ return { string [ ] } Allowed data values
* /
OO . ui . TagMultiselectWidget . prototype . getAllowedValues = function ( ) {
return this . allowedValues ;
} ;
/ * *
* Add a value to the allowed values list
*
* @ param { string } value Allowed data value
* /
OO . ui . TagMultiselectWidget . prototype . addAllowedValue = function ( value ) {
if ( this . allowedValues . indexOf ( value ) === - 1 ) {
this . allowedValues . push ( value ) ;
}
} ;
/ * *
* Get the datas of the currently selected items
*
* @ return { string [ ] | Object [ ] } Datas of currently selected items
* /
OO . ui . TagMultiselectWidget . prototype . getValue = function ( ) {
return this . getItems ( )
. filter ( function ( item ) {
return item . isValid ( ) ;
} )
. map ( function ( item ) {
return item . getData ( ) ;
} ) ;
} ;
/ * *
* Set the value of this widget by datas .
*
2017-05-10 01:21:26 +00:00
* @ param { string | string [ ] | Object | Object [ ] } valueObject An object representing the data
2017-04-11 23:41:59 +00:00
* and label of the value . If the widget allows arbitrary values ,
* the items will be added as - is . Otherwise , the data value will
* be checked against allowedValues .
* This object must contain at least a data key . Example :
* { data : 'foo' , label : 'Foo item' }
* For multiple items , use an array of objects . For example :
2018-12-05 13:16:46 +00:00
* [
* { data : 'foo' , label : 'Foo item' } ,
* { data : 'bar' , label : 'Bar item' }
* ]
2017-04-11 23:41:59 +00:00
* Value can also be added with plaintext array , for example :
* [ 'foo' , 'bar' , 'bla' ] or a single string , like 'foo'
* /
OO . ui . TagMultiselectWidget . prototype . setValue = function ( valueObject ) {
valueObject = Array . isArray ( valueObject ) ? valueObject : [ valueObject ] ;
this . clearItems ( ) ;
valueObject . forEach ( function ( obj ) {
if ( typeof obj === 'string' ) {
this . addTag ( obj ) ;
} else {
this . addTag ( obj . data , obj . label ) ;
}
} . bind ( this ) ) ;
} ;
/ * *
* Add tag to the display area
*
* @ param { string | Object } data Tag data
* @ param { string } [ label ] Tag label . If no label is provided , the
* stringified version of the data will be used instead .
* @ return { boolean } Item was added successfully
* /
OO . ui . TagMultiselectWidget . prototype . addTag = function ( data , label ) {
var newItemWidget ,
isValid = this . isAllowedData ( data ) ;
2018-11-07 01:01:56 +00:00
if ( this . isUnderLimit ( ) && ( isValid || this . allowDisplayInvalidTags ) ) {
2017-04-11 23:41:59 +00:00
newItemWidget = this . createTagItemWidget ( data , label ) ;
newItemWidget . toggleValid ( isValid ) ;
this . addItems ( [ newItemWidget ] ) ;
2017-05-10 01:21:26 +00:00
return true ;
2017-04-11 23:41:59 +00:00
}
2018-11-07 01:01:56 +00:00
2017-05-10 01:21:26 +00:00
return false ;
2017-04-11 23:41:59 +00:00
} ;
2018-11-07 01:01:56 +00:00
/ * *
2018-11-08 22:42:29 +00:00
* Check whether the number of current tags is within the limit .
2018-11-07 01:01:56 +00:00
*
2018-11-08 22:42:29 +00:00
* @ return { boolean } True if current tag count is within the limit or
* if 'tagLimit' is not set
2018-11-07 01:01:56 +00:00
* /
OO . ui . TagMultiselectWidget . prototype . isUnderLimit = function ( ) {
2018-11-08 22:42:29 +00:00
return ! this . tagLimit ||
this . getItemCount ( ) < this . tagLimit ;
2018-11-07 01:01:56 +00:00
} ;
2017-04-11 23:41:59 +00:00
/ * *
* Remove tag by its data property .
*
* @ param { string | Object } data Tag data
* /
OO . ui . TagMultiselectWidget . prototype . removeTagByData = function ( data ) {
2018-01-10 01:04:08 +00:00
var item = this . findItemFromData ( data ) ;
2017-04-11 23:41:59 +00:00
this . removeItems ( [ item ] ) ;
} ;
/ * *
* Construct a OO . ui . TagItemWidget ( or a subclass thereof ) from given label and data .
*
* @ protected
* @ param { string } data Item data
* @ param { string } label The label text .
* @ return { OO . ui . TagItemWidget }
* /
OO . ui . TagMultiselectWidget . prototype . createTagItemWidget = function ( data , label ) {
label = label || data ;
return new OO . ui . TagItemWidget ( { data : data , label : label } ) ;
} ;
/ * *
* Given an item , returns the item after it . If the item is already the
* last item , return ` this.input ` . If no item is passed , returns the
* very first item .
*
* @ protected
* @ param { OO . ui . TagItemWidget } [ item ] Tag item
* @ return { OO . ui . Widget } The next widget available .
* /
OO . ui . TagMultiselectWidget . prototype . getNextItem = function ( item ) {
var itemIndex = this . items . indexOf ( item ) ;
if ( item === undefined || itemIndex === - 1 ) {
return this . items [ 0 ] ;
}
if ( itemIndex === this . items . length - 1 ) { // Last item
if ( this . hasInput ) {
return this . input ;
} else {
// Return first item
return this . items [ 0 ] ;
}
} else {
return this . items [ itemIndex + 1 ] ;
}
} ;
/ * *
* Given an item , returns the item before it . If the item is already the
* first item , return ` this.input ` . If no item is passed , returns the
* very last item .
*
* @ protected
* @ param { OO . ui . TagItemWidget } [ item ] Tag item
* @ return { OO . ui . Widget } The previous widget available .
* /
OO . ui . TagMultiselectWidget . prototype . getPreviousItem = function ( item ) {
var itemIndex = this . items . indexOf ( item ) ;
if ( item === undefined || itemIndex === - 1 ) {
return this . items [ this . items . length - 1 ] ;
}
if ( itemIndex === 0 ) {
if ( this . hasInput ) {
return this . input ;
} else {
// Return the last item
return this . items [ this . items . length - 1 ] ;
}
} else {
return this . items [ itemIndex - 1 ] ;
}
} ;
/ * *
* Update the dimensions of the text input field to encompass all available area .
* This is especially relevant for when the input is at the edge of a line
* and should get smaller . The usual operation ( as an inline - block with min - width )
* does not work in that case , pushing the input downwards to the next line .
*
* @ private
* /
OO . ui . TagMultiselectWidget . prototype . updateInputSize = function ( ) {
var $lastItem , direction , contentWidth , currentWidth , bestWidth ;
if ( this . inputPosition === 'inline' && ! this . isDisabled ( ) ) {
2017-07-11 22:15:55 +00:00
if ( this . input . $input [ 0 ] . scrollWidth === 0 ) {
// Input appears to be attached but not visible.
// Don't attempt to adjust its size, because our measurements
// are going to fail anyway.
return ;
}
2017-04-11 23:41:59 +00:00
this . input . $input . css ( 'width' , '1em' ) ;
$lastItem = this . $group . children ( ) . last ( ) ;
direction = OO . ui . Element . static . getDir ( this . $handle ) ;
// Get the width of the input with the placeholder text as
// the value and save it so that we don't keep recalculating
if (
this . contentWidthWithPlaceholder === undefined &&
this . input . getValue ( ) === '' &&
this . input . $input . attr ( 'placeholder' ) !== undefined
) {
this . input . setValue ( this . input . $input . attr ( 'placeholder' ) ) ;
this . contentWidthWithPlaceholder = this . input . $input [ 0 ] . scrollWidth ;
this . input . setValue ( '' ) ;
}
// Always keep the input wide enough for the placeholder text
contentWidth = Math . max (
this . input . $input [ 0 ] . scrollWidth ,
// undefined arguments in Math.max lead to NaN
( this . contentWidthWithPlaceholder === undefined ) ?
0 : this . contentWidthWithPlaceholder
) ;
currentWidth = this . input . $input . width ( ) ;
if ( contentWidth < currentWidth ) {
this . updateIfHeightChanged ( ) ;
// All is fine, don't perform expensive calculations
return ;
}
if ( $lastItem . length === 0 ) {
bestWidth = this . $content . innerWidth ( ) ;
} else {
bestWidth = direction === 'ltr' ?
this . $content . innerWidth ( ) - $lastItem . position ( ) . left - $lastItem . outerWidth ( ) :
$lastItem . position ( ) . left ;
}
2019-03-07 09:22:27 +00:00
// Some safety margin for sanity, because I *really* don't feel like finding out where the
// few pixels this is off by are coming from.
2018-11-01 02:57:41 +00:00
bestWidth -= 13 ;
2017-04-11 23:41:59 +00:00
if ( contentWidth > bestWidth ) {
// This will result in the input getting shifted to the next line
2018-11-01 02:57:41 +00:00
bestWidth = this . $content . innerWidth ( ) - 13 ;
2017-04-11 23:41:59 +00:00
}
this . input . $input . width ( Math . floor ( bestWidth ) ) ;
this . updateIfHeightChanged ( ) ;
} else {
this . updateIfHeightChanged ( ) ;
}
} ;
/ * *
* Determine if widget height changed , and if so ,
* emit the resize event . This is useful for when there are either
* menus or popups attached to the bottom of the widget , to allow
* them to change their positioning in case the widget moved down
* or up .
*
* @ private
* /
OO . ui . TagMultiselectWidget . prototype . updateIfHeightChanged = function ( ) {
var height = this . $element . height ( ) ;
if ( height !== this . height ) {
this . height = height ;
this . emit ( 'resize' ) ;
}
} ;
/ * *
* Check whether all items in the widget are valid
*
* @ return { boolean } Widget is valid
* /
OO . ui . TagMultiselectWidget . prototype . checkValidity = function ( ) {
return this . getItems ( ) . every ( function ( item ) {
return item . isValid ( ) ;
} ) ;
} ;
/ * *
* Set the valid state of this item
*
* @ param { boolean } [ valid ] Item is valid
* @ fires valid
* /
OO . ui . TagMultiselectWidget . prototype . toggleValid = function ( valid ) {
valid = valid === undefined ? ! this . valid : ! ! valid ;
if ( this . valid !== valid ) {
this . valid = valid ;
this . setFlags ( { invalid : ! this . valid } ) ;
this . emit ( 'valid' , this . valid ) ;
}
} ;
/ * *
* Get the current valid state of the widget
*
* @ return { boolean } Widget is valid
* /
OO . ui . TagMultiselectWidget . prototype . isValid = function ( ) {
return this . valid ;
} ;
/ * *
2019-03-07 09:22:27 +00:00
* PopupTagMultiselectWidget is a { @ link OO . ui . TagMultiselectWidget OO . ui . TagMultiselectWidget }
* intended to use a popup . The popup can be configured to have a default input to insert values
* into the widget .
2017-04-11 23:41:59 +00:00
*
* @ example
2019-01-11 16:19:16 +00:00
* // A PopupTagMultiselectWidget.
2017-04-11 23:41:59 +00:00
* var widget = new OO . ui . PopupTagMultiselectWidget ( ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( widget . $element ) ;
2017-04-11 23:41:59 +00:00
*
* // Example: A PopupTagMultiselectWidget with an external popup.
* var popupInput = new OO . ui . TextInputWidget ( ) ,
* widget = new OO . ui . PopupTagMultiselectWidget ( {
* popupInput : popupInput ,
* popup : {
* $content : popupInput . $element
* }
* } ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( widget . $element ) ;
2017-04-11 23:41:59 +00:00
*
* @ class
* @ extends OO . ui . TagMultiselectWidget
* @ mixins OO . ui . mixin . PopupElement
*
* @ param { Object } config Configuration object
2017-04-26 01:28:38 +00:00
* @ cfg { jQuery } [ $overlay ] An overlay for the popup .
2018-01-17 05:47:34 +00:00
* See < https : //www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
2017-04-11 23:41:59 +00:00
* @ cfg { Object } [ popup ] Configuration options for the popup
* @ cfg { OO . ui . InputWidget } [ popupInput ] An input widget inside the popup that will be
* focused when the popup is opened and will be used as replacement for the
* general input in the widget .
2018-12-22 11:36:59 +00:00
* @ deprecated
2017-04-11 23:41:59 +00:00
* /
OO . ui . PopupTagMultiselectWidget = function OoUiPopupTagMultiselectWidget ( config ) {
var defaultInput ,
defaultConfig = { popup : { } } ;
config = config || { } ;
// Parent constructor
2019-03-07 09:22:27 +00:00
OO . ui . PopupTagMultiselectWidget . parent . call ( this , $ . extend ( {
inputPosition : 'none'
} , config ) ) ;
2017-04-11 23:41:59 +00:00
2019-03-07 09:22:27 +00:00
this . $overlay = ( config . $overlay === true ?
OO . ui . getDefaultOverlay ( ) : config . $overlay ) || this . $element ;
2017-04-11 23:41:59 +00:00
if ( ! config . popup ) {
// For the default base implementation, we give a popup
// with an input widget inside it. For any other use cases
// the popup needs to be populated externally and the
// event handled to add tags separately and manually
defaultInput = new OO . ui . TextInputWidget ( ) ;
defaultConfig . popupInput = defaultInput ;
defaultConfig . popup . $content = defaultInput . $element ;
2018-04-11 01:48:10 +00:00
defaultConfig . popup . padded = true ;
2017-04-11 23:41:59 +00:00
this . $element . addClass ( 'oo-ui-popupTagMultiselectWidget-defaultPopup' ) ;
}
// Add overlay, and add that to the autoCloseIgnore
defaultConfig . popup . $overlay = this . $overlay ;
defaultConfig . popup . $autoCloseIgnore = this . hasInput ?
this . input . $element . add ( this . $overlay ) : this . $overlay ;
// Allow extending any of the above
config = $ . extend ( defaultConfig , config ) ;
// Mixin constructors
OO . ui . mixin . PopupElement . call ( this , config ) ;
if ( this . hasInput ) {
this . input . $input . on ( 'focus' , this . popup . toggle . bind ( this . popup , true ) ) ;
}
// Configuration options
this . popupInput = config . popupInput ;
if ( this . popupInput ) {
this . popupInput . connect ( this , {
enter : 'onPopupInputEnter'
} ) ;
}
// Events
2017-04-26 01:28:38 +00:00
this . on ( 'resize' , this . popup . updateDimensions . bind ( this . popup ) ) ;
2019-03-07 09:22:27 +00:00
this . popup . connect ( this , {
toggle : 'onPopupToggle'
} ) ;
this . $tabIndexed . on ( 'focus' , this . onFocus . bind ( this ) ) ;
2017-04-11 23:41:59 +00:00
// Initialize
this . $element
. append ( this . popup . $element )
. addClass ( 'oo-ui-popupTagMultiselectWidget' ) ;
2018-12-22 11:36:59 +00:00
// Deprecation warning
OO . ui . warnDeprecation ( 'PopupTagMultiselectWidget: Deprecated widget. Use MenuTagMultiselectWidget instead. See T208821.' ) ;
2017-04-11 23:41:59 +00:00
} ;
/* Initialization */
OO . inheritClass ( OO . ui . PopupTagMultiselectWidget , OO . ui . TagMultiselectWidget ) ;
OO . mixinClass ( OO . ui . PopupTagMultiselectWidget , OO . ui . mixin . PopupElement ) ;
/* Methods */
/ * *
2017-05-16 22:34:01 +00:00
* Focus event handler .
*
* @ private
2017-04-11 23:41:59 +00:00
* /
2017-05-16 22:34:01 +00:00
OO . ui . PopupTagMultiselectWidget . prototype . onFocus = function ( ) {
2017-04-11 23:41:59 +00:00
this . popup . toggle ( true ) ;
} ;
/ * *
* Respond to popup toggle event
*
* @ param { boolean } isVisible Popup is visible
* /
OO . ui . PopupTagMultiselectWidget . prototype . onPopupToggle = function ( isVisible ) {
if ( isVisible && this . popupInput ) {
this . popupInput . focus ( ) ;
}
} ;
/ * *
* Respond to popup input enter event
* /
OO . ui . PopupTagMultiselectWidget . prototype . onPopupInputEnter = function ( ) {
if ( this . popupInput ) {
this . addTagByPopupValue ( this . popupInput . getValue ( ) ) ;
this . popupInput . setValue ( '' ) ;
}
} ;
/ * *
* @ inheritdoc
* /
OO . ui . PopupTagMultiselectWidget . prototype . onTagSelect = function ( item ) {
if ( this . popupInput && this . allowEditTags ) {
this . popupInput . setValue ( item . getData ( ) ) ;
this . removeItems ( [ item ] ) ;
this . popup . toggle ( true ) ;
this . popupInput . focus ( ) ;
} else {
// Parent
OO . ui . PopupTagMultiselectWidget . parent . prototype . onTagSelect . call ( this , item ) ;
}
} ;
/ * *
* Add a tag by the popup value .
* Whatever is responsible for setting the value in the popup should call
* this method to add a tag , or use the regular methods like # addTag or
* # setValue directly .
*
* @ param { string } data The value of item
* @ param { string } [ label ] The label of the tag . If not given , the data is used .
* /
OO . ui . PopupTagMultiselectWidget . prototype . addTagByPopupValue = function ( data , label ) {
this . addTag ( data , label ) ;
} ;
/ * *
2019-03-07 09:22:27 +00:00
* MenuTagMultiselectWidget is a { @ link OO . ui . TagMultiselectWidget OO . ui . TagMultiselectWidget }
* intended to use a menu of selectable options .
2017-04-11 23:41:59 +00:00
*
* @ example
2019-01-11 16:19:16 +00:00
* // A basic MenuTagMultiselectWidget.
2017-04-11 23:41:59 +00:00
* var widget = new OO . ui . MenuTagMultiselectWidget ( {
* inputPosition : 'outline' ,
* options : [
2018-06-06 16:49:23 +00:00
* { data : 'option1' , label : 'Option 1' , icon : 'tag' } ,
2017-04-11 23:41:59 +00:00
* { data : 'option2' , label : 'Option 2' } ,
* { data : 'option3' , label : 'Option 3' } ,
* ] ,
* selected : [ 'option1' , 'option2' ]
* } ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( widget . $element ) ;
2017-04-11 23:41:59 +00:00
*
* @ class
* @ extends OO . ui . TagMultiselectWidget
*
* @ constructor
* @ param { Object } [ config ] Configuration object
2018-02-07 01:17:20 +00:00
* @ cfg { boolean } [ clearInputOnChoose = true ] Clear the text input value when a menu option is chosen
2017-04-11 23:41:59 +00:00
* @ cfg { Object } [ menu ] Configuration object for the menu widget
2017-04-26 01:28:38 +00:00
* @ cfg { jQuery } [ $overlay ] An overlay for the menu .
2018-01-17 05:47:34 +00:00
* See < https : //www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
2017-04-11 23:41:59 +00:00
* @ cfg { Object [ ] } [ options = [ ] ] Array of menu options in the format ` { data: …, label: … } `
* /
OO . ui . MenuTagMultiselectWidget = function OoUiMenuTagMultiselectWidget ( config ) {
2019-03-21 16:15:22 +00:00
var $autoCloseIgnore = $ ( [ ] ) ;
2017-04-11 23:41:59 +00:00
config = config || { } ;
// Parent constructor
OO . ui . MenuTagMultiselectWidget . parent . call ( this , config ) ;
2019-03-21 16:15:22 +00:00
$autoCloseIgnore = $autoCloseIgnore . add ( this . $group ) ;
if ( this . hasInput ) {
$autoCloseIgnore = $autoCloseIgnore . add ( this . input . $element ) ;
}
2019-03-07 09:22:27 +00:00
this . $overlay = ( config . $overlay === true ?
OO . ui . getDefaultOverlay ( ) : config . $overlay ) || this . $element ;
this . clearInputOnChoose = config . clearInputOnChoose === undefined ||
! ! config . clearInputOnChoose ;
2017-04-11 23:41:59 +00:00
this . menu = this . createMenuWidget ( $ . extend ( {
widget : this ,
2019-03-21 16:15:22 +00:00
hideOnChoose : false ,
2017-04-11 23:41:59 +00:00
input : this . hasInput ? this . input : null ,
$input : this . hasInput ? this . input . $input : null ,
filterFromInput : ! ! this . hasInput ,
2019-03-21 16:15:22 +00:00
highlightOnFilter : ! this . allowArbitrary ,
multiselect : true ,
$autoCloseIgnore : $autoCloseIgnore ,
2017-05-10 01:21:26 +00:00
$floatableContainer : this . hasInput && this . inputPosition === 'outline' ?
2017-04-11 23:41:59 +00:00
this . input . $element : this . $element ,
$overlay : this . $overlay ,
disabled : this . isDisabled ( )
} , config . menu ) ) ;
this . addOptions ( config . options || [ ] ) ;
// Events
this . menu . connect ( this , {
choose : 'onMenuChoose' ,
toggle : 'onMenuToggle'
} ) ;
if ( this . hasInput ) {
2019-03-07 09:22:27 +00:00
this . input . connect ( this , {
change : 'onInputChange'
} ) ;
2017-04-11 23:41:59 +00:00
}
2019-03-07 09:22:27 +00:00
this . connect ( this , {
resize : 'onResize'
} ) ;
2017-04-11 23:41:59 +00:00
// Initialization
2019-03-07 09:22:27 +00:00
this . $overlay . append ( this . menu . $element ) ;
this . $element . addClass ( 'oo-ui-menuTagMultiselectWidget' ) ;
2019-01-11 16:19:16 +00:00
// Remove MenuSelectWidget's generic focus owner ARIA attribute
// TODO: Should this widget have a `role` that is compatible with this attribute?
this . menu . $focusOwner . removeAttr ( 'aria-expanded' ) ;
2019-03-07 09:22:27 +00:00
// TagMultiselectWidget already does this, but it doesn't work right because this.menu is
// not yet set up while the parent constructor runs, and #getAllowedValues rejects everything.
2017-05-31 19:26:04 +00:00
if ( config . selected ) {
this . setValue ( config . selected ) ;
}
2017-04-11 23:41:59 +00:00
} ;
/* Initialization */
OO . inheritClass ( OO . ui . MenuTagMultiselectWidget , OO . ui . TagMultiselectWidget ) ;
/* Methods */
/ * *
* Respond to resize event
* /
OO . ui . MenuTagMultiselectWidget . prototype . onResize = function ( ) {
// Reposition the menu
this . menu . position ( ) ;
} ;
/ * *
* @ inheritdoc
* /
OO . ui . MenuTagMultiselectWidget . prototype . onInputFocus = function ( ) {
// Parent method
OO . ui . MenuTagMultiselectWidget . parent . prototype . onInputFocus . call ( this ) ;
this . menu . toggle ( true ) ;
} ;
/ * *
* Respond to input change event
* /
OO . ui . MenuTagMultiselectWidget . prototype . onInputChange = function ( ) {
this . menu . toggle ( true ) ;
} ;
/ * *
2019-03-21 16:15:22 +00:00
* Respond to menu choose event , which is intentional by the user .
2017-04-11 23:41:59 +00:00
*
2019-03-21 16:15:22 +00:00
* @ param { OO . ui . OptionWidget } menuItem Selected menu items
* @ param { boolean } selected Item is selected
2017-04-11 23:41:59 +00:00
* /
2019-03-21 16:15:22 +00:00
OO . ui . MenuTagMultiselectWidget . prototype . onMenuChoose = function ( menuItem , selected ) {
2018-02-07 01:17:20 +00:00
if ( this . hasInput && this . clearInputOnChoose ) {
this . input . setValue ( '' ) ;
}
2019-03-21 16:15:22 +00:00
if ( selected && ! this . findItemFromData ( menuItem . getData ( ) ) ) {
// The menu item is selected, add it to the tags
this . addTag ( menuItem . getData ( ) , menuItem . getLabel ( ) ) ;
} else {
// The menu item was unselected, remove the tag
this . removeTagByData ( menuItem . getData ( ) ) ;
}
2017-04-11 23:41:59 +00:00
} ;
/ * *
* Respond to menu toggle event . Reset item highlights on hide .
*
* @ param { boolean } isVisible The menu is visible
* /
OO . ui . MenuTagMultiselectWidget . prototype . onMenuToggle = function ( isVisible ) {
if ( ! isVisible ) {
this . menu . highlightItem ( null ) ;
2019-03-21 16:15:22 +00:00
this . menu . scrollToTop ( ) ;
2017-04-11 23:41:59 +00:00
}
2019-01-11 16:19:16 +00:00
setTimeout ( function ( ) {
// Remove MenuSelectWidget's generic focus owner ARIA attribute
// TODO: Should this widget have a `role` that is compatible with this attribute?
this . menu . $focusOwner . removeAttr ( 'aria-expanded' ) ;
} . bind ( this ) ) ;
2017-04-11 23:41:59 +00:00
} ;
/ * *
* @ inheritdoc
* /
OO . ui . MenuTagMultiselectWidget . prototype . onTagSelect = function ( tagItem ) {
2018-01-10 01:04:08 +00:00
var menuItem = this . menu . findItemFromData ( tagItem . getData ( ) ) ;
2018-05-09 01:38:43 +00:00
if ( ! this . allowArbitrary ) {
// Override the base behavior from TagMultiselectWidget; the base behavior
// in TagMultiselectWidget is to remove the tag to edit it in the input,
// but in our case, we want to utilize the menu selection behavior, and
// definitely not remove the item.
// If there is an input that is used for filtering, erase the value so we don't filter
if ( this . hasInput && this . menu . filterFromInput ) {
this . input . setValue ( '' ) ;
}
2018-01-10 01:04:08 +00:00
2018-05-09 01:38:43 +00:00
this . focus ( ) ;
2019-03-21 16:15:22 +00:00
// Highlight the menu item
this . menu . highlightItem ( menuItem ) ;
this . menu . scrollItemIntoView ( menuItem ) ;
2018-05-09 01:38:43 +00:00
} else {
// Use the default
OO . ui . MenuTagMultiselectWidget . parent . prototype . onTagSelect . call ( this , tagItem ) ;
}
2017-04-11 23:41:59 +00:00
} ;
2019-03-21 16:15:22 +00:00
/ * *
* @ inheritdoc
* /
OO . ui . MenuTagMultiselectWidget . prototype . removeItems = function ( items ) {
var widget = this ;
// Parent
OO . ui . MenuTagMultiselectWidget . parent . prototype . removeItems . call ( this , items ) ;
items . forEach ( function ( tagItem ) {
var menuItem = widget . menu . findItemFromData ( tagItem . getData ( ) ) ;
if ( menuItem ) {
// Synchronize the menu selection - unselect the removed tag
widget . menu . unselectItem ( menuItem ) ;
}
} ) ;
} ;
/ * *
* @ inheritdoc
* /
OO . ui . MenuTagMultiselectWidget . prototype . setValue = function ( valueObject ) {
valueObject = Array . isArray ( valueObject ) ? valueObject : [ valueObject ] ;
// We override this method from the parent, to make sure we are adding proper
// menu items, and are accounting for cases where we have this widget with
// a menu but also 'allowArbitrary'
if ( ! this . menu ) {
return ;
}
this . clearItems ( ) ;
valueObject . forEach ( function ( obj ) {
var data , label , menuItem ;
if ( typeof obj === 'string' ) {
data = label = obj ;
} else {
data = obj . data ;
label = obj . label ;
}
// Check if the item is in the menu
menuItem = this . menu . getItemFromLabel ( label ) || this . menu . findItemFromData ( data ) ;
if ( menuItem ) {
// Menu item found, add the menu item
this . addTag ( menuItem . getData ( ) , menuItem . getLabel ( ) ) ;
// Make sure that item is also selected
this . menu . selectItem ( menuItem ) ;
} else if ( this . allowArbitrary ) {
// If the item isn't in the menu, only add it if we
// allow for arbitrary values
this . addTag ( data , label ) ;
}
} . bind ( this ) ) ;
} ;
2018-08-01 22:51:30 +00:00
/ * *
* @ inheritdoc
* /
OO . ui . MenuTagMultiselectWidget . prototype . setDisabled = function ( isDisabled ) {
// Parent method
OO . ui . MenuTagMultiselectWidget . parent . prototype . setDisabled . call ( this , isDisabled ) ;
if ( this . menu ) {
// Protect against calling setDisabled() before the menu was initialized
this . menu . setDisabled ( isDisabled ) ;
}
} ;
2018-03-07 07:13:15 +00:00
/ * *
* Highlight the first selectable item in the menu , if configured .
*
* @ private
* @ chainable
* /
OO . ui . MenuTagMultiselectWidget . prototype . initializeMenuSelection = function ( ) {
2019-03-21 16:15:22 +00:00
var highlightedItem ;
this . menu . highlightItem (
this . allowArbitrary ?
null :
this . menu . findFirstSelectableItem ( )
) ;
highlightedItem = this . menu . findHighlightedItem ( ) ;
// Scroll to the highlighted item, if it exists
if ( highlightedItem ) {
this . menu . scrollItemIntoView ( highlightedItem ) ;
2018-03-07 07:13:15 +00:00
}
} ;
2017-04-11 23:41:59 +00:00
/ * *
* @ inheritdoc
* /
OO . ui . MenuTagMultiselectWidget . prototype . addTagFromInput = function ( ) {
2018-11-08 22:42:29 +00:00
var val = this . input . getValue ( ) ,
// Look for a highlighted item first
// Then look for the element that fits the data
item = this . menu . findHighlightedItem ( ) || this . menu . findItemFromData ( val ) ,
data = item ? item . getData ( ) : val ,
isValid = this . isAllowedData ( data ) ;
2018-03-21 00:36:37 +00:00
2017-04-11 23:41:59 +00:00
// Override the parent method so we add from the menu
// rather than directly from the input
2018-11-08 22:42:29 +00:00
if ( ! val ) {
return ;
2017-05-31 19:26:04 +00:00
}
2018-11-08 22:42:29 +00:00
if ( isValid || this . allowDisplayInvalidTags ) {
2017-05-31 19:26:04 +00:00
this . clearInput ( ) ;
2018-11-08 22:42:29 +00:00
if ( item ) {
this . addTag ( data , item . getLabel ( ) ) ;
} else {
this . addTag ( val ) ;
}
2017-04-11 23:41:59 +00:00
}
} ;
/ * *
* Return the visible items in the menu . This is mainly used for when
* the menu is filtering results .
*
* @ return { OO . ui . MenuOptionWidget [ ] } Visible results
* /
OO . ui . MenuTagMultiselectWidget . prototype . getMenuVisibleItems = function ( ) {
return this . menu . getItems ( ) . filter ( function ( menuItem ) {
return menuItem . isVisible ( ) ;
} ) ;
} ;
/ * *
* Create the menu for this widget . This is in a separate method so that
* child classes can override this without polluting the constructor with
* unnecessary extra objects that will be overidden .
*
* @ param { Object } menuConfig Configuration options
* @ return { OO . ui . MenuSelectWidget } Menu widget
* /
OO . ui . MenuTagMultiselectWidget . prototype . createMenuWidget = function ( menuConfig ) {
2017-05-10 01:21:26 +00:00
return new OO . ui . MenuSelectWidget ( menuConfig ) ;
2017-04-11 23:41:59 +00:00
} ;
/ * *
* Add options to the menu
*
2017-05-10 01:21:26 +00:00
* @ param { Object [ ] } menuOptions Object defining options
2017-04-11 23:41:59 +00:00
* /
OO . ui . MenuTagMultiselectWidget . prototype . addOptions = function ( menuOptions ) {
var widget = this ,
items = menuOptions . map ( function ( obj ) {
2018-06-06 16:49:23 +00:00
return widget . createMenuOptionWidget ( obj . data , obj . label , obj . icon ) ;
2017-04-11 23:41:59 +00:00
} ) ;
this . menu . addItems ( items ) ;
} ;
/ * *
* Create a menu option widget .
*
* @ param { string } data Item data
* @ param { string } [ label ] Item label
2018-06-06 16:49:23 +00:00
* @ param { string } [ icon ] Symbolic icon name
2017-04-11 23:41:59 +00:00
* @ return { OO . ui . OptionWidget } Option widget
* /
2018-06-06 16:49:23 +00:00
OO . ui . MenuTagMultiselectWidget . prototype . createMenuOptionWidget = function ( data , label , icon ) {
2017-04-11 23:41:59 +00:00
return new OO . ui . MenuOptionWidget ( {
data : data ,
2018-06-06 16:49:23 +00:00
label : label || data ,
icon : icon
2017-04-11 23:41:59 +00:00
} ) ;
} ;
/ * *
* Get the menu
*
* @ return { OO . ui . MenuSelectWidget } Menu
* /
OO . ui . MenuTagMultiselectWidget . prototype . getMenu = function ( ) {
return this . menu ;
} ;
/ * *
2017-04-18 23:41:08 +00:00
* Get the allowed values list
*
* @ return { string [ ] } Allowed data values
2017-04-11 23:41:59 +00:00
* /
2017-04-18 23:41:08 +00:00
OO . ui . MenuTagMultiselectWidget . prototype . getAllowedValues = function ( ) {
2017-05-31 19:26:04 +00:00
var menuDatas = [ ] ;
if ( this . menu ) {
// If the parent constructor is calling us, we're not ready yet, this.menu is not set up.
menuDatas = this . menu . getItems ( ) . map ( function ( menuItem ) {
return menuItem . getData ( ) ;
} ) ;
}
2017-04-18 23:41:08 +00:00
return this . allowedValues . concat ( menuDatas ) ;
2017-04-11 23:41:59 +00:00
} ;
2016-02-01 22:28:13 +00:00
/ * *
* SelectFileWidgets allow for selecting files , using the HTML5 File API . These
2019-01-11 16:19:16 +00:00
* widgets can be configured with { @ link OO . ui . mixin . IconElement icons } , { @ link
* OO . ui . mixin . IndicatorElement indicators } and { @ link OO . ui . mixin . TitledElement titles } .
2018-01-17 05:47:34 +00:00
* Please see the [ OOUI documentation on MediaWiki ] [ 1 ] for more information and examples .
2016-02-01 22:28:13 +00:00
*
2019-03-21 16:15:22 +00:00
* Although SelectFileWidget inherits from SelectFileInputWidget , it does not
* behave as an InputWidget , and can ' t be used in HTML forms .
*
2016-02-01 22:28:13 +00:00
* @ example
2019-01-11 16:19:16 +00:00
* // A file select widget.
2016-02-01 22:28:13 +00:00
* var selectFile = new OO . ui . SelectFileWidget ( ) ;
2019-01-11 16:19:16 +00:00
* $ ( document . body ) . append ( selectFile . $element ) ;
2016-02-01 22:28:13 +00:00
*
2018-01-17 05:47:34 +00:00
* [ 1 ] : https : //www.mediawiki.org/wiki/OOUI/Widgets
2016-02-01 22:28:13 +00:00
*
* @ class
2019-03-21 16:15:22 +00:00
* @ extends OO . ui . SelectFileInputWidget
2016-02-01 22:28:13 +00:00
* @ mixins OO . ui . mixin . PendingElement
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* @ cfg { string } [ notsupported ] Text to display when file support is missing in the browser .
* @ cfg { boolean } [ droppable = true ] Whether to accept files by drag and drop .
2019-03-21 16:15:22 +00:00
* @ cfg { boolean } [ buttonOnly = false ] Show only the select file button , no info field . Requires
* showDropTarget to be false .
2019-03-07 09:22:27 +00:00
* @ cfg { boolean } [ showDropTarget = false ] Whether to show a drop target . Requires droppable to be
2019-04-04 21:29:43 +00:00
* true . Not yet supported in multiple file mode .
2016-03-01 22:00:31 +00:00
* @ cfg { number } [ thumbnailSizeLimit = 20 ] File size limit in MiB above which to not try and show a
2019-03-07 09:22:27 +00:00
* preview ( for performance ) .
2016-02-01 22:28:13 +00:00
* /
OO . ui . SelectFileWidget = function OoUiSelectFileWidget ( config ) {
2019-03-21 16:15:22 +00:00
var dragHandler , droppable ,
isSupported = this . constructor . static . isSupported ( ) ;
2019-03-14 01:03:02 +00:00
2016-02-01 22:28:13 +00:00
// Configuration initialization
config = $ . extend ( {
notsupported : OO . ui . msg ( 'ooui-selectfile-not-supported' ) ,
droppable : true ,
2019-03-21 16:15:22 +00:00
buttonOnly : false ,
2016-02-09 21:34:30 +00:00
showDropTarget : false ,
2019-03-21 16:15:22 +00:00
thumbnailSizeLimit : 20
2016-02-01 22:28:13 +00:00
} , config ) ;
2019-03-21 16:15:22 +00:00
if ( ! isSupported ) {
config . disabled = true ;
}
2016-02-01 22:28:13 +00:00
// Parent constructor
OO . ui . SelectFileWidget . parent . call ( this , config ) ;
// Mixin constructors
2019-03-21 16:15:22 +00:00
OO . ui . mixin . PendingElement . call ( this ) ;
2016-02-01 22:28:13 +00:00
2019-03-21 16:15:22 +00:00
if ( ! isSupported ) {
this . info . setValue ( config . notsupported ) ;
2016-02-01 22:28:13 +00:00
}
2019-03-21 16:15:22 +00:00
// Properties
droppable = config . droppable && isSupported ;
2019-04-04 21:29:43 +00:00
// TODO: Support drop target when multiple is set
this . showDropTarget = droppable && config . showDropTarget && ! this . multiple ;
2019-03-21 16:15:22 +00:00
this . thumbnailSizeLimit = config . thumbnailSizeLimit ;
2016-02-01 22:28:13 +00:00
// Initialization
2019-03-14 01:03:02 +00:00
if ( this . showDropTarget ) {
2016-02-09 21:34:30 +00:00
this . selectButton . setIcon ( 'upload' ) ;
this . $thumbnail = $ ( '<div>' ) . addClass ( 'oo-ui-selectFileWidget-thumbnail' ) ;
this . setPendingElement ( this . $thumbnail ) ;
2016-08-03 16:41:35 +00:00
this . $element
2019-03-21 16:15:22 +00:00
. addClass ( 'oo-ui-selectFileWidget-dropTarget' )
2016-02-01 22:28:13 +00:00
. on ( {
click : this . onDropTargetClick . bind ( this )
2016-02-09 21:34:30 +00:00
} )
. append (
this . $thumbnail ,
2019-03-21 16:15:22 +00:00
this . info . $element ,
2016-02-09 21:34:30 +00:00
this . selectButton . $element ,
$ ( '<span>' )
. addClass ( 'oo-ui-selectFileWidget-dropLabel' )
. text ( OO . ui . msg ( 'ooui-selectfile-dragdrop-placeholder' ) )
) ;
2019-03-21 16:15:22 +00:00
this . fieldLayout . $element . remove ( ) ;
} else if ( config . buttonOnly ) {
2019-06-05 16:32:46 +00:00
// Copy over any classes that may have been added already.
// Ensure no events are bound to this.$element before here.
this . selectButton . $element
. addClass ( this . $element . attr ( 'class' ) )
. addClass ( 'oo-ui-selectFileWidget-buttonOnly' ) ;
// Set this.$element to just be the button
this . $element = this . selectButton . $element ;
}
// Events
if ( droppable ) {
dragHandler = this . onDragEnterOrOver . bind ( this ) ;
this . $element . on ( {
dragenter : dragHandler ,
dragover : dragHandler ,
dragleave : this . onDragLeave . bind ( this ) ,
drop : this . onDrop . bind ( this )
} ) ;
2016-02-01 22:28:13 +00:00
}
2019-03-21 16:15:22 +00:00
2019-04-04 21:29:43 +00:00
this . $input
. on ( 'click' , function ( e ) {
// Prevents dropTarget to get clicked which calls
// a click on this input
e . stopPropagation ( ) ;
} ) ;
2019-03-21 16:15:22 +00:00
this . $element . addClass ( 'oo-ui-selectFileWidget' ) ;
2016-02-09 21:34:30 +00:00
this . updateUI ( ) ;
2016-02-01 22:28:13 +00:00
} ;
/* Setup */
2019-03-21 16:15:22 +00:00
OO . inheritClass ( OO . ui . SelectFileWidget , OO . ui . SelectFileInputWidget ) ;
2016-02-01 22:28:13 +00:00
OO . mixinClass ( OO . ui . SelectFileWidget , OO . ui . mixin . PendingElement ) ;
/* Static Properties */
/ * *
* Check if this widget is supported
*
* @ static
* @ return { boolean }
* /
OO . ui . SelectFileWidget . static . isSupported = function ( ) {
var $input ;
if ( OO . ui . SelectFileWidget . static . isSupportedCache === null ) {
2016-02-22 22:36:25 +00:00
$input = $ ( '<input>' ) . attr ( 'type' , 'file' ) ;
2016-02-01 22:28:13 +00:00
OO . ui . SelectFileWidget . static . isSupportedCache = $input [ 0 ] . files !== undefined ;
}
return OO . ui . SelectFileWidget . static . isSupportedCache ;
} ;
OO . ui . SelectFileWidget . static . isSupportedCache = null ;
/* Events */
/ * *
* @ event change
*
* A change event is emitted when the on / off state of the toggle changes .
*
* @ param { File | null } value New value
* /
/* Methods */
/ * *
* Get the current value of the field
*
2019-04-04 21:29:43 +00:00
* For single file widgets returns a File or null .
* For multiple file widgets returns a list of Files .
*
* @ return { File | File [ ] | null }
2016-02-01 22:28:13 +00:00
* /
OO . ui . SelectFileWidget . prototype . getValue = function ( ) {
2019-04-04 21:29:43 +00:00
return this . multiple ? this . currentFiles : this . currentFiles [ 0 ] ;
2016-02-01 22:28:13 +00:00
} ;
/ * *
* Set the current value of the field
*
2019-04-04 21:29:43 +00:00
* @ param { File [ ] | null } files Files to select
2016-02-01 22:28:13 +00:00
* /
2019-04-04 21:29:43 +00:00
OO . ui . SelectFileWidget . prototype . setValue = function ( files ) {
if ( files && ! this . multiple ) {
files = files . slice ( 0 , 1 ) ;
}
function comparableFile ( file ) {
// Use extend to convert to plain objects so they can be compared.
return $ . extend ( { } , file ) ;
}
if ( ! OO . compare (
files && files . map ( comparableFile ) ,
this . currentFiles && this . currentFiles . map ( comparableFile )
) ) {
this . currentFiles = files || [ ] ;
this . emit ( 'change' , this . currentFiles ) ;
2016-02-01 22:28:13 +00:00
}
} ;
/ * *
2019-03-21 16:15:22 +00:00
* @ inheritdoc
* /
OO . ui . SelectFileWidget . prototype . getFilename = function ( ) {
2019-04-04 21:29:43 +00:00
return this . currentFiles . map ( function ( file ) {
return file . name ;
} ) . join ( ', ' ) ;
2019-03-21 16:15:22 +00:00
} ;
/ * *
* Disable InputWidget # onEdit listener , onFileSelected is used instead .
* @ inheritdoc
* /
OO . ui . SelectFileWidget . prototype . onEdit = function ( ) { } ;
/ * *
* @ inheritdoc
2016-02-01 22:28:13 +00:00
* /
OO . ui . SelectFileWidget . prototype . updateUI = function ( ) {
2019-03-21 16:15:22 +00:00
// Too early, or not supported
if ( ! this . selectButton || ! this . constructor . static . isSupported ( ) ) {
return ;
}
// Parent method
OO . ui . SelectFileWidget . super . prototype . updateUI . call ( this ) ;
2019-04-04 21:29:43 +00:00
if ( this . currentFiles . length ) {
2019-03-21 16:15:22 +00:00
this . $element . removeClass ( 'oo-ui-selectFileInputWidget-empty' ) ;
if ( this . showDropTarget ) {
this . pushPending ( ) ;
2019-04-04 21:29:43 +00:00
this . loadAndGetImageUrl ( this . currentFiles [ 0 ] ) . done ( function ( url ) {
2019-03-21 16:15:22 +00:00
this . $thumbnail . css ( 'background-image' , 'url( ' + url + ' )' ) ;
} . bind ( this ) ) . fail ( function ( ) {
this . $thumbnail . append (
new OO . ui . IconWidget ( {
icon : 'attachment' ,
classes : [ 'oo-ui-selectFileWidget-noThumbnail-icon' ]
} ) . $element
) ;
} . bind ( this ) ) . always ( function ( ) {
this . popPending ( ) ;
} . bind ( this ) ) ;
this . $element . off ( 'click' ) ;
}
2016-02-01 22:28:13 +00:00
} else {
2019-03-21 16:15:22 +00:00
if ( this . showDropTarget ) {
this . $element . off ( 'click' ) ;
this . $element . on ( {
click : this . onDropTargetClick . bind ( this )
} ) ;
this . $thumbnail
. empty ( )
. css ( 'background-image' , '' ) ;
2016-02-01 22:28:13 +00:00
}
2019-03-21 16:15:22 +00:00
this . $element . addClass ( 'oo-ui-selectFileInputWidget-empty' ) ;
2016-02-01 22:28:13 +00:00
}
} ;
2016-02-09 21:34:30 +00:00
/ * *
* If the selected file is an image , get its URL and load it .
*
2019-04-04 21:29:43 +00:00
* @ param { File } file File
2016-02-09 21:34:30 +00:00
* @ return { jQuery . Promise } Promise resolves with the image URL after it has loaded
* /
2019-04-04 21:29:43 +00:00
OO . ui . SelectFileWidget . prototype . loadAndGetImageUrl = function ( file ) {
2016-02-09 21:34:30 +00:00
var deferred = $ . Deferred ( ) ,
reader = new FileReader ( ) ;
if (
( OO . getProp ( file , 'type' ) || '' ) . indexOf ( 'image/' ) === 0 &&
file . size < this . thumbnailSizeLimit * 1024 * 1024
) {
reader . onload = function ( event ) {
var img = document . createElement ( 'img' ) ;
img . addEventListener ( 'load' , function ( ) {
if (
img . naturalWidth === 0 ||
img . naturalHeight === 0 ||
img . complete === false
) {
deferred . reject ( ) ;
} else {
deferred . resolve ( event . target . result ) ;
}
} ) ;
img . src = event . target . result ;
} ;
reader . readAsDataURL ( file ) ;
} else {
deferred . reject ( ) ;
}
return deferred . promise ( ) ;
} ;
2016-02-01 22:28:13 +00:00
/ * *
2019-03-21 16:15:22 +00:00
* @ inheritdoc
2016-02-01 22:28:13 +00:00
* /
OO . ui . SelectFileWidget . prototype . onFileSelected = function ( e ) {
2019-04-04 21:29:43 +00:00
var files ;
2016-02-01 22:28:13 +00:00
2019-04-04 21:29:43 +00:00
if ( this . inputClearing ) {
return ;
2016-02-01 22:28:13 +00:00
}
2019-04-04 21:29:43 +00:00
files = this . filterFiles ( e . target . files || [ ] ) ;
// After a file is selected clear the native widget to avoid confusion
this . inputClearing = true ;
this . $input [ 0 ] . value = '' ;
this . inputClearing = false ;
this . setValue ( files ) ;
2016-02-01 22:28:13 +00:00
} ;
/ * *
* Handle drop target click events .
*
* @ private
* @ param { jQuery . Event } e Key press event
2019-04-24 18:36:00 +00:00
* @ return { undefined | boolean } False to prevent default if event is handled
2016-02-01 22:28:13 +00:00
* /
OO . ui . SelectFileWidget . prototype . onDropTargetClick = function ( ) {
2019-03-21 16:15:22 +00:00
if ( ! this . isDisabled ( ) && this . $input ) {
2019-02-21 11:08:08 +00:00
this . $input . trigger ( 'click' ) ;
2016-02-01 22:28:13 +00:00
return false ;
}
} ;
/ * *
* Handle drag enter and over events
*
* @ private
* @ param { jQuery . Event } e Drag event
2019-04-24 18:36:00 +00:00
* @ return { undefined | boolean } False to prevent default if event is handled
2016-02-01 22:28:13 +00:00
* /
OO . ui . SelectFileWidget . prototype . onDragEnterOrOver = function ( e ) {
2019-04-04 21:29:43 +00:00
var itemsOrFiles ,
hasDroppableFile = false ,
2016-02-01 22:28:13 +00:00
dt = e . originalEvent . dataTransfer ;
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
2019-03-21 16:15:22 +00:00
if ( this . isDisabled ( ) ) {
2016-02-01 22:28:13 +00:00
this . $element . removeClass ( 'oo-ui-selectFileWidget-canDrop' ) ;
dt . dropEffect = 'none' ;
return false ;
}
// DataTransferItem and File both have a type property, but in Chrome files
// have no information at this point.
2019-04-04 21:29:43 +00:00
itemsOrFiles = dt . items || dt . files ;
if ( itemsOrFiles && itemsOrFiles . length ) {
if ( this . filterFiles ( itemsOrFiles ) . length ) {
hasDroppableFile = true ;
2016-02-01 22:28:13 +00:00
}
// dt.types is Array-like, but not an Array
} else if ( Array . prototype . indexOf . call ( OO . getProp ( dt , 'types' ) || [ ] , 'Files' ) !== - 1 ) {
// File information is not available at this point for security so just assume
// it is acceptable for now.
// https://bugzilla.mozilla.org/show_bug.cgi?id=640534
2019-04-04 21:29:43 +00:00
hasDroppableFile = true ;
2016-02-01 22:28:13 +00:00
}
2019-04-04 21:29:43 +00:00
this . $element . toggleClass ( 'oo-ui-selectFileWidget-canDrop' , hasDroppableFile ) ;
if ( ! hasDroppableFile ) {
2016-02-01 22:28:13 +00:00
dt . dropEffect = 'none' ;
}
return false ;
} ;
/ * *
* Handle drag leave events
*
* @ private
* @ param { jQuery . Event } e Drag event
* /
OO . ui . SelectFileWidget . prototype . onDragLeave = function ( ) {
this . $element . removeClass ( 'oo-ui-selectFileWidget-canDrop' ) ;
} ;
/ * *
* Handle drop events
*
* @ private
* @ param { jQuery . Event } e Drop event
2019-04-24 18:36:00 +00:00
* @ return { undefined | boolean } False to prevent default if event is handled
2016-02-01 22:28:13 +00:00
* /
OO . ui . SelectFileWidget . prototype . onDrop = function ( e ) {
2019-04-04 21:29:43 +00:00
var files ,
2016-02-01 22:28:13 +00:00
dt = e . originalEvent . dataTransfer ;
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
this . $element . removeClass ( 'oo-ui-selectFileWidget-canDrop' ) ;
2019-03-21 16:15:22 +00:00
if ( this . isDisabled ( ) ) {
2016-02-01 22:28:13 +00:00
return false ;
}
2019-04-04 21:29:43 +00:00
files = this . filterFiles ( dt . files || [ ] ) ;
this . setValue ( files ) ;
2016-02-01 22:28:13 +00:00
return false ;
} ;
/ * *
* @ inheritdoc
* /
OO . ui . SelectFileWidget . prototype . setDisabled = function ( disabled ) {
2019-03-21 16:15:22 +00:00
disabled = disabled || ! this . constructor . static . isSupported ( ) ;
// Parent method
2016-02-01 22:28:13 +00:00
OO . ui . SelectFileWidget . parent . prototype . setDisabled . call ( this , disabled ) ;
} ;
/ * *
2019-03-07 09:22:27 +00:00
* SearchWidgets combine a { @ link OO . ui . TextInputWidget text input field } ,
* where users can type a search query , and a menu of search results ,
* which is displayed beneath the query field .
* Unlike { @ link OO . ui . mixin . LookupElement lookup menus } , search result menus are always visible
* to the user . Users can choose an item from the menu or type a query into the text field to
* search for a matching result item .
2016-02-01 22:28:13 +00:00
* In general , search widgets are used inside a separate { @ link OO . ui . Dialog dialog } window .
*
* Each time the query is changed , the search result menu is cleared and repopulated . Please see
2018-01-17 05:47:34 +00:00
* the [ OOUI demos ] [ 1 ] for an example .
2016-02-01 22:28:13 +00:00
*
2018-01-17 05:47:34 +00:00
* [ 1 ] : https : //doc.wikimedia.org/oojs-ui/master/demos/#SearchInputWidget-type-search
2016-02-01 22:28:13 +00:00
*
* @ class
* @ extends OO . ui . Widget
*
* @ constructor
* @ param { Object } [ config ] Configuration options
* @ cfg { string | jQuery } [ placeholder ] Placeholder text for query input
* @ cfg { string } [ value ] Initial query value
2019-09-04 19:42:13 +00:00
* @ cfg { OO . ui . InputWidget } [ input ] { @ link OO . ui . InputWidget Input widget } for search . Defaults
* to a { @ link OO . ui . SearchInputWidget search input widget } if not provided .
2016-02-01 22:28:13 +00:00
* /
OO . ui . SearchWidget = function OoUiSearchWidget ( config ) {
// Configuration initialization
config = config || { } ;
// Parent constructor
OO . ui . SearchWidget . parent . call ( this , config ) ;
// Properties
2019-09-04 19:42:13 +00:00
this . query = config . input || new OO . ui . SearchInputWidget ( {
2016-02-01 22:28:13 +00:00
placeholder : config . placeholder ,
value : config . value
} ) ;
this . results = new OO . ui . SelectWidget ( ) ;
this . $query = $ ( '<div>' ) ;
this . $results = $ ( '<div>' ) ;
// Events
this . query . connect ( this , {
change : 'onQueryChange' ,
enter : 'onQueryEnter'
} ) ;
this . query . $input . on ( 'keydown' , this . onQueryKeydown . bind ( this ) ) ;
// Initialization
this . $query
. addClass ( 'oo-ui-searchWidget-query' )
. append ( this . query . $element ) ;
this . $results
. addClass ( 'oo-ui-searchWidget-results' )
. append ( this . results . $element ) ;
this . $element
. addClass ( 'oo-ui-searchWidget' )
. append ( this . $results , this . $query ) ;
} ;
/* Setup */
OO . inheritClass ( OO . ui . SearchWidget , OO . ui . Widget ) ;
/* Methods */
/ * *
* Handle query key down events .
*
* @ private
* @ param { jQuery . Event } e Key down event
* /
OO . ui . SearchWidget . prototype . onQueryKeydown = function ( e ) {
var highlightedItem , nextItem ,
dir = e . which === OO . ui . Keys . DOWN ? 1 : ( e . which === OO . ui . Keys . UP ? - 1 : 0 ) ;
if ( dir ) {
2017-09-20 00:58:44 +00:00
highlightedItem = this . results . findHighlightedItem ( ) ;
2016-02-01 22:28:13 +00:00
if ( ! highlightedItem ) {
2018-01-17 05:47:34 +00:00
highlightedItem = this . results . findSelectedItem ( ) ;
2016-02-01 22:28:13 +00:00
}
2017-09-20 00:58:44 +00:00
nextItem = this . results . findRelativeSelectableItem ( highlightedItem , dir ) ;
2016-02-01 22:28:13 +00:00
this . results . highlightItem ( nextItem ) ;
nextItem . scrollElementIntoView ( ) ;
}
} ;
/ * *
* Handle select widget select events .
*
* Clears existing results . Subclasses should repopulate items according to new query .
*
* @ private
* @ param { string } value New value
* /
OO . ui . SearchWidget . prototype . onQueryChange = function ( ) {
// Reset
this . results . clearItems ( ) ;
} ;
/ * *
* Handle select widget enter key events .
*
* Chooses highlighted item .
*
* @ private
* @ param { string } value New value
* /
OO . ui . SearchWidget . prototype . onQueryEnter = function ( ) {
2017-09-20 00:58:44 +00:00
var highlightedItem = this . results . findHighlightedItem ( ) ;
2016-02-01 22:28:13 +00:00
if ( highlightedItem ) {
this . results . chooseItem ( highlightedItem ) ;
}
} ;
/ * *
* Get the query input .
*
* @ return { OO . ui . TextInputWidget } Query input
* /
OO . ui . SearchWidget . prototype . getQuery = function ( ) {
return this . query ;
} ;
/ * *
* Get the search results menu .
*
* @ return { OO . ui . SelectWidget } Menu of search results
* /
OO . ui . SearchWidget . prototype . getResults = function ( ) {
return this . results ;
} ;
} ( OO ) ) ;
2017-05-10 01:21:26 +00:00
2018-05-29 23:31:25 +00:00
//# sourceMappingURL=oojs-ui-widgets.js.map.json