[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/widgets/ -> custom-html-widgets.js (source)

   1  /**
   2   * @output wp-admin/js/widgets/custom-html-widgets.js
   3   */
   4  
   5  /* global wp */
   6  /* eslint consistent-this: [ "error", "control" ] */
   7  /* eslint no-magic-numbers: ["error", { "ignore": [0,1,-1] }] */
   8  
   9  /**
  10   * @namespace wp.customHtmlWidget
  11   * @memberOf wp
  12   */
  13  wp.customHtmlWidgets = ( function( $ ) {
  14      'use strict';
  15  
  16      var component = {
  17          idBases: [ 'custom_html' ],
  18          codeEditorSettings: {},
  19          l10n: {
  20              errorNotice: {
  21                  singular: '',
  22                  plural: ''
  23              }
  24          }
  25      };
  26  
  27      component.CustomHtmlWidgetControl = Backbone.View.extend(/** @lends wp.customHtmlWidgets.CustomHtmlWidgetControl.prototype */{
  28  
  29          /**
  30           * View events.
  31           *
  32           * @type {Object}
  33           */
  34          events: {},
  35  
  36          /**
  37           * Text widget control.
  38           *
  39           * @constructs wp.customHtmlWidgets.CustomHtmlWidgetControl
  40           * @augments Backbone.View
  41           * @abstract
  42           *
  43           * @param {Object} options - Options.
  44           * @param {jQuery} options.el - Control field container element.
  45           * @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
  46           *
  47           * @return {void}
  48           */
  49          initialize: function initialize( options ) {
  50              var control = this;
  51  
  52              if ( ! options.el ) {
  53                  throw new Error( 'Missing options.el' );
  54              }
  55              if ( ! options.syncContainer ) {
  56                  throw new Error( 'Missing options.syncContainer' );
  57              }
  58  
  59              Backbone.View.prototype.initialize.call( control, options );
  60              control.syncContainer = options.syncContainer;
  61              control.widgetIdBase = control.syncContainer.parent().find( '.id_base' ).val();
  62              control.widgetNumber = control.syncContainer.parent().find( '.widget_number' ).val();
  63              control.customizeSettingId = 'widget_' + control.widgetIdBase + '[' + String( control.widgetNumber ) + ']';
  64  
  65              control.$el.addClass( 'custom-html-widget-fields' );
  66              control.$el.html( wp.template( 'widget-custom-html-control-fields' )( { codeEditorDisabled: component.codeEditorSettings.disabled } ) );
  67  
  68              control.errorNoticeContainer = control.$el.find( '.code-editor-error-container' );
  69              control.currentErrorAnnotations = [];
  70              control.saveButton = control.syncContainer.add( control.syncContainer.parent().find( '.widget-control-actions' ) ).find( '.widget-control-save, #savewidget' );
  71              control.saveButton.addClass( 'custom-html-widget-save-button' ); // To facilitate style targeting.
  72  
  73              control.fields = {
  74                  title: control.$el.find( '.title' ),
  75                  content: control.$el.find( '.content' )
  76              };
  77  
  78              // Sync input fields to hidden sync fields which actually get sent to the server.
  79              _.each( control.fields, function( fieldInput, fieldName ) {
  80                  fieldInput.on( 'input change', function updateSyncField() {
  81                      var syncInput = control.syncContainer.find( '.sync-input.' + fieldName );
  82                      if ( syncInput.val() !== fieldInput.val() ) {
  83                          syncInput.val( fieldInput.val() );
  84                          syncInput.trigger( 'change' );
  85                      }
  86                  });
  87  
  88                  // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event.
  89                  fieldInput.val( control.syncContainer.find( '.sync-input.' + fieldName ).val() );
  90              });
  91          },
  92  
  93          /**
  94           * Update input fields from the sync fields.
  95           *
  96           * This function is called at the widget-updated and widget-synced events.
  97           * A field will only be updated if it is not currently focused, to avoid
  98           * overwriting content that the user is entering.
  99           *
 100           * @return {void}
 101           */
 102          updateFields: function updateFields() {
 103              var control = this, syncInput;
 104  
 105              if ( ! control.fields.title.is( document.activeElement ) ) {
 106                  syncInput = control.syncContainer.find( '.sync-input.title' );
 107                  control.fields.title.val( syncInput.val() );
 108              }
 109  
 110              /*
 111               * Prevent updating content when the editor is focused or if there are current error annotations,
 112               * to prevent the editor's contents from getting sanitized as soon as a user removes focus from
 113               * the editor. This is particularly important for users who cannot unfiltered_html.
 114               */
 115              control.contentUpdateBypassed = control.fields.content.is( document.activeElement ) || control.editor && control.editor.codemirror.state.focused || 0 !== control.currentErrorAnnotations.length;
 116              if ( ! control.contentUpdateBypassed ) {
 117                  syncInput = control.syncContainer.find( '.sync-input.content' );
 118                  control.fields.content.val( syncInput.val() );
 119              }
 120          },
 121  
 122          /**
 123           * Show linting error notice.
 124           *
 125           * @param {Array} errorAnnotations - Error annotations.
 126           * @return {void}
 127           */
 128          updateErrorNotice: function( errorAnnotations ) {
 129              var control = this, errorNotice, message = '', customizeSetting;
 130  
 131              if ( 1 === errorAnnotations.length ) {
 132                  message = component.l10n.errorNotice.singular.replace( '%d', '1' );
 133              } else if ( errorAnnotations.length > 1 ) {
 134                  message = component.l10n.errorNotice.plural.replace( '%d', String( errorAnnotations.length ) );
 135              }
 136  
 137              if ( control.fields.content[0].setCustomValidity ) {
 138                  control.fields.content[0].setCustomValidity( message );
 139              }
 140  
 141              if ( wp.customize && wp.customize.has( control.customizeSettingId ) ) {
 142                  customizeSetting = wp.customize( control.customizeSettingId );
 143                  customizeSetting.notifications.remove( 'htmlhint_error' );
 144                  if ( 0 !== errorAnnotations.length ) {
 145                      customizeSetting.notifications.add( 'htmlhint_error', new wp.customize.Notification( 'htmlhint_error', {
 146                          message: message,
 147                          type: 'error'
 148                      } ) );
 149                  }
 150              } else if ( 0 !== errorAnnotations.length ) {
 151                  errorNotice = $( '<div class="inline notice notice-error notice-alt" role="alert"></div>' );
 152                  errorNotice.append( $( '<p></p>', {
 153                      text: message
 154                  } ) );
 155                  control.errorNoticeContainer.empty();
 156                  control.errorNoticeContainer.append( errorNotice );
 157                  control.errorNoticeContainer.slideDown( 'fast' );
 158                  wp.a11y.speak( message );
 159              } else {
 160                  control.errorNoticeContainer.slideUp( 'fast' );
 161              }
 162          },
 163  
 164          /**
 165           * Initialize editor.
 166           *
 167           * @return {void}
 168           */
 169          initializeEditor: function initializeEditor() {
 170              var control = this, settings;
 171  
 172              if ( component.codeEditorSettings.disabled ) {
 173                  return;
 174              }
 175  
 176              settings = _.extend( {}, component.codeEditorSettings, {
 177  
 178                  /**
 179                   * Handle tabbing to the field before the editor.
 180                   *
 181                   * @ignore
 182                   *
 183                   * @return {void}
 184                   */
 185                  onTabPrevious: function onTabPrevious() {
 186                      control.fields.title.focus();
 187                  },
 188  
 189                  /**
 190                   * Handle tabbing to the field after the editor.
 191                   *
 192                   * @ignore
 193                   *
 194                   * @return {void}
 195                   */
 196                  onTabNext: function onTabNext() {
 197                      var tabbables = control.syncContainer.add( control.syncContainer.parent().find( '.widget-position, .widget-control-actions' ) ).find( ':tabbable' );
 198                      tabbables.first().focus();
 199                  },
 200  
 201                  /**
 202                   * Disable save button and store linting errors for use in updateFields.
 203                   *
 204                   * @ignore
 205                   *
 206                   * @param {Array} errorAnnotations - Error notifications.
 207                   * @return {void}
 208                   */
 209                  onChangeLintingErrors: function onChangeLintingErrors( errorAnnotations ) {
 210                      control.currentErrorAnnotations = errorAnnotations;
 211                  },
 212  
 213                  /**
 214                   * Update error notice.
 215                   *
 216                   * @ignore
 217                   *
 218                   * @param {Array} errorAnnotations - Error annotations.
 219                   * @return {void}
 220                   */
 221                  onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
 222                      control.saveButton.toggleClass( 'validation-blocked disabled', errorAnnotations.length > 0 );
 223                      control.updateErrorNotice( errorAnnotations );
 224                  }
 225              });
 226  
 227              control.editor = wp.codeEditor.initialize( control.fields.content, settings );
 228  
 229              // Improve the editor accessibility.
 230              $( control.editor.codemirror.display.lineDiv )
 231                  .attr({
 232                      role: 'textbox',
 233                      'aria-multiline': 'true',
 234                      'aria-labelledby': control.fields.content[0].id + '-label',
 235                      'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4'
 236                  });
 237  
 238              // Focus the editor when clicking on its label.
 239              $( '#' + control.fields.content[0].id + '-label' ).on( 'click', function() {
 240                  control.editor.codemirror.focus();
 241              });
 242  
 243              control.fields.content.on( 'change', function() {
 244                  if ( this.value !== control.editor.codemirror.getValue() ) {
 245                      control.editor.codemirror.setValue( this.value );
 246                  }
 247              });
 248              control.editor.codemirror.on( 'change', function() {
 249                  var value = control.editor.codemirror.getValue();
 250                  if ( value !== control.fields.content.val() ) {
 251                      control.fields.content.val( value ).trigger( 'change' );
 252                  }
 253              });
 254  
 255              // Make sure the editor gets updated if the content was updated on the server (sanitization) but not updated in the editor since it was focused.
 256              control.editor.codemirror.on( 'blur', function() {
 257                  if ( control.contentUpdateBypassed ) {
 258                      control.syncContainer.find( '.sync-input.content' ).trigger( 'change' );
 259                  }
 260              });
 261  
 262              // Prevent hitting Esc from collapsing the widget control.
 263              if ( wp.customize ) {
 264                  control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
 265                      var escKeyCode = 27;
 266                      if ( escKeyCode === event.keyCode ) {
 267                          event.stopPropagation();
 268                      }
 269                  });
 270              }
 271          }
 272      });
 273  
 274      /**
 275       * Mapping of widget ID to instances of CustomHtmlWidgetControl subclasses.
 276       *
 277       * @alias wp.customHtmlWidgets.widgetControls
 278       *
 279       * @type {Object.<string, wp.textWidgets.CustomHtmlWidgetControl>}
 280       */
 281      component.widgetControls = {};
 282  
 283      /**
 284       * Handle widget being added or initialized for the first time at the widget-added event.
 285       *
 286       * @alias wp.customHtmlWidgets.handleWidgetAdded
 287       *
 288       * @param {jQuery.Event} event - Event.
 289       * @param {jQuery}       widgetContainer - Widget container element.
 290       *
 291       * @return {void}
 292       */
 293      component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
 294          var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone, fieldContainer, syncContainer;
 295          widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
 296  
 297          idBase = widgetForm.find( '> .id_base' ).val();
 298          if ( -1 === component.idBases.indexOf( idBase ) ) {
 299              return;
 300          }
 301  
 302          // Prevent initializing already-added widgets.
 303          widgetId = widgetForm.find( '.widget-id' ).val();
 304          if ( component.widgetControls[ widgetId ] ) {
 305              return;
 306          }
 307  
 308          /*
 309           * Create a container element for the widget control fields.
 310           * This is inserted into the DOM immediately before the the .widget-content
 311           * element because the contents of this element are essentially "managed"
 312           * by PHP, where each widget update cause the entire element to be emptied
 313           * and replaced with the rendered output of WP_Widget::form() which is
 314           * sent back in Ajax request made to save/update the widget instance.
 315           * To prevent a "flash of replaced DOM elements and re-initialized JS
 316           * components", the JS template is rendered outside of the normal form
 317           * container.
 318           */
 319          fieldContainer = $( '<div></div>' );
 320          syncContainer = widgetContainer.find( '.widget-content:first' );
 321          syncContainer.before( fieldContainer );
 322  
 323          widgetControl = new component.CustomHtmlWidgetControl({
 324              el: fieldContainer,
 325              syncContainer: syncContainer
 326          });
 327  
 328          component.widgetControls[ widgetId ] = widgetControl;
 329  
 330          /*
 331           * Render the widget once the widget parent's container finishes animating,
 332           * as the widget-added event fires with a slideDown of the container.
 333           * This ensures that the textarea is visible and the editor can be initialized.
 334           */
 335          renderWhenAnimationDone = function() {
 336              if ( ! ( wp.customize ? widgetContainer.parent().hasClass( 'expanded' ) : widgetContainer.hasClass( 'open' ) ) ) { // Core merge: The wp.customize condition can be eliminated with this change being in core: https://github.com/xwp/wordpress-develop/pull/247/commits/5322387d
 337                  setTimeout( renderWhenAnimationDone, animatedCheckDelay );
 338              } else {
 339                  widgetControl.initializeEditor();
 340              }
 341          };
 342          renderWhenAnimationDone();
 343      };
 344  
 345      /**
 346       * Setup widget in accessibility mode.
 347       *
 348       * @alias wp.customHtmlWidgets.setupAccessibleMode
 349       *
 350       * @return {void}
 351       */
 352      component.setupAccessibleMode = function setupAccessibleMode() {
 353          var widgetForm, idBase, widgetControl, fieldContainer, syncContainer;
 354          widgetForm = $( '.editwidget > form' );
 355          if ( 0 === widgetForm.length ) {
 356              return;
 357          }
 358  
 359          idBase = widgetForm.find( '.id_base' ).val();
 360          if ( -1 === component.idBases.indexOf( idBase ) ) {
 361              return;
 362          }
 363  
 364          fieldContainer = $( '<div></div>' );
 365          syncContainer = widgetForm.find( '> .widget-inside' );
 366          syncContainer.before( fieldContainer );
 367  
 368          widgetControl = new component.CustomHtmlWidgetControl({
 369              el: fieldContainer,
 370              syncContainer: syncContainer
 371          });
 372  
 373          widgetControl.initializeEditor();
 374      };
 375  
 376      /**
 377       * Sync widget instance data sanitized from server back onto widget model.
 378       *
 379       * This gets called via the 'widget-updated' event when saving a widget from
 380       * the widgets admin screen and also via the 'widget-synced' event when making
 381       * a change to a widget in the customizer.
 382       *
 383       * @alias wp.customHtmlWidgets.handleWidgetUpdated
 384       *
 385       * @param {jQuery.Event} event - Event.
 386       * @param {jQuery}       widgetContainer - Widget container element.
 387       * @return {void}
 388       */
 389      component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
 390          var widgetForm, widgetId, widgetControl, idBase;
 391          widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
 392  
 393          idBase = widgetForm.find( '> .id_base' ).val();
 394          if ( -1 === component.idBases.indexOf( idBase ) ) {
 395              return;
 396          }
 397  
 398          widgetId = widgetForm.find( '> .widget-id' ).val();
 399          widgetControl = component.widgetControls[ widgetId ];
 400          if ( ! widgetControl ) {
 401              return;
 402          }
 403  
 404          widgetControl.updateFields();
 405      };
 406  
 407      /**
 408       * Initialize functionality.
 409       *
 410       * This function exists to prevent the JS file from having to boot itself.
 411       * When WordPress enqueues this script, it should have an inline script
 412       * attached which calls wp.textWidgets.init().
 413       *
 414       * @alias wp.customHtmlWidgets.init
 415       *
 416       * @param {Object} settings - Options for code editor, exported from PHP.
 417       *
 418       * @return {void}
 419       */
 420      component.init = function init( settings ) {
 421          var $document = $( document );
 422          _.extend( component.codeEditorSettings, settings );
 423  
 424          $document.on( 'widget-added', component.handleWidgetAdded );
 425          $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
 426  
 427          /*
 428           * Manually trigger widget-added events for media widgets on the admin
 429           * screen once they are expanded. The widget-added event is not triggered
 430           * for each pre-existing widget on the widgets admin screen like it is
 431           * on the customizer. Likewise, the customizer only triggers widget-added
 432           * when the widget is expanded to just-in-time construct the widget form
 433           * when it is actually going to be displayed. So the following implements
 434           * the same for the widgets admin screen, to invoke the widget-added
 435           * handler when a pre-existing media widget is expanded.
 436           */
 437          $( function initializeExistingWidgetContainers() {
 438              var widgetContainers;
 439              if ( 'widgets' !== window.pagenow ) {
 440                  return;
 441              }
 442              widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
 443              widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
 444                  var widgetContainer = $( this );
 445                  component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
 446              });
 447  
 448              // Accessibility mode.
 449              if ( document.readyState === 'complete' ) {
 450                  // Page is fully loaded.
 451                  component.setupAccessibleMode();
 452              } else {
 453                  // Page is still loading.
 454                  $( window ).on( 'load', function() {
 455                      component.setupAccessibleMode();
 456                  });
 457              }
 458          });
 459      };
 460  
 461      return component;
 462  })( jQuery );


Generated : Tue Dec 17 08:20:01 2024 Cross-referenced by PHPXref