Merge "Convert applicable ResourceLoader modules to use packageFiles"

This commit is contained in:
jenkins-bot 2023-08-24 22:21:10 +00:00 committed by Gerrit Code Review
commit 2738060873
9 changed files with 463 additions and 443 deletions

1
.gitignore vendored
View file

@ -7,3 +7,4 @@
/vendor/
/composer.lock
.eslintcache
.env

View file

@ -32,16 +32,29 @@
"ResourceModules": {
"ext.CodeMirror": {
"dependencies": [
"ext.CodeMirror.data",
"jquery.textSelection",
"mediawiki.api",
"mediawiki.user",
"user.options"
],
"packageFiles": [
"ext.CodeMirror.js",
{
"name": "ext.CodeMirror.data.js",
"callback": "MediaWiki\\Extension\\CodeMirror\\DataScript::makeScript"
}
]
},
"ext.CodeMirror.WikiEditor": {
"dependencies": [
"ext.CodeMirror",
"jquery.textSelection",
"mediawiki.user",
"user.options",
"jquery.ui",
"oojs-ui.styles.icons-editing-styling"
],
"scripts": [
"ext.CodeMirror.js"
"packageFiles": [
"ext.CodeMirror.WikiEditor.js"
],
"styles": [
"ext.CodeMirror.less"
@ -50,14 +63,6 @@
"codemirror-toggle-label"
]
},
"ext.CodeMirror.data": {
"scripts": [
{
"name": "ext.CodeMirror.data.js",
"callback": "MediaWiki\\Extension\\CodeMirror\\DataScript::makeScript"
}
]
},
"ext.CodeMirror.lib": {
"scripts": [
"lib/codemirror/lib/codemirror.js"
@ -76,7 +81,7 @@
]
},
"ext.CodeMirror.mode.mediawiki": {
"scripts": "mode/mediawiki/mediawiki.js",
"packageFiles": "mode/mediawiki/mediawiki.js",
"styles": [
"mode/mediawiki/mediawiki.less",
"mode/mediawiki/colorblind-colors.less"
@ -131,7 +136,8 @@
"ext.visualEditor.mwcore",
"ext.visualEditor.mwmeta",
"mediawiki.api",
"user.options"
"user.options",
"ext.CodeMirror"
],
"scripts": [
"modules/ve-cm/ve.ui.CodeMirrorAction.js",
@ -153,13 +159,13 @@
"usecodemirror": 0
},
"QUnitTestModule": {
"localBasePath": "",
"remoteExtPath": "CodeMirror",
"scripts": [
"resources/mode/mediawiki/tests/qunit/CodeMirror.mediawiki.test.js"
"localBasePath": "resources/mode/mediawiki/tests",
"remoteExtPath": "CodeMirror/resources/mode/mediawiki/tests",
"packageFiles": [
"qunit/CodeMirror.mediawiki.test.js"
],
"dependencies": [
"ext.CodeMirror.data",
"ext.CodeMirror",
"ext.CodeMirror.lib",
"ext.CodeMirror.mode.mediawiki"
]

View file

@ -61,7 +61,7 @@ class Hooks implements
*/
public function onBeforePageDisplay( $out, $skin ): void {
if ( $this->isCodeMirrorOnPage( $out ) ) {
$out->addModules( 'ext.CodeMirror' );
$out->addModules( 'ext.CodeMirror.WikiEditor' );
if ( $this->userOptionsLookup->getOption( $out->getUser(), 'usecodemirror' ) ) {
// These modules are predelivered for performance when needed

View file

@ -9,6 +9,7 @@
"CodeMirror": "readonly"
},
"rules": {
"max-len": "off"
"max-len": "off",
"no-implicit-globals": "off"
}
}

View file

@ -0,0 +1,388 @@
function init() {
var extCodeMirror = require( 'ext.CodeMirror' );
var codeMirror, $textbox1, realtimePreviewHandler;
var useCodeMirror = mw.user.options.get( 'usecodemirror' ) > 0;
var originHooksTextarea = $.valHooks.textarea;
// 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
var cmTextSelection = {
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 ) {
var caretPos = codeMirror.doc.indexFromPos( codeMirror.doc.getCursor( true ) ),
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;
}
var namespaces = mw.config.get( 'wgCodeMirrorLineNumberingNamespaces' );
// 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
var codeMirrorCoreModules = [
'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() {
var $codeMirror = $( codeMirror.getWrapperElement() );
// Only add jQuery UI's resizing corner if realtime preview is not enabled,
// 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() );
} else {
// RL module jquery.ui
$codeMirror.resizable( {
handles: 'se',
resize: function () {
// Keep at 100% parent width, don't modify height here
codeMirror.setSize( '100%', null );
}
} );
// Match the height of the textarea.
codeMirror.setSize( null, $textbox1.height() );
}
var $resizableHandle = $codeMirror.find( '.ui-resizable-handle' );
mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).add( function ( realtimePreview ) {
// 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() );
} );
mw.hook( 'ext.WikiEditor.realtimepreview.resize' ).add( function ( resizingBar ) {
// 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() );
} );
mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).add( function () {
// 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() {
var config = mw.config.get( 'extCodeMirrorConfig' );
mw.loader.using( codeMirrorCoreModules.concat( config.pluginModules ), function () {
var $codeMirror, cmOptions,
selectionStart = $textbox1.prop( 'selectionStart' ),
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;
cmOptions = {
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 );
$codeMirror = $( codeMirror.getWrapperElement() );
codeMirror.on( 'focus', function () {
$textbox1.triggerHandler( 'focus' );
} );
codeMirror.on( 'blur', function () {
$textbox1.triggerHandler( 'blur' );
} );
mw.hook( 'editRecovery.loadEnd' ).add( function ( data ) {
codeMirror.on( 'change', data.fieldChangeHandler );
} );
// Allow textSelection() functions to work with CodeMirror editing field.
$codeMirror.textSelection( 'register', cmTextSelection );
// Also override textSelection() functions for the "real" hidden textarea to route to
// CodeMirror. We unregister this when switching to normal textarea mode.
$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
var $button = $( '#mw-editbutton-codemirror' );
$button.toggleClass( 'mw-editbutton-codemirror-active', !!useCodeMirror );
// WikiEditor2010 OOUI ToggleButtonWidget
if ( $button.data( 'setActive' ) ) {
$button.data( 'setActive' )( !!useCodeMirror );
}
}
/**
* Enables or disables CodeMirror
*/
function switchCodeMirror() {
var selectionObj, selectionStart, selectionEnd, scrollTop, hasFocus, $codeMirror;
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() {
var $codeMirrorButton,
context = $textbox1.data( 'wikiEditor-context' ),
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();
}
}
}
}
}
}
}
);
$codeMirrorButton = toolbar.$toolbar.find( '.tool[rel=CodeMirror]' );
$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
} );
}
$( function () {
// Add CodeMirror button to the enhanced editing toolbar.
mw.hook( 'wikiEditor.toolbarReady' ).add( function ( $textarea ) {
$textbox1 = $textarea;
addCodeMirrorToWikiEditor();
} );
} );
// Synchronize textarea with CodeMirror before leaving
window.addEventListener( 'beforeunload', function () {
if ( codeMirror ) {
codeMirror.save();
}
} );
}
if ( mw.loader.getState( 'ext.wikiEditor' ) ) {
init();
}

