mediawiki-extensions-Visual.../modules/ve/ui/ve.ui.Context.js
Trevor Parscal 8fc98868c9 Fixed inspector behavior
ve.ui.Inspector
* Removed disabled state and interfaces - this isn't needed
* Renamed prepareSelection to onInitialize
* Using event emitter to run onInitialize, onOpen and onClose methods
* Left removal up to the child class to handle in the onClose method
* Replaced calls on context to close inspector to calling close directly
* Renamed prepareSelection stub to onInitialize
* Emitting initialize event from within the open method
* Added recursion guarding to close method
* Changed the close method's argument to be remove instead of accept - the more common case is to save changes, and the only time you wouldn't save changes is if you were to remove the annotation
* Moved focus restore to close method

ve.ui.Context
* Moved the majority of the code in openInspector and closeInspector to event handlers for onInspectorOpen and onInspectorClose
* Updated calls to closeInspector re: accept->remove argument change

ve.ui.LinkInspector
* Renamed prepareSelection to onInitialize and rewrote logic and documentation
* Removed unused onLocationInputChange method
* Moved restore focus (now it's in the inspector base class)

ve.dm.SurfaceFragment
* Added word mode for expandRange

ve.dm.Surface
* Added locking/unlocking while processing transactions - this was not an issue before because this was effectively being done manually throughout ce (which needs to be cleaned up) but once we started using the content action to insert content dm and ce started playing off each other and inserting in a loop - we already do this for undo/redo so it makes sense to do it here as well

ve.InspectorAction
* Updated arguments re: close method's accept->remove argument change

Change-Id: I38995d4101fda71bfb2e6fe516603507ce820937
2012-11-21 12:01:14 -08:00

333 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,
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();
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.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 );
};