Merge "CodeMirror: fix toggle-related issues"

This commit is contained in:
jenkins-bot 2024-12-09 22:24:15 +00:00 committed by Gerrit Code Review
commit aac752edc7
6 changed files with 137 additions and 17 deletions

View file

@ -92,6 +92,12 @@ class CodeMirror {
* @type {Function|null}
*/
this.editRecoveryHandler = null;
/**
* The form `submit` event handler.
*
* @type {Function|null}
*/
this.formSubmitEventHandler = null;
/**
* jQuery.textSelection overrides for CodeMirror.
*
@ -464,13 +470,14 @@ class CodeMirror {
if ( !this.surface ) {
this.$textarea.hide();
if ( this.$textarea[ 0 ].form ) {
this.$textarea[ 0 ].form.addEventListener( 'submit', () => {
this.formSubmitEventHandler = () => {
this.$textarea.val( this.view.state.doc.toString() );
const scrollTop = document.getElementById( 'wpScrolltop' );
if ( scrollTop ) {
scrollTop.value = this.view.scrollDOM.scrollTop;
}
} );
};
this.$textarea[ 0 ].form.addEventListener( 'submit', this.formSubmitEventHandler );
}
}
@ -517,7 +524,7 @@ class CodeMirror {
*/
destroy() {
const scrollTop = this.view.scrollDOM.scrollTop;
const hasFocus = this.view.hasFocus;
const hasFocus = this.surface ? this.surface.getView().isFocused() : this.view.hasFocus;
const { from, to } = this.view.state.selection.ranges[ 0 ];
$( this.view.dom ).textSelection( 'unregister' );
this.$textarea.textSelection( 'unregister' );
@ -536,6 +543,12 @@ class CodeMirror {
this.$textarea.scrollTop( scrollTop );
this.textSelection = null;
// remove all hook handlers and event listeners
if ( this.formSubmitEventHandler && this.$textarea[ 0 ].form ) {
this.$textarea[ 0 ].form.removeEventListener( 'submit', this.formSubmitEventHandler );
this.formSubmitEventHandler = null;
}
/**
* Called just after CodeMirror is destroyed and the original textarea is restored.
*

View file

@ -1,4 +1,5 @@
const {
Extension,
autocompletion,
acceptCompletion,
keymap

View file

@ -1406,15 +1406,17 @@ const mediaWikiLang = ( config = { bidiIsolation: false }, mwConfig = null ) =>
if ( handler ) {
mw.hook( 'ext.CodeMirror.ready' ).remove( handler );
}
handler = ( $textarea, cm ) => {
if ( config.templateFolding !== false ) {
cm.preferences.registerExtension( 'templateFolding', templateFoldingExtension, cm.view );
}
if ( config.autocomplete !== false ) {
cm.preferences.registerExtension( 'autocomplete', autocompleteExtension, cm.view );
}
if ( config.bidiIsolation ) {
cm.preferences.registerExtension( 'bidiIsolation', bidiIsolationExtension, cm.view );
handler = ( _$textarea, cm ) => {
if ( cm.view ) { // T380840
if ( config.templateFolding !== false ) {
cm.preferences.registerExtension( 'templateFolding', templateFoldingExtension, cm.view );
}
if ( config.autocomplete !== false ) {
cm.preferences.registerExtension( 'autocomplete', autocompleteExtension, cm.view );
}
if ( config.bidiIsolation ) {
cm.preferences.registerExtension( 'bidiIsolation', bidiIsolationExtension, cm.view );
}
}
};
mw.hook( 'ext.CodeMirror.ready' ).add( handler );

View file

@ -36,7 +36,7 @@ class CodeMirrorWikiEditor extends CodeMirror {
* @param {LanguageSupport|Extension} langExtension Language support and its extension(s).
* @stable to call and override
*/
constructor( $textarea, langExtension ) {
constructor( $textarea, langExtension = [] ) {
super( $textarea );
/**
* Language support and its extension(s).
@ -56,6 +56,18 @@ class CodeMirrorWikiEditor extends CodeMirror {
* @type {Function|null}
*/
this.realtimePreviewHandler = null;
/**
* The `ext.WikiEditor.realtimepreview.enable` hook handler.
*
* @type {Function|null}
*/
this.realtimePreviewEnableHandler = null;
/**
* The `ext.WikiEditor.realtimepreview.disable` hook handler.
*
* @type {Function|null}
*/
this.realtimePreviewDisableHandler = null;
/**
* The WikiEditor search button, which is usurped to open the CodeMirror search panel.
*
@ -192,12 +204,24 @@ class CodeMirrorWikiEditor extends CodeMirror {
* @private
*/
addRealtimePreviewHandler() {
mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).add( ( realtimePreview ) => {
this.realtimePreviewEnableHandler = ( realtimePreview ) => {
this.realtimePreviewHandler = realtimePreview.getEventHandler().bind( realtimePreview );
} );
mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).add( () => {
};
this.realtimePreviewDisableHandler = () => {
this.realtimePreviewHandler = null;
} );
};
mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).add( this.realtimePreviewEnableHandler );
mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).add( this.realtimePreviewDisableHandler );
}
/**
* Removes the Realtime Preview handler.
*
* @private
*/
removeRealtimePreviewHandler() {
mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).remove( this.realtimePreviewEnableHandler );
mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).remove( this.realtimePreviewDisableHandler );
}
/**
@ -292,6 +316,7 @@ class CodeMirrorWikiEditor extends CodeMirror {
if ( this.view ) {
this.setCodeMirrorPreference( false );
this.destroy();
this.removeRealtimePreviewHandler();
this.$searchBtn.replaceWith( this.$oldSearchBtn );
this.$textarea.wikiEditor( 'removeFromToolbar', {
section: 'advanced',

View file

@ -1,6 +1,7 @@
mw.loader = { getState: jest.fn() };
const CodeMirrorWikiEditor = require( '../../resources/codemirror.wikieditor.js' ),
mediaWikiLang = require( '../../resources/codemirror.mediawiki.js' ),
$textarea = $( '<textarea>' )
.text( 'The Smashing Pumpkins' ),
cmWe = new CodeMirrorWikiEditor( $textarea );
@ -61,3 +62,61 @@ describe( 'updateToolbarButton', () => {
expect( btn.classList.contains( 'mw-editbutton-codemirror-active' ) ).toBeTruthy();
} );
} );
describe( 'Hook handlers and event listeners', () => {
const textarea = document.createElement( 'textarea' ),
editform = document.createElement( 'form' ),
events = {};
editform.append( textarea );
editform.addEventListener = jest.fn( ( event, callback ) => {
events[ event ] = callback;
} );
editform.removeEventListener = jest.fn( ( event ) => {
delete events[ event ];
} );
const cmWe3 = new CodeMirrorWikiEditor( textarea );
cmWe3.langExtension = mediaWikiLang( {
bidiIsolation: false
}, {
tags: {},
functionSynonyms: [ {}, {} ],
doubleUnderscore: [ {}, {} ],
urlProtocols: 'http://'
} );
cmWe3.$textarea.wikiEditor = jest.fn();
it( 'should remove submit event listener when CodeMirror is off', () => {
cmWe3.switchCodeMirror();
expect( typeof events.submit ).toBe( 'function' );
cmWe3.switchCodeMirror();
expect( events.submit ).toBeUndefined();
} );
it( 'should remove realtime preview hook handler when CodeMirror is off', () => {
cmWe3.switchCodeMirror();
expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.enable' ].length ).toBe( 1 );
expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.disable' ].length ).toBe( 1 );
cmWe3.switchCodeMirror();
expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.enable' ].length ).toBe( 0 );
expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.disable' ].length ).toBe( 0 );
} );
it( 'T380840', () => {
cmWe3.switchCodeMirror();
cmWe3.switchCodeMirror();
expect( cmWe3.view ).toBeNull();
mw.hook( 'ext.CodeMirror.ready' ).fire( cmWe3.$textarea, cmWe3 );
} );
it( 'only 1 ext.CodeMirror.ready hook handler', () => {
mediaWikiLang( {
bidiIsolation: false
}, {
tags: {},
functionSynonyms: [ {}, {} ],
doubleUnderscore: [ {}, {} ],
urlProtocols: 'http://'
} );
expect( mw.hook.mockHooks[ 'ext.CodeMirror.ready' ].length ).toBe( 1 );
} );
} );

View file

@ -22,6 +22,26 @@ mw.user = Object.assign( mw.user, {
mw.config.get = jest.fn().mockReturnValue( '1000+ edits' );
mw.track = jest.fn();
mw.Api.prototype.saveOption = jest.fn();
mw.hook = jest.fn( ( name ) => ( {
fire: jest.fn( ( ...args ) => {
if ( mw.hook.mockHooks[ name ] ) {
mw.hook.mockHooks[ name ].forEach( ( callback ) => callback( ...args ) );
}
} ),
add: jest.fn( ( callback ) => {
if ( !mw.hook.mockHooks[ name ] ) {
mw.hook.mockHooks[ name ] = [];
}
mw.hook.mockHooks[ name ].push( callback );
} ),
remove: jest.fn( ( callback ) => {
if ( mw.hook.mockHooks[ name ] ) {
mw.hook.mockHooks[ name ] = mw.hook.mockHooks[ name ]
.filter( ( cb ) => cb !== callback );
}
} )
} ) );
mw.hook.mockHooks = {};
global.$ = require( 'jquery' );
$.fn.textSelection = () => {};
window.matchMedia = jest.fn().mockReturnValue( { matches: false } );