VisualEditor source mode support

Long-term todo:
* Performance will be poor on large pages due
  to using a auto-height textarea which CodeMirror
  doesn't optimise.

Change-Id: I16598fcdbeee51e6fae88376ec81f1c8552b383d
This commit is contained in:
Ed Sanders 2017-03-21 14:42:04 +00:00
parent c9b4aa65fd
commit 7b01a98ad0
11 changed files with 312 additions and 6 deletions

View file

@ -7,7 +7,10 @@
},
"globals": {
"mediaWiki": false,
"CodeMirror": false
"CodeMirror": false,
"ve": false,
"mw": false,
"OO": false
},
"rules": {
"dot-notation": [ "error", { "allowKeywords": true } ]

View file

@ -107,8 +107,8 @@ class CodeMirrorHooks {
*/
public static function onMakeGlobalVariablesScript( array &$vars, OutputPage $out ) {
$context = $out->getContext();
// add CodeMirror vars only for edit pages
if ( self::isCodeMirrorEnabled( $context ) ) {
// add CodeMirror vars on edit pages, or if VE is installed
if ( self::isCodeMirrorEnabled( $context ) || class_exists( 'VisualEditorHooks' ) ) {
$vars['extCodeMirrorConfig'] = self::getConfiguraton( $context );
}
}

View file

@ -15,7 +15,7 @@ module.exports = function ( grunt ) {
},
stylelint: {
all: [
'**/*.css',
'**/*.{css,less}',
'!resources/lib/**',
'!node_modules/**'
]

View file

@ -96,6 +96,37 @@
"ext.CodeMirror.lib.mode.clike",
"ext.CodeMirror.lib"
]
},
"ext.CodeMirror.visualEditor.init": {
"scripts": [
"modules/ve-cm/ve.ui.CodeMirror.init.js"
],
"styles": [
"modules/ve-cm/ve.ui.CodeMirror.init.less"
],
"messages": [
"codemirror-toggle-label"
],
"targets": [
"desktop"
]
},
"ext.CodeMirror.visualEditor": {
"dependencies": [
"ext.visualEditor.mwcore",
"ext.CodeMirror.lib",
"ext.CodeMirror.mode.mediawiki",
"mediawiki.api",
"mediawiki.api.options",
"user.options"
],
"scripts": [
"modules/ve-cm/ve.ui.CodeMirrorAction.js",
"modules/ve-cm/ve.ui.CodeMirrorTool.js"
],
"targets": [
"desktop"
]
}
},
"ResourceFileModulePaths": {
@ -113,6 +144,10 @@
"CodeMirrorHooks::onGetPreferences"
]
},
"VisualEditorPluginModules": [
"ext.CodeMirror.visualEditor.init",
"ext.CodeMirror.visualEditor"
],
"config": {
"CodeMirrorEnableFrontend": true
},

View file

@ -6,5 +6,6 @@
},
"codemirror-desc": "Provides syntax highlighting in wikitext editor",
"codemirror-enable-label": "Enable CodeMirror (Syntax highlight)",
"codemirror-disable-label": "Disable CodeMirror (Syntax highlight)"
"codemirror-disable-label": "Disable CodeMirror (Syntax highlight)",
"codemirror-toggle-label": "Syntax highlighting"
}

View file

@ -9,5 +9,6 @@
},
"codemirror-desc": "{{desc|name=Code Mirror|url=https://www.mediawiki.org/wiki/Extension:CodeMirror}}\n\nAdditional info: Discription of \"Syntax highlighting\" in wiki\n[[mw:Extension:SyntaxHighlight GeSHi]]",
"codemirror-enable-label": "Title tooltip for button to enable CodeMirror in the editing toolbar.",
"codemirror-disable-label": "Title tooltip for button to disable CodeMirror in the editing toolbar."
"codemirror-disable-label": "Title tooltip for button to disable CodeMirror in the editing toolbar.",
"codemirror-toggle-label": "Title tooltip for button to toggle CodeMirror in the editing toolbar."
}

View file

@ -29,3 +29,8 @@
.wikiEditor-ui-toolbar .tool-codemirror-off {
.background-image-svg( 'images/cm-off.svg', 'images/cm-off.png' );
}
.oo-ui-popupWidget.ve-init-mw-switchPopupWidget {
// Increase z-index to above scrollbar
z-index: 7;
}

