From f8a89ccf32106ba1cd8d540f15a67c17e68155a6 Mon Sep 17 00:00:00 2001 From: bhsd <2545473905@qq.com> Date: Wed, 4 Dec 2024 13:03:39 +0800 Subject: [PATCH] CodeMirror: fix toggle-related issues The hook handlers and event listeners associated with `CodeMirror.prototype.initialize()` are now removed when CM is toggled off. For 2017 Wikitext Editor, the focus is always set on the VE surface view. Bug: T380840 Bug: T380983 Bug: T381358 Change-Id: Ib83f3d49c3d0496cb570f62e75f3fdc0d700be47 --- resources/codemirror.js | 19 +++++- .../codemirror.mediawiki.autocomplete.js | 1 + resources/codemirror.mediawiki.js | 20 ++++--- resources/codemirror.wikieditor.js | 35 +++++++++-- tests/jest/codemirror.wikieditor.test.js | 59 +++++++++++++++++++ tests/jest/setup.js | 20 +++++++ 6 files changed, 137 insertions(+), 17 deletions(-) diff --git a/resources/codemirror.js b/resources/codemirror.js index 2e8e4c77..2d943517 100644 --- a/resources/codemirror.js +++ b/resources/codemirror.js @@ -91,6 +91,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. * @@ -462,13 +468,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 ); } } @@ -515,7 +522,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' ); @@ -534,6 +541,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. * diff --git a/resources/codemirror.mediawiki.autocomplete.js b/resources/codemirror.mediawiki.autocomplete.js index 502d84fb..363c0d5f 100644 --- a/resources/codemirror.mediawiki.autocomplete.js +++ b/resources/codemirror.mediawiki.autocomplete.js @@ -1,4 +1,5 @@ const { + Extension, autocompletion, acceptCompletion, keymap diff --git a/resources/codemirror.mediawiki.js b/resources/codemirror.mediawiki.js index 63d09797..0a61b1ee 100644 --- a/resources/codemirror.mediawiki.js +++ b/resources/codemirror.mediawiki.js @@ -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 ); diff --git a/resources/codemirror.wikieditor.js b/resources/codemirror.wikieditor.js index cf2651b9..05d6e4a4 100644 --- a/resources/codemirror.wikieditor.js +++ b/resources/codemirror.wikieditor.js @@ -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', diff --git a/tests/jest/codemirror.wikieditor.test.js b/tests/jest/codemirror.wikieditor.test.js index 82f5c27a..cd5507af 100644 --- a/tests/jest/codemirror.wikieditor.test.js +++ b/tests/jest/codemirror.wikieditor.test.js @@ -1,6 +1,7 @@ mw.loader = { getState: jest.fn() }; const CodeMirrorWikiEditor = require( '../../resources/codemirror.wikieditor.js' ), + mediaWikiLang = require( '../../resources/codemirror.mediawiki.js' ), $textarea = $( '