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


Generated : Sat Sep 12 08:20:32 2026 Cross-referenced by PHPXref