Isolate build step to CM6 library and restructure files to work with RL
CodeMirror 6 requires the use of NPM, but we can still bundle all CM
packages into one file, and then everything else (i.e. our code) is
managed by ResourceLoader as per usual. This makes contribution
considerably easier as we no longer need a build step for each change.
CM5 files are now under resources/legacy, and the CM6 files are moved to
the root of the resources/ directory. Only one file,
codemirror.bundle.js, is managed by Rollup, while everything else is RL.
The Rollup output for now is put under resources/lib/ alongside the CM5
upstream files.
This patch is *mostly* renames of files, along with changing ECMAScript
Module (ESM) syntax into the CommonJS style that ResourceLoader prefers.
We also remove more modern JS syntax (i.e. private class methods) that
we were able to use before because we had a build step with Babel.
This patch should effectively make no user-facing changes, or to the
ResourceLoader modules we offer in Extension:CodeMirror.
Finally, bump version in extension.json to 6, to match the upstream lib,
and add Bhsd as an author :-)
Bug: T368053
Change-Id: Ie258e49f5df8db23a7344ac3c4c9300aaa991042
2024-06-21 03:21:09 +00:00
|
|
|
const {
|
|
|
|
EditorSelection,
|
|
|
|
EditorView,
|
|
|
|
Extension,
|
2024-08-09 23:04:15 +00:00
|
|
|
LanguageSupport,
|
|
|
|
openSearchPanel
|
Isolate build step to CM6 library and restructure files to work with RL
CodeMirror 6 requires the use of NPM, but we can still bundle all CM
packages into one file, and then everything else (i.e. our code) is
managed by ResourceLoader as per usual. This makes contribution
considerably easier as we no longer need a build step for each change.
CM5 files are now under resources/legacy, and the CM6 files are moved to
the root of the resources/ directory. Only one file,
codemirror.bundle.js, is managed by Rollup, while everything else is RL.
The Rollup output for now is put under resources/lib/ alongside the CM5
upstream files.
This patch is *mostly* renames of files, along with changing ECMAScript
Module (ESM) syntax into the CommonJS style that ResourceLoader prefers.
We also remove more modern JS syntax (i.e. private class methods) that
we were able to use before because we had a build step with Babel.
This patch should effectively make no user-facing changes, or to the
ResourceLoader modules we offer in Extension:CodeMirror.
Finally, bump version in extension.json to 6, to match the upstream lib,
and add Bhsd as an author :-)
Bug: T368053
Change-Id: Ie258e49f5df8db23a7344ac3c4c9300aaa991042
2024-06-21 03:21:09 +00:00
|
|
|
} = require( 'ext.CodeMirror.v6.lib' );
|
|
|
|
const CodeMirror = require( 'ext.CodeMirror.v6' );
|
2024-02-14 01:01:08 +00:00
|
|
|
|
2023-09-19 17:59:29 +00:00
|
|
|
/**
|
2024-03-19 03:10:11 +00:00
|
|
|
* 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.
|
|
|
|
*
|
2024-04-15 17:30:49 +00:00
|
|
|
* @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 );
|
|
|
|
* cmWe.initialize( [ cmWe.defaultExtensions, mediawikiLang() ] );
|
|
|
|
* cmWe.addCodeMirrorToWikiEditor();
|
|
|
|
* } );
|
2024-03-19 03:10:11 +00:00
|
|
|
* @extends CodeMirror
|
2023-09-19 17:59:29 +00:00
|
|
|
*/
|
2024-03-19 03:10:11 +00:00
|
|
|
class CodeMirrorWikiEditor extends CodeMirror {
|
2024-03-11 18:10:08 +00:00
|
|
|
/**
|
|
|
|
* @constructor
|
2024-03-19 03:10:11 +00:00
|
|
|
* @param {jQuery} $textarea The textarea to replace with CodeMirror.
|
|
|
|
* @param {LanguageSupport|Extension} langExtension Language support and its extension(s).
|
|
|
|
* @stable to call and override
|
2024-03-11 18:10:08 +00:00
|
|
|
*/
|
|
|
|
constructor( $textarea, langExtension ) {
|
2023-09-19 17:59:29 +00:00
|
|
|
super( $textarea );
|
2024-03-19 03:10:11 +00:00
|
|
|
/**
|
|
|
|
* Language support and its extension(s).
|
2024-04-23 19:13:38 +00:00
|
|
|
*
|
2024-03-19 03:10:11 +00:00
|
|
|
* @type {LanguageSupport|Extension}
|
|
|
|
*/
|
2024-03-11 18:10:08 +00:00
|
|
|
this.langExtension = langExtension;
|
2024-03-19 03:10:11 +00:00
|
|
|
/**
|
|
|
|
* Whether CodeMirror is currently enabled.
|
2024-04-23 19:13:38 +00:00
|
|
|
*
|
2024-03-19 03:10:11 +00:00
|
|
|
* @type {boolean}
|
|
|
|
*/
|
2023-12-06 18:49:40 +00:00
|
|
|
this.useCodeMirror = mw.user.options.get( 'usecodemirror' ) > 0;
|
2024-03-21 21:16:04 +00:00
|
|
|
/**
|
|
|
|
* The [Realtime Preview](https://w.wiki/9XgX) handler.
|
2024-04-23 19:13:38 +00:00
|
|
|
*
|
2024-03-21 21:16:04 +00:00
|
|
|
* @type {Function|null}
|
|
|
|
*/
|
|
|
|
this.realtimePreviewHandler = null;
|
2024-08-09 23:04:15 +00:00
|
|
|
/**
|
|
|
|
* The old WikiEditor search button, to be restored if CodeMirror is disabled.
|
|
|
|
*
|
|
|
|
* @type {jQuery}
|
|
|
|
*/
|
|
|
|
this.$oldSearchBtn = null;
|
2023-09-19 17:59:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
setCodeMirrorPreference( prefValue ) {
|
2024-03-11 18:10:08 +00:00
|
|
|
// Save state for function updateToolbarButton()
|
|
|
|
this.useCodeMirror = prefValue;
|
2024-06-08 03:10:11 +00:00
|
|
|
CodeMirror.setCodeMirrorPreference( prefValue );
|
2023-09-19 17:59:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-03-19 03:10:11 +00:00
|
|
|
* Replaces the default textarea with CodeMirror.
|
|
|
|
*
|
|
|
|
* @fires CodeMirrorWikiEditor~'ext.CodeMirror.switch'
|
|
|
|
* @stable to call
|
2023-09-19 17:59:29 +00:00
|
|
|
*/
|
|
|
|
enableCodeMirror() {
|
2023-10-10 19:23:03 +00:00
|
|
|
// If CodeMirror is already loaded, abort.
|
|
|
|
if ( this.view ) {
|
2023-09-19 17:59:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const selectionStart = this.$textarea.prop( 'selectionStart' ),
|
|
|
|
selectionEnd = this.$textarea.prop( 'selectionEnd' ),
|
|
|
|
scrollTop = this.$textarea.scrollTop(),
|
|
|
|
hasFocus = this.$textarea.is( ':focus' );
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Default configuration, which we may conditionally add to later.
|
|
|
|
* @see https://codemirror.net/docs/ref/#state.Extension
|
|
|
|
*/
|
|
|
|
const extensions = [
|
2024-03-19 03:10:11 +00:00
|
|
|
this.defaultExtensions,
|
2024-03-11 18:10:08 +00:00
|
|
|
this.langExtension,
|
2024-03-21 21:16:04 +00:00
|
|
|
EditorView.updateListener.of( ( update ) => {
|
|
|
|
if ( update.docChanged && typeof this.realtimePreviewHandler === 'function' ) {
|
|
|
|
this.realtimePreviewHandler();
|
|
|
|
}
|
|
|
|
} )
|
2023-09-19 17:59:29 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
this.initialize( extensions );
|
2024-03-21 21:16:04 +00:00
|
|
|
this.addRealtimePreviewHandler();
|
2023-09-19 17:59:29 +00:00
|
|
|
|
|
|
|
// Sync scroll position, selections, and focus state.
|
2024-02-21 07:29:53 +00:00
|
|
|
requestAnimationFrame( () => {
|
|
|
|
this.view.scrollDOM.scrollTop = scrollTop;
|
2023-09-19 17:59:29 +00:00
|
|
|
} );
|
2024-02-21 07:29:53 +00:00
|
|
|
if ( selectionStart !== 0 || selectionEnd !== 0 ) {
|
|
|
|
const range = EditorSelection.range( selectionStart, selectionEnd ),
|
|
|
|
scrollEffect = EditorView.scrollIntoView( range );
|
|
|
|
scrollEffect.value.isSnapshot = true;
|
|
|
|
this.view.dispatch( {
|
|
|
|
selection: EditorSelection.create( [ range ] ),
|
|
|
|
effects: scrollEffect
|
|
|
|
} );
|
|
|
|
}
|
2023-09-19 17:59:29 +00:00
|
|
|
if ( hasFocus ) {
|
|
|
|
this.view.focus();
|
|
|
|
}
|
2023-10-10 20:11:15 +00:00
|
|
|
|
2024-08-09 23:04:15 +00:00
|
|
|
// 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
|
|
|
|
const $searchBtn = $( '.wikiEditor-ui .group-search a' );
|
|
|
|
this.$oldSearchBtn = $searchBtn.clone( true );
|
|
|
|
$searchBtn.off( 'click keydown keypress' )
|
|
|
|
.on( 'click keydown', ( e ) => {
|
|
|
|
if ( e.type === 'click' || ( e.type === 'keydown' && e.key === 'Enter' ) ) {
|
|
|
|
openSearchPanel( this.view );
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
2024-03-19 03:10:11 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
* @stable to use
|
|
|
|
*/
|
2023-10-10 20:11:15 +00:00
|
|
|
mw.hook( 'ext.CodeMirror.switch' ).fire( true, $( this.view.dom ) );
|
2023-09-19 17:59:29 +00:00
|
|
|
}
|
2024-02-28 05:14:04 +00:00
|
|
|
|
2024-03-21 21:16:04 +00:00
|
|
|
/**
|
|
|
|
* Adds the Realtime Preview handler. Realtime Preview reads from the textarea
|
|
|
|
* via jQuery.textSelection, which will bubble up to CodeMirror automatically.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
addRealtimePreviewHandler() {
|
|
|
|
mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).add( ( realtimePreview ) => {
|
|
|
|
this.realtimePreviewHandler = realtimePreview.getEventHandler().bind( realtimePreview );
|
|
|
|
} );
|
|
|
|
mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).add( () => {
|
|
|
|
this.realtimePreviewHandler = null;
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2023-09-19 17:59:29 +00:00
|
|
|
/**
|
2024-03-19 03:10:11 +00:00
|
|
|
* Adds the CodeMirror button to WikiEditor.
|
|
|
|
*
|
|
|
|
* @stable to call
|
2023-09-19 17:59:29 +00:00
|
|
|
*/
|
|
|
|
addCodeMirrorToWikiEditor() {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.$textarea.wikiEditor(
|
|
|
|
'addToToolbar',
|
|
|
|
{
|
|
|
|
section: 'main',
|
|
|
|
groups: {
|
|
|
|
codemirror: {
|
|
|
|
tools: {
|
|
|
|
CodeMirror: {
|
2024-08-09 06:04:19 +00:00
|
|
|
type: 'element',
|
|
|
|
element: () => {
|
|
|
|
// OOUI has already been loaded by WikiEditor.
|
|
|
|
const button = new OO.ui.ToggleButtonWidget( {
|
|
|
|
label: mw.msg( 'codemirror-toggle-label-short' ),
|
|
|
|
icon: 'syntax-highlight',
|
|
|
|
value: this.useCodeMirror,
|
|
|
|
framed: false,
|
|
|
|
classes: [ 'tool', 'cm-mw-toggle-wikieditor' ]
|
|
|
|
} );
|
|
|
|
button.on( 'change', this.switchCodeMirror.bind( this ) );
|
|
|
|
return button.$element;
|
2023-09-19 17:59:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const $codeMirrorButton = toolbar.$toolbar.find( '.tool[rel=CodeMirror]' );
|
2024-01-10 00:02:44 +00:00
|
|
|
$codeMirrorButton.attr( 'id', 'mw-editbutton-codemirror' );
|
|
|
|
|
|
|
|
// Hide non-applicable buttons until WikiEditor better supports a read-only mode (T188817).
|
|
|
|
if ( this.readOnly ) {
|
|
|
|
this.$textarea.data( 'wikiEditor-context' ).$ui.addClass( 'ext-codemirror-readonly' );
|
|
|
|
}
|
2023-09-19 17:59:29 +00:00
|
|
|
|
|
|
|
if ( this.useCodeMirror ) {
|
|
|
|
this.enableCodeMirror();
|
|
|
|
}
|
|
|
|
this.updateToolbarButton();
|
|
|
|
|
2024-06-08 03:10:11 +00:00
|
|
|
CodeMirror.logUsage( {
|
2023-09-19 17:59:29 +00:00
|
|
|
editor: 'wikitext',
|
|
|
|
enabled: this.useCodeMirror,
|
|
|
|
toggled: false,
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector,camelcase
|
|
|
|
edit_start_ts_ms: parseInt( $( 'input[name="wpStarttime"]' ).val(), 10 ) * 1000 || 0
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-03-19 03:10:11 +00:00
|
|
|
* Updates CodeMirror button on the toolbar according to the current state (on/off).
|
|
|
|
*
|
|
|
|
* @private
|
2023-09-19 17:59:29 +00:00
|
|
|
*/
|
|
|
|
updateToolbarButton() {
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
const $button = $( '#mw-editbutton-codemirror' );
|
|
|
|
$button.toggleClass( 'mw-editbutton-codemirror-active', this.useCodeMirror );
|
|
|
|
|
|
|
|
// WikiEditor2010 OOUI ToggleButtonWidget
|
|
|
|
if ( $button.data( 'setActive' ) ) {
|
|
|
|
$button.data( 'setActive' )( this.useCodeMirror );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-03-19 03:10:11 +00:00
|
|
|
* Enables or disables CodeMirror.
|
|
|
|
*
|
|
|
|
* @fires CodeMirrorWikiEditor~'ext.CodeMirror.switch'
|
|
|
|
* @stable to call
|
2023-09-19 17:59:29 +00:00
|
|
|
*/
|
|
|
|
switchCodeMirror() {
|
|
|
|
if ( this.view ) {
|
|
|
|
this.setCodeMirrorPreference( false );
|
2024-03-20 00:58:50 +00:00
|
|
|
this.destroy();
|
2024-08-09 23:04:15 +00:00
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
$( '.wikiEditor-ui .group-search a' ).replaceWith( this.$oldSearchBtn );
|
2023-10-10 20:11:15 +00:00
|
|
|
mw.hook( 'ext.CodeMirror.switch' ).fire( false, this.$textarea );
|
2023-09-19 17:59:29 +00:00
|
|
|
} else {
|
|
|
|
this.enableCodeMirror();
|
|
|
|
this.setCodeMirrorPreference( true );
|
|
|
|
}
|
|
|
|
this.updateToolbarButton();
|
|
|
|
|
2024-06-08 03:10:11 +00:00
|
|
|
CodeMirror.logUsage( {
|
2023-09-19 17:59:29 +00:00
|
|
|
editor: 'wikitext',
|
|
|
|
enabled: this.useCodeMirror,
|
|
|
|
toggled: true,
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector,camelcase
|
|
|
|
edit_start_ts_ms: parseInt( $( 'input[name="wpStarttime"]' ).val(), 10 ) * 1000 || 0
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
}
|
2024-03-19 03:10:11 +00:00
|
|
|
|
Isolate build step to CM6 library and restructure files to work with RL
CodeMirror 6 requires the use of NPM, but we can still bundle all CM
packages into one file, and then everything else (i.e. our code) is
managed by ResourceLoader as per usual. This makes contribution
considerably easier as we no longer need a build step for each change.
CM5 files are now under resources/legacy, and the CM6 files are moved to
the root of the resources/ directory. Only one file,
codemirror.bundle.js, is managed by Rollup, while everything else is RL.
The Rollup output for now is put under resources/lib/ alongside the CM5
upstream files.
This patch is *mostly* renames of files, along with changing ECMAScript
Module (ESM) syntax into the CommonJS style that ResourceLoader prefers.
We also remove more modern JS syntax (i.e. private class methods) that
we were able to use before because we had a build step with Babel.
This patch should effectively make no user-facing changes, or to the
ResourceLoader modules we offer in Extension:CodeMirror.
Finally, bump version in extension.json to 6, to match the upstream lib,
and add Bhsd as an author :-)
Bug: T368053
Change-Id: Ie258e49f5df8db23a7344ac3c4c9300aaa991042
2024-06-21 03:21:09 +00:00
|
|
|
module.exports = CodeMirrorWikiEditor;
|