mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 13:56:44 +00:00
CodeMirror 6 for VE 2017 wikitext editor
Add new temporary ext.CodeMirror.visualEditor.init RL module which selects the temporary ext.CodeMirror.visualEditor.v6 or non-v6 based on $wgCodeMirrorV6. This will allow us to deploy CM6 further. As a result of this work, the core CodeMirror class now has knowledge of ve.ui.Surface. Other changes: * Add Compartment for specialCharsExtension so it can be disabled in VE. * Add option to mediaWikiLang() to disable template folding. * Add support for RTL wikis where $wgCodeMirrorRTL is enabled. * Make CodeMirror.logUsage() and setCodeMirrorPreference() static. * Fix unit and linting tests. Some code courtesy of Fandom, GPLv2-or-later; see: https://github.com/Wikia/mediawiki-extensions-CodeMirror/commit/ef297c48c Bug: T357482 Change-Id: I15453b33e77e1c1b4d5e5183e41e53d56ff14c3e
This commit is contained in:
parent
7d3482f89e
commit
925775778a
|
@ -1,4 +1,3 @@
|
|||
/resources/lib/
|
||||
/resources/dist/
|
||||
/vendor
|
||||
/docs
|
||||
|
|
|
@ -278,6 +278,34 @@
|
|||
"packageFiles": [
|
||||
"codemirror.wikieditor.mediawiki.init.js"
|
||||
]
|
||||
},
|
||||
"ext.CodeMirror.v6.visualEditor": {
|
||||
"dependencies": [
|
||||
"ext.visualEditor.mwcore",
|
||||
"ext.visualEditor.mwmeta",
|
||||
"mediawiki.api",
|
||||
"user.options",
|
||||
"ext.CodeMirror.v6"
|
||||
],
|
||||
"scripts": [
|
||||
"ve-cm/ve.ui.CodeMirrorAction.v6.js",
|
||||
"ve-cm/ve.ui.CodeMirrorTool.v6.js"
|
||||
],
|
||||
"styles": [
|
||||
"ve-cm/ve.ui.CodeMirror.v6.less"
|
||||
],
|
||||
"messages": [
|
||||
"codemirror-toggle-label"
|
||||
]
|
||||
},
|
||||
"ext.CodeMirror.visualEditor.init": {
|
||||
"packageFiles": [
|
||||
"ve-cm/ve.ui.CodeMirror.init.js",
|
||||
{
|
||||
"name": "ext.CodeMirror.data.js",
|
||||
"callback": "MediaWiki\\Extension\\CodeMirror\\DataScript::makeScript"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ResourceFileModulePaths": {
|
||||
|
@ -333,7 +361,7 @@
|
|||
},
|
||||
"VisualEditor": {
|
||||
"PluginModules": [
|
||||
"ext.CodeMirror.visualEditor"
|
||||
"ext.CodeMirror.visualEditor.init"
|
||||
]
|
||||
},
|
||||
"EventLogging": {
|
||||
|
|
|
@ -58,8 +58,10 @@ class DataScript {
|
|||
|
||||
// initialize configuration
|
||||
$config = [
|
||||
'useV6' => $mwConfig->get( 'CodeMirrorV6' ),
|
||||
'lineNumberingNamespaces' => $mwConfig->get( 'CodeMirrorLineNumberingNamespaces' ),
|
||||
'templateFoldingNamespaces' => $mwConfig->get( 'CodeMirrorTemplateFoldingNamespaces' ),
|
||||
'isSupportedRtlWiki' => $mwConfig->get( 'CodeMirrorRTL' ),
|
||||
'pluginModules' => $registry->getAttribute( 'CodeMirrorPluginModules' ),
|
||||
'tagModes' => $tagModes,
|
||||
'tags' => array_fill_keys( $tagNames, true ),
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
"TagStyle": "https://codemirror.net/docs/ref/#language.TagStyle",
|
||||
"Tooltip": "https://codemirror.net/docs/ref/#view.Tooltip",
|
||||
"Tree": "https://lezer.codemirror.net/docs/ref/#common.Tree",
|
||||
"ViewUpdate": "https://codemirror.net/docs/ref/#view.ViewUpdate"
|
||||
"ViewUpdate": "https://codemirror.net/docs/ref/#view.ViewUpdate",
|
||||
"ve.ui.Surface": "https://doc.wikimedia.org/visualeditor-standalone/master/ve.ui.Surface.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"commonjs": true
|
||||
},
|
||||
"globals": {
|
||||
"Tree": "readonly"
|
||||
"Tree": "readonly",
|
||||
"ve": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"max-len": "off",
|
||||
|
|
|
@ -37,10 +37,23 @@ class CodeMirror {
|
|||
/**
|
||||
* Instantiate a new CodeMirror instance.
|
||||
*
|
||||
* @param {HTMLTextAreaElement|jQuery|string} textarea Textarea to add syntax highlighting to.
|
||||
* @param {HTMLTextAreaElement|jQuery|string|ve.ui.Surface} textarea Textarea to
|
||||
* add syntax highlighting to.
|
||||
* @constructor
|
||||
*/
|
||||
constructor( textarea ) {
|
||||
if ( textarea.constructor.name === 'VeUiMWWikitextSurface' ) {
|
||||
/**
|
||||
* The VisualEditor surface CodeMirror is bound to.
|
||||
*
|
||||
* @type {ve.ui.Surface}
|
||||
*/
|
||||
this.surface = textarea;
|
||||
|
||||
// Let the content editable mimic the textarea.
|
||||
// eslint-disable-next-line no-jquery/variable-pattern
|
||||
textarea = this.surface.getView().$attachedRootNode;
|
||||
}
|
||||
/**
|
||||
* The textarea that CodeMirror is bound to.
|
||||
*
|
||||
|
@ -64,7 +77,9 @@ class CodeMirror {
|
|||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.readOnly = this.$textarea.prop( 'readonly' );
|
||||
this.readOnly = this.surface ?
|
||||
this.surface.getModel().isReadOnly() :
|
||||
this.$textarea.prop( 'readonly' );
|
||||
/**
|
||||
* The [edit recovery]{@link https://www.mediawiki.org/wiki/Manual:Edit_Recovery} handler.
|
||||
*
|
||||
|
@ -78,11 +93,17 @@ class CodeMirror {
|
|||
*/
|
||||
this.textSelection = null;
|
||||
/**
|
||||
* Language direction extension.
|
||||
* Compartment for the language direction Extension.
|
||||
*
|
||||
* @type {Compartment}
|
||||
*/
|
||||
this.dirCompartment = new Compartment();
|
||||
/**
|
||||
* Compartment for the special characters Extension.
|
||||
*
|
||||
* @type {Compartment}
|
||||
*/
|
||||
this.specialCharsCompartment = new Compartment();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,7 +118,7 @@ class CodeMirror {
|
|||
const extensions = [
|
||||
this.contentAttributesExtension,
|
||||
this.phrasesExtension,
|
||||
this.specialCharsExtension,
|
||||
this.specialCharsCompartment.of( this.specialCharsExtension ),
|
||||
this.heightExtension,
|
||||
this.updateExtension,
|
||||
this.bracketMatchingExtension,
|
||||
|
@ -187,7 +208,7 @@ class CodeMirror {
|
|||
get heightExtension() {
|
||||
return EditorView.theme( {
|
||||
'&': {
|
||||
height: `${ this.$textarea.outerHeight() }px`
|
||||
height: this.surface ? '100%' : `${ this.$textarea.outerHeight() }px`
|
||||
},
|
||||
'.cm-scroller': {
|
||||
overflow: 'auto'
|
||||
|
@ -205,7 +226,9 @@ class CodeMirror {
|
|||
*/
|
||||
get contentAttributesExtension() {
|
||||
const classList = [];
|
||||
// T245568: Sync text editor font preferences with CodeMirror
|
||||
// T245568: Sync text editor font preferences with CodeMirror,
|
||||
// but don't do this for the 2017 wikitext editor.
|
||||
if ( !this.surface ) {
|
||||
const fontClass = Array.from( this.$textarea[ 0 ].classList )
|
||||
.find( ( style ) => style.startsWith( 'mw-editfont-' ) );
|
||||
if ( fontClass ) {
|
||||
|
@ -219,6 +242,7 @@ class CodeMirror {
|
|||
) {
|
||||
classList.push( 'cm-mw-colorblind-colors' );
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
// .cm-content element (the contenteditable area)
|
||||
|
@ -379,7 +403,9 @@ class CodeMirror {
|
|||
|
||||
// Set up the initial EditorState of CodeMirror with contents of the native textarea.
|
||||
this.state = EditorState.create( {
|
||||
doc: this.$textarea.textSelection( 'getContents' ),
|
||||
doc: this.surface ?
|
||||
this.surface.getDom() :
|
||||
this.$textarea.textSelection( 'getContents' ),
|
||||
extensions
|
||||
} );
|
||||
|
||||
|
@ -387,6 +413,7 @@ class CodeMirror {
|
|||
this.addCodeMirrorToDom();
|
||||
|
||||
// Hide native textarea and sync CodeMirror contents upon submission.
|
||||
if ( !this.surface ) {
|
||||
this.$textarea.hide();
|
||||
if ( this.$textarea[ 0 ].form ) {
|
||||
this.$textarea[ 0 ].form.addEventListener( 'submit', () => {
|
||||
|
@ -397,6 +424,7 @@ class CodeMirror {
|
|||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
// Register $.textSelection() on the .cm-editor element.
|
||||
$( this.view.dom ).textSelection( 'register', this.cmTextSelection );
|
||||
|
@ -426,7 +454,9 @@ class CodeMirror {
|
|||
|
||||
this.view = new EditorView( {
|
||||
state: this.state,
|
||||
parent: this.$textarea.parent()[ 0 ]
|
||||
parent: this.surface ?
|
||||
this.surface.getView().$element[ 0 ] :
|
||||
this.$textarea.parent()[ 0 ]
|
||||
} );
|
||||
}
|
||||
|
||||
|
@ -443,7 +473,9 @@ class CodeMirror {
|
|||
$( this.view.dom ).textSelection( 'unregister' );
|
||||
this.$textarea.textSelection( 'unregister' );
|
||||
this.$textarea.unwrap( '.ext-codemirror-wrapper' );
|
||||
if ( !this.surface ) {
|
||||
this.$textarea.val( this.view.state.doc.toString() );
|
||||
}
|
||||
this.view.destroy();
|
||||
this.view = null;
|
||||
this.$textarea.show();
|
||||
|
@ -470,8 +502,10 @@ class CodeMirror {
|
|||
*
|
||||
* @param {Object} data
|
||||
* @stable to call
|
||||
* @internal
|
||||
* @ignore
|
||||
*/
|
||||
logUsage( data ) {
|
||||
static logUsage( data ) {
|
||||
/* eslint-disable camelcase */
|
||||
const event = Object.assign( {
|
||||
session_token: mw.user.sessionId(),
|
||||
|
@ -491,7 +525,7 @@ class CodeMirror {
|
|||
* @param {boolean} prefValue True, if CodeMirror should be enabled by default, otherwise false.
|
||||
* @stable to call and override
|
||||
*/
|
||||
setCodeMirrorPreference( prefValue ) {
|
||||
static setCodeMirrorPreference( prefValue ) {
|
||||
// Skip for unnamed users
|
||||
if ( !mw.user.isNamed() ) {
|
||||
return;
|
||||
|
|
|
@ -1262,6 +1262,7 @@ class CodeMirrorModeMediaWiki {
|
|||
* @param {Object} [config] Configuration options for the MediaWiki mode.
|
||||
* @param {boolean} [config.bidiIsolation=false] Enable bidi isolation around HTML tags.
|
||||
* This should generally always be enabled on RTL pages, but it comes with a performance cost.
|
||||
* @param {boolean} [config.templateFolding=true] Enable template folding.
|
||||
* @param {Object|null} [mwConfig] Ignore; used only by unit tests.
|
||||
* @return {LanguageSupport}
|
||||
* @stable to call
|
||||
|
@ -1277,10 +1278,11 @@ const mediaWikiLang = ( config = { bidiIsolation: false }, mwConfig = null ) =>
|
|||
)
|
||||
) ];
|
||||
|
||||
// Add template folding if in supported namespace.
|
||||
const templateFoldingNs = mwConfig.templateFoldingNamespaces;
|
||||
// Set to [] to disable everywhere, or null to enable everywhere.
|
||||
if ( !templateFoldingNs || templateFoldingNs.includes( mw.config.get( 'wgNamespaceNumber' ) ) ) {
|
||||
const templateFoldingNs = mwConfig.templateFoldingNamespaces;
|
||||
const shouldUseFolding = !templateFoldingNs || templateFoldingNs.includes( mw.config.get( 'wgNamespaceNumber' ) );
|
||||
// Add template folding if in supported namespace.
|
||||
if ( shouldUseFolding && ( config.templateFolding || config.templateFolding === undefined ) ) {
|
||||
langExtension.push( templateFoldingExtension );
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class CodeMirrorWikiEditor extends CodeMirror {
|
|||
setCodeMirrorPreference( prefValue ) {
|
||||
// Save state for function updateToolbarButton()
|
||||
this.useCodeMirror = prefValue;
|
||||
super.setCodeMirrorPreference( prefValue );
|
||||
CodeMirror.setCodeMirrorPreference( prefValue );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -193,7 +193,7 @@ class CodeMirrorWikiEditor extends CodeMirror {
|
|||
}
|
||||
this.updateToolbarButton();
|
||||
|
||||
this.logUsage( {
|
||||
CodeMirror.logUsage( {
|
||||
editor: 'wikitext',
|
||||
enabled: this.useCodeMirror,
|
||||
toggled: false,
|
||||
|
@ -235,7 +235,7 @@ class CodeMirrorWikiEditor extends CodeMirror {
|
|||
}
|
||||
this.updateToolbarButton();
|
||||
|
||||
this.logUsage( {
|
||||
CodeMirror.logUsage( {
|
||||
editor: 'wikitext',
|
||||
enabled: this.useCodeMirror,
|
||||
toggled: true,
|
||||
|
|
7
resources/ve-cm/ve.ui.CodeMirror.init.js
Normal file
7
resources/ve-cm/ve.ui.CodeMirror.init.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
require( '../../ext.CodeMirror.data.js' );
|
||||
|
||||
if ( mw.config.get( 'extCodeMirrorConfig' ).useV6 ) {
|
||||
mw.loader.load( 'ext.CodeMirror.v6.visualEditor' );
|
||||
} else {
|
||||
mw.loader.load( 'ext.CodeMirror.visualEditor' );
|
||||
}
|
131
resources/ve-cm/ve.ui.CodeMirror.v6.less
Normal file
131
resources/ve-cm/ve.ui.CodeMirror.v6.less
Normal file
|
@ -0,0 +1,131 @@
|
|||
.ve-init-mw-desktopArticleTarget {
|
||||
.cm-editor {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
// stylelint-disable-next-line declaration-no-important
|
||||
position: absolute !important;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
// Skin specific paddings
|
||||
.skin-vector & {
|
||||
padding: 0 1rem;
|
||||
|
||||
@media screen and ( min-width: 982px ) {
|
||||
.skin-vector-legacy& {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skin-minerva &,
|
||||
.skin-monobook & {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
background-color: transparent;
|
||||
padding-right: 8px;
|
||||
/* @noflip */
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
/* RTL rules need to apply to the content, not the interface language */
|
||||
.cm-editor[ dir='rtl' ] .cm-gutters {
|
||||
/* @noflip */
|
||||
padding-left: 8px;
|
||||
/* @noflip */
|
||||
padding-right: initial;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cm-gutterElement {
|
||||
// stylelint-disable-next-line declaration-no-important
|
||||
padding: 0 3px 0 4px !important;
|
||||
}
|
||||
|
||||
.CodeMirror-selected {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Ensure surfaces are using identical font rules
|
||||
.cm-scroller,
|
||||
.cm-scroller *,
|
||||
.ve-ui-mwWikitextSurface .ve-ce-paragraphNode {
|
||||
// The following are already set by mw-editfont-monospace on the parent: font-size, font-family
|
||||
word-wrap: break-word;
|
||||
// Support: Chrome<76, Firefox<69
|
||||
// Fallback for browsers which don't support break-spaces
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
-webkit-hyphens: manual;
|
||||
-moz-hyphens: manual;
|
||||
-ms-hyphens: manual;
|
||||
hyphens: manual;
|
||||
-webkit-font-variant-ligatures: contextual;
|
||||
font-variant-ligatures: contextual;
|
||||
|
||||
// Monospace fonts can change width when bold
|
||||
// stylelint-disable-next-line declaration-no-important
|
||||
font-weight: normal !important;
|
||||
// T252965
|
||||
line-break: initial;
|
||||
}
|
||||
|
||||
.cm-mw-section-1,
|
||||
.cm-mw-section-1 ~ *,
|
||||
.cm-mw-section-2,
|
||||
.cm-mw-section-2 ~ *,
|
||||
/* TODO: remove overqualified `span` after CM6 upgrade */
|
||||
span.cm-mw-section-3 ~ *,
|
||||
span.cm-mw-section-4 ~ *,
|
||||
span.cm-mw-section-5 ~ *,
|
||||
span.cm-mw-section-6 ~ * {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.mw-content-ltr .ve-ce-paragraphNode {
|
||||
margin-left: 6px !important; /* stylelint-disable-line declaration-no-important */
|
||||
}
|
||||
|
||||
.mw-content-rtl .ve-ce-paragraphNode {
|
||||
/* @noflip */
|
||||
margin-right: 6px !important; /* stylelint-disable-line declaration-no-important */
|
||||
}
|
||||
|
||||
.cm-activeLineGutter {
|
||||
background-color: transparent;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ve-ce-documentNode-codeEditor-hide {
|
||||
opacity: 0.4;
|
||||
|
||||
&::selection,
|
||||
& *::selection {
|
||||
background: #6da9f7 !important; /* stylelint-disable-line declaration-no-important */
|
||||
}
|
||||
}
|
||||
|
||||
.ve-ce-documentNode-codeEditor-webkit-hide {
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
253
resources/ve-cm/ve.ui.CodeMirrorAction.v6.js
Normal file
253
resources/ve-cm/ve.ui.CodeMirrorAction.v6.js
Normal file
|
@ -0,0 +1,253 @@
|
|||
/*!
|
||||
* VisualEditor UserInterface CodeMirrorAction class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 ) {
|
||||
const action = this,
|
||||
surface = this.surface,
|
||||
surfaceView = surface.getView(),
|
||||
doc = surface.getModel().getDocument();
|
||||
|
||||
if ( !surface.mirror && enable !== false ) {
|
||||
surface.mirror = true;
|
||||
mw.loader.using( [
|
||||
'ext.CodeMirror.v6',
|
||||
'ext.CodeMirror.v6.lib',
|
||||
'ext.CodeMirror.v6.mode.mediawiki',
|
||||
'jquery.client'
|
||||
] ).then( ( require ) => {
|
||||
const CodeMirror = require( 'ext.CodeMirror.v6' );
|
||||
const codeMirrorLib = require( 'ext.CodeMirror.v6.lib' );
|
||||
const mediawikiLang = require( 'ext.CodeMirror.v6.mode.mediawiki' );
|
||||
|
||||
if ( !surface.mirror ) {
|
||||
// Action was toggled to false since promise started
|
||||
return;
|
||||
}
|
||||
|
||||
// The VE/CM overlay technique only works with monospace fonts
|
||||
// (as we use width-changing bold as a highlight) so revert any editfont user preference
|
||||
surfaceView.$element.removeClass( 'mw-editfont-sans-serif mw-editfont-serif' )
|
||||
.addClass( 'mw-editfont-monospace' );
|
||||
|
||||
if ( mw.user.options.get( 'usecodemirror-colorblind' ) ) {
|
||||
surfaceView.$element.addClass( 'cm-mw-colorblind-colors' );
|
||||
}
|
||||
|
||||
surface.mirror = new CodeMirror( surface );
|
||||
const lineHeightExtension = codeMirrorLib.EditorView.theme( {
|
||||
'.cm-content': {
|
||||
lineHeight: 1.5
|
||||
}
|
||||
} );
|
||||
|
||||
const profile = $.client.profile();
|
||||
const supportsTransparentText = 'WebkitTextFillColor' in document.body.style &&
|
||||
// Disable on Firefox+OSX (T175223)
|
||||
!( profile.layout === 'gecko' && profile.platform === 'mac' );
|
||||
|
||||
surfaceView.$documentNode.addClass(
|
||||
supportsTransparentText ?
|
||||
've-ce-documentNode-codeEditor-webkit-hide' :
|
||||
've-ce-documentNode-codeEditor-hide'
|
||||
);
|
||||
|
||||
// TODO: pass bidiIsolation option to mediawikiLang() when it's more stable.
|
||||
surface.mirror.initialize( surface.mirror.defaultExtensions.concat( mediawikiLang( {
|
||||
templateFolding: false
|
||||
} ), lineHeightExtension ) );
|
||||
|
||||
// Disable the Extension that highlights special characters.
|
||||
surface.mirror.view.dispatch( {
|
||||
effects: surface.mirror.specialCharsCompartment.reconfigure(
|
||||
codeMirrorLib.EditorView.editorAttributes.of( [] )
|
||||
)
|
||||
} );
|
||||
|
||||
// Account for the gutter width in the margin.
|
||||
action.updateGutterWidth( doc.getDir() );
|
||||
|
||||
// Set focus on the surface view.
|
||||
surfaceView.focus();
|
||||
|
||||
/* Events */
|
||||
|
||||
// As the action is regenerated each time, we need to store bound listeners
|
||||
// in the mirror for later disconnection.
|
||||
surface.mirror.veTransactionListener = action.onDocumentPrecommit.bind( action );
|
||||
surface.mirror.veSelectListener = action.onSelect.bind( action );
|
||||
surface.mirror.vePositionListener = action.onPosition.bind( action );
|
||||
|
||||
doc.on( 'precommit', surface.mirror.veTransactionListener );
|
||||
surface.getModel().on( 'select', surface.mirror.veSelectListener );
|
||||
surfaceView.on( 'position', surface.mirror.vePositionListener );
|
||||
} );
|
||||
} else if ( surface.mirror && enable !== true ) {
|
||||
if ( surface.mirror !== true ) {
|
||||
surfaceView.off( 'position', surface.mirror.vePositionListener );
|
||||
doc.off( 'precommit', surface.mirror.veTransactionListener );
|
||||
surface.getModel().off( 'select', surface.mirror.veSelectListener );
|
||||
|
||||
// Restore edit-font
|
||||
// eslint-disable-next-line mediawiki/class-doc
|
||||
surfaceView.$element.removeClass( 'mw-editfont-monospace' )
|
||||
.addClass( 'mw-editfont-' + mw.user.options.get( 'editfont' ) );
|
||||
|
||||
surfaceView.$documentNode.removeClass(
|
||||
've-ce-documentNode-codeEditor-webkit-hide',
|
||||
've-ce-documentNode-codeEditor-hide'
|
||||
);
|
||||
// Reset gutter.
|
||||
surfaceView.$documentNode.css( {
|
||||
'margin-left': '',
|
||||
'margin-right': ''
|
||||
} );
|
||||
|
||||
// Set focus on the surface view.
|
||||
surface.getView().focus();
|
||||
|
||||
surface.mirror.destroy();
|
||||
surface.mirror.view = null;
|
||||
}
|
||||
|
||||
surface.mirror = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update margins to account for the CodeMirror gutter.
|
||||
*
|
||||
* @param {string} dir Document direction
|
||||
*/
|
||||
ve.ui.CodeMirrorAction.prototype.updateGutterWidth = function ( dir ) {
|
||||
const guttersWidth = this.surface.mirror.view.dom.querySelector( '.cm-gutters' ).offsetWidth;
|
||||
this.surface.getView().$documentNode.css( {
|
||||
'margin-left': dir === 'rtl' ? 0 : guttersWidth - 6,
|
||||
'margin-right': dir === 'rtl' ? guttersWidth - 6 : 0
|
||||
} );
|
||||
// Also update width of .cm-content due to apparent Chromium bug.
|
||||
this.surface.mirror.view.contentDOM.style.width = 'calc(100% - ' + ( guttersWidth + 1 ) + 'px)';
|
||||
};
|
||||
|
||||
/**
|
||||
* Mirror document directionality changes to CodeMirror.
|
||||
*/
|
||||
ve.ui.CodeMirrorAction.prototype.onPosition = function () {
|
||||
const codeMirrorLib = require( 'ext.CodeMirror.v6.lib' );
|
||||
const veDir = this.surface.getView().getDocument().getDir();
|
||||
const cmView = this.surface.mirror.view;
|
||||
const cmDir = cmView.textDirection === codeMirrorLib.Direction.LTR ? 'ltr' : 'rtl';
|
||||
if ( veDir !== cmDir ) {
|
||||
cmView.dispatch( {
|
||||
effects: this.surface.mirror.dirCompartment.reconfigure(
|
||||
codeMirrorLib.EditorView.editorAttributes.of( { dir: veDir } )
|
||||
)
|
||||
} );
|
||||
this.updateGutterWidth( veDir );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle select events from the surface model
|
||||
*
|
||||
* @param {ve.dm.Selection} selection
|
||||
*/
|
||||
ve.ui.CodeMirrorAction.prototype.onSelect = function ( selection ) {
|
||||
const range = selection.getCoveringRange();
|
||||
|
||||
// Do not re-trigger bracket matching as long as something is selected
|
||||
if ( !range || !range.isCollapsed() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = this.surface.getModel().getSourceOffsetFromOffset( range.from );
|
||||
|
||||
this.surface.mirror.view.dispatch( {
|
||||
selection: {
|
||||
anchor: offset,
|
||||
head: offset
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle precommit events from the document.
|
||||
*
|
||||
* The document is still in it's 'old' state before the transaction
|
||||
* has been applied at this point.
|
||||
*
|
||||
* @param {ve.dm.Transaction} tx
|
||||
*/
|
||||
ve.ui.CodeMirrorAction.prototype.onDocumentPrecommit = function ( tx ) {
|
||||
const replacements = [],
|
||||
action = this,
|
||||
store = this.surface.getModel().getDocument().getStore();
|
||||
let offset = 0;
|
||||
|
||||
const documentNode = document.querySelector( '.ve-ce-documentNode' );
|
||||
const guttersWidth = parseInt( document.querySelector( '.cm-gutters' ).offsetWidth );
|
||||
const marginProperty = this.surface.getModel().getDocument().getDir() === 'rtl' ? 'margin-right' : 'margin-left';
|
||||
// XXX: Why 6px?
|
||||
documentNode.style[ marginProperty ] = ( guttersWidth - 6 ) + 'px';
|
||||
|
||||
tx.operations.forEach( ( op ) => {
|
||||
if ( op.type === 'retain' ) {
|
||||
offset += op.length;
|
||||
} else if ( op.type === 'replace' ) {
|
||||
replacements.push( {
|
||||
from: action.surface.getModel().getSourceOffsetFromOffset( offset ),
|
||||
to: action.surface.getModel().getSourceOffsetFromOffset( offset + op.remove.length ),
|
||||
insert: new ve.dm.ElementLinearData( store, op.insert ).getSourceText()
|
||||
} );
|
||||
offset += op.remove.length;
|
||||
}
|
||||
} );
|
||||
|
||||
// Apply replacements in reverse to avoid having to shift offsets
|
||||
for ( let i = replacements.length - 1; i >= 0; i-- ) {
|
||||
this.surface.mirror.view.dispatch( { changes: replacements[ i ] } );
|
||||
}
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-global-selector
|
||||
const isRTL = $( '.mw-body-content .mw-parser-output' ).attr( 'dir' ) === 'rtl';
|
||||
if ( !isRTL || ( isRTL && mw.config.get( 'extCodeMirrorConfig' ).isSupportedRtlWiki ) ) {
|
||||
ve.ui.actionFactory.register( ve.ui.CodeMirrorAction );
|
||||
}
|
93
resources/ve-cm/ve.ui.CodeMirrorTool.v6.js
Normal file
93
resources/ve-cm/ve.ui.CodeMirrorTool.v6.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* VisualEditor 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 );
|
||||
|
||||
this.extCodeMirror = require( 'ext.CodeMirror.v6' );
|
||||
|
||||
// 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 = 'highlight';
|
||||
ve.ui.CodeMirrorTool.static.group = 'utility';
|
||||
ve.ui.CodeMirrorTool.static.commandName = 'codeMirror';
|
||||
ve.ui.CodeMirrorTool.static.deactivateOnSelect = false;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.CodeMirrorTool.prototype.onSelect = function () {
|
||||
// Parent method
|
||||
ve.ui.CodeMirrorTool.super.prototype.onSelect.apply( this, arguments );
|
||||
|
||||
const useCodeMirror = !!this.toolbar.surface.mirror;
|
||||
this.setActive( useCodeMirror );
|
||||
|
||||
this.extCodeMirror.setCodeMirrorPreference( useCodeMirror );
|
||||
this.extCodeMirror.logUsage( {
|
||||
editor: 'wikitext-2017',
|
||||
enabled: useCodeMirror,
|
||||
toggled: true,
|
||||
// eslint-disable-next-line camelcase
|
||||
edit_start_ts_ms: ( this.toolbar.target.startTimeStamp * 1000 ) || 0
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.CodeMirrorTool.prototype.onSurfaceChange = function ( oldSurface, newSurface ) {
|
||||
const isDisabled = newSurface.getMode() !== 'source';
|
||||
|
||||
this.setDisabled( isDisabled );
|
||||
if ( !isDisabled ) {
|
||||
const command = this.getCommand();
|
||||
const surface = this.toolbar.getSurface();
|
||||
const useCodeMirror = mw.user.options.get( 'usecodemirror' ) > 0;
|
||||
command.execute( surface, [ useCodeMirror ] );
|
||||
this.setActive( useCodeMirror );
|
||||
|
||||
if ( this.toolbar.target.startTimeStamp ) {
|
||||
this.extCodeMirror.logUsage( {
|
||||
editor: 'wikitext-2017',
|
||||
enabled: useCodeMirror,
|
||||
toggled: false,
|
||||
// eslint-disable-next-line camelcase
|
||||
edit_start_ts_ms: ( this.toolbar.target.startTimeStamp * 1000 ) || 0
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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'
|
||||
)
|
||||
);
|
|
@ -60,7 +60,7 @@ describe( 'initialize', () => {
|
|||
|
||||
describe( 'logUsage', () => {
|
||||
it( 'should track usage of CodeMirror with the correct data', () => {
|
||||
cm.logUsage( {
|
||||
CodeMirror.logUsage( {
|
||||
editor: 'wikitext',
|
||||
enabled: true,
|
||||
toggled: false
|
||||
|
@ -85,14 +85,14 @@ describe( 'setCodeMirrorPreference', () => {
|
|||
} );
|
||||
|
||||
it( 'should save using the API with the correct value', () => {
|
||||
cm.setCodeMirrorPreference( true );
|
||||
CodeMirror.setCodeMirrorPreference( true );
|
||||
expect( mw.Api.prototype.saveOption ).toHaveBeenCalledWith( 'usecodemirror', 1 );
|
||||
expect( mw.user.options.set ).toHaveBeenCalledWith( 'usecodemirror', 1 );
|
||||
} );
|
||||
|
||||
it( 'should not save preferences if the user is not named', () => {
|
||||
mw.user.isNamed = jest.fn().mockReturnValue( false );
|
||||
cm.setCodeMirrorPreference( true );
|
||||
CodeMirror.setCodeMirrorPreference( true );
|
||||
expect( mw.Api.prototype.saveOption ).toHaveBeenCalledTimes( 0 );
|
||||
expect( mw.user.options.set ).toHaveBeenCalledTimes( 0 );
|
||||
} );
|
||||
|
|
|
@ -15,6 +15,9 @@ class DataScriptTest extends \MediaWikiIntegrationTestCase {
|
|||
|
||||
$script = DataScript::makeScript( $context );
|
||||
$this->assertStringContainsString( '"extCodeMirrorConfig":', $script );
|
||||
$this->assertStringContainsString( '"lineNumberingNamespaces":', $script );
|
||||
$this->assertStringContainsString( '"templateFoldingNamespaces":', $script );
|
||||
$this->assertStringContainsString( '"isSupportedRtlWiki":', $script );
|
||||
$this->assertStringContainsString( '"pluginModules":', $script );
|
||||
$this->assertStringContainsString( '"tagModes":', $script );
|
||||
$this->assertStringContainsString( '"tags":', $script );
|
||||
|
|
Loading…
Reference in a new issue