View file

@ -0,0 +1,12 @@
( function ( ve, mw ) {
mw.libs.ve.targetLoader.addPlugin( function () {
var i, target, index;
for ( i in ve.init.mw ) {
target = ve.init.mw[ i ];
if ( target === ve.init.mw.DesktopArticleTarget ) {
index = target.static.actionGroups[ 1 ].include.indexOf( 'changeDirectionality' );
target.static.actionGroups[ 1 ].include.splice( index, 0, 'codeMirror' );
}
}
} );
}( ve, mediaWiki ) );

View file

@ -0,0 +1,49 @@
.ve-init-mw-desktopArticleTarget {
.CodeMirror {
height: auto;
z-index: -1;
position: absolute;
top: 0;
left: 0;
font-size: 1.17216em;
line-height: 1.5em;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0 1.14286em; /* 1/0.875 */
@media screen and ( min-width: 982px ) {
padding: 0 1.71429em; /* surface-margin-left (1.5em) / (mw-body-content font-size) 0.875em */
}
}
.CodeMirror pre,
.CodeMirror-lines {
padding: 0;
}
.CodeMirror-scroll {
margin-right: 0;
overflow: auto !important; /* stylelint-disable-line declaration-no-important */
}
.CodeMirror-sizer {
border-right: 0;
}
.CodeMirror pre.cm-mw-section-1,
.CodeMirror pre.cm-mw-section-2 {
font-size: inherit;
line-height: inherit;
font-weight: bold;
}
}
.ve-ce-documentNode-codeEditor-hide {
opacity: 0.3;
}
.ve-ce-documentNode-codeEditor-webkit-hide {
-webkit-text-fill-color: transparent;
}

View file

@ -0,0 +1,122 @@
/*!
* VisualEditor UserInterface CodeMirrorAction class.
*
* @copyright 2011-2017 VisualEditor Team and others; see http://ve.mit-license.org
*/
/**
* CodeMirror action
*
* @class
* @extends ve.ui.Action
* @constructor
* @param {ve.ui.Surface} surface Surface to act on
*/
ve.ui.CodeMirrorAction = function VeUiCodeMirrorAction() {
// Parent constructor
ve.ui.CodeMirrorAction.super.apply( this, arguments );
};
/* Inheritance */
OO.inheritClass( ve.ui.CodeMirrorAction, ve.ui.Action );
/* Static Properties */
ve.ui.CodeMirrorAction.static.name = 'codeMirror';
/**
* @inheritdoc
*/
ve.ui.CodeMirrorAction.static.methods = [ 'toggle' ];
/* Methods */
/**
* @method
* @param {boolean} [enable] State to force toggle to, inverts current state if undefined
* @return {boolean} Action was executed
*/
ve.ui.CodeMirrorAction.prototype.toggle = function ( enable ) {
var surface = this.surface,
surfaceView = surface.getView(),
doc = surface.getModel().getDocument();
if ( !surface.mirror && enable !== false ) {
surface.mirror = CodeMirror( surfaceView.$element[ 0 ], {
value: surface.getDom(),
mwConfig: mw.config.get( 'extCodeMirrorConfig' ),
lineWrapping: true,
tabSize: 1,
scrollbarStyle: 'null',
viewportMargin: 5,
// select mediawiki as text input mode
mode: 'text/mediawiki',
extraKeys: {
Tab: false
}
} );
surfaceView.$documentNode.addClass(
'WebkitTextFillColor' in document.body.style ?
've-ce-documentNode-codeEditor-webkit-hide' :
've-ce-documentNode-codeEditor-webkit'
);
// As the action is regenerated each time, we need to store the bound listener
// in the mirror for later disconnection.
surface.mirror.veTransactionListener = this.onDocumentTransact.bind( this, surface );
doc.on( 'transact', surface.mirror.veTransactionListener );
} else if ( surface.mirror && enable !== true ) {
doc.off( 'transact', surface.mirror.veTransactionListener );
surfaceView.$documentNode.removeClass(
've-ce-documentNode-codeEditor-webkit-hide ve-ce-documentNode-codeEditor-webkit'
);
surface.mirror.getWrapperElement().remove();
surface.mirror = null;
}
return true;
};
ve.ui.CodeMirrorAction.prototype.onDocumentTransact = function ( surface, tx ) {
var node, textRange, line,
doc = surface.getModel().getDocument(),
mirror = surface.mirror,
modifiedRange = tx.getModifiedRange( doc ),
nodes = doc.selectNodes( modifiedRange, 'leaves' );
// TODO: Iterate over operations and perform a replaceRange for each replace operation
if ( nodes.length === 1 && nodes[ 0 ].node instanceof ve.dm.TextNode ) {
node = nodes[ 0 ].node.parent;
textRange = nodes[ 0 ].nodeRange;
line = node.parent.children.indexOf( node );
if ( tx.operations.every( function ( op ) {
return op.type === 'retain' || ( op.type === 'replace' && op.remove.length === 0 );
} ) ) {
// Single line insert
mirror.replaceRange(
doc.data.getText( true, modifiedRange ),
{ line: line, ch: modifiedRange.start - textRange.start }
);
} else {
// Single line replace
mirror.replaceRange(
doc.data.getText( true, textRange ),
{ line: line, ch: 0 },
{ line: line, ch: mirror.getLine( line ).length }
);
}
} else {
// Fallback - flush whole doc
mirror.setValue( surface.getDom() );
}
};
/* Registration */
ve.ui.actionFactory.register( ve.ui.CodeMirrorAction );

