mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-29 08:34:54 +00:00
bf98af7bcb
The logic in ve.ui.LinkInspector.onUpdate was very flawed. This patch makes it so: * When something happens, if there's an inspector open then so long as the selection hasn't changed the inspector is updated (such as the window being resized) * If the selection does change, the inspector is closed * If there's no inspector open, we try to show a menu of available inspectors Change-Id: I859123a5fcd36bc2afb2e578f81f30a944c8583a
334 lines
8.7 KiB
JavaScript
334 lines
8.7 KiB
JavaScript
/**
|
|
* VisualEditor user interface Context class.
|
|
*
|
|
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* Creates an ve.ui.Context object.
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @param surface
|
|
* @param {jQuery} $overlay DOM selection to add nodes to
|
|
*/
|
|
ve.ui.Context = function VeUiContext( surface, $overlay ) {
|
|
// Properties
|
|
this.surface = surface;
|
|
this.inspectors = {};
|
|
this.inspector = null;
|
|
this.position = null;
|
|
this.visible = false;
|
|
this.selecting = false;
|
|
this.selection = null;
|
|
this.frame = null;
|
|
this.menu = null;
|
|
this.toolbar = null;
|
|
this.$ = $( '<div class="ve-ui-context"></div>' );
|
|
this.$callout = $( '<div class="ve-ui-context-callout"></div>' );
|
|
this.$inner = $( '<div class="ve-ui-context-inner"></div>' );
|
|
this.$overlay = $( '<div class="ve-ui-context-frame-overlay"></div>' );
|
|
this.$menu = $( '<div class="ve-ui-context-menu"></div>' );
|
|
this.$inspectors = $( '<div class="ve-ui-context-inspectors"></div>' );
|
|
|
|
// Initialization
|
|
this.$.append( this.$callout, this.$inner, this.$overlay );
|
|
this.$inner.append( this.$menu, this.$inspectors );
|
|
( $overlay || $( 'body' ) ).append( this.$ );
|
|
this.frame = new ve.ui.Frame( this.constructor.static.frameOptions, this.$inspectors );
|
|
|
|
// Events
|
|
this.surface.getModel().addListenerMethods( this, { 'change': 'onChange' } );
|
|
this.surface.getView()
|
|
.addListenerMethods( this, {
|
|
'selectionStart': 'onSelectionStart',
|
|
'selectionEnd': 'onSelectionEnd'
|
|
} );
|
|
$(window).on( 'resize', ve.bind( this.update, this ) );
|
|
};
|
|
|
|
/* Static Members */
|
|
|
|
ve.ui.Context.static = {};
|
|
|
|
ve.ui.Context.static.frameOptions = {
|
|
'stylesheets': [
|
|
ve.init.platform.getModulesUrl() + '/ve/ui/styles/ve.ui.Inspector.css',
|
|
ve.init.platform.getModulesUrl() + (
|
|
window.devicePixelRatio > 1 ?
|
|
'/ve/ui/styles/ve.ui.Inspector.Icons-vector.css' :
|
|
'/ve/ui/styles/ve.ui.Inspector.Icons-raster.css'
|
|
)
|
|
]
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Responds to change events on the model.
|
|
*
|
|
* Changes are ignored while the user is selecting text.
|
|
*
|
|
* @method
|
|
* @param {ve.dm.Transaction} tx Change transaction
|
|
* @param {ve.Range} selection Change selection
|
|
*/
|
|
ve.ui.Context.prototype.onChange = function ( tx, selection ) {
|
|
if ( selection && !this.selecting ) {
|
|
this.update();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Responds to selection start events on the view.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ui.Context.prototype.onSelectionStart = function () {
|
|
this.selecting = true;
|
|
this.hide();
|
|
};
|
|
|
|
/**
|
|
* Responds to selection end events on the view.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ui.Context.prototype.onSelectionEnd = function () {
|
|
this.selecting = false;
|
|
this.update();
|
|
};
|
|
|
|
/**
|
|
* Responds to an inspector being opened.
|
|
*
|
|
* @method
|
|
* @param {String} name Name of inspector being opened
|
|
*/
|
|
ve.ui.Context.prototype.onInspectorOpen = function ( name ) {
|
|
var inspector = this.inspectors[name];
|
|
// Close menu
|
|
if ( this.menu ) {
|
|
this.obscure( this.$menu );
|
|
}
|
|
// Fade in context if menu is closed - at this point, menu could be null or not open
|
|
if ( this.menu === null || !this.menu.isOpen() ) {
|
|
this.$.fadeIn( 'fast' );
|
|
}
|
|
// Remember which inspector is open
|
|
this.inspector = name;
|
|
// Resize frame to the size of the inspector.
|
|
this.frame.setSize( inspector.$.outerWidth(), inspector.$.outerHeight() );
|
|
// Cache selection, in the case of manually opened inspector.
|
|
this.selection = this.surface.getModel().getSelection();
|
|
// Show context
|
|
this.show();
|
|
};
|
|
|
|
/**
|
|
* Responds to an inspector being closed.
|
|
*
|
|
* @method
|
|
* @param {String} name Name of inspector being closed
|
|
* @param {Boolean} remove Annotation should be removed
|
|
*/
|
|
ve.ui.Context.prototype.onInspectorClose = function () {
|
|
this.obscure( this.$inspectors );
|
|
this.inspector = null;
|
|
this.hide();
|
|
this.update();
|
|
};
|
|
|
|
/**
|
|
* Gets the surface this context is being used in.
|
|
*
|
|
* @method
|
|
* @returns {ve.Surface} Surface of context
|
|
*/
|
|
ve.ui.Context.prototype.getSurface = function () {
|
|
return this.surface;
|
|
};
|
|
|
|
/**
|
|
* Gets the frame that inspectors are being rendered in.
|
|
*
|
|
* @method
|
|
* @returns {ve.ui.Frame} Inspector frame
|
|
*/
|
|
ve.ui.Context.prototype.getFrame = function () {
|
|
return this.frame;
|
|
};
|
|
|
|
/**
|
|
* Updates the context menu.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ui.Context.prototype.update = function () {
|
|
var inspectors,
|
|
fragment = this.surface.getModel().getFragment(),
|
|
selection = fragment.getRange(),
|
|
annotations = fragment.getAnnotations();
|
|
|
|
// Update the inspector if the change didn't affect the selection
|
|
if ( this.inspector && selection.equals( this.selection ) ) {
|
|
this.show();
|
|
} else {
|
|
this.hide();
|
|
}
|
|
|
|
if ( !this.inspector ) {
|
|
inspectors = ve.ui.inspectorFactory.getInspectorsForAnnotations( annotations );
|
|
if ( inspectors.length ) {
|
|
// The selection is inspectable but not being inspected
|
|
this.$menu.empty();
|
|
// Create inspector toolbar
|
|
this.toolbar = new ve.ui.Toolbar(
|
|
$( '<div class="ve-ui-context-toolbar"></div>' ),
|
|
this.surface,
|
|
[{ 'name': 'inspectors', 'items' : inspectors }]
|
|
);
|
|
// Note: Menu attaches the provided $tool element to the container.
|
|
this.menu = new ve.ui.Menu(
|
|
[ { 'name': 'tools', '$': this.toolbar.$ } ], // Tools
|
|
null, // Callback
|
|
this.$menu, // Container
|
|
this.$inner // Parent
|
|
);
|
|
if ( !this.visible ) {
|
|
this.show();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remember selection for next time
|
|
this.selection = selection.clone();
|
|
};
|
|
|
|
ve.ui.Context.prototype.show = function () {
|
|
var selectionRect = this.surface.getView().getSelectionRect();
|
|
|
|
this.position = new ve.Position( selectionRect.end.x, selectionRect.end.y );
|
|
this.$.css( this.position );
|
|
|
|
// Show context
|
|
this.$.css( 'visibility', 'visible' );
|
|
|
|
if ( this.inspector ) {
|
|
// Reveal inspector
|
|
this.reveal( this.$inspectors );
|
|
this.$overlay.show();
|
|
} else {
|
|
if ( !this.visible ) {
|
|
// Fade in the context.
|
|
this.$.fadeIn( 'fast' );
|
|
this.visible = true;
|
|
}
|
|
// Reveal menu
|
|
this.reveal( this.$menu );
|
|
}
|
|
// Position inner context.
|
|
this.positionInner();
|
|
};
|
|
|
|
ve.ui.Context.prototype.hide = function () {
|
|
if ( this.inspector ) {
|
|
this.closeInspector();
|
|
this.$overlay.hide();
|
|
}
|
|
if ( this.menu ) {
|
|
this.obscure( this.$menu );
|
|
this.menu = null;
|
|
}
|
|
this.$.css( 'visibility', 'hidden' );
|
|
this.visible = false;
|
|
};
|
|
|
|
/**
|
|
* Positions the context
|
|
*
|
|
* @param {jQuery} $overlay
|
|
* @param {jQuery} $element
|
|
*/
|
|
ve.ui.Context.prototype.positionInner = function () {
|
|
var $container = this.inspector ? this.$inspectors : this.$menu,
|
|
width = $container.outerWidth( true ),
|
|
height = $container.outerHeight( true ),
|
|
left = -( width / 2 );
|
|
|
|
// Clamp on left boundary
|
|
if ( this.position.left < width / 2 ) {
|
|
left = -( this.$.children().outerWidth( true ) / 2 ) - ( this.position.left / 2 );
|
|
// Clamp on right boundary
|
|
} else if ( $( 'body' ).width() - this.position.left < width ) {
|
|
left = -( width - ( ( $( 'body' ).width() - this.position.left ) / 2) );
|
|
}
|
|
// Apply dimensions to inner
|
|
this.$inner.css( { 'left': left, 'height': height, 'width': width } );
|
|
};
|
|
|
|
/**
|
|
* Positions an overlay element below another element.
|
|
*
|
|
* TODO: Does this really need to be here? Why are we halving the width of $inner?
|
|
*
|
|
* @param {jQuery} $overlay
|
|
* @param {jQuery} $element
|
|
*/
|
|
ve.ui.Context.prototype.positionOverlayBelow = function ( $overlay, $element ) {
|
|
// Set iframe overlay below element.
|
|
$overlay.css( {
|
|
'left': $element.offset().left - ( this.$inner.width() / 2 ),
|
|
'top': $element.offset().top + $element.outerHeight( true ),
|
|
// RTL position fix.
|
|
'width': $overlay.children().outerWidth( true )
|
|
} );
|
|
};
|
|
|
|
ve.ui.Context.prototype.initInspector = function ( name ) {
|
|
var inspector;
|
|
// Add inspector on demand.
|
|
if ( ve.ui.inspectorFactory.lookup( name ) ) {
|
|
if ( !( name in this.inspectors ) ) {
|
|
inspector = this.inspectors[name] = ve.ui.inspectorFactory.create( name, this );
|
|
inspector.on( 'open', ve.bind( this.onInspectorOpen, this, name ) );
|
|
inspector.on( 'close', ve.bind( this.onInspectorClose, this ) );
|
|
inspector.$.hide();
|
|
this.frame.$.append( inspector.$ );
|
|
this.obscure( this.$inspectors );
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
ve.ui.Context.prototype.openInspector = function ( name ) {
|
|
// Auto-initialize the inspector
|
|
if ( !this.initInspector( name ) ) {
|
|
throw new Error( 'Missing inspector. Can not open nonexistent inspector: ' + name );
|
|
}
|
|
// Only allow one inspector open at a time
|
|
if ( this.inspector ) {
|
|
this.closeInspector();
|
|
}
|
|
// Open the inspector
|
|
this.inspectors[name].open();
|
|
};
|
|
|
|
ve.ui.Context.prototype.closeInspector = function ( remove ) {
|
|
// Quietly ignore if nothing is open
|
|
if ( this.inspector ) {
|
|
// Close the current inspector
|
|
this.inspectors[this.inspector].close( remove );
|
|
}
|
|
};
|
|
|
|
ve.ui.Context.prototype.reveal = function ( $element ) {
|
|
$element.css( 'top', 0 );
|
|
};
|
|
|
|
ve.ui.Context.prototype.obscure = function ( $element ) {
|
|
$element.css( 'top', -5000 );
|
|
};
|