/**
* DraggableGroupElement is a mixin class used to create a group element to
* contain draggable elements, which are items that can be clicked and dragged by a mouse.
* The class is used with OO.ui.DraggableElement.
*
* @abstract
* @class
* @mixins OO.ui.GroupElement
*
* @constructor
* @param {Object} [config] Configuration options
* @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
* should match the layout of the items. Items displayed in a single row
* or in several rows should use horizontal orientation. The vertical orientation should only be
* used when the items are displayed in a single column. Defaults to 'vertical'
*/
OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
// Configuration initialization
config = config || {};
// Parent constructor
OO.ui.GroupElement.call( this, config );
// Properties
this.orientation = config.orientation || 'vertical';
this.dragItem = null;
this.itemDragOver = null;
this.itemKeys = {};
this.sideInsertion = '';
// Events
this.aggregate( {
dragstart: 'itemDragStart',
dragend: 'itemDragEnd',
drop: 'itemDrop'
} );
this.connect( this, {
itemDragStart: 'onItemDragStart',
itemDrop: 'onItemDrop',
itemDragEnd: 'onItemDragEnd'
} );
this.$element.on( {
dragover: $.proxy( this.onDragOver, this ),
dragleave: $.proxy( this.onDragLeave, this )
} );
// Initialize
if ( Array.isArray( config.items ) ) {
this.addItems( config.items );
}
this.$placeholder = $( '
' )
.addClass( 'oo-ui-draggableGroupElement-placeholder' );
this.$element
.addClass( 'oo-ui-draggableGroupElement' )
.append( this.$status )
.toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
.prepend( this.$placeholder );
};
/* Setup */
OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
/* Events */
/**
* A 'reorder' event is emitted when the order of items in the group changes.
*
* @event reorder
* @param {OO.ui.DraggableElement} item Reordered item
* @param {number} [newIndex] New index for the item
*/
/* Methods */
/**
* Respond to item drag start event
*
* @private
* @param {OO.ui.DraggableElement} item Dragged item
*/
OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
var i, len;
// Map the index of each object
for ( i = 0, len = this.items.length; i < len; i++ ) {
this.items[ i ].setIndex( i );
}
if ( this.orientation === 'horizontal' ) {
// Set the height of the indicator
this.$placeholder.css( {
height: item.$element.outerHeight(),
width: 2
} );
} else {
// Set the width of the indicator
this.$placeholder.css( {
height: 2,
width: item.$element.outerWidth()
} );
}
this.setDragItem( item );
};
/**
* Respond to item drag end event
*
* @private
*/
OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
this.unsetDragItem();
return false;
};
/**
* Handle drop event and switch the order of the items accordingly
*
* @private
* @param {OO.ui.DraggableElement} item Dropped item
* @fires reorder
*/
OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
var toIndex = item.getIndex();
// Check if the dropped item is from the current group
// TODO: Figure out a way to configure a list of legally droppable
// elements even if they are not yet in the list
if ( this.getDragItem() ) {
// If the insertion point is 'after', the insertion index
// is shifted to the right (or to the left in RTL, hence 'after')
if ( this.sideInsertion === 'after' ) {
toIndex++;
}
// Emit change event
this.emit( 'reorder', this.getDragItem(), toIndex );
}
this.unsetDragItem();
// Return false to prevent propogation
return false;
};
/**
* Handle dragleave event.
*
* @private
*/
OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
// This means the item was dragged outside the widget
this.$placeholder
.css( 'left', 0 )
.addClass( 'oo-ui-element-hidden' );
};
/**
* Respond to dragover event
*
* @private
* @param {jQuery.Event} event Event details
*/
OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
clientX = e.originalEvent.clientX,
clientY = e.originalEvent.clientY;
// Get the OptionWidget item we are dragging over
dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
$optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
if ( $optionWidget[ 0 ] ) {
itemOffset = $optionWidget.offset();
itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
itemPosition = $optionWidget.position();
itemIndex = $optionWidget.data( 'index' );
}
if (
itemOffset &&
this.isDragging() &&
itemIndex !== this.getDragItem().getIndex()
) {
if ( this.orientation === 'horizontal' ) {
// Calculate where the mouse is relative to the item width
itemSize = itemBoundingRect.width;
itemMidpoint = itemBoundingRect.left + itemSize / 2;
dragPosition = clientX;
// Which side of the item we hover over will dictate
// where the placeholder will appear, on the left or
// on the right
cssOutput = {
left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
top: itemPosition.top
};
} else {
// Calculate where the mouse is relative to the item height
itemSize = itemBoundingRect.height;
itemMidpoint = itemBoundingRect.top + itemSize / 2;
dragPosition = clientY;
// Which side of the item we hover over will dictate
// where the placeholder will appear, on the top or
// on the bottom
cssOutput = {
top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
left: itemPosition.left
};
}
// Store whether we are before or after an item to rearrange
// For horizontal layout, we need to account for RTL, as this is flipped
if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
} else {
this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
}
// Add drop indicator between objects
this.$placeholder
.css( cssOutput )
.removeClass( 'oo-ui-element-hidden' );
} else {
// This means the item was dragged outside the widget
this.$placeholder
.css( 'left', 0 )
.addClass( 'oo-ui-element-hidden' );
}
// Prevent default
e.preventDefault();
};
/**
* Set a dragged item
*
* @param {OO.ui.DraggableElement} item Dragged item
*/
OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
this.dragItem = item;
};
/**
* Unset the current dragged item
*/
OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
this.dragItem = null;
this.itemDragOver = null;
this.$placeholder.addClass( 'oo-ui-element-hidden' );
this.sideInsertion = '';
};
/**
* Get the item that is currently being dragged.
*
* @return {OO.ui.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
*/
OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
return this.dragItem;
};
/**
* Check if an item in the group is currently being dragged.
*
* @return {Boolean} Item is being dragged
*/
OO.ui.DraggableGroupElement.prototype.isDragging = function () {
return this.getDragItem() !== null;
};