mediawiki-extensions-Visual.../modules/ve/ui/widgets/ve.ui.PopupWidget.js
Ed Sanders 6b5ed9c711 Fix callout position when popup widget has been moved
We calculate an overlap offset for when the popup widget has been moved
to avoid going out of the viewport, but that offset is only applied to the
body and not the callout.

Change-Id: Ib9d08d60ccfa6562378640c526faf1e8363abbba
2013-06-25 11:07:44 -07:00

184 lines
4.1 KiB
JavaScript

/*!
* VisualEditor UserInterface PopupWidget class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Creates an ve.ui.PopupWidget object.
*
* @class
* @extends ve.ui.Widget
*
* @constructor
* @param {Object} [config] Config options
* @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
*/
ve.ui.PopupWidget = function VeUiPopupWidget( config ) {
// Config intialization
config = config || {};
// Parent constructor
ve.ui.Widget.call( this, config );
// Properties
this.visible = false;
this.$callout = this.$$( '<div>' );
this.$body = this.$$( '<div>' );
this.transitionTimeout = null;
this.align = config.align || 'center';
this.autoClose = !!config.autoClose;
// Events
this.$.add( this.$body ).add( this.$callout )
.on( 'mousedown', function ( e ) {
// Cancel only local mousedown events
return e.target !== this;
} );
// Initialization
if ( this.autoClose ) {
// Tab index on body so that it may blur
this.$body.attr( 'tabindex', 1 );
// Listen for blur events
this.$body.on( 'blur', ve.bind( this.onPopupBlur, this ) );
}
this.$
.addClass( 've-ui-popupWidget' )
.append(
this.$body.addClass( 've-ui-popupWidget-body' ),
this.$callout.addClass( 've-ui-popupWidget-callout' )
);
};
/* Inheritance */
ve.inheritClass( ve.ui.PopupWidget, ve.ui.Widget );
/* Events */
/**
* @event hide
*/
/* Methods */
/**
* Handle blur events.
*
* @param {jQuery.Event} e Blur event
*/
ve.ui.PopupWidget.prototype.onPopupBlur = function () {
var $body = this.$body;
// Find out what is focused after blur
setTimeout( ve.bind( function () {
var $focused = $body.find( ':focus' );
// Is there a focused child element?
if ( $focused.length > 0 ) {
// Bind a one off blur event to that focused child element
$focused.one( 'blur', ve.bind( function () {
setTimeout( ve.bind( function () {
if ( $body.find( ':focus' ).length === 0 ) {
// Be sure focus is not the popup itself.
if ( $body.is( ':focus' ) ) {
return;
}
// Not a child and not the popup itself, so hide.
this.hide();
}
}, this ), 0 );
}, this ) );
} else {
this.hide();
}
}, this ), 0 );
};
/**
* Show the context.
*
* @method
* @chainable
*/
ve.ui.PopupWidget.prototype.show = function () {
this.$.show();
this.visible = true;
if ( this.autoClose ) {
// Focus body so that it may blur
this.$body.focus();
}
return this;
};
/**
* Hide the context.
*
* @method
* @chainable
*/
ve.ui.PopupWidget.prototype.hide = function () {
this.$.hide();
this.visible = false;
this.emit( 'hide' );
return this;
};
/**
* Updates the position and size.
*
* @method
* @chainable
*/
ve.ui.PopupWidget.prototype.display = function ( x, y, width, height, transition ) {
var left, overlapLeft, overlapRight,
overlapOffset = 0, padding = 7;
switch ( this.align ) {
case 'left':
// Inset callout from left
left = -padding;
break;
case 'right':
// Inset callout from right
left = -width + padding;
break;
default:
// Place callout in center
left = -width / 2;
break;
}
// Prevent viewport clipping, using padding between body and popup edges
overlapRight = this.$$( 'body' ).width() - ( x + ( width + left + ( padding * 2 ) ) );
overlapLeft = x + ( left - ( padding * 2 ) );
if ( overlapRight < 0 ) {
overlapOffset = overlapRight;
} else if ( overlapLeft < 0 ) {
overlapOffset -= overlapLeft;
}
// Prevent transition from being interrupted
clearTimeout( this.transitionTimeout );
if ( transition ) {
// Enable transition
this.$.addClass( 've-ui-popupWidget-transitioning' );
// Prevent transitioning after transition is complete
this.transitionTimeout = setTimeout( ve.bind( function () {
this.$.removeClass( 've-ui-popupWidget-transitioning' );
}, this ), 200 );
} else {
// Prevent transitioning immediately
this.$.removeClass( 've-ui-popupWidget-transitioning' );
}
// Position body relative to anchor and adjust size
this.$body.css( {
'left': left + overlapOffset, 'width': width, 'height': height === undefined ? 'auto' : height
} );
return this;
};