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