2023-08-17 04:58:59 +00:00
|
|
|
function init() {
|
2024-06-14 11:53:32 +00:00
|
|
|
const extCodeMirror = require( 'ext.CodeMirror' );
|
|
|
|
let codeMirror, $textbox1, realtimePreviewHandler;
|
2023-08-17 04:58:59 +00:00
|
|
|
|
2024-06-14 11:53:32 +00:00
|
|
|
let useCodeMirror = mw.user.options.get( 'usecodemirror' ) > 0;
|
2023-08-17 04:58:59 +00:00
|
|
|
|
2024-06-14 11:53:32 +00:00
|
|
|
const originHooksTextarea = $.valHooks.textarea;
|
2023-08-17 04:58:59 +00:00
|
|
|
// define jQuery hook for searching and replacing text using JS if CodeMirror is enabled, see Bug: T108711
|
|
|
|
$.valHooks.textarea = {
|
|
|
|
get: function ( elem ) {
|
|
|
|
if ( elem.id === 'wpTextbox1' && codeMirror ) {
|
|
|
|
return codeMirror.doc.getValue();
|
|
|
|
} else if ( originHooksTextarea ) {
|
|
|
|
return originHooksTextarea.get( elem );
|
|
|
|
}
|
|
|
|
return elem.value;
|
|
|
|
},
|
|
|
|
set: function ( elem, value ) {
|
|
|
|
if ( elem.id === 'wpTextbox1' && codeMirror ) {
|
|
|
|
return codeMirror.doc.setValue( value );
|
|
|
|
} else if ( originHooksTextarea ) {
|
|
|
|
return originHooksTextarea.set( elem, value );
|
|
|
|
}
|
|
|
|
elem.value = value;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// jQuery.textSelection overrides for CodeMirror.
|
|
|
|
// See jQuery.textSelection.js for method documentation
|
2024-06-14 11:53:32 +00:00
|
|
|
const cmTextSelection = {
|
2023-08-17 04:58:59 +00:00
|
|
|
getContents: function () {
|
|
|
|
return codeMirror.doc.getValue();
|
|
|
|
},
|
|
|
|
setContents: function ( content ) {
|
|
|
|
codeMirror.doc.setValue( content );
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
getSelection: function () {
|
|
|
|
return codeMirror.doc.getSelection();
|
|
|
|
},
|
|
|
|
setSelection: function ( options ) {
|
|
|
|
codeMirror.doc.setSelection(
|
|
|
|
codeMirror.doc.posFromIndex( options.start ),
|
|
|
|
codeMirror.doc.posFromIndex( options.end )
|
|
|
|
);
|
|
|
|
codeMirror.focus();
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
replaceSelection: function ( value ) {
|
|
|
|
codeMirror.doc.replaceSelection( value );
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
getCaretPosition: function ( options ) {
|
2024-06-14 11:53:32 +00:00
|
|
|
const caretPos = codeMirror.doc.indexFromPos( codeMirror.doc.getCursor( true ) ),
|
2023-08-17 04:58:59 +00:00
|
|
|
endPos = codeMirror.doc.indexFromPos( codeMirror.doc.getCursor( false ) );
|
|
|
|
if ( options.startAndEnd ) {
|
|
|
|
return [ caretPos, endPos ];
|
|
|
|
}
|
|
|
|
return caretPos;
|
|
|
|
},
|
|
|
|
scrollToCaretPosition: function () {
|
|
|
|
codeMirror.scrollIntoView( null );
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save CodeMirror enabled pref.
|
|
|
|
*
|
|
|
|
* @param {boolean} prefValue True, if CodeMirror should be enabled by default, otherwise false.
|
|
|
|
*/
|
|
|
|
function setCodeEditorPreference( prefValue ) {
|
|
|
|
useCodeMirror = prefValue; // Save state for function updateToolbarButton()
|
|
|
|
extCodeMirror.setCodeEditorPreference( prefValue );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {boolean}
|
|
|
|
*/
|
|
|
|
function isLineNumbering() {
|
|
|
|
// T285660: Backspace related bug on Android browsers as of 2021
|
|
|
|
if ( /Android\b/.test( navigator.userAgent ) ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-06-14 11:53:32 +00:00
|
|
|
const namespaces = mw.config.get( 'wgCodeMirrorLineNumberingNamespaces' );
|
2023-08-17 04:58:59 +00:00
|
|
|
// Set to [] to disable everywhere, or null to enable everywhere
|
|
|
|
return !namespaces ||
|
|
|
|
namespaces.indexOf( mw.config.get( 'wgNamespaceNumber' ) ) !== -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Keep these modules in sync with MediaWiki\Extension\CodeMirror\Hooks.php
|
2024-06-14 11:53:32 +00:00
|
|
|
const codeMirrorCoreModules = [
|
2023-08-17 04:58:59 +00:00
|
|
|
'ext.CodeMirror.lib',
|
|
|
|
'ext.CodeMirror.mode.mediawiki'
|
|
|
|
];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the size of the CodeMirror element,
|
|
|
|
* and react to changes coming from WikiEditor (including Realtime Preview if its enabled).
|
|
|
|
*/
|
|
|
|
function setupSizing() {
|
2024-06-14 11:53:32 +00:00
|
|
|
const $codeMirror = $( codeMirror.getWrapperElement() );
|
2023-08-17 04:58:59 +00:00
|
|
|
|
2024-03-18 21:22:09 +00:00
|
|
|
// Only add resizing corner if realtime preview is enabled,
|
2023-08-17 04:58:59 +00:00
|
|
|
// because that feature provides height resizing (even when preview isn't used).
|
|
|
|
if ( mw.loader.getState( 'ext.wikiEditor.realtimepreview' ) === 'ready' ) {
|
|
|
|
codeMirror.setSize( '100%', $textbox1.parent().height() );
|
|
|
|
}
|
2024-06-14 11:53:32 +00:00
|
|
|
const $resizableHandle = $codeMirror.find( '.ui-resizable-handle' );
|
|
|
|
mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).add( ( realtimePreview ) => {
|
2023-08-17 04:58:59 +00:00
|
|
|
// CodeMirror may have been turned on and then off again before realtimepreview is enabled, in which case it will be null.
|
|
|
|
if ( !codeMirror ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Get rid of the corner resize handle, because realtimepreview provides its own.
|
|
|
|
$resizableHandle.hide();
|
|
|
|
// Add listener for CodeMirror changes.
|
|
|
|
realtimePreviewHandler = realtimePreview.getEventHandler().bind( realtimePreview );
|
|
|
|
codeMirror.on( 'change', realtimePreviewHandler );
|
|
|
|
// Fix the width and height of the CodeMirror area.
|
|
|
|
codeMirror.setSize( '100%', realtimePreview.twoPaneLayout.$element.height() );
|
|
|
|
} );
|
2024-06-14 11:53:32 +00:00
|
|
|
mw.hook( 'ext.WikiEditor.realtimepreview.resize' ).add( ( resizingBar ) => {
|
2023-08-17 04:58:59 +00:00
|
|
|
// CodeMirror may have been turned off after realtimepreview was opened, in which case it will be null.
|
|
|
|
if ( !codeMirror ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Keep in sync with the height of the pane.
|
|
|
|
codeMirror.setSize( '100%', resizingBar.getResizedPane().height() );
|
|
|
|
} );
|
2024-06-14 11:53:32 +00:00
|
|
|
mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).add( () => {
|
2023-08-17 04:58:59 +00:00
|
|
|
// Re-show the corner resize handle.
|
|
|
|
$resizableHandle.show();
|
|
|
|
// CodeMirror may have been turned off after realtimepreview was opened, in which case it will be null.
|
|
|
|
if ( !codeMirror ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
codeMirror.refresh(); // T305333
|
|
|
|
codeMirror.off( 'change', realtimePreviewHandler );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces the default textarea with CodeMirror
|
|
|
|
*/
|
|
|
|
function enableCodeMirror() {
|
2024-06-14 11:53:32 +00:00
|
|
|
const config = mw.config.get( 'extCodeMirrorConfig' );
|
|
|
|
mw.loader.using( codeMirrorCoreModules.concat( config.pluginModules ), () => {
|
2024-06-14 11:57:43 +00:00
|
|
|
const selectionStart = $textbox1.prop( 'selectionStart' ),
|
2023-08-17 04:58:59 +00:00
|
|
|
selectionEnd = $textbox1.prop( 'selectionEnd' ),
|
|
|
|
scrollTop = $textbox1.scrollTop(),
|
|
|
|
hasFocus = $textbox1.is( ':focus' );
|
|
|
|
|
|
|
|
// If CodeMirror is already loaded or wikEd gadget is enabled, abort. See T178348.
|
|
|
|
// FIXME: Would be good to replace the wikEd check with something more generic.
|
|
|
|
if ( codeMirror || mw.user.options.get( 'gadget-wikEd' ) > 0 ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// T174055: Do not redefine the browser history navigation keys (T175378: for PC only)
|
|
|
|
CodeMirror.keyMap.pcDefault[ 'Alt-Left' ] = false;
|
|
|
|
CodeMirror.keyMap.pcDefault[ 'Alt-Right' ] = false;
|
|
|
|
|
2024-06-14 11:57:43 +00:00
|
|
|
const cmOptions = {
|
2023-08-17 04:58:59 +00:00
|
|
|
mwConfig: config,
|
|
|
|
// styleActiveLine: true, // disabled since Bug: T162204, maybe should be optional
|
|
|
|
lineWrapping: true,
|
|
|
|
lineNumbers: isLineNumbering(),
|
|
|
|
readOnly: $textbox1[ 0 ].readOnly,
|
|
|
|
// select mediawiki as text input mode
|
|
|
|
mode: 'text/mediawiki',
|
|
|
|
extraKeys: {
|
|
|
|
Tab: false,
|
|
|
|
'Shift-Tab': false,
|
|
|
|
// T174514: Move the cursor at the beginning/end of the current wrapped line
|
|
|
|
Home: 'goLineLeft',
|
|
|
|
End: 'goLineRight'
|
|
|
|
},
|
|
|
|
inputStyle: 'contenteditable',
|
|
|
|
spellcheck: true,
|
|
|
|
viewportMargin: Infinity
|
|
|
|
};
|
|
|
|
|
|
|
|
cmOptions.matchBrackets = {
|
|
|
|
highlightNonMatching: false,
|
|
|
|
maxHighlightLineLength: 10000
|
|
|
|
};
|
|
|
|
|
|
|
|
codeMirror = CodeMirror.fromTextArea( $textbox1[ 0 ], cmOptions );
|
2024-06-14 11:57:43 +00:00
|
|
|
const $codeMirror = $( codeMirror.getWrapperElement() );
|
2023-08-17 04:58:59 +00:00
|
|
|
|
2024-06-14 11:53:32 +00:00
|
|
|
codeMirror.on( 'focus', () => {
|
2024-04-10 15:54:37 +00:00
|
|
|
$textbox1[ 0 ].dispatchEvent( new Event( 'focus' ) );
|
2023-08-17 04:58:59 +00:00
|
|
|
} );
|
2024-06-14 11:53:32 +00:00
|
|
|
codeMirror.on( 'blur', () => {
|
2024-04-10 15:54:37 +00:00
|
|
|
$textbox1[ 0 ].dispatchEvent( new Event( 'blur' ) );
|
2023-08-17 04:58:59 +00:00
|
|
|
} );
|
2024-10-11 20:47:51 +00:00
|
|
|
codeMirror.on( 'keydown', ( _, e ) => {
|
|
|
|
if ( e.ctrlKey || e.metaKey ) {
|
|
|
|
// Possibly a WikiEditor keyboard shortcut
|
|
|
|
if ( !$textbox1[ 0 ].dispatchEvent( new KeyboardEvent( 'keydown', e ) ) ) {
|
|
|
|
// If it was actually a WikiEditor keyboard shortcut, the default would be prevented
|
|
|
|
// for the dispatched event and hence dispatchEvent() would return false
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} );
|
2024-06-14 11:53:32 +00:00
|
|
|
mw.hook( 'editRecovery.loadEnd' ).add( ( data ) => {
|
2023-08-17 04:58:59 +00:00
|
|
|
codeMirror.on( 'change', data.fieldChangeHandler );
|
|
|
|
} );
|
|
|
|
|
|
|
|
// Allow textSelection() functions to work with CodeMirror editing field.
|
|
|
|
$codeMirror.textSelection( 'register', cmTextSelection );
|
2023-09-19 17:59:29 +00:00
|
|
|
// Also override textSelection() functions for the "real" hidden textarea to route to CodeMirror.
|
|
|
|
// We unregister this when switching to normal textarea mode.
|
2023-08-17 04:58:59 +00:00
|
|
|
$textbox1.textSelection( 'register', cmTextSelection );
|
|
|
|
|
|
|
|
setupSizing( $textbox1, codeMirror );
|
|
|
|
|
|
|
|
if ( hasFocus ) {
|
|
|
|
codeMirror.focus();
|
|
|
|
}
|
|
|
|
codeMirror.doc.setSelection( codeMirror.doc.posFromIndex( selectionEnd ), codeMirror.doc.posFromIndex( selectionStart ) );
|
|
|
|
codeMirror.scrollTo( null, scrollTop );
|
|
|
|
|
|
|
|
// HACK: <textarea> font size varies by browser (chrome/FF/IE)
|
|
|
|
$codeMirror.css( {
|
|
|
|
'font-size': $textbox1.css( 'font-size' ),
|
|
|
|
'line-height': $textbox1.css( 'line-height' )
|
|
|
|
} );
|
|
|
|
|
|
|
|
// use direction and language of the original textbox
|
|
|
|
$codeMirror.attr( {
|
|
|
|
dir: $textbox1.attr( 'dir' ),
|
|
|
|
lang: $textbox1.attr( 'lang' )
|
|
|
|
} );
|
|
|
|
|
|
|
|
$( codeMirror.getInputField() )
|
|
|
|
// T259347: Use accesskey of the original textbox
|
|
|
|
.attr( 'accesskey', $textbox1.attr( 'accesskey' ) )
|
|
|
|
// T194102: UniversalLanguageSelector integration is buggy, disabling it completely
|
|
|
|
.addClass( 'noime' );
|
|
|
|
|
|
|
|
if ( mw.user.options.get( 'usecodemirror-colorblind' ) ) {
|
|
|
|
$codeMirror.addClass( 'cm-mw-colorblind-colors' );
|
|
|
|
}
|
|
|
|
|
|
|
|
// T305333: Reload CodeMirror to fix a cursor caret issue.
|
|
|
|
codeMirror.refresh();
|
|
|
|
|
|
|
|
mw.hook( 'ext.CodeMirror.switch' ).fire( true, $codeMirror );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates CodeMirror button on the toolbar according to the current state (on/off)
|
|
|
|
*/
|
|
|
|
function updateToolbarButton() {
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
2024-06-14 11:53:32 +00:00
|
|
|
const $button = $( '#mw-editbutton-codemirror' );
|
2023-08-17 04:58:59 +00:00
|
|
|
|
|
|
|
$button.toggleClass( 'mw-editbutton-codemirror-active', !!useCodeMirror );
|
|
|
|
|
|
|
|
// WikiEditor2010 OOUI ToggleButtonWidget
|
|
|
|
if ( $button.data( 'setActive' ) ) {
|
|
|
|
$button.data( 'setActive' )( !!useCodeMirror );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enables or disables CodeMirror
|
|
|
|
*/
|
|
|
|
function switchCodeMirror() {
|
2024-06-14 11:53:32 +00:00
|
|
|
let selectionObj, selectionStart, selectionEnd, scrollTop, hasFocus, $codeMirror;
|
2023-08-17 04:58:59 +00:00
|
|
|
|
|
|
|
if ( codeMirror ) {
|
|
|
|
scrollTop = codeMirror.getScrollInfo().top;
|
|
|
|
selectionObj = codeMirror.doc.listSelections()[ 0 ];
|
|
|
|
selectionStart = codeMirror.doc.indexFromPos( selectionObj.head );
|
|
|
|
selectionEnd = codeMirror.doc.indexFromPos( selectionObj.anchor );
|
|
|
|
hasFocus = codeMirror.hasFocus();
|
|
|
|
$codeMirror = $( codeMirror.getWrapperElement() );
|
|
|
|
setCodeEditorPreference( false );
|
|
|
|
$codeMirror.textSelection( 'unregister' );
|
|
|
|
$textbox1.textSelection( 'unregister' );
|
|
|
|
codeMirror.toTextArea();
|
|
|
|
codeMirror = null;
|
|
|
|
if ( hasFocus ) {
|
|
|
|
$textbox1.trigger( 'focus' );
|
|
|
|
}
|
|
|
|
$textbox1.prop( 'selectionStart', selectionStart );
|
|
|
|
$textbox1.prop( 'selectionEnd', selectionEnd );
|
|
|
|
$textbox1.scrollTop( scrollTop );
|
|
|
|
|
|
|
|
mw.hook( 'ext.CodeMirror.switch' ).fire( false, $textbox1 );
|
|
|
|
} else {
|
|
|
|
enableCodeMirror();
|
|
|
|
setCodeEditorPreference( true );
|
|
|
|
}
|
|
|
|
updateToolbarButton();
|
|
|
|
|
|
|
|
extCodeMirror.logUsage( {
|
|
|
|
editor: 'wikitext',
|
|
|
|
enabled: codeMirror !== null,
|
|
|
|
toggled: true,
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector,camelcase
|
|
|
|
edit_start_ts_ms: parseInt( $( 'input[name="wpStarttime"]' ).val() ) * 1000 || 0
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds the CodeMirror button to WikiEditor
|
|
|
|
*/
|
|
|
|
function addCodeMirrorToWikiEditor() {
|
2024-06-14 11:57:43 +00:00
|
|
|
const context = $textbox1.data( 'wikiEditor-context' ),
|
2023-08-17 04:58:59 +00:00
|
|
|
toolbar = context && context.modules && context.modules.toolbar;
|
|
|
|
|
|
|
|
// Guard against something having removed WikiEditor (T271457)
|
|
|
|
if ( !toolbar ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$textbox1.wikiEditor(
|
|
|
|
'addToToolbar',
|
|
|
|
{
|
|
|
|
section: 'main',
|
|
|
|
groups: {
|
|
|
|
codemirror: {
|
|
|
|
tools: {
|
|
|
|
CodeMirror: {
|
|
|
|
label: mw.msg( 'codemirror-toggle-label' ),
|
|
|
|
type: 'toggle',
|
|
|
|
oouiIcon: 'highlight',
|
|
|
|
action: {
|
|
|
|
type: 'callback',
|
|
|
|
execute: function () {
|
|
|
|
switchCodeMirror();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2024-06-14 11:57:43 +00:00
|
|
|
const $codeMirrorButton = toolbar.$toolbar.find( '.tool[rel=CodeMirror]' );
|
2023-08-17 04:58:59 +00:00
|
|
|
$codeMirrorButton
|
|
|
|
.attr( 'id', 'mw-editbutton-codemirror' );
|
|
|
|
|
|
|
|
if ( useCodeMirror ) {
|
|
|
|
enableCodeMirror();
|
|
|
|
}
|
|
|
|
updateToolbarButton();
|
|
|
|
|
|
|
|
extCodeMirror.logUsage( {
|
|
|
|
editor: 'wikitext',
|
|
|
|
enabled: useCodeMirror,
|
|
|
|
toggled: false,
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector,camelcase
|
|
|
|
edit_start_ts_ms: parseInt( $( 'input[name="wpStarttime"]' ).val() ) * 1000 || 0
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2024-03-15 12:39:30 +00:00
|
|
|
// Add CodeMirror button to the enhanced editing toolbar.
|
2024-06-14 11:53:32 +00:00
|
|
|
mw.hook( 'wikiEditor.toolbarReady' ).add( ( $textarea ) => {
|
2024-03-15 12:39:30 +00:00
|
|
|
$textbox1 = $textarea;
|
|
|
|
addCodeMirrorToWikiEditor();
|
2023-08-17 04:58:59 +00:00
|
|
|
} );
|
|
|
|
|
|
|
|
// Synchronize textarea with CodeMirror before leaving
|
2024-06-14 11:53:32 +00:00
|
|
|
window.addEventListener( 'beforeunload', () => {
|
2023-08-17 04:58:59 +00:00
|
|
|
if ( codeMirror ) {
|
|
|
|
codeMirror.save();
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( mw.loader.getState( 'ext.wikiEditor' ) ) {
|
|
|
|
init();
|
|
|
|
}
|