View file

@ -1,408 +1,40 @@
( function () {
var codeMirror, $textbox1, realtimePreviewHandler;
require( './ext.CodeMirror.data.js' );
// Exit if WikiEditor is disabled
if ( !mw.loader.getState( 'ext.wikiEditor' ) ) {
/**
* Log usage of CodeMirror.
*
* @param {Object} data
*/
function logUsage( data ) {
var event, editCountBucket;
/* eslint-disable camelcase */
event = $.extend( {
session_token: mw.user.sessionId(),
user_id: mw.user.getId()
}, data );
editCountBucket = mw.config.get( 'wgUserEditCountBucket' );
if ( editCountBucket !== null ) {
event.user_edit_count_bucket = editCountBucket;
}
/* eslint-enable camelcase */
mw.track( 'event.CodeMirrorUsage', event );
}
/**
* Save CodeMirror enabled preference.
*
* @param {boolean} prefValue True, if CodeMirror should be enabled by default, otherwise false.
*/
function setCodeEditorPreference( prefValue ) {
if ( !mw.user.isNamed() ) { // Skip it for unnamed users
return;
}
new mw.Api().saveOption( 'usecodemirror', prefValue ? 1 : 0 );
mw.user.options.set( 'usecodemirror', prefValue ? 1 : 0 );
}
var useCodeMirror = mw.user.options.get( 'usecodemirror' ) > 0;
var api = new mw.Api();
var originHooksTextarea = $.valHooks.textarea;
// 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
var cmTextSelection = {
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 ) {
var caretPos = codeMirror.doc.indexFromPos( codeMirror.doc.getCursor( true ) ),
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()
if ( !mw.user.isNamed() ) { // Skip it for unnamed users
return;
}
api.saveOption( 'usecodemirror', prefValue ? 1 : 0 );
mw.user.options.set( 'usecodemirror', prefValue ? 1 : 0 );
}
/**
* @return {boolean}
*/
function isLineNumbering() {
// T285660: Backspace related bug on Android browsers as of 2021
if ( /Android\b/.test( navigator.userAgent ) ) {
return false;
}
var namespaces = mw.config.get( 'wgCodeMirrorLineNumberingNamespaces' );
// 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 CodeMirrorHooks.php
var codeMirrorCoreModules = [
'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() {
var $codeMirror = $( codeMirror.getWrapperElement() );
// Only add jQuery UI's resizing corner if realtime preview is not enabled,
// 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() );
} else {
// RL module jquery.ui
$codeMirror.resizable( {
handles: 'se',
resize: function () {
// Keep at 100% parent width, don't modify height here
codeMirror.setSize( '100%', null );
}
} );
// Match the height of the textarea.
codeMirror.setSize( null, $textbox1.height() );
}
var $resizableHandle = $codeMirror.find( '.ui-resizable-handle' );
mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).add( function ( realtimePreview ) {
// 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() );
} );
mw.hook( 'ext.WikiEditor.realtimepreview.resize' ).add( function ( resizingBar ) {
// 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() );
} );
mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).add( function () {
// 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() {
var config = mw.config.get( 'extCodeMirrorConfig' );
mw.loader.using( codeMirrorCoreModules.concat( config.pluginModules ), function () {
var $codeMirror, cmOptions,
selectionStart = $textbox1.prop( 'selectionStart' ),
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;
cmOptions = {
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 );
$codeMirror = $( codeMirror.getWrapperElement() );
codeMirror.on( 'focus', function () {
$textbox1.triggerHandler( 'focus' );
} );
codeMirror.on( 'blur', function () {
$textbox1.triggerHandler( 'blur' );
} );
mw.hook( 'editRecovery.loadEnd' ).add( function ( data ) {
codeMirror.on( 'change', data.fieldChangeHandler );
} );
// Allow textSelection() functions to work with CodeMirror editing field.
$codeMirror.textSelection( 'register', cmTextSelection );
// Also override textSelection() functions for the "real" hidden textarea to route to
// CodeMirror. We unregister this when switching to normal textarea mode.
$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 );
} );
}
function logUsage( data ) {
var event, editCountBucket;
/* eslint-disable camelcase */
event = $.extend( {
session_token: mw.user.sessionId(),
user_id: mw.user.getId()
}, data );
editCountBucket = mw.config.get( 'wgUserEditCountBucket' );
if ( editCountBucket !== null ) {
event.user_edit_count_bucket = editCountBucket;
}
/* eslint-enable camelcase */
mw.track( 'event.CodeMirrorUsage', event );
}
/**
* Updates CodeMirror button on the toolbar according to the current state (on/off)
*/
function updateToolbarButton() {
// eslint-disable-next-line no-jquery/no-global-selector
var $button = $( '#mw-editbutton-codemirror' );
$button.toggleClass( 'mw-editbutton-codemirror-active', !!useCodeMirror );
// WikiEditor2010 OOUI ToggleButtonWidget
if ( $button.data( 'setActive' ) ) {
$button.data( 'setActive' )( !!useCodeMirror );
}
}
/**
* Enables or disables CodeMirror
*/
function switchCodeMirror() {
var selectionObj, selectionStart, selectionEnd, scrollTop, hasFocus, $codeMirror;
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();
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() {
var $codeMirrorButton,
context = $textbox1.data( 'wikiEditor-context' ),
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();
}
}
}
}
}
}
}
);
$codeMirrorButton = toolbar.$toolbar.find( '.tool[rel=CodeMirror]' );
$codeMirrorButton
.attr( 'id', 'mw-editbutton-codemirror' );
if ( useCodeMirror ) {
enableCodeMirror();
}
updateToolbarButton();
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
} );
}
$( function () {
// Add CodeMirror button to the enhanced editing toolbar.
mw.hook( 'wikiEditor.toolbarReady' ).add( function ( $textarea ) {
$textbox1 = $textarea;
addCodeMirrorToWikiEditor();
} );
} );
// Synchronize textarea with CodeMirror before leaving
window.addEventListener( 'beforeunload', function () {
if ( codeMirror ) {
codeMirror.save();
}
} );
}() );
module.exports = {
logUsage: logUsage,
setCodeEditorPreference: setCodeEditorPreference
};

