[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

   1  /**
   2   * @output wp-admin/js/code-editor.js
   3   */
   4  
   5  if ( 'undefined' === typeof window.wp ) {
   6      /**
   7       * @namespace wp
   8       */
   9      window.wp = {};
  10  }
  11  if ( 'undefined' === typeof window.wp.codeEditor ) {
  12      /**
  13       * @namespace wp.codeEditor
  14       */
  15      window.wp.codeEditor = {};
  16  }
  17  
  18  ( function( $, wp ) {
  19      'use strict';
  20  
  21      /**
  22       * Default settings for code editor.
  23       *
  24       * @since 4.9.0
  25       * @type {object}
  26       */
  27      wp.codeEditor.defaultSettings = {
  28          codemirror: {},
  29          csslint: {},
  30          htmlhint: {},
  31          jshint: {},
  32          onTabNext: function() {},
  33          onTabPrevious: function() {},
  34          onChangeLintingErrors: function() {},
  35          onUpdateErrorNotice: function() {}
  36      };
  37  
  38      /**
  39       * Configure linting.
  40       *
  41       * @param {CodeMirror} editor - Editor.
  42       * @param {Object}     settings - Code editor settings.
  43       * @param {Object}     settings.codeMirror - Settings for CodeMirror.
  44       * @param {Function}   settings.onChangeLintingErrors - Callback for when there are changes to linting errors.
  45       * @param {Function}   settings.onUpdateErrorNotice - Callback to update error notice.
  46       *
  47       * @return {void}
  48       */
  49  	function configureLinting( editor, settings ) { // eslint-disable-line complexity
  50          var currentErrorAnnotations = [], previouslyShownErrorAnnotations = [];
  51  
  52          /**
  53           * Call the onUpdateErrorNotice if there are new errors to show.
  54           *
  55           * @return {void}
  56           */
  57  		function updateErrorNotice() {
  58              if ( settings.onUpdateErrorNotice && ! _.isEqual( currentErrorAnnotations, previouslyShownErrorAnnotations ) ) {
  59                  settings.onUpdateErrorNotice( currentErrorAnnotations, editor );
  60                  previouslyShownErrorAnnotations = currentErrorAnnotations;
  61              }
  62          }
  63  
  64          /**
  65           * Get lint options.
  66           *
  67           * @return {Object} Lint options.
  68           */
  69  		function getLintOptions() { // eslint-disable-line complexity
  70              var options = editor.getOption( 'lint' );
  71  
  72              if ( ! options ) {
  73                  return false;
  74              }
  75  
  76              if ( true === options ) {
  77                  options = {};
  78              } else if ( _.isObject( options ) ) {
  79                  options = $.extend( {}, options );
  80              }
  81  
  82              /*
  83               * Note that rules must be sent in the "deprecated" lint.options property 
  84               * to prevent linter from complaining about unrecognized options.
  85               * See <https://github.com/codemirror/CodeMirror/pull/4944>.
  86               */
  87              if ( ! options.options ) {
  88                  options.options = {};
  89              }
  90  
  91              // Configure JSHint.
  92              if ( 'javascript' === settings.codemirror.mode && settings.jshint ) {
  93                  $.extend( options.options, settings.jshint );
  94              }
  95  
  96              // Configure CSSLint.
  97              if ( 'css' === settings.codemirror.mode && settings.csslint ) {
  98                  $.extend( options.options, settings.csslint );
  99              }
 100  
 101              // Configure HTMLHint.
 102              if ( 'htmlmixed' === settings.codemirror.mode && settings.htmlhint ) {
 103                  options.options.rules = $.extend( {}, settings.htmlhint );
 104  
 105                  if ( settings.jshint ) {
 106                      options.options.rules.jshint = settings.jshint;
 107                  }
 108                  if ( settings.csslint ) {
 109                      options.options.rules.csslint = settings.csslint;
 110                  }
 111              }
 112  
 113              // Wrap the onUpdateLinting CodeMirror event to route to onChangeLintingErrors and onUpdateErrorNotice.
 114              options.onUpdateLinting = (function( onUpdateLintingOverridden ) {
 115                  return function( annotations, annotationsSorted, cm ) {
 116                      var errorAnnotations = _.filter( annotations, function( annotation ) {
 117                          return 'error' === annotation.severity;
 118                      } );
 119  
 120                      if ( onUpdateLintingOverridden ) {
 121                          onUpdateLintingOverridden.apply( annotations, annotationsSorted, cm );
 122                      }
 123  
 124                      // Skip if there are no changes to the errors.
 125                      if ( _.isEqual( errorAnnotations, currentErrorAnnotations ) ) {
 126                          return;
 127                      }
 128  
 129                      currentErrorAnnotations = errorAnnotations;
 130  
 131                      if ( settings.onChangeLintingErrors ) {
 132                          settings.onChangeLintingErrors( errorAnnotations, annotations, annotationsSorted, cm );
 133                      }
 134  
 135                      /*
 136                       * Update notifications when the editor is not focused to prevent error message
 137                       * from overwhelming the user during input, unless there are now no errors or there
 138                       * were previously errors shown. In these cases, update immediately so they can know
 139                       * that they fixed the errors.
 140                       */
 141                      if ( ! editor.state.focused || 0 === currentErrorAnnotations.length || previouslyShownErrorAnnotations.length > 0 ) {
 142                          updateErrorNotice();
 143                      }
 144                  };
 145              })( options.onUpdateLinting );
 146  
 147              return options;
 148          }
 149  
 150          editor.setOption( 'lint', getLintOptions() );
 151  
 152          // Keep lint options populated.
 153          editor.on( 'optionChange', function( cm, option ) {
 154              var options, gutters, gutterName = 'CodeMirror-lint-markers';
 155              if ( 'lint' !== option ) {
 156                  return;
 157              }
 158              gutters = editor.getOption( 'gutters' ) || [];
 159              options = editor.getOption( 'lint' );
 160              if ( true === options ) {
 161                  if ( ! _.contains( gutters, gutterName ) ) {
 162                      editor.setOption( 'gutters', [ gutterName ].concat( gutters ) );
 163                  }
 164                  editor.setOption( 'lint', getLintOptions() ); // Expand to include linting options.
 165              } else if ( ! options ) {
 166                  editor.setOption( 'gutters', _.without( gutters, gutterName ) );
 167              }
 168  
 169              // Force update on error notice to show or hide.
 170              if ( editor.getOption( 'lint' ) ) {
 171                  editor.performLint();
 172              } else {
 173                  currentErrorAnnotations = [];
 174                  updateErrorNotice();
 175              }
 176          } );
 177  
 178          // Update error notice when leaving the editor.
 179          editor.on( 'blur', updateErrorNotice );
 180  
 181          // Work around hint selection with mouse causing focus to leave editor.
 182          editor.on( 'startCompletion', function() {
 183              editor.off( 'blur', updateErrorNotice );
 184          } );
 185          editor.on( 'endCompletion', function() {
 186              var editorRefocusWait = 500;
 187              editor.on( 'blur', updateErrorNotice );
 188  
 189              // Wait for editor to possibly get re-focused after selection.
 190              _.delay( function() {
 191                  if ( ! editor.state.focused ) {
 192                      updateErrorNotice();
 193                  }
 194              }, editorRefocusWait );
 195          });
 196  
 197          /*
 198           * Make sure setting validities are set if the user tries to click Publish
 199           * while an autocomplete dropdown is still open. The Customizer will block
 200           * saving when a setting has an error notifications on it. This is only
 201           * necessary for mouse interactions because keyboards will have already
 202           * blurred the field and cause onUpdateErrorNotice to have already been
 203           * called.
 204           */
 205          $( document.body ).on( 'mousedown', function( event ) {
 206              if ( editor.state.focused && ! $.contains( editor.display.wrapper, event.target ) && ! $( event.target ).hasClass( 'CodeMirror-hint' ) ) {
 207                  updateErrorNotice();
 208              }
 209          });
 210      }
 211  
 212      /**
 213       * Configure tabbing.
 214       *
 215       * @param {CodeMirror} codemirror - Editor.
 216       * @param {Object}     settings - Code editor settings.
 217       * @param {Object}     settings.codeMirror - Settings for CodeMirror.
 218       * @param {Function}   settings.onTabNext - Callback to handle tabbing to the next tabbable element.
 219       * @param {Function}   settings.onTabPrevious - Callback to handle tabbing to the previous tabbable element.
 220       *
 221       * @return {void}
 222       */
 223  	function configureTabbing( codemirror, settings ) {
 224          var $textarea = $( codemirror.getTextArea() );
 225  
 226          codemirror.on( 'blur', function() {
 227              $textarea.data( 'next-tab-blurs', false );
 228          });
 229          codemirror.on( 'keydown', function onKeydown( editor, event ) {
 230              var tabKeyCode = 9, escKeyCode = 27;
 231  
 232              // Take note of the ESC keypress so that the next TAB can focus outside the editor.
 233              if ( escKeyCode === event.keyCode ) {
 234                  $textarea.data( 'next-tab-blurs', true );
 235                  return;
 236              }
 237  
 238              // Short-circuit if tab key is not being pressed or the tab key press should move focus.
 239              if ( tabKeyCode !== event.keyCode || ! $textarea.data( 'next-tab-blurs' ) ) {
 240                  return;
 241              }
 242  
 243              // Focus on previous or next focusable item.
 244              if ( event.shiftKey ) {
 245                  settings.onTabPrevious( codemirror, event );
 246              } else {
 247                  settings.onTabNext( codemirror, event );
 248              }
 249  
 250              // Reset tab state.
 251              $textarea.data( 'next-tab-blurs', false );
 252  
 253              // Prevent tab character from being added.
 254              event.preventDefault();
 255          });
 256      }
 257  
 258      /**
 259       * @typedef {object} wp.codeEditor~CodeEditorInstance
 260       * @property {object} settings - The code editor settings.
 261       * @property {CodeMirror} codemirror - The CodeMirror instance.
 262       */
 263  
 264      /**
 265       * Initialize Code Editor (CodeMirror) for an existing textarea.
 266       *
 267       * @since 4.9.0
 268       *
 269       * @param {string|jQuery|Element} textarea - The HTML id, jQuery object, or DOM Element for the textarea that is used for the editor.
 270       * @param {Object}                [settings] - Settings to override defaults.
 271       * @param {Function}              [settings.onChangeLintingErrors] - Callback for when the linting errors have changed.
 272       * @param {Function}              [settings.onUpdateErrorNotice] - Callback for when error notice should be displayed.
 273       * @param {Function}              [settings.onTabPrevious] - Callback to handle tabbing to the previous tabbable element.
 274       * @param {Function}              [settings.onTabNext] - Callback to handle tabbing to the next tabbable element.
 275       * @param {Object}                [settings.codemirror] - Options for CodeMirror.
 276       * @param {Object}                [settings.csslint] - Rules for CSSLint.
 277       * @param {Object}                [settings.htmlhint] - Rules for HTMLHint.
 278       * @param {Object}                [settings.jshint] - Rules for JSHint.
 279       *
 280       * @return {CodeEditorInstance} Instance.
 281       */
 282      wp.codeEditor.initialize = function initialize( textarea, settings ) {
 283          var $textarea, codemirror, instanceSettings, instance;
 284          if ( 'string' === typeof textarea ) {
 285              $textarea = $( '#' + textarea );
 286          } else {
 287              $textarea = $( textarea );
 288          }
 289  
 290          instanceSettings = $.extend( {}, wp.codeEditor.defaultSettings, settings );
 291          instanceSettings.codemirror = $.extend( {}, instanceSettings.codemirror );
 292  
 293          codemirror = wp.CodeMirror.fromTextArea( $textarea[0], instanceSettings.codemirror );
 294  
 295          configureLinting( codemirror, instanceSettings );
 296  
 297          instance = {
 298              settings: instanceSettings,
 299              codemirror: codemirror
 300          };
 301  
 302          if ( codemirror.showHint ) {
 303              codemirror.on( 'keyup', function( editor, event ) { // eslint-disable-line complexity
 304                  var shouldAutocomplete, isAlphaKey = /^[a-zA-Z]$/.test( event.key ), lineBeforeCursor, innerMode, token;
 305                  if ( codemirror.state.completionActive && isAlphaKey ) {
 306                      return;
 307                  }
 308  
 309                  // Prevent autocompletion in string literals or comments.
 310                  token = codemirror.getTokenAt( codemirror.getCursor() );
 311                  if ( 'string' === token.type || 'comment' === token.type ) {
 312                      return;
 313                  }
 314  
 315                  innerMode = wp.CodeMirror.innerMode( codemirror.getMode(), token.state ).mode.name;
 316                  lineBeforeCursor = codemirror.doc.getLine( codemirror.doc.getCursor().line ).substr( 0, codemirror.doc.getCursor().ch );
 317                  if ( 'html' === innerMode || 'xml' === innerMode ) {
 318                      shouldAutocomplete =
 319                          '<' === event.key ||
 320                          '/' === event.key && 'tag' === token.type ||
 321                          isAlphaKey && 'tag' === token.type ||
 322                          isAlphaKey && 'attribute' === token.type ||
 323                          '=' === token.string && token.state.htmlState && token.state.htmlState.tagName;
 324                  } else if ( 'css' === innerMode ) {
 325                      shouldAutocomplete =
 326                          isAlphaKey ||
 327                          ':' === event.key ||
 328                          ' ' === event.key && /:\s+$/.test( lineBeforeCursor );
 329                  } else if ( 'javascript' === innerMode ) {
 330                      shouldAutocomplete = isAlphaKey || '.' === event.key;
 331                  } else if ( 'clike' === innerMode && 'php' === codemirror.options.mode ) {
 332                      shouldAutocomplete = 'keyword' === token.type || 'variable' === token.type;
 333                  }
 334                  if ( shouldAutocomplete ) {
 335                      codemirror.showHint( { completeSingle: false } );
 336                  }
 337              });
 338          }
 339  
 340          // Facilitate tabbing out of the editor.
 341          configureTabbing( codemirror, settings );
 342  
 343          return instance;
 344      };
 345  
 346  })( window.jQuery, window.wp );


Generated: Thu Mar 28 01:00:02 2024 Cross-referenced by PHPXref 0.7.1