[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> code-editor.js (source)

   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 );


Generated : Wed May 27 08:20:05 2026 Cross-referenced by PHPXref