View file

@ -22,6 +22,7 @@
CodeMirror.defineMode( 'mediawiki', function ( config /* , parserConfig */ ) {
var mwConfig = config.mwConfig,
// eslint-disable-next-line security/detect-non-literal-regexp
urlProtocols = new RegExp( '^(?:' + mwConfig.urlProtocols + ')', 'i' ),
permittedHtmlTags = { b: true, bdi: true, del: true, i: true, ins: true,
u: true, font: true, big: true, small: true, sub: true, sup: true,
@ -419,6 +420,7 @@
function eatHtmlTagAttribute( name ) {
return function ( stream, state ) {
// eslint-disable-next-line security/detect-unsafe-regex
if ( stream.match( /^(?:"[^<">]*"|'[^<'>]*'|[^>/<{&~])+/ ) ) {
return makeLocalStyle( 'mw-htmltag-attribute', state );
}
@ -439,6 +441,7 @@
function eatExtTagAttribute( name ) {
return function ( stream, state ) {
// eslint-disable-next-line security/detect-unsafe-regex
if ( stream.match( /^(?:"[^">]*"|'[^'>]*'|[^>/<{&~])+/ ) ) {
return makeLocalStyle( 'mw-exttag-attribute mw-ext-' + name, state );
}
@ -464,6 +467,7 @@
var origString = false,
from = stream.pos,
to,
// eslint-disable-next-line security/detect-non-literal-regexp
pattern = new RegExp( '</' + name + '\\s*>', 'i' ),
m = pattern.exec( from ? stream.string.slice( from ) : stream.string );
@ -653,6 +657,7 @@
}
break;
case '=':
// eslint-disable-next-line security/detect-unsafe-regex
tmp = stream.match( /^(={0,5})(.+?(=\1\s*)(<!--(?!.*-->.*\S).*?)?)$/ );
if ( tmp ) { // Title
stream.backUp( tmp[ 2 ].length );
@ -752,6 +757,7 @@
return makeLocalStyle( 'mw-parserfunction-bracket', state );
}
// Check for parser function without '#'
// eslint-disable-next-line security/detect-unsafe-regex
name = stream.match( /^([^\s\u00a0}[\]<{'|&:]+)(:|[\s\u00a0]*)(\}\}?)?(.)?/ );
if ( name ) {
stream.backUp( name[ 0 ].length );

View file

@ -61,7 +61,6 @@ ve.ui.CodeMirrorAction.prototype.toggle = function ( enable ) {
if ( !surface.mirror && enable !== false ) {
surface.mirror = true;
mw.loader.using( [
'ext.CodeMirror.data',
'ext.CodeMirror.lib',
'ext.CodeMirror.mode.mediawiki',
'jquery.client'

View file

@ -12,6 +12,8 @@ ve.ui.CodeMirrorTool = function VeUiCodeMirrorTool() {
// Parent constructor
ve.ui.CodeMirrorTool.super.apply( this, arguments );
this.extCodeMirror = require( 'ext.CodeMirror' );
// Events
this.toolbar.connect( this, { surfaceChange: 'onSurfaceChange' } );
};
@ -30,21 +32,6 @@ ve.ui.CodeMirrorTool.static.group = 'utility';
ve.ui.CodeMirrorTool.static.commandName = 'codeMirror';
ve.ui.CodeMirrorTool.static.deactivateOnSelect = false;
// TODO: consolidate with code in ext.CodeMirror.js, see T272035
ve.ui.CodeMirrorTool.prototype.logUsage = function ( data ) {
/* eslint-disable camelcase */
var event = $.extend( {
session_token: mw.user.sessionId(),
user_id: mw.user.getId()
}, data );
var editCountBucket = mw.config.get( 'wgUserEditCountBucket' );
if ( editCountBucket !== null ) {
event.user_edit_count_bucket = editCountBucket;
}
/* eslint-enable camelcase */
mw.track( 'event.CodeMirrorUsage', event );
};
/**
* @inheritdoc
*/
@ -58,7 +45,7 @@ ve.ui.CodeMirrorTool.prototype.onSelect = function () {
new mw.Api().saveOption( 'usecodemirror', useCodeMirror ? 1 : 0 );
mw.user.options.set( 'usecodemirror', useCodeMirror ? 1 : 0 );
this.logUsage( {
this.extCodeMirror.logUsage( {
editor: 'wikitext-2017',
enabled: useCodeMirror,
toggled: true,
@ -82,7 +69,7 @@ ve.ui.CodeMirrorTool.prototype.onSurfaceChange = function ( oldSurface, newSurfa
this.setActive( useCodeMirror );
if ( this.toolbar.target.startTimeStamp ) {
this.logUsage( {
this.extCodeMirror.logUsage( {
editor: 'wikitext-2017',
enabled: useCodeMirror,
toggled: false,