mediawiki-extensions-Visual.../modules/ve-mw/ui/widgets/ve.ui.MWAceEditorWidget.js
Ed Sanders 470a4a551b AceEditorWidget: Refactor to ensure setValue runs synchronously
Also ensure setEditorWidget is only called once when
loadingPromise resolves.

Change-Id: I1a95bda5e9ff17d7575326f29fa01d273193c4a5
2015-11-16 11:36:08 -08:00

219 lines
5.7 KiB
JavaScript

/*!
* VisualEditor UserInterface MWAceEditorWidget class.
*
* @copyright 2011-2015 VisualEditor Team and others; see http://ve.mit-license.org
*/
/* global ace, require */
/**
* Text input widget which use an Ace editor instance when available
*
* For the most part this can be treated just like a TextInputWidget with
* a few extra considerations:
*
* - For performance it is recommended to destroy the editor when
* you are finished with it, using #teardown. If you need to use
* the widget again let the editor can be restored with #setup.
* - After setting an initial value the undo stack can be reset
* using clearUndoStack so that you can't undo past the initial
* state.
*
* @class
* @extends ve.ui.WhitespacePreservingTextInputWidget
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.ui.MWAceEditorWidget = function VeUiMWAceEditorWidget( config ) {
// Configuration
config = config || {};
this.$ace = $( '<div dir="ltr">' );
this.editor = null;
// Initialise to a rejected promise for the setValue call in the parent constructor
this.loadingPromise = $.Deferred().reject().promise();
this.styleHeight = null;
// Parent constructor
ve.ui.MWAceEditorWidget.super.call( this, config );
// Clear the fake loading promise and setup properly
this.loadingPromise = null;
this.setup();
this.$element
.append( this.$ace )
.addClass( 've-ui-mwAceEditorWidget' );
};
/* Inheritance */
OO.inheritClass( ve.ui.MWAceEditorWidget, ve.ui.WhitespacePreservingTextInputWidget );
/* Events */
/**
* The editor has resized
* @event resize
*/
/* Methods */
/**
* Setup the Ace editor instance
*/
ve.ui.MWAceEditorWidget.prototype.setup = function () {
if ( !this.loadingPromise ) {
this.loadingPromise = mw.loader.getState( 'ext.codeEditor.ace.modes' ) ?
mw.loader.using( 'ext.codeEditor.ace.modes' ).then( this.setupEditor.bind( this ) ) :
$.Deferred().reject().promise();
}
};
/**
* Destroy the Ace editor instance
*/
ve.ui.MWAceEditorWidget.prototype.teardown = function () {
var widget = this;
this.loadingPromise.done( function () {
widget.$input.removeClass( 'oo-ui-element-hidden' );
widget.editor.destroy();
widget.editor = null;
} ).always( function () {
widget.loadingPromise = null;
} );
};
/**
* Setup the Ace editor
*
* @fires resize
*/
ve.ui.MWAceEditorWidget.prototype.setupEditor = function () {
this.$input.addClass( 'oo-ui-element-hidden' );
this.editor = ace.edit( this.$ace[ 0 ] );
this.editor.setOptions( {
minLines: this.minRows || 3,
maxLines: this.autosize ? this.maxRows : this.minRows || 3
} );
this.editor.getSession().on( 'change', this.onEditorChange.bind( this ) );
this.editor.renderer.on( 'resize', this.onEditorResize.bind( this ) );
this.setEditorValue( this.getValue() );
this.editor.resize();
};
/**
* @inheritdoc
*/
ve.ui.MWAceEditorWidget.prototype.setValue = function ( value ) {
// Always do something synchronously so that getValue can be used immediately.
// setEditorValue is called once when the loadingPromise resolves in setupEditor.
if ( this.loadingPromise.state() === 'resolved' ) {
this.setEditorValue( value );
} else {
ve.ui.MWAceEditorWidget.super.prototype.setValue.call( this, value );
}
return this;
};
/**
* Set the value of the Ace editor widget
*
* @param {string} value Value
*/
ve.ui.MWAceEditorWidget.prototype.setEditorValue = function ( value ) {
var selectionState;
if ( value !== this.editor.getValue() ) {
selectionState = this.editor.session.selection.toJSON();
this.editor.setValue( value );
this.editor.session.selection.fromJSON( selectionState );
}
};
/**
* Handle change events from the Ace editor
*/
ve.ui.MWAceEditorWidget.prototype.onEditorChange = function () {
// Call setValue on the parent to keep the value property in sync with the editor
ve.ui.MWAceEditorWidget.super.prototype.setValue.call( this, this.editor.getValue() );
};
/**
* Handle resize events from the Ace editor
*
* @fires resize
*/
ve.ui.MWAceEditorWidget.prototype.onEditorResize = function () {
// On the first setup the editor doesn't resize until the end of the cycle
setTimeout( this.emit.bind( this, 'resize' ) );
};
/**
* Clear the editor's undo stack
*
* @chainable
*/
ve.ui.MWAceEditorWidget.prototype.clearUndoStack = function () {
var widget = this;
this.loadingPromise.done( function () {
widget.editor.session.setUndoManager(
new ace.UndoManager()
);
} );
return this;
};
/**
* Toggle the visibility of line numbers
*
* @param {boolean} visible Visible
* @chainable
*/
ve.ui.MWAceEditorWidget.prototype.toggleLineNumbers = function ( visible ) {
var widget = this;
this.loadingPromise.done( function () {
widget.editor.renderer.setOption( 'showLineNumbers', visible );
} );
return this;
};
/**
* Set the language mode of the editor (programming language)
*
* @param {string} lang Language
* @chainable
*/
ve.ui.MWAceEditorWidget.prototype.setLanguage = function ( lang ) {
var widget = this;
this.loadingPromise.done( function () {
widget.editor.getSession().setMode( 'ace/mode/' + ( require( 'ace/mode/' + lang ) ? lang : 'text' ) );
} );
return this;
};
/**
* Focus the editor
*/
ve.ui.MWAceEditorWidget.prototype.focus = function () {
var widget = this;
this.loadingPromise.done( function () {
widget.editor.focus();
} ).fail( function () {
ve.ui.MWAceEditorWidget.super.prototype.focus.call( widget );
} );
};
/**
* @inheritdoc
*/
ve.ui.MWAceEditorWidget.prototype.adjustSize = function () {
var widget = this;
// If the editor has loaded, resize events are emitted from #onEditorResize
// so do nothing here, otherwise call the parent method.
this.loadingPromise.fail( function () {
// Parent method
ve.ui.MWAceEditorWidget.super.prototype.adjustSize.call( widget );
} );
};