mediawiki-extensions-Visual.../modules/ve/ui/ve.ui.SurfaceToolbar.js
Timo Tijhof 7db65f386c Rename @emits to @fires so we're forward compatible with JSDuck 5
Instead of using @emits in both, use our custom @fires in
production (JSDuck 4), and in the future it'll just naturally
use the native one.

This way we can also index oojs without issues, which seems to
have started using @fires already.

Change-Id: I7c3b56dd112626d57fa87ab995d205fb782a0149
2013-10-22 19:11:16 +00:00

295 lines
8.3 KiB
JavaScript

/*!
* VisualEditor UserInterface SurfaceToolbar class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* UserInterface surface toolbar.
*
* @class
* @extends ve.ui.Toolbar
*
* @constructor
* @param {ve.ui.Surface} surface Surface to control
* @param {Object} [options] Configuration options
*/
ve.ui.SurfaceToolbar = function VeUiSurfaceToolbar( surface, options ) {
var toolbar = this;
// Configuration initialization
options = options || {};
// Parent constructor
ve.ui.Toolbar.call( this, ve.ui.toolFactory, options );
// Properties
this.surface = surface;
this.floating = false;
this.floatable = false;
this.$window = null;
this.$surfaceView = null;
this.elementOffset = null;
this.windowEvents = {
// jQuery puts a guid on our prototype function when we use ve.bind,
// we don't want that because that means calling $window.off( toolbarB.windowEvents )
// will effectively also unbind toolbarA.windowEvents as they would share a guid.
// Though jQuery does not share the reference (both A and B have the correct context
// bound), it does unbind them. Use a regular closure instead.
'resize': function () {
return toolbar.onWindowResize.apply( toolbar, arguments );
},
'scroll': function () {
return toolbar.onWindowScroll.apply( toolbar, arguments );
}
};
this.surfaceViewEvents = {
'keyup': function () {
return toolbar.onSurfaceViewKeyUp.apply( toolbar, arguments );
}
};
// Events
this.surface.getModel().connect( this, { 'contextChange': 'onContextChange' } );
};
/* Inheritance */
OO.inheritClass( ve.ui.SurfaceToolbar, ve.ui.Toolbar );
/* 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
*/
/**
* Whenever the toolbar $bar position is updated, the changes that took place.
*
* @event position
* @param {jQuery} $bar Toolbar bar
* @param {Object} update
* @param {boolean} [update.floating] Whether the toolbar is in floating mode
* @param {Object} [update.css] One or more css properties that changed
* @param {Object} [update.offset] Updated offset object (from jQuery.fn.offset, though
* it also includes `offset.right`)
*/
/* Methods */
/**
* Handle window resize events while toolbar floating is enabled.
*
* @param {jQuery.Event} e Window resize event
*/
ve.ui.SurfaceToolbar.prototype.onWindowScroll = function () {
var scrollTop = this.$window.scrollTop();
if ( scrollTop > this.elementOffset.top ) {
this.float();
} else if ( this.floating ) {
this.unfloat();
}
};
/**
* 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.
*
* @fires position
* @param {jQuery.Event} e Window scroll event
*/
ve.ui.SurfaceToolbar.prototype.onWindowResize = function () {
var update = {},
offset = this.elementOffset;
// Update right offset after resize (see #float)
offset.right = this.$window.width() - this.$.outerWidth() - offset.left;
update.offset = offset;
if ( this.floating ) {
update.css = { 'right': offset.right };
this.$bar.css( update.css );
}
// If we're not floating, toolbar position didn't change.
// But the dimensions did naturally change on resize, as did the right offset.
// Which e.g. mw.ViewPageTarget's toolbarTracker needs.
this.emit( 'position', this.$bar, update );
};
/**
* Method to scroll to the cursor position while toolbar is floating on keyup only if
* the cursor is obscured by the toolbar.
*/
ve.ui.SurfaceToolbar.prototype.onSurfaceViewKeyUp = function () {
var barHeight, scrollTo, obscured, cursorPos = this.surface.view.getSelectionRect();
if ( !cursorPos ) {
return;
}
barHeight = this.$bar.height();
scrollTo = this.$bar.offset().top - barHeight + ( cursorPos.end.y - cursorPos.start.y );
obscured = cursorPos.start.y - this.$window.scrollTop() < barHeight;
// 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 which the toolbar controls.
*
* @returns {ve.ui.Surface} Surface being controlled
*/
ve.ui.SurfaceToolbar.prototype.getSurface = function () {
return this.surface;
};
/**
* Handle context changes on the surface.
*
* @fires updateState
*/
ve.ui.SurfaceToolbar.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 ) );
};
/**
* Sets up handles and preloads required information for the toolbar to work.
* This must be called immediately after it is attached to a visible document.
*/
ve.ui.SurfaceToolbar.prototype.initialize = function () {
// Parent method
ve.ui.Toolbar.prototype.initialize.call( this );
// Properties
this.$window = $( this.getElementWindow() );
this.$surfaceView = this.surface.getView().$;
this.elementOffset = this.$.offset();
this.elementOffset.right = this.$window.width() - this.$.outerWidth() - this.elementOffset.left;
// Initial position. Could be invalidated by the first
// call to onWindowScroll, but users of this event (e.g toolbarTracking)
// need to also now the non-floating position.
this.emit( 'position', this.$bar, {
'floating': false,
'offset': this.elementOffset
} );
if ( this.floatable ) {
this.$window.on( this.windowEvents );
this.$surfaceView.on( this.surfaceViewEvents );
// The page may start with a non-zero scroll position
this.onWindowScroll();
}
};
/**
* Destroys toolbar, removing event handlers and DOM elements.
*
* Call this whenever you are done using a toolbar.
*/
ve.ui.SurfaceToolbar.prototype.destroy = function () {
this.disableFloatable();
this.surface.getModel().disconnect( this, { 'contextChange': 'onContextChange' } );
// Parent method
ve.ui.Toolbar.prototype.destroy.call( this );
};
/**
* Float the toolbar.
*
* @fires position
*/
ve.ui.SurfaceToolbar.prototype.float = function () {
var update;
if ( !this.floating ) {
// When switching into floating mode, set the height of the wrapper and
// move the bar to the same offset as the in-flow element
update = {
'css': { 'left': this.elementOffset.left, 'right': this.elementOffset.right },
'floating': true
};
this.$
.css( 'height', this.$.height() )
.addClass( 've-ui-surfaceToolbar-floating' );
this.$bar.css( update.css );
this.floating = true;
this.emit( 'position', this.$bar, update );
}
};
/**
* Reset the toolbar to it's default non-floating position.
*
* @fires position
*/
ve.ui.SurfaceToolbar.prototype.unfloat = function () {
if ( this.floating ) {
this.$
.css( 'height', '' )
.removeClass( 've-ui-surfaceToolbar-floating' );
this.$bar.css( { 'left': '', 'right': '' } );
this.floating = false;
this.emit( 'position', this.$bar, { 'floating': false } );
}
};
/**
* Set automatic floating behavior to the toolbar.
*
* Toolbar floating is not enabled by default, call this on setup to enable it.
* This will not make it float, but it will start listening for events that
* will result in it potentially being floated and defloated accordingly.
*/
ve.ui.SurfaceToolbar.prototype.enableFloatable = function () {
this.floatable = true;
if ( this.initialized ) {
this.$window.on( this.windowEvents );
this.$surfaceView.on( this.surfaceViewEvents );
}
};
/**
* Remove automatic floating behavior to the toolbar.
*/
ve.ui.SurfaceToolbar.prototype.disableFloatable = function () {
if ( this.$window ) {
this.$window.off( this.windowEvents );
}
if ( this.$surfaceView ) {
this.$surfaceView.off( this.surfaceViewEvents );
}
if ( this.floating ) {
this.unfloat();
}
this.floatable = false;
};