View file

@ -0,0 +1,78 @@
/**
* MediaWiki UserInterface CodeMirror tool.
*
* @class
* @abstract
* @extends ve.ui.Tool
* @constructor
* @param {OO.ui.ToolGroup} toolGroup
* @param {Object} [config] Configuration options
*/
ve.ui.CodeMirrorTool = function VeUiCodeMirrorTool() {
// Parent constructor
ve.ui.CodeMirrorTool.super.apply( this, arguments );
// Events
this.toolbar.connect( this, { surfaceChange: 'onSurfaceChange' } );
};
/* Inheritance */
OO.inheritClass( ve.ui.CodeMirrorTool, ve.ui.Tool );
/* Static properties */
ve.ui.CodeMirrorTool.static.name = 'codeMirror';
ve.ui.CodeMirrorTool.static.autoAddToCatchall = false;
ve.ui.CodeMirrorTool.static.title = OO.ui.deferMsg( 'codemirror-toggle-label' );
ve.ui.CodeMirrorTool.static.icon = 'code';
ve.ui.CodeMirrorTool.static.group = 'codeMirror';
ve.ui.CodeMirrorTool.static.commandName = 'codeMirror';
ve.ui.CodeMirrorTool.static.deactivateOnSelect = false;
/**
* @inheritdoc
*/
ve.ui.CodeMirrorTool.prototype.onSelect = function () {
var useCodeMirror;
// Parent method
ve.ui.CodeMirrorTool.super.prototype.onSelect.apply( this, arguments );
useCodeMirror = !!this.toolbar.surface.mirror;
this.setActive( useCodeMirror );
new mw.Api().saveOption( 'usecodemirror', useCodeMirror ? 1 : 0 );
mw.user.options.set( 'usecodemirror', useCodeMirror ? 1 : 0 );
};
/**
* @inheritdoc
*/
ve.ui.CodeMirrorTool.prototype.onSurfaceChange = function ( oldSurface, newSurface ) {
var useCodeMirror,
isDisabled = newSurface.getMode() !== 'source',
command = this.getCommand(),
surface = this.toolbar.getSurface();
this.setDisabled( isDisabled );
if ( !isDisabled ) {
useCodeMirror = mw.user.options.get( 'usecodemirror' ) > 0;
command.execute( surface, [ useCodeMirror ] );
this.setActive( useCodeMirror );
}
};
ve.ui.CodeMirrorTool.prototype.onUpdateState = function () {};
/* Registration */
ve.ui.toolFactory.register( ve.ui.CodeMirrorTool );
/* Command */
ve.ui.commandRegistry.register(
new ve.ui.Command(
'codeMirror', 'codeMirror', 'toggle'
)
);