From 40d04f7d7183b2e2cd37d7dbabc0110595a6bf4e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Feb 2026 07:03:44 +0000 Subject: [PATCH] Code Editor: Allow saving with Ctrl/Cmd+S in Theme/Plugin Editors. * Keyboard shortcuts work when CodeMirror is not enabled (due to syntax highlighting not being enabled), and when the user is not focused inside the CodeMirror editor. * The autocomplete trigger is switched from `keyup` to `inputRead` to improve reliability, support IME composition, and prevent conflicts with modifier keys (e.g., releasing `Ctrl`/`Cmd` before `s` after a save). * A `updateErrorNotice` method is exposed on the code editor instance to ensure validation errors are displayed when a save via shortcut is attempted, preventing "silent" failures. Otherwise, the linting error notice is only shown when focus leaves the editor. * The form submission is modernized by replacing the deprecated jQuery `.submit()` shorthand with `.trigger( 'submit' )`. Developed in https://github.com/WordPress/wordpress-develop/pull/10851 Props westonruter, Junaidkbr, evansolomon, desrosj, mukesh27, jonsurrell, spiraltee, chexee, andrewryno, tusharaddweb, gauri87, huzaifaalmesbah, ocean90, karmatosed, johnbillion, scribu, jcnetsys. Fixes #17133. Built from https://develop.svn.wordpress.org/trunk@61588 git-svn-id: http://core.svn.wordpress.org/trunk@60899 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/js/code-editor.js | 48 ++++++++++++++++++-------- wp-admin/js/code-editor.min.js | 2 +- wp-admin/js/theme-plugin-editor.js | 30 +++++++++++++++- wp-admin/js/theme-plugin-editor.min.js | 2 +- wp-includes/version.php | 2 +- 5 files changed, 66 insertions(+), 18 deletions(-) diff --git a/wp-admin/js/code-editor.js b/wp-admin/js/code-editor.js index 4266f39292..bc57026385 100644 --- a/wp-admin/js/code-editor.js +++ b/wp-admin/js/code-editor.js @@ -46,7 +46,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { * @param {Function} settings.onChangeLintingErrors - Callback for when there are changes to linting errors. * @param {Function} settings.onUpdateErrorNotice - Callback to update error notice. * - * @return {void} + * @return {Function} Update error notice function. */ function configureLinting( editor, settings ) { // eslint-disable-line complexity var currentErrorAnnotations = [], previouslyShownErrorAnnotations = []; @@ -82,7 +82,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { } /* - * Note that rules must be sent in the "deprecated" lint.options property + * Note that rules must be sent in the "deprecated" lint.options property * to prevent linter from complaining about unrecognized options. * See . */ @@ -209,6 +209,8 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { updateErrorNotice(); } }); + + return updateErrorNotice; } /** @@ -261,6 +263,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { * @typedef {object} wp.codeEditor~CodeEditorInstance * @property {object} settings - The code editor settings. * @property {CodeMirror} codemirror - The CodeMirror instance. + * @property {Function} updateErrorNotice - Force update the error notice. */ /** @@ -282,7 +285,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { * @return {CodeEditorInstance} Instance. */ wp.codeEditor.initialize = function initialize( textarea, settings ) { - var $textarea, codemirror, instanceSettings, instance; + var $textarea, codemirror, instanceSettings, instance, updateErrorNotice; if ( 'string' === typeof textarea ) { $textarea = $( '#' + textarea ); } else { @@ -294,16 +297,33 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { codemirror = wp.CodeMirror.fromTextArea( $textarea[0], instanceSettings.codemirror ); - configureLinting( codemirror, instanceSettings ); + updateErrorNotice = configureLinting( codemirror, instanceSettings ); instance = { settings: instanceSettings, - codemirror: codemirror + codemirror, + updateErrorNotice, }; if ( codemirror.showHint ) { - codemirror.on( 'keyup', function( editor, event ) { // eslint-disable-line complexity - var shouldAutocomplete, isAlphaKey = /^[a-zA-Z]$/.test( event.key ), lineBeforeCursor, innerMode, token; + codemirror.on( 'inputRead', function( editor, change ) { + var shouldAutocomplete, isAlphaKey, lineBeforeCursor, innerMode, token, char; + + // Only trigger autocompletion for typed input or IME composition. + if ( '+input' !== change.origin && ! change.origin.startsWith( '*compose' ) ) { + return; + } + + // Only trigger autocompletion for single-character inputs. + // The text property is an array of strings, one for each line. + // We check that there is only one line and that line has only one character. + if ( 1 !== change.text.length || 1 !== change.text[0].length ) { + return; + } + + char = change.text[0]; + isAlphaKey = /^[a-zA-Z]$/.test( char ); + if ( codemirror.state.completionActive && isAlphaKey ) { return; } @@ -318,11 +338,11 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { lineBeforeCursor = codemirror.doc.getLine( codemirror.doc.getCursor().line ).substr( 0, codemirror.doc.getCursor().ch ); if ( 'html' === innerMode || 'xml' === innerMode ) { shouldAutocomplete = ( - '<' === event.key || - ( '/' === event.key && 'tag' === token.type ) || + '<' === char || + ( '/' === char && 'tag' === token.type ) || ( isAlphaKey && 'tag' === token.type ) || ( isAlphaKey && 'attribute' === token.type ) || - ( '=' === event.key && ( + ( '=' === char && ( token.state.htmlState?.tagName || token.state.curState?.htmlState?.tagName ) ) @@ -330,17 +350,17 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { } else if ( 'css' === innerMode ) { shouldAutocomplete = isAlphaKey || - ':' === event.key || - ( ' ' === event.key && /:\s+$/.test( lineBeforeCursor ) ); + ':' === char || + ( ' ' === char && /:\s+$/.test( lineBeforeCursor ) ); } else if ( 'javascript' === innerMode ) { - shouldAutocomplete = isAlphaKey || '.' === event.key; + shouldAutocomplete = isAlphaKey || '.' === char; } else if ( 'clike' === innerMode && 'php' === codemirror.options.mode ) { shouldAutocomplete = isAlphaKey && ( 'keyword' === token.type || 'variable' === token.type ); } if ( shouldAutocomplete ) { codemirror.showHint( { completeSingle: false } ); } - }); + } ); } // Facilitate tabbing out of the editor. diff --git a/wp-admin/js/code-editor.min.js b/wp-admin/js/code-editor.min.js index e16666ef3a..e89d6f9d3a 100644 --- a/wp-admin/js/code-editor.min.js +++ b/wp-admin/js/code-editor.min.js @@ -1,2 +1,2 @@ /*! This file is auto-generated */ -void 0===window.wp&&(window.wp={}),void 0===window.wp.codeEditor&&(window.wp.codeEditor={}),function(u,d){"use strict";function s(r,s){var a=[],d=[];function c(){s.onUpdateErrorNotice&&!_.isEqual(a,d)&&(s.onUpdateErrorNotice(a,r),d=a)}function i(){var i,t=r.getOption("lint");return!!t&&(!0===t?t={}:_.isObject(t)&&(t=u.extend({},t)),t.options||(t.options={}),"javascript"===s.codemirror.mode&&s.jshint&&u.extend(t.options,s.jshint),"css"===s.codemirror.mode&&s.csslint&&u.extend(t.options,s.csslint),"htmlmixed"===s.codemirror.mode&&s.htmlhint&&(t.options.rules=u.extend({},s.htmlhint),s.jshint&&(t.options.rules.jshint=s.jshint),s.csslint)&&(t.options.rules.csslint=s.csslint),t.onUpdateLinting=(i=t.onUpdateLinting,function(t,e,n){var o=_.filter(t,function(t){return"error"===t.severity});i&&i.apply(t,e,n),!_.isEqual(o,a)&&(a=o,s.onChangeLintingErrors&&s.onChangeLintingErrors(o,t,e,n),!r.state.focused||0===a.length||0