mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-29 00:30:44 +00:00
87fd3b7756
Add rtl/ltr-related classes to the toolbar based on cursor context, so certain icons can change based on inline and block directions. Change-Id: I1b6e450226bf8da820fb622f28c1c3062c534bb0
360 lines
11 KiB
JavaScript
360 lines
11 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 surface toolbar.
|
|
*
|
|
* @class
|
|
* @extends OO.ui.Toolbar
|
|
*
|
|
* @constructor
|
|
* @param {ve.ui.Surface} surface Surface to control
|
|
* @param {Object} [options] Configuration options
|
|
*/
|
|
ve.ui.Toolbar = function VeUiToolbar( surface, options ) {
|
|
var toolbar = this;
|
|
|
|
// Configuration initialization
|
|
options = options || {};
|
|
|
|
// Parent constructor
|
|
OO.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 );
|
|
}
|
|
};
|
|
// default directions:
|
|
this.contextDirection = { 'inline': 'ltr', 'block': 'ltr' };
|
|
this.$element
|
|
.addClass( 've-ui-dir-inline-' + this.contextDirection.inline )
|
|
.addClass( 've-ui-dir-block-' + this.contextDirection.block );
|
|
// Events
|
|
this.surface.getModel().connect( this, { 'contextChange': 'onContextChange' } );
|
|
this.surface.connect( this, { 'addCommand': 'onSurfaceAddCommand' } );
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
OO.inheritClass( ve.ui.Toolbar, OO.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.Toolbar.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.Toolbar.prototype.onWindowResize = function () {
|
|
var update = {},
|
|
offset = this.elementOffset;
|
|
|
|
// Update right offset after resize (see #float)
|
|
offset.right = this.$window.width() - this.$element.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.
|
|
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.Toolbar.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 ) {
|
|
this.$( 'html, body' ).animate( { scrollTop: scrollTo }, 0 );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle context changes on the surface.
|
|
*
|
|
* @fires updateState
|
|
*/
|
|
ve.ui.Toolbar.prototype.onContextChange = function () {
|
|
var i, len, leafNodes, dirInline, dirBlock, fragmentAnnotation,
|
|
currentNodes = {
|
|
fragNodes: null,
|
|
fragAnnotations: null,
|
|
'dm': {},
|
|
'ce': {}
|
|
},
|
|
fragment = this.surface.getModel().getFragment( null, false ),
|
|
doc = this.surface.getView().getDocument(),
|
|
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 );
|
|
}
|
|
}
|
|
// Update context direction for button icons UI:
|
|
|
|
// block direction (direction of the current node)
|
|
currentNodes.fragNodes = fragment.getCoveredNodes();
|
|
if ( currentNodes.fragNodes.length > 1 ) {
|
|
// selection of multiple nodes
|
|
currentNodes.dm.block = fragment.getSiblingNodes()[0].node.parent;
|
|
} else {
|
|
// selection of a single node
|
|
currentNodes.dm.block = currentNodes.fragNodes[0].node;
|
|
}
|
|
// get the direction of the block:
|
|
currentNodes.ce.block = doc.getNodeFromOffset( currentNodes.dm.block.getRange().start );
|
|
dirBlock = currentNodes.ce.block.$element.css( 'direction' );
|
|
// by default, inline and block are the same, unless there's an inline-specific direction
|
|
dirInline = dirBlock;
|
|
// 'inline' direction is set by language annotation:
|
|
fragmentAnnotation = fragment.getAnnotations();
|
|
if ( fragmentAnnotation.hasAnnotationWithName( 'meta/language' ) ) {
|
|
dirInline = fragmentAnnotation.getAnnotationsByName( 'meta/language' ).get( 0 ).getAttribute( 'dir' );
|
|
}
|
|
if ( dirInline !== this.contextDirection.inline ) {
|
|
// remove previous class:
|
|
this.$element.removeClass( 've-ui-dir-inline-rtl ve-ui-dir-inline-ltr' );
|
|
this.$element.addClass( 've-ui-dir-inline-' + dirInline );
|
|
this.contextDirection.inline = dirInline;
|
|
}
|
|
if ( dirBlock !== this.contextDirection.block ) {
|
|
this.$element.removeClass( 've-ui-dir-block-rtl ve-ui-dir-block-ltr' );
|
|
this.$element.addClass( 've-ui-dir-block-' + dirBlock );
|
|
this.contextDirection.block = dirBlock;
|
|
}
|
|
this.emit( 'updateState', nodes, fragment.getAnnotations(), fragment.getAnnotations( true ) );
|
|
};
|
|
|
|
/**
|
|
* Handle command being added to surface.
|
|
*
|
|
* If a matching tool is present, it's label will be updated.
|
|
*
|
|
* @param {string} name Symbolic name of command and trigger
|
|
* @param {ve.ui.Command} command Command that's been registered
|
|
* @param {ve.ui.Trigger} trigger Trigger to associate with command
|
|
*/
|
|
ve.ui.Toolbar.prototype.onSurfaceAddCommand = function ( name ) {
|
|
if ( this.tools[name] ) {
|
|
this.tools[name].updateLabel();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.Toolbar.prototype.getToolAccelerator = function ( name ) {
|
|
var trigger = this.surface.getTriggers()[name];
|
|
return trigger instanceof ve.ui.Trigger ? trigger.getMessage() : undefined;
|
|
};
|
|
|
|
/**
|
|
* Gets the surface which the toolbar controls.
|
|
*
|
|
* @returns {ve.ui.Surface} Surface being controlled
|
|
*/
|
|
ve.ui.Toolbar.prototype.getSurface = function () {
|
|
return this.surface;
|
|
};
|
|
|
|
/**
|
|
* 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.Toolbar.prototype.initialize = function () {
|
|
// Parent method
|
|
OO.ui.Toolbar.prototype.initialize.call( this );
|
|
|
|
// Properties
|
|
this.$window = this.$( this.getElementWindow() );
|
|
this.$surfaceView = this.surface.getView().$element;
|
|
this.elementOffset = this.$element.offset();
|
|
this.elementOffset.right = this.$window.width() - this.$element.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.Toolbar.prototype.destroy = function () {
|
|
this.disableFloatable();
|
|
this.surface.getModel().disconnect( this, { 'contextChange': 'onContextChange' } );
|
|
|
|
// Parent method
|
|
OO.ui.Toolbar.prototype.destroy.call( this );
|
|
};
|
|
|
|
/**
|
|
* Float the toolbar.
|
|
*
|
|
* @fires position
|
|
*/
|
|
ve.ui.Toolbar.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.$element
|
|
.css( 'height', this.$element.height() )
|
|
.addClass( 've-ui-toolbar-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.Toolbar.prototype.unfloat = function () {
|
|
if ( this.floating ) {
|
|
this.$element
|
|
.css( 'height', '' )
|
|
.removeClass( 've-ui-toolbar-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.Toolbar.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.Toolbar.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;
|
|
};
|