' );
this.$anchor = $( '
' );
// If undefined, will be computed lazily in updateDimensions()
this.$container = config.$container;
this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
this.autoClose = !!config.autoClose;
this.$autoCloseIgnore = config.$autoCloseIgnore;
this.transitionTimeout = null;
this.anchor = null;
this.width = config.width !== undefined ? config.width : 320;
this.height = config.height !== undefined ? config.height : null;
this.setAlignment( config.align );
this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
this.onMouseDownHandler = this.onMouseDown.bind( this );
this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
// Events
this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
// Initialization
this.toggleAnchor( config.anchor === undefined || config.anchor );
this.$body.addClass( 'oo-ui-popupWidget-body' );
this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
this.$head
.addClass( 'oo-ui-popupWidget-head' )
.append( this.$label, this.closeButton.$element );
if ( !config.head ) {
this.$head.addClass( 'oo-ui-element-hidden' );
}
this.$popup
.addClass( 'oo-ui-popupWidget-popup' )
.append( this.$head, this.$body );
this.$element
.addClass( 'oo-ui-popupWidget' )
.append( this.$popup, this.$anchor );
// Move content, which was added to #$element by OO.ui.Widget, to the body
if ( config.$content instanceof jQuery ) {
this.$body.append( config.$content );
}
if ( config.padded ) {
this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
}
// Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
// that reference properties not initialized at that time of parent class construction
// TODO: Find a better way to handle post-constructor setup
this.visible = false;
this.$element.addClass( 'oo-ui-element-hidden' );
};
/* Setup */
OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
/* Methods */
/**
* Handles mouse down events.
*
* @private
* @param {MouseEvent} e Mouse down event
*/
OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
if (
this.isVisible() &&
!$.contains( this.$element[ 0 ], e.target ) &&
( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
) {
this.toggle( false );
}
};
/**
* Bind mouse down listener.
*
* @private
*/
OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
// Capture clicks outside popup
this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
};
/**
* Handles close button click events.
*
* @private
*/
OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
if ( this.isVisible() ) {
this.toggle( false );
}
};
/**
* Unbind mouse down listener.
*
* @private
*/
OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
};
/**
* Handles key down events.
*
* @private
* @param {KeyboardEvent} e Key down event
*/
OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
if (
e.which === OO.ui.Keys.ESCAPE &&
this.isVisible()
) {
this.toggle( false );
e.preventDefault();
e.stopPropagation();
}
};
/**
* Bind key down listener.
*
* @private
*/
OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
};
/**
* Unbind key down listener.
*
* @private
*/
OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
};
/**
* Show, hide, or toggle the visibility of the anchor.
*
* @param {boolean} [show] Show anchor, omit to toggle
*/
OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
show = show === undefined ? !this.anchored : !!show;
if ( this.anchored !== show ) {
if ( show ) {
this.$element.addClass( 'oo-ui-popupWidget-anchored' );
} else {
this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
}
this.anchored = show;
}
};
/**
* Check if the anchor is visible.
*
* @return {boolean} Anchor is visible
*/
OO.ui.PopupWidget.prototype.hasAnchor = function () {
return this.anchor;
};
/**
* @inheritdoc
*/
OO.ui.PopupWidget.prototype.toggle = function ( show ) {
show = show === undefined ? !this.isVisible() : !!show;
var change = show !== this.isVisible();
// Parent method
OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
if ( change ) {
if ( show ) {
if ( this.autoClose ) {
this.bindMouseDownListener();
this.bindKeyDownListener();
}
this.updateDimensions();
this.toggleClipping( true );
} else {
this.toggleClipping( false );
if ( this.autoClose ) {
this.unbindMouseDownListener();
this.unbindKeyDownListener();
}
}
}
return this;
};
/**
* Set the size of the popup.
*
* Changing the size may also change the popup's position depending on the alignment.
*
* @param {number} width Width in pixels
* @param {number} height Height in pixels
* @param {boolean} [transition=false] Use a smooth transition
* @chainable
*/
OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
this.width = width;
this.height = height !== undefined ? height : null;
if ( this.isVisible() ) {
this.updateDimensions( transition );
}
};
/**
* Update the size and position.
*
* Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
* be called automatically.
*
* @param {boolean} [transition=false] Use a smooth transition
* @chainable
*/
OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
align = this.align,
widget = this;
if ( !this.$container ) {
// Lazy-initialize $container if not specified in constructor
this.$container = $( this.getClosestScrollableElementContainer() );
}
// Set height and width before measuring things, since it might cause our measurements
// to change (e.g. due to scrollbars appearing or disappearing)
this.$popup.css( {
width: this.width,
height: this.height !== null ? this.height : 'auto'
} );
// If we are in RTL, we need to flip the alignment, unless it is center
if ( align === 'forwards' || align === 'backwards' ) {
if ( this.$container.css( 'direction' ) === 'rtl' ) {
align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
} else {
align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
}
}
// Compute initial popupOffset based on alignment
popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
// Figure out if this will cause the popup to go beyond the edge of the container
originOffset = this.$element.offset().left;
containerLeft = this.$container.offset().left;
containerWidth = this.$container.innerWidth();
containerRight = containerLeft + containerWidth;
popupLeft = popupOffset - this.containerPadding;
popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
overlapLeft = ( originOffset + popupLeft ) - containerLeft;
overlapRight = containerRight - ( originOffset + popupRight );
// Adjust offset to make the popup not go beyond the edge, if needed
if ( overlapRight < 0 ) {
popupOffset += overlapRight;
} else if ( overlapLeft < 0 ) {
popupOffset -= overlapLeft;
}
// Adjust offset to avoid anchor being rendered too close to the edge
// $anchor.width() doesn't work with the pure CSS anchor (returns 0)
// TODO: Find a measurement that works for CSS anchors and image anchors
anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
if ( popupOffset + this.width < anchorWidth ) {
popupOffset = anchorWidth - this.width;
} else if ( -popupOffset < anchorWidth ) {
popupOffset = -anchorWidth;
}
// Prevent transition from being interrupted
clearTimeout( this.transitionTimeout );
if ( transition ) {
// Enable transition
this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
}
// Position body relative to anchor
this.$popup.css( 'margin-left', popupOffset );
if ( transition ) {
// Prevent transitioning after transition is complete
this.transitionTimeout = setTimeout( function () {
widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
}, 200 );
} else {
// Prevent transitioning immediately
this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
}
// Reevaluate clipping state since we've relocated and resized the popup
this.clip();
return this;
};
/**
* Set popup alignment
* @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
* `backwards` or `forwards`.
*/
OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
// Validate alignment and transform deprecated values
if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
} else {
this.align = 'center';
}
};
/**
* Get popup alignment
* @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
* `backwards` or `forwards`.
*/
OO.ui.PopupWidget.prototype.getAlignment = function () {
return this.align;
};