From 60c1ee7d56a4176170d1430bb8f991827ad329af Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Wed, 21 Oct 2015 16:30:57 +0100 Subject: [PATCH] Introduce Ace editor widget The widget attempts to load the ext.codeEditor.ace.modes module and if it fails, will fall back to regular TextWidget behaviour. Bug: T49742 Change-Id: Ie483f6eba25e3732a396c18decc0e1844b806b23 --- extension.json | 2 + .../widgets/ve.ui.MWAceEditorWidget.css | 22 +++ modules/ve-mw/ui/ve.ui.MWExtensionWindow.js | 4 +- .../ui/widgets/ve.ui.MWAceEditorWidget.js | 170 ++++++++++++++++++ 4 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 modules/ve-mw/ui/styles/widgets/ve.ui.MWAceEditorWidget.css create mode 100644 modules/ve-mw/ui/widgets/ve.ui.MWAceEditorWidget.js diff --git a/extension.json b/extension.json index 90d3dfcebb..3ff78dcc93 100644 --- a/extension.json +++ b/extension.json @@ -965,6 +965,7 @@ "modules/ve-mw/ui/ve.ui.MWExtensionWindow.js", "modules/ve-mw/ui/commands/ve.ui.MWWikitextWarningCommand.js", "modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js", + "modules/ve-mw/ui/widgets/ve.ui.MWAceEditorWidget.js", "modules/ve-mw/ui/widgets/ve.ui.MWTargetWidget.js", "modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js", "modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js", @@ -987,6 +988,7 @@ "modules/ve-mw/ui/styles/dialogs/ve.ui.MWWelcomeDialog.css", "modules/ve-mw/ui/styles/dialogs/ve.ui.MWSaveDialog.css", "modules/ve-mw/ui/styles/tools/ve.ui.MWPopupTool.css", + "modules/ve-mw/ui/styles/widgets/ve.ui.MWAceEditorWidget.css", "modules/ve-mw/ui/styles/widgets/ve.ui.MWTocWidget.css", "modules/ve-mw/ui/styles/tools/ve.ui.MWEducationPopupTool.css" ], diff --git a/modules/ve-mw/ui/styles/widgets/ve.ui.MWAceEditorWidget.css b/modules/ve-mw/ui/styles/widgets/ve.ui.MWAceEditorWidget.css new file mode 100644 index 0000000000..b11d5843f4 --- /dev/null +++ b/modules/ve-mw/ui/styles/widgets/ve.ui.MWAceEditorWidget.css @@ -0,0 +1,22 @@ +/*! + * VisualEditor MediaWiki UserInterface MWAceEditorWidget styles. + * + * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +.ve-ui-mwAceEditorWidget .ace_editor { + border: 1px solid #ccc; + margin: 1px; + font-family: monospace, Courier; + font-size: inherit; + line-height: 1.5; +} + +.ve-ui-mwAceEditorWidget .ace_focus { + /* TODO: Move to mediawiki theme only */ + border-color: #347bff; + /* HACK: Make border grow out as inset doesn't overlap absolute positioned children */ + border-width: 2px; + margin: 0; +} diff --git a/modules/ve-mw/ui/ve.ui.MWExtensionWindow.js b/modules/ve-mw/ui/ve.ui.MWExtensionWindow.js index 9b473722ee..e9973a5bdc 100644 --- a/modules/ve-mw/ui/ve.ui.MWExtensionWindow.js +++ b/modules/ve-mw/ui/ve.ui.MWExtensionWindow.js @@ -53,9 +53,9 @@ ve.ui.MWExtensionWindow.static.dir = null; ve.ui.MWExtensionWindow.prototype.initialize = function () { this.input = new ve.ui.WhitespacePreservingTextInputWidget( { limit: 1, - multiline: true + multiline: true, + classes: [ 've-ui-mwExtensionWindow-input' ] } ); - this.input.$element.addClass( 've-ui-mwExtensionWindow-input' ); }; /** diff --git a/modules/ve-mw/ui/widgets/ve.ui.MWAceEditorWidget.js b/modules/ve-mw/ui/widgets/ve.ui.MWAceEditorWidget.js new file mode 100644 index 0000000000..4b13dcab72 --- /dev/null +++ b/modules/ve-mw/ui/widgets/ve.ui.MWAceEditorWidget.js @@ -0,0 +1,170 @@ +/*! + * VisualEditor UserInterface MWAceEditorWidget class. + * + * @copyright 2011-2015 VisualEditor Team and others; see http://ve.mit-license.org + */ + +/* global ace, require */ + +/** + * Text input widget which hides but preserves leading and trailing whitespace + * + * @class + * @extends ve.ui.WhitespacePreservingTextInputWidget + * + * @constructor + * @param {Object} [config] Configuration options + */ +ve.ui.MWAceEditorWidget = function VeUiMWAceEditorWidget( config ) { + // Configuration + config = config || {}; + + this.$ace = $( '
' ); + 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.moduleRegistry.hasOwnProperty( '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 + */ +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.emit.bind( this, 'resize' ) ); + this.editor.resize(); +}; + +/** + * @inheritdoc + */ +ve.ui.MWAceEditorWidget.prototype.setValue = function ( value ) { + var widget = this; + this.loadingPromise.done( function () { + widget.editor.setValue( value ); + widget.editor.selection.moveTo( 0, 0 ); + } ).fail( function () { + ve.ui.MWAceEditorWidget.super.prototype.setValue.call( widget, value ); + } ); +}; + +/** + * 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() ); +}; + +/** + * Toggle the visibility of line numbers + * + * @param {boolean} visible Visible + */ +ve.ui.MWAceEditorWidget.prototype.toggleLineNumbers = function ( visible ) { + var widget = this; + this.loadingPromise.done( function () { + widget.editor.renderer.setShowGutter( visible ); + } ); +}; + +/** + * Set the language mode of the editor (programming language) + * + * @param {string} lang Language + */ +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' ) ); + } ); +}; + +/** + * 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 + * TODO Move this upstream to OOUI + */ +ve.ui.MWAceEditorWidget.prototype.adjustSize = function () { + var styleHeight, + widget = this; + + // Parent method + ve.ui.MWAceEditorWidget.super.prototype.adjustSize.call( this ); + + // Implement resize events for plain TextWidget + this.loadingPromise.fail( function () { + styleHeight = widget.$input[ 0 ].style.height; + + if ( styleHeight !== widget.styleHeight ) { + widget.styleHeight = styleHeight; + widget.emit( 'resize' ); + } + } ); +};