Make WordPress Core

Changeset 61588


Ignore:
Timestamp:
02/04/2026 07:02:45 AM (6 months ago)
Author:
westonruter
Message:

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.

Location:
trunk/src/js/_enqueues/wp
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/_enqueues/wp/code-editor.js

    r61579 r61588  
    4747         * @param {Function}   settings.onUpdateErrorNotice - Callback to update error notice.
    4848         *
    49          * @return {void}
     49         * @return {Function} Update error notice function.
    5050         */
    5151        function configureLinting( editor, settings ) { // eslint-disable-line complexity
     
    8383
    8484                        /*
    85                          * Note that rules must be sent in the "deprecated" lint.options property 
     85                         * Note that rules must be sent in the "deprecated" lint.options property
    8686                         * to prevent linter from complaining about unrecognized options.
    8787                         * See <https://github.com/codemirror/CodeMirror/pull/4944>.
     
    210210                        }
    211211                });
     212
     213                return updateErrorNotice;
    212214        }
    213215
     
    262264         * @property {object} settings - The code editor settings.
    263265         * @property {CodeMirror} codemirror - The CodeMirror instance.
     266         * @property {Function} updateErrorNotice - Force update the error notice.
    264267         */
    265268
     
    283286         */
    284287        wp.codeEditor.initialize = function initialize( textarea, settings ) {
    285                 var $textarea, codemirror, instanceSettings, instance;
     288                var $textarea, codemirror, instanceSettings, instance, updateErrorNotice;
    286289                if ( 'string' === typeof textarea ) {
    287290                        $textarea = $( '#' + textarea );
     
    295298                codemirror = wp.CodeMirror.fromTextArea( $textarea[0], instanceSettings.codemirror );
    296299
    297                 configureLinting( codemirror, instanceSettings );
     300                updateErrorNotice = configureLinting( codemirror, instanceSettings );
    298301
    299302                instance = {
    300303                        settings: instanceSettings,
    301                         codemirror: codemirror
     304                        codemirror,
     305                        updateErrorNotice,
    302306                };
    303307
    304308                if ( codemirror.showHint ) {
    305                         codemirror.on( 'keyup', function( editor, event ) { // eslint-disable-line complexity
    306                                 var shouldAutocomplete, isAlphaKey = /^[a-zA-Z]$/.test( event.key ), lineBeforeCursor, innerMode, token;
     309                        codemirror.on( 'inputRead', function( editor, change ) {
     310                                var shouldAutocomplete, isAlphaKey, lineBeforeCursor, innerMode, token, char;
     311
     312                                // Only trigger autocompletion for typed input or IME composition.
     313                                if ( '+input' !== change.origin && ! change.origin.startsWith( '*compose' ) ) {
     314                                        return;
     315                                }
     316
     317                                // Only trigger autocompletion for single-character inputs.
     318                                // The text property is an array of strings, one for each line.
     319                                // We check that there is only one line and that line has only one character.
     320                                if ( 1 !== change.text.length || 1 !== change.text[0].length ) {
     321                                        return;
     322                                }
     323
     324                                char = change.text[0];
     325                                isAlphaKey = /^[a-zA-Z]$/.test( char );
     326
    307327                                if ( codemirror.state.completionActive && isAlphaKey ) {
    308328                                        return;
     
    319339                                if ( 'html' === innerMode || 'xml' === innerMode ) {
    320340                                        shouldAutocomplete = (
    321                                                 '<' === event.key ||
    322                                                 ( '/' === event.key && 'tag' === token.type ) ||
     341                                                '<' === char ||
     342                                                ( '/' === char && 'tag' === token.type ) ||
    323343                                                ( isAlphaKey && 'tag' === token.type ) ||
    324344                                                ( isAlphaKey && 'attribute' === token.type ) ||
    325                                                 ( '=' === event.key && (
     345                                                ( '=' === char && (
    326346                                                        token.state.htmlState?.tagName ||
    327347                                                        token.state.curState?.htmlState?.tagName
     
    331351                                        shouldAutocomplete =
    332352                                                isAlphaKey ||
    333                                                 ':' === event.key ||
    334                                                 ( ' ' === event.key && /:\s+$/.test( lineBeforeCursor ) );
     353                                                ':' === char ||
     354                                                ( ' ' === char && /:\s+$/.test( lineBeforeCursor ) );
    335355                                } else if ( 'javascript' === innerMode ) {
    336                                         shouldAutocomplete = isAlphaKey || '.' === event.key;
     356                                        shouldAutocomplete = isAlphaKey || '.' === char;
    337357                                } else if ( 'clike' === innerMode && 'php' === codemirror.options.mode ) {
    338358                                        shouldAutocomplete = isAlphaKey && ( 'keyword' === token.type || 'variable' === token.type );
     
    341361                                        codemirror.showHint( { completeSingle: false } );
    342362                                }
    343                         });
     363                        } );
    344364                }
    345365
  • trunk/src/js/_enqueues/wp/theme-plugin-editor.js

    r59789 r61588  
    33 */
    44
    5 /* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1] }] */
     5/* eslint-env es2020 */
     6
     7/* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1, 9, 1000] }] */
    68
    79if ( ! window.wp ) {
     
    8082                        } else {
    8183                                component.docsLookUpButton.prop( 'disabled', false );
     84                        }
     85                } );
     86
     87                // Initiate saving the file when not focused in CodeMirror or when the user has syntax highlighting turned off.
     88                $( window ).on( 'keydown', function( event ) {
     89                        if (
     90                                ( event.ctrlKey || event.metaKey ) &&
     91                                ( 's' === event.key.toLowerCase() ) &&
     92                                ( ! component.instance || ! component.instance.codemirror.hasFocus() )
     93                        ) {
     94                                event.preventDefault();
     95                                component.form.trigger( 'submit' );
    8296                        }
    8397                } );
     
    192206                }
    193207
     208                if ( component.instance && component.instance.updateErrorNotice ) {
     209                        component.instance.updateErrorNotice();
     210                }
     211
    194212                // Scroll to the line that has the error.
    195213                if ( component.lintErrors.length ) {
     
    400418                editor.codemirror.on( 'change', component.onChange );
    401419
     420                function onSaveShortcut() {
     421                        component.form.trigger( 'submit' );
     422                }
     423
     424                editor.codemirror.setOption( 'extraKeys', {
     425                        ...( editor.codemirror.getOption( 'extraKeys' ) || {} ),
     426                        'Ctrl-S': onSaveShortcut,
     427                        'Cmd-S': onSaveShortcut,
     428                } );
     429
    402430                // Improve the editor accessibility.
    403431                $( editor.codemirror.display.lineDiv )
Note: See TracChangeset for help on using the changeset viewer.