[ 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  /* 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 );


Generated : Thu May 7 08:20:02 2026 Cross-referenced by PHPXref