Make WordPress Core

Changeset 61588


Ignore:
Timestamp:
02/04/2026 07:02:45 AM (12 hours 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.