mediawiki-extensions-Visual.../modules/ve/ui/ve.ui.Context.js
Trevor Parscal 05e39c1733 Always apply inspected annotations to the right range
When you leave the inspector by changing the selection, we need to apply changes to the old selection.

ve.ui.Inspector
* Added initialSelection
* Change getMatchingAnnotations to use a given fragment rather than generating it's own
* Set initialSelection on open

ve.ui.Context
* Make hiding the context accept changes

ve.ui.LinkInspector
* Passing a fragment into getMatchingAnnotations now
* Using fragment API instead of actions API to control the range of the fragment

Change-Id: If6c8845285d87d0f144b15d50c38e192c797be59
2012-11-19 17:54:55 -08:00

315 lines
8.3 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();
};
/**
* 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,
hide = true,
fragment = this.surface.getModel().getFragment(),
selection = fragment.getRange(),
annotations = fragment.getAnnotations();
if ( this.inspector && selection.equals( this.selection ) ) {
// Something other than the selection has changed
this.show();
hide = false;
} else {
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
);
this.show();
hide = false;
}
}
if ( hide ) {
this.hide();
this.menu = null;
}
// 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( true );
this.$overlay.hide();
}
if ( this.menu ) {
this.obscure( this.$menu );
}
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.$.hide();
this.frame.$.append( inspector.$ );
this.obscure( this.$inspectors );
}
return true;
}
return false;
};
ve.ui.Context.prototype.openInspector = function ( name ) {
if ( !this.initInspector( name ) ) {
throw new Error( 'Missing inspector. Can not open nonexistent inspector: ' + name );
}
var inspector = this.inspectors[name];
// Prepare the inspector to be opened
inspector.prepareSelection();
// HACK: prepareSelection probably caused an annotationChange event which closed the context
// before we could even open it - by executing the rest of this function later we can let the
// stack clear and then finally open the context and inspector once the dust has settled.
setTimeout( ve.bind( function () {
// 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' );
}
// Open the inspector by name.
inspector.open();
// Resize frame to the size of the inspector.
this.frame.setSize( inspector.$.outerWidth(), inspector.$.outerHeight() );
// Save name of inspector open.
this.inspector = name;
// Cache selection, in the case of manually opened inspector.
this.selection = this.surface.getModel().getSelection();
// Set inspector
this.show();
}, this ), 0 );
};
ve.ui.Context.prototype.closeInspector = function ( accept ) {
if ( this.inspector ) {
this.obscure( this.$inspectors );
this.inspectors[this.inspector].close( accept );
this.inspector = null;
this.hide();
}
this.update();
};
ve.ui.Context.prototype.reveal = function ( $element ) {
$element.css( 'top', 0 );
};
ve.ui.Context.prototype.obscure = function ( $element ) {
$element.css( 'top', -5000 );
};