const {
EditorView,
Extension,
LanguageSupport,
openSearchPanel
} = require( 'ext.CodeMirror.v6.lib' );
const CodeMirror = require( 'ext.CodeMirror.v6' );
/**
* CodeMirror integration with
* [WikiEditor](https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:WikiEditor).
*
* Use this class if you want WikiEditor's toolbar. If you don't need the toolbar,
* using {@link CodeMirror} directly will be considerably more efficient.
*
* @example
* mw.loader.using( [
* 'ext.wikiEditor',
* 'ext.CodeMirror.v6.WikiEditor',
* 'ext.CodeMirror.v6.mode.mediawiki'
* ] ).then( ( require ) => {
* mw.addWikiEditor( myTextarea );
* const CodeMirrorWikiEditor = require( 'ext.CodeMirror.v6.WikiEditor' );
* const mediawikiLang = require( 'ext.CodeMirror.v6.mode.mediawiki' );
* const cmWe = new CodeMirrorWikiEditor( myTextarea, mediawikiLang() );
* cmWe.initialize();
* } );
* @class
* @extends CodeMirror
*/
class CodeMirrorWikiEditor extends CodeMirror {
/**
* @constructor
* @param {HTMLTextAreaElement|jQuery|string} textarea The textarea to replace with CodeMirror.
* @param {LanguageSupport|Extension} [langExtension] Language support and its extension(s).
* @stable to call and override
*/
constructor( textarea, langExtension = [] ) {
super( textarea, langExtension );
/**
* The [Realtime Preview](https://w.wiki/Cgpp) handler.
*
* @type {Function|null}
*/
this.realtimePreviewHandler = null;
/**
* The WikiEditor search button, which is usurped to open the CodeMirror search panel.
*
* @type {jQuery|null}
*/
this.$searchBtn = null;
/**
* The old WikiEditor search button, to be restored if CodeMirror is disabled.
*
* @type {jQuery|null}
*/
this.$oldSearchBtn = null;
}
/**
* @inheritDoc
*/
get defaultExtensions() {
return [
...super.defaultExtensions,
EditorView.updateListener.of( ( update ) => {
if ( update.docChanged && typeof this.realtimePreviewHandler === 'function' ) {
this.realtimePreviewHandler();
}
} )
];
}
/**
* @inheritDoc
*/
initialize( extensions = this.defaultExtensions ) {
if ( this.view ) {
// Already initialized.
return;
}
const context = this.$textarea.data( 'wikiEditor-context' );
const toolbar = context && context.modules && context.modules.toolbar;
// Guard against something having removed WikiEditor (T271457)
if ( !toolbar ) {
return;
}
// Remove the initial toggle button that may have been added by the init script.
this.$textarea.wikiEditor( 'removeFromToolbar', {
section: 'main',
group: 'codemirror'
} );
// Add 'Syntax' button to main toolbar.
this.$textarea.wikiEditor(
'addToToolbar',
{
section: 'main',
groups: {
codemirror: {
tools: {
CodeMirror: {
type: 'element',
element: () => {
// OOUI has already been loaded by WikiEditor.
const button = new OO.ui.ToggleButtonWidget( {
label: mw.msg( 'codemirror-toggle-label-short' ),
title: mw.msg( 'codemirror-toggle-label' ),
icon: 'syntax-highlight',
value: !this.isActive,
framed: false,
classes: [ 'tool', 'cm-mw-toggle-wikieditor' ]
} );
button.on( 'change', () => this.toggle() );
return button.$element;
}
}
}
}
}
}
);
// Set the ID of the CodeMirror button for styling.
const codeMirrorButton = toolbar.$toolbar[ 0 ].querySelector( '.tool[rel=CodeMirror]' );
codeMirrorButton.id = 'mw-editbutton-codemirror';
// Hide non-applicable buttons until WikiEditor better supports a read-only mode (T188817).
if ( this.readOnly ) {
context.$ui.addClass( 'ext-codemirror-readonly' );
}
super.initialize( extensions );
this.fireSwitchHook();
}
/**
* @private
*/
fireSwitchHook() {
if ( !this.switchHook ) {
/**
* @type {Hook}
* @private
*/
this.switchHook = mw.hook( 'ext.CodeMirror.switch' );
this.switchHook.deprecate( 'Use "ext.CodeMirror.toggle" instead.' );
}
/**
* Called after CodeMirror is enabled or disabled in WikiEditor.
*
* @event CodeMirrorWikiEditor~'ext.CodeMirror.switch'
* @param {boolean} enabled Whether CodeMirror is enabled.
* @param {jQuery} $textarea The current "editor", either the
* original textarea or the `.cm-editor` element.
* @deprecated since MediaWiki 1.44, use
* {@link event:'ext.CodeMirror.toggle' ext.CodeMirror.toggle} instead.
*/
this.switchHook.fire(
this.isActive,
this.isActive ? $( this.view.dom ) : this.$textarea
);
}
/**
* @inheritDoc
*/
toggle( force ) {
super.toggle( force );
this.fireSwitchHook();
}
/**
* @inheritDoc
*/
activate() {
super.activate();
CodeMirror.setCodeMirrorPreference( true );
this.addRealtimePreviewHandler();
// Hijack the search button to open the CodeMirror search panel
// instead of the WikiEditor search dialog.
// eslint-disable-next-line no-jquery/no-global-selector
this.$searchBtn = $( '.wikiEditor-ui .group-search .tool' );
this.$oldSearchBtn = this.$searchBtn.clone( true );
this.$searchBtn.find( 'a' )
.off( 'click keydown keypress' )
.on( 'click keydown', ( e ) => {
if ( e.type === 'click' || ( e.type === 'keydown' && e.key === 'Enter' ) ) {
openSearchPanel( this.view );
e.preventDefault();
}
} );
// Add a 'Settings' button to the search group of the toolbar, in the 'Advanced' section.
this.$textarea.wikiEditor(
'addToToolbar',
{
section: 'advanced',
groups: {
codemirror: {
tools: {
CodeMirrorPreferences: {
type: 'element',
element: () => {
const button = new OO.ui.ButtonWidget( {
title: mw.msg( 'codemirror-prefs-title' ),
icon: 'settings',
framed: false,
classes: [ 'tool' ]
} );
button.on( 'click',
() => this.preferences.toggle( this.view, true )
);
return button.$element;
}
}
}
}
}
}
);
}
/**
* @inheritDoc
*/
deactivate() {
super.deactivate();
CodeMirror.setCodeMirrorPreference( false );
// Restore original search button.
this.$searchBtn.replaceWith( this.$oldSearchBtn );
// Remove the CodeMirror preferences button from the toolbar.
this.$textarea.wikiEditor( 'removeFromToolbar', {
section: 'advanced',
group: 'codemirror'
} );
}
/**
* @inheritDoc
*/
destroy() {
super.destroy();
this.$textarea.wikiEditor( 'removeFromToolbar', {
section: 'main',
group: 'codemirror'
} );
if ( this.readOnly ) {
this.$textarea.data( 'wikiEditor-context' ).$ui.removeClass( 'ext-codemirror-readonly' );
}
this.switchHook = null;
}
/**
* Log usage of CodeMirror to the VisualEditorFeatureUse schema.
* Reimplements ext.wikiEditor's logEditFeature method (GPL-2.0+), which isn't exported.
*
* @see https://phabricator.wikimedia.org/T373710
* @see https://meta.wikimedia.org/wiki/Schema:VisualEditorFeatureUse
* @see https://www.mediawiki.org/wiki/VisualEditor/FeatureUse_data_dictionary
* @inheritDoc
*/
logEditFeature( action ) {
if ( mw.config.get( 'wgMFMode' ) !== null ) {
// Visiting a ?action=edit URL can, depending on user settings, result
// in the MobileFrontend overlay appearing on top of WikiEditor. In
// these cases, don't log anything.
return;
}
mw.track( 'visualEditorFeatureUse', {
feature: 'codemirror',
action,
// eslint-disable-next-line camelcase
editor_interface: 'wikitext',
// FIXME T249944
platform: 'desktop',
integration: 'page'
} );
}
/**
* Adds the Realtime Preview handler. Realtime Preview reads from the textarea
* via jQuery.textSelection, which will bubble up to CodeMirror automatically.
*
* @private
*/
addRealtimePreviewHandler() {
this.addMwHook( 'ext.WikiEditor.realtimepreview.enable', ( realtimePreview ) => {
this.realtimePreviewHandler = realtimePreview.getEventHandler().bind( realtimePreview );
} );
this.addMwHook( 'ext.WikiEditor.realtimepreview.disable', () => {
this.realtimePreviewHandler = null;
} );
}
}
module.exports = CodeMirrorWikiEditor;