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