mediawiki-extensions-Visual.../modules/ve/ui/ve.ui.Toolbar.js
Rob Moen a3e2507cfe Listen to keypress in ve.ce.surface.$ rather than window
Bug: 50538
Change-Id: Ifddf1bae1ee799ec3467a81c597702fc3f8be089
2013-07-03 22:28:17 +00:00

295 lines
7.8 KiB
JavaScript

/*!
* VisualEditor UserInterface Toolbar class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* UserInterface toolbar.
*
* @class
* @extends ve.Element
* @mixins ve.EventEmitter
*
* @constructor
* @param {ve.ui.Surface} surface
* @param {Object} [config] Config options
* @cfg {boolean} [actions] Add an actions section opposite to the tools
* @cfg {boolean} [shadow] Add a shadow below the toolbar
*/
ve.ui.Toolbar = function VeUiToolbar( surface, options ) {
// Configuration initialization
options = options || {};
// Parent constructor
ve.Element.call( this, options );
// Mixin constructors
ve.EventEmitter.call( this );
// Properties
this.surface = surface;
this.$bar = this.$$( '<div>' );
this.$tools = this.$$( '<div>' );
this.$actions = this.$$( '<div>' );
this.floating = false;
this.$window = null;
this.windowEvents = {
'resize': ve.bind( this.onWindowResize, this ),
'scroll': ve.bind( this.onWindowScroll, this )
};
this.surfaceViewEvents = {
'keypress': ve.bind( this.onSurfaceViewKeyPress, this )
};
// Events
this.$
.add( this.$bar ).add( this.$tools ).add( this.$actions )
.on( 'mousedown', false );
// Initialization
this.$tools.addClass( 've-ui-toolbar-tools' );
this.$bar.addClass( 've-ui-toolbar-bar' ).append( this.$tools );
if ( options.actions ) {
this.$actions.addClass( 've-ui-toolbar-actions' );
this.$bar.append( this.$actions );
}
this.$bar.append( '<div style="clear:both"></div>' );
if ( options.shadow ) {
this.$bar.append( '<div class="ve-ui-toolbar-shadow"></div>' );
}
this.$.addClass( 've-ui-toolbar' ).append( this.$bar );
// Events
this.surface.getModel().connect( this, { 'contextChange': 'onContextChange' } );
};
/* Inheritance */
ve.inheritClass( ve.ui.Toolbar, ve.Element );
ve.mixinClass( ve.ui.Toolbar, ve.EventEmitter );
/* Events */
/**
* @event updateState
* @see ve.dm.SurfaceFragment#getAnnotations
* @param {ve.dm.Node[]} nodes List of nodes covered by the current selection
* @param {ve.dm.AnnotationSet} full Annotations that cover all of the current selection
* @param {ve.dm.AnnotationSet} partial Annotations that cover some or all of the current selection
*/
/* Methods */
/**
* Handle window resize events while toolbar floating is enabled.
*
* @returns {jQuery.Event} e Window resize event
*/
ve.ui.Toolbar.prototype.onWindowScroll = function () {
var scrollTop = this.$window.scrollTop(),
toolbarOffset = this.$.offset();
if ( scrollTop > toolbarOffset.top ) {
this.setPosition(
0,
toolbarOffset.left,
this.$window.width() - this.$.outerWidth() - toolbarOffset.left
);
} else if ( this.floating ) {
this.resetPosition();
}
};
/**
* Handle window resize events while toolbar floating is enabled.
*
* Toolbar will stick to the top of the screen unless it would be over or under the last visible
* branch node in the root of the document being edited, at which point it will stop just above it.
*
* @see ve.ui.Surface#event-toolbarPosition
* @returns {jQuery.Event} e Window scroll event
*/
ve.ui.Toolbar.prototype.onWindowResize = function () {
var offset = this.$.offset();
if ( this.floating ) {
this.$bar.css( {
'left': offset.left,
'right': this.$window.width() - this.$.outerWidth() - offset.left
} );
this.surface.emit( 'toolbarPosition', this.$bar );
}
};
/**
* Method to scroll to the cursor position while toolbar is floating on keypress only if
* the cursor is obscured by the toolbar.
*
*/
ve.ui.Toolbar.prototype.onSurfaceViewKeyPress = function () {
var cursorPos = this.surface.view.getSelectionRect(),
scrollTo = cursorPos.end.y - this.surface.view.$.offset().top,
obscured = cursorPos.end.y - this.$window.scrollTop() < this.$.height() + this.$.offset().top;
// If toolbar is floating and cursor is obscured, scroll cursor into view
if ( obscured && this.floating ) {
$( 'html,body' ).animate( { scrollTop: scrollTo }, 0 );
}
};
/**
* Gets the surface the toolbar controls.
*
* @method
* @returns {ve.ui.Surface} Surface being controlled
*/
ve.ui.Toolbar.prototype.getSurface = function () {
return this.surface;
};
/**
* Handle context changes on the surface.
*
* @method
* @emits updateState
*/
ve.ui.Toolbar.prototype.onContextChange = function () {
var i, len, leafNodes,
fragment = this.surface.getModel().getFragment( null, false ),
nodes = [];
leafNodes = fragment.getLeafNodes();
for ( i = 0, len = leafNodes.length; i < len; i++ ) {
if ( len === 1 || !leafNodes[i].range || leafNodes[i].range.getLength() ) {
nodes.push( leafNodes[i].node );
}
}
this.emit( 'updateState', nodes, fragment.getAnnotations(), fragment.getAnnotations( true ) );
};
/**
* Initialize all tools and groups.
*
* @method
*/
ve.ui.Toolbar.prototype.addTools = function ( tools ) {
var i, j, group, $group, tool;
for ( i = 0; i < tools.length; i++ ) {
group = tools[i];
// Create group
$group = this.$$( '<div class="ve-ui-toolbar-group"></div>' )
.on( 'mousedown', false );
if ( group.label ) {
$group.append(
this.$$( '<div class="ve-ui-toolbar-label"></div>' ).html( group.label )
);
}
// Add tools
for ( j = 0; j < group.items.length; j++ ) {
tool = false;
try {
tool = ve.ui.toolFactory.create( group.items[j], this );
} catch( e ) {}
if ( tool ) {
$group.append( tool.$ );
}
}
// Append group
this.$tools.append( $group );
}
};
/**
* Destroys toolbar, removing event handlers and DOM elements.
*
* Call this whenever you are done using a toolbar.
*
* @method
*/
ve.ui.Toolbar.prototype.destroy = function () {
this.disableFloating();
this.surface.getModel().disconnect( this, { 'contextChange': 'onContextChange' } );
this.$.remove();
};
/**
* Float the toolbar.
*
* @see ve.ui.Surface#event-toolbarPosition
* @param {number} top Top position, in pixels
* @param {number} left Left position, in pixels
* @param {number} right Right position, in pixels
*/
ve.ui.Toolbar.prototype.setPosition = function ( top, left, right ) {
// When switching from default position, manually set the height of the wrapper
if ( !this.floating ) {
this.$
.css( 'height', this.$.height() )
.addClass( 've-ui-toolbar-floating' );
this.floating = true;
}
this.$bar.css( { 'top': top, 'left': left, 'right': right } );
if ( top > 0 ) {
this.$.addClass( 've-ui-toolbar-bottom' );
} else {
this.$.removeClass( 've-ui-toolbar-bottom' );
}
this.surface.emit( 'toolbarPosition', this.$bar );
};
/**
* Reset the toolbar to it's default position.
*
* @see ve.ui.Surface#event-toolbarPosition
*/
ve.ui.Toolbar.prototype.resetPosition = function () {
this.$
.css( 'height', 'auto' )
.removeClass( 've-ui-toolbar-floating ve-ui-toolbar-bottom' );
this.$bar.css( { 'top': 0, 'left': 0, 'right': 0 } );
this.floating = false;
this.surface.emit( 'toolbarPosition', this.$bar );
};
/**
* Add automatic floating behavior to the toolbar.
*
* Toolbar floating is not enabled by default, call this on setup to enable it.
*
* @method
*/
ve.ui.Toolbar.prototype.enableFloating = function () {
this.$window = $( this.getElementWindow() ).on( this.windowEvents );
this.$surfaceView = this.surface.getView().$.on( this.surfaceViewEvents );
// TODO: Place this is a DOM attach event for this.$
setTimeout( ve.bind( function () {
// The page may load with a non-zero scroll without trigger the scroll event
this.onWindowScroll();
}, this ), 0 );
};
/**
* Remove automatic floating behavior to the toolbar.
*
* @method
*/
ve.ui.Toolbar.prototype.disableFloating = function () {
if ( this.$window ) {
this.$window.off( this.windowEvents );
this.$window = null;
}
if ( this.$surfaceView ) {
this.$surfaceView.off( this.surfaceViewEvents );
this.$surfaceView = null;
}
if ( this.floating ) {
this.resetPosition();
}
};