| [ 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 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 * Handles the Code Editor (CodeMirror) functionality. 142 * 143 * @param {JQueryStatic} $ The jQuery object. 144 * @param {wp} wp The WordPress global object. 145 */ 146 ( function( $, wp ) { 147 'use strict'; 148 149 /** 150 * Default settings for code editor. 151 * 152 * @since 4.9.0 153 * @type {CodeEditorSettings} 154 */ 155 wp.codeEditor.defaultSettings = { 156 codemirror: {}, 157 csslint: {}, 158 htmlhint: {}, 159 jshint: {}, 160 onTabNext: function() {}, 161 onTabPrevious: function() {}, 162 onChangeLintingErrors: function() {}, 163 onUpdateErrorNotice: function() {}, 164 }; 165 166 /** 167 * Configures linting. 168 * 169 * @param {CodeEditorSettings} settings - Code editor settings. 170 * 171 * @return {LintingController} Linting controller. 172 */ 173 function configureLinting( settings ) { 174 /** @type {LintAnnotation[]} */ 175 let currentErrorAnnotations = []; 176 177 /** @type {LintAnnotation[]} */ 178 let previouslyShownErrorAnnotations = []; 179 180 /** 181 * Call the onUpdateErrorNotice if there are new errors to show. 182 * 183 * @param {import('codemirror').Editor} editor - Editor. 184 * @return {void} 185 */ 186 function updateErrorNotice( editor ) { 187 if ( settings.onUpdateErrorNotice && ! _.isEqual( currentErrorAnnotations, previouslyShownErrorAnnotations ) ) { 188 settings.onUpdateErrorNotice( currentErrorAnnotations, /** @type {CodeMirrorEditor} */ ( editor ) ); 189 previouslyShownErrorAnnotations = currentErrorAnnotations; 190 } 191 } 192 193 /** 194 * Gets the lint options. 195 * 196 * @return {CombinedLintOptions|false} Lint options. 197 */ 198 function getLintOptions() { 199 /** @type {CombinedLintOptions | boolean} */ 200 let options = settings.codemirror?.lint ?? false; 201 202 if ( ! options ) { 203 return false; 204 } 205 206 if ( true === options ) { 207 options = {}; 208 } else if ( _.isObject( options ) ) { 209 options = $.extend( {}, options ); 210 } 211 const linterOptions = /** @type {CombinedLintOptions} */ ( options ); 212 213 // Configure JSHint. 214 if ( 'javascript' === settings.codemirror?.mode && settings.jshint ) { 215 $.extend( linterOptions, settings.jshint ); 216 } 217 218 // Configure CSSLint. 219 if ( 'css' === settings.codemirror?.mode && settings.csslint ) { 220 $.extend( linterOptions, settings.csslint ); 221 } 222 223 // Configure HTMLHint. 224 if ( 'htmlmixed' === settings.codemirror?.mode && settings.htmlhint ) { 225 linterOptions.rules = $.extend( {}, settings.htmlhint ); 226 227 if ( settings.jshint && linterOptions.rules ) { 228 linterOptions.rules.jshint = settings.jshint; 229 } 230 if ( settings.csslint && linterOptions.rules ) { 231 linterOptions.rules.csslint = settings.csslint; 232 } 233 } 234 235 // Wrap the onUpdateLinting CodeMirror event to route to onChangeLintingErrors and onUpdateErrorNotice. 236 linterOptions.onUpdateLinting = (function( onUpdateLintingOverridden ) { 237 /** 238 * Wraps the onUpdateLinting event to filter errors, detect state changes, and manage error notice visibility. 239 * 240 * @param {LintAnnotation[]} annotations - Annotations. 241 * @param {LintAnnotation[]} annotationsSorted - Sorted annotations. 242 * @param {CodeMirrorEditor} cm - Editor. 243 */ 244 return function( annotations, annotationsSorted, cm ) { 245 const errorAnnotations = annotations.filter( function( annotation ) { 246 return 'error' === annotation.severity; 247 } ); 248 249 if ( onUpdateLintingOverridden ) { 250 onUpdateLintingOverridden( annotations, annotationsSorted, cm ); 251 } 252 253 // Skip if there are no changes to the errors. 254 if ( _.isEqual( errorAnnotations, currentErrorAnnotations ) ) { 255 return; 256 } 257 258 currentErrorAnnotations = errorAnnotations; 259 260 if ( settings.onChangeLintingErrors ) { 261 settings.onChangeLintingErrors( errorAnnotations, annotations, annotationsSorted, cm ); 262 } 263 264 /* 265 * Update notifications when the editor is not focused to prevent error message 266 * from overwhelming the user during input, unless there are now no errors or there 267 * were previously errors shown. In these cases, update immediately so they can know 268 * that they fixed the errors. 269 */ 270 if ( ! cm.state.focused || 0 === currentErrorAnnotations.length || previouslyShownErrorAnnotations.length > 0 ) { 271 updateErrorNotice( cm ); 272 } 273 }; 274 })( linterOptions.onUpdateLinting ); 275 276 return linterOptions; 277 } 278 279 return { 280 getLintOptions, 281 /** 282 * Initializes the CodeMirror editor. 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 );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Thu Sep 10 08:20:30 2026 | Cross-referenced by PHPXref |