Skip to content

Commit 845f7ce

Browse files
committed
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 WordPress#10851 Props westonruter, Junaidkbr, evansolomon, desrosj, mukesh27, jonsurrell, spiraltee, chexee, andrewryno, tusharaddweb, gauri87, huzaifaalmesbah, ocean90, karmatosed, johnbillion, scribu, jcnetsys. Fixes #17133. git-svn-id: https://develop.svn.wordpress.org/trunk@61588 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 6357346 commit 845f7ce

File tree

2 files changed

+63
-15
lines changed

2 files changed

+63
-15
lines changed

src/js/_enqueues/wp/code-editor.js

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) {
4646
* @param {Function} settings.onChangeLintingErrors - Callback for when there are changes to linting errors.
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
5252
var currentErrorAnnotations = [], previouslyShownErrorAnnotations = [];
@@ -82,7 +82,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) {
8282
}
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>.
8888
*/
@@ -209,6 +209,8 @@ if ( 'undefined' === typeof window.wp.codeEditor ) {
209209
updateErrorNotice();
210210
}
211211
});
212+
213+
return updateErrorNotice;
212214
}
213215

214216
/**
@@ -261,6 +263,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) {
261263
* @typedef {object} wp.codeEditor~CodeEditorInstance
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

266269
/**
@@ -282,7 +285,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) {
282285
* @return {CodeEditorInstance} Instance.
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 );
288291
} else {
@@ -294,16 +297,33 @@ if ( 'undefined' === typeof window.wp.codeEditor ) {
294297

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;
309329
}
@@ -318,29 +338,29 @@ if ( 'undefined' === typeof window.wp.codeEditor ) {
318338
lineBeforeCursor = codemirror.doc.getLine( codemirror.doc.getCursor().line ).substr( 0, codemirror.doc.getCursor().ch );
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
328348
) )
329349
);
330350
} else if ( 'css' === innerMode ) {
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 );
339359
}
340360
if ( shouldAutocomplete ) {
341361
codemirror.showHint( { completeSingle: false } );
342362
}
343-
});
363+
} );
344364
}
345365

346366
// Facilitate tabbing out of the editor.

src/js/_enqueues/wp/theme-plugin-editor.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
* @output wp-admin/js/theme-plugin-editor.js
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

79
if ( ! window.wp ) {
810
window.wp = {};
@@ -81,6 +83,18 @@ wp.themePluginEditor = (function( $ ) {
8183
component.docsLookUpButton.prop( 'disabled', false );
8284
}
8385
} );
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' );
96+
}
97+
} );
8498
};
8599

86100
/**
@@ -191,6 +205,10 @@ wp.themePluginEditor = (function( $ ) {
191205
return;
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 ) {
196214
component.instance.codemirror.setCursor( component.lintErrors[0].from.line );
@@ -399,6 +417,16 @@ wp.themePluginEditor = (function( $ ) {
399417
editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings );
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 )
404432
.attr({

0 commit comments

Comments
 (0)