[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> updates.js (source)

   1  /**
   2   * Functions for ajaxified updates, deletions and installs inside the WordPress admin.
   3   *
   4   * @version 4.2.0
   5   * @output wp-admin/js/updates.js
   6   */
   7  
   8  /* global pagenow */
   9  
  10  /**
  11   * @param {jQuery}  $                                   jQuery object.
  12   * @param {object}  wp                                  WP object.
  13   * @param {object}  settings                            WP Updates settings.
  14   * @param {string}  settings.ajax_nonce                 Ajax nonce.
  15   * @param {object=} settings.plugins                    Base names of plugins in their different states.
  16   * @param {Array}   settings.plugins.all                Base names of all plugins.
  17   * @param {Array}   settings.plugins.active             Base names of active plugins.
  18   * @param {Array}   settings.plugins.inactive           Base names of inactive plugins.
  19   * @param {Array}   settings.plugins.upgrade            Base names of plugins with updates available.
  20   * @param {Array}   settings.plugins.recently_activated Base names of recently activated plugins.
  21   * @param {object=} settings.themes                     Plugin/theme status information or null.
  22   * @param {number}  settings.themes.all                 Amount of all themes.
  23   * @param {number}  settings.themes.upgrade             Amount of themes with updates available.
  24   * @param {number}  settings.themes.disabled            Amount of disabled themes.
  25   * @param {object=} settings.totals                     Combined information for available update counts.
  26   * @param {number}  settings.totals.count               Holds the amount of available updates.
  27   */
  28  (function( $, wp, settings ) {
  29      var $document = $( document ),
  30          __ = wp.i18n.__,
  31          _x = wp.i18n._x,
  32          sprintf = wp.i18n.sprintf;
  33  
  34      wp = wp || {};
  35  
  36      /**
  37       * The WP Updates object.
  38       *
  39       * @since 4.2.0
  40       *
  41       * @namespace wp.updates
  42       */
  43      wp.updates = {};
  44  
  45      /**
  46       * Removed in 5.5.0, needed for back-compatibility.
  47       *
  48       * @since 4.2.0
  49       * @deprecated 5.5.0
  50       *
  51       * @type {object}
  52       */
  53      wp.updates.l10n = {
  54          searchResults: '',
  55          searchResultsLabel: '',
  56          noPlugins: '',
  57          noItemsSelected: '',
  58          updating: '',
  59          pluginUpdated: '',
  60          themeUpdated: '',
  61          update: '',
  62          updateNow: '',
  63          pluginUpdateNowLabel: '',
  64          updateFailedShort: '',
  65          updateFailed: '',
  66          pluginUpdatingLabel: '',
  67          pluginUpdatedLabel: '',
  68          pluginUpdateFailedLabel: '',
  69          updatingMsg: '',
  70          updatedMsg: '',
  71          updateCancel: '',
  72          beforeunload: '',
  73          installNow: '',
  74          pluginInstallNowLabel: '',
  75          installing: '',
  76          pluginInstalled: '',
  77          themeInstalled: '',
  78          installFailedShort: '',
  79          installFailed: '',
  80          pluginInstallingLabel: '',
  81          themeInstallingLabel: '',
  82          pluginInstalledLabel: '',
  83          themeInstalledLabel: '',
  84          pluginInstallFailedLabel: '',
  85          themeInstallFailedLabel: '',
  86          installingMsg: '',
  87          installedMsg: '',
  88          importerInstalledMsg: '',
  89          aysDelete: '',
  90          aysDeleteUninstall: '',
  91          aysBulkDelete: '',
  92          aysBulkDeleteThemes: '',
  93          deleting: '',
  94          deleteFailed: '',
  95          pluginDeleted: '',
  96          themeDeleted: '',
  97          livePreview: '',
  98          activatePlugin: '',
  99          activateTheme: '',
 100          activatePluginLabel: '',
 101          activateThemeLabel: '',
 102          activateImporter: '',
 103          activateImporterLabel: '',
 104          unknownError: '',
 105          connectionError: '',
 106          nonceError: '',
 107          pluginsFound: '',
 108          noPluginsFound: '',
 109          autoUpdatesEnable: '',
 110          autoUpdatesEnabling: '',
 111          autoUpdatesEnabled: '',
 112          autoUpdatesDisable: '',
 113          autoUpdatesDisabling: '',
 114          autoUpdatesDisabled: '',
 115          autoUpdatesError: ''
 116      };
 117  
 118      wp.updates.l10n = window.wp.deprecateL10nObject( 'wp.updates.l10n', wp.updates.l10n, '5.5.0' );
 119  
 120      /**
 121       * User nonce for ajax calls.
 122       *
 123       * @since 4.2.0
 124       *
 125       * @type {string}
 126       */
 127      wp.updates.ajaxNonce = settings.ajax_nonce;
 128  
 129      /**
 130       * Current search term.
 131       *
 132       * @since 4.6.0
 133       *
 134       * @type {string}
 135       */
 136      wp.updates.searchTerm = '';
 137  
 138      /**
 139       * Whether filesystem credentials need to be requested from the user.
 140       *
 141       * @since 4.2.0
 142       *
 143       * @type {bool}
 144       */
 145      wp.updates.shouldRequestFilesystemCredentials = false;
 146  
 147      /**
 148       * Filesystem credentials to be packaged along with the request.
 149       *
 150       * @since 4.2.0
 151       * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
 152       *
 153       * @type {Object}
 154       * @property {Object} filesystemCredentials.ftp                Holds FTP credentials.
 155       * @property {string} filesystemCredentials.ftp.host           FTP host. Default empty string.
 156       * @property {string} filesystemCredentials.ftp.username       FTP user name. Default empty string.
 157       * @property {string} filesystemCredentials.ftp.password       FTP password. Default empty string.
 158       * @property {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
 159       *                                                             Default empty string.
 160       * @property {Object} filesystemCredentials.ssh                Holds SSH credentials.
 161       * @property {string} filesystemCredentials.ssh.publicKey      The public key. Default empty string.
 162       * @property {string} filesystemCredentials.ssh.privateKey     The private key. Default empty string.
 163       * @property {string} filesystemCredentials.fsNonce            Filesystem credentials form nonce.
 164       * @property {bool}   filesystemCredentials.available          Whether filesystem credentials have been provided.
 165       *                                                             Default 'false'.
 166       */
 167      wp.updates.filesystemCredentials = {
 168          ftp:       {
 169              host:           '',
 170              username:       '',
 171              password:       '',
 172              connectionType: ''
 173          },
 174          ssh:       {
 175              publicKey:  '',
 176              privateKey: ''
 177          },
 178          fsNonce: '',
 179          available: false
 180      };
 181  
 182      /**
 183       * Whether we're waiting for an Ajax request to complete.
 184       *
 185       * @since 4.2.0
 186       * @since 4.6.0 More accurately named `ajaxLocked`.
 187       *
 188       * @type {bool}
 189       */
 190      wp.updates.ajaxLocked = false;
 191  
 192      /**
 193       * Admin notice template.
 194       *
 195       * @since 4.6.0
 196       *
 197       * @type {function}
 198       */
 199      wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
 200  
 201      /**
 202       * Update queue.
 203       *
 204       * If the user tries to update a plugin while an update is
 205       * already happening, it can be placed in this queue to perform later.
 206       *
 207       * @since 4.2.0
 208       * @since 4.6.0 More accurately named `queue`.
 209       *
 210       * @type {Array.object}
 211       */
 212      wp.updates.queue = [];
 213  
 214      /**
 215       * Holds a jQuery reference to return focus to when exiting the request credentials modal.
 216       *
 217       * @since 4.2.0
 218       *
 219       * @type {jQuery}
 220       */
 221      wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
 222  
 223      /**
 224       * Adds or updates an admin notice.
 225       *
 226       * @since 4.6.0
 227       *
 228       * @param {Object}  data
 229       * @param {*=}      data.selector      Optional. Selector of an element to be replaced with the admin notice.
 230       * @param {string=} data.id            Optional. Unique id that will be used as the notice's id attribute.
 231       * @param {string=} data.className     Optional. Class names that will be used in the admin notice.
 232       * @param {string=} data.message       Optional. The message displayed in the notice.
 233       * @param {number=} data.successes     Optional. The amount of successful operations.
 234       * @param {number=} data.errors        Optional. The amount of failed operations.
 235       * @param {Array=}  data.errorMessages Optional. Error messages of failed operations.
 236       *
 237       */
 238      wp.updates.addAdminNotice = function( data ) {
 239          var $notice = $( data.selector ),
 240              $headerEnd = $( '.wp-header-end' ),
 241              $adminNotice;
 242  
 243          delete data.selector;
 244          $adminNotice = wp.updates.adminNotice( data );
 245  
 246          // Check if this admin notice already exists.
 247          if ( ! $notice.length ) {
 248              $notice = $( '#' + data.id );
 249          }
 250  
 251          if ( $notice.length ) {
 252              $notice.replaceWith( $adminNotice );
 253          } else if ( $headerEnd.length ) {
 254              $headerEnd.after( $adminNotice );
 255          } else {
 256              if ( 'customize' === pagenow ) {
 257                  $( '.customize-themes-notifications' ).append( $adminNotice );
 258              } else {
 259                  $( '.wrap' ).find( '> h1' ).after( $adminNotice );
 260              }
 261          }
 262  
 263          $document.trigger( 'wp-updates-notice-added' );
 264      };
 265  
 266      /**
 267       * Handles Ajax requests to WordPress.
 268       *
 269       * @since 4.6.0
 270       *
 271       * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
 272       * @param {Object} data   Data that needs to be passed to the ajax callback.
 273       * @return {$.promise}    A jQuery promise that represents the request,
 274       *                        decorated with an abort() method.
 275       */
 276      wp.updates.ajax = function( action, data ) {
 277          var options = {};
 278  
 279          if ( wp.updates.ajaxLocked ) {
 280              wp.updates.queue.push( {
 281                  action: action,
 282                  data:   data
 283              } );
 284  
 285              // Return a Deferred object so callbacks can always be registered.
 286              return $.Deferred();
 287          }
 288  
 289          wp.updates.ajaxLocked = true;
 290  
 291          if ( data.success ) {
 292              options.success = data.success;
 293              delete data.success;
 294          }
 295  
 296          if ( data.error ) {
 297              options.error = data.error;
 298              delete data.error;
 299          }
 300  
 301          options.data = _.extend( data, {
 302              action:          action,
 303              _ajax_nonce:     wp.updates.ajaxNonce,
 304              _fs_nonce:       wp.updates.filesystemCredentials.fsNonce,
 305              username:        wp.updates.filesystemCredentials.ftp.username,
 306              password:        wp.updates.filesystemCredentials.ftp.password,
 307              hostname:        wp.updates.filesystemCredentials.ftp.hostname,
 308              connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
 309              public_key:      wp.updates.filesystemCredentials.ssh.publicKey,
 310              private_key:     wp.updates.filesystemCredentials.ssh.privateKey
 311          } );
 312  
 313          return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
 314      };
 315  
 316      /**
 317       * Actions performed after every Ajax request.
 318       *
 319       * @since 4.6.0
 320       *
 321       * @param {Object}  response
 322       * @param {Array=}  response.debug     Optional. Debug information.
 323       * @param {string=} response.errorCode Optional. Error code for an error that occurred.
 324       */
 325      wp.updates.ajaxAlways = function( response ) {
 326          if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) {
 327              wp.updates.ajaxLocked = false;
 328              wp.updates.queueChecker();
 329          }
 330  
 331          if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) {
 332              _.map( response.debug, function( message ) {
 333                  // Remove all HTML tags and write a message to the console.
 334                  window.console.log( wp.sanitize.stripTagsAndEncodeText( message ) );
 335              } );
 336          }
 337      };
 338  
 339      /**
 340       * Refreshes update counts everywhere on the screen.
 341       *
 342       * @since 4.7.0
 343       */
 344      wp.updates.refreshCount = function() {
 345          var $adminBarUpdates              = $( '#wp-admin-bar-updates' ),
 346              $dashboardNavMenuUpdateCount  = $( 'a[href="update-core.php"] .update-plugins' ),
 347              $pluginsNavMenuUpdateCount    = $( 'a[href="plugins.php"] .update-plugins' ),
 348              $appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ),
 349              itemCount;
 350  
 351          $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
 352          $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total );
 353  
 354          // Remove the update count from the toolbar if it's zero.
 355          if ( 0 === settings.totals.counts.total ) {
 356              $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
 357          }
 358  
 359          // Update the "Updates" menu item.
 360          $dashboardNavMenuUpdateCount.each( function( index, element ) {
 361              element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.total );
 362          } );
 363          if ( settings.totals.counts.total > 0 ) {
 364              $dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total );
 365          } else {
 366              $dashboardNavMenuUpdateCount.remove();
 367          }
 368  
 369          // Update the "Plugins" menu item.
 370          $pluginsNavMenuUpdateCount.each( function( index, element ) {
 371              element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins );
 372          } );
 373          if ( settings.totals.counts.total > 0 ) {
 374              $pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins );
 375          } else {
 376              $pluginsNavMenuUpdateCount.remove();
 377          }
 378  
 379          // Update the "Appearance" menu item.
 380          $appearanceNavMenuUpdateCount.each( function( index, element ) {
 381              element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes );
 382          } );
 383          if ( settings.totals.counts.total > 0 ) {
 384              $appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes );
 385          } else {
 386              $appearanceNavMenuUpdateCount.remove();
 387          }
 388  
 389          // Update list table filter navigation.
 390          if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
 391              itemCount = settings.totals.counts.plugins;
 392          } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
 393              itemCount = settings.totals.counts.themes;
 394          }
 395  
 396          if ( itemCount > 0 ) {
 397              $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
 398          } else {
 399              $( '.subsubsub .upgrade' ).remove();
 400              $( '.subsubsub li:last' ).html( function() { return $( this ).children(); } );
 401          }
 402      };
 403  
 404      /**
 405       * Decrements the update counts throughout the various menus.
 406       *
 407       * This includes the toolbar, the "Updates" menu item and the menu items
 408       * for plugins and themes.
 409       *
 410       * @since 3.9.0
 411       *
 412       * @param {string} type The type of item that was updated or deleted.
 413       *                      Can be 'plugin', 'theme'.
 414       */
 415      wp.updates.decrementCount = function( type ) {
 416          settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 );
 417  
 418          if ( 'plugin' === type ) {
 419              settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 );
 420          } else if ( 'theme' === type ) {
 421              settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 );
 422          }
 423  
 424          wp.updates.refreshCount( type );
 425      };
 426  
 427      /**
 428       * Sends an Ajax request to the server to update a plugin.
 429       *
 430       * @since 4.2.0
 431       * @since 4.6.0 More accurately named `updatePlugin`.
 432       *
 433       * @param {Object}               args         Arguments.
 434       * @param {string}               args.plugin  Plugin basename.
 435       * @param {string}               args.slug    Plugin slug.
 436       * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess
 437       * @param {updatePluginError=}   args.error   Optional. Error callback. Default: wp.updates.updatePluginError
 438       * @return {$.promise} A jQuery promise that represents the request,
 439       *                     decorated with an abort() method.
 440       */
 441      wp.updates.updatePlugin = function( args ) {
 442          var $updateRow, $card, $message, message;
 443  
 444          args = _.extend( {
 445              success: wp.updates.updatePluginSuccess,
 446              error: wp.updates.updatePluginError
 447          }, args );
 448  
 449          if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
 450              $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
 451              $message   = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
 452              message    = sprintf(
 453                  /* translators: %s: Plugin name and version. */
 454                   _x( 'Updating %s...', 'plugin' ),
 455                  $updateRow.find( '.plugin-title strong' ).text()
 456              );
 457          } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
 458              $card    = $( '.plugin-card-' + args.slug );
 459              $message = $card.find( '.update-now' ).addClass( 'updating-message' );
 460              message    = sprintf(
 461                  /* translators: %s: Plugin name and version. */
 462                   _x( 'Updating %s...', 'plugin' ),
 463                  $message.data( 'name' )
 464              );
 465  
 466              // Remove previous error messages, if any.
 467              $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
 468          }
 469  
 470          if ( $message.html() !== __( 'Updating...' ) ) {
 471              $message.data( 'originaltext', $message.html() );
 472          }
 473  
 474          $message
 475              .attr( 'aria-label', message )
 476              .text( __( 'Updating...' ) );
 477  
 478          $document.trigger( 'wp-plugin-updating', args );
 479  
 480          return wp.updates.ajax( 'update-plugin', args );
 481      };
 482  
 483      /**
 484       * Updates the UI appropriately after a successful plugin update.
 485       *
 486       * @since 4.2.0
 487       * @since 4.6.0 More accurately named `updatePluginSuccess`.
 488       * @since 5.5.0 Auto-update "time to next update" text cleared.
 489       *
 490       * @param {Object} response            Response from the server.
 491       * @param {string} response.slug       Slug of the plugin to be updated.
 492       * @param {string} response.plugin     Basename of the plugin to be updated.
 493       * @param {string} response.pluginName Name of the plugin to be updated.
 494       * @param {string} response.oldVersion Old version of the plugin.
 495       * @param {string} response.newVersion New version of the plugin.
 496       */
 497      wp.updates.updatePluginSuccess = function( response ) {
 498          var $pluginRow, $updateMessage, newText;
 499  
 500          if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
 501              $pluginRow     = $( 'tr[data-plugin="' + response.plugin + '"]' )
 502                  .removeClass( 'update' )
 503                  .addClass( 'updated' );
 504              $updateMessage = $pluginRow.find( '.update-message' )
 505                  .removeClass( 'updating-message notice-warning' )
 506                  .addClass( 'updated-message notice-success' ).find( 'p' );
 507  
 508              // Update the version number in the row.
 509              newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
 510              $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
 511  
 512              // Clear the "time to next auto-update" text.
 513              $pluginRow.find( '.auto-update-time' ).empty();
 514          } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
 515              $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
 516                  .removeClass( 'updating-message' )
 517                  .addClass( 'button-disabled updated-message' );
 518          }
 519  
 520          $updateMessage
 521              .attr(
 522                  'aria-label',
 523                  sprintf(
 524                      /* translators: %s: Plugin name and version. */
 525                      _x( '%s updated!', 'plugin' ),
 526                      response.pluginName
 527                  )
 528              )
 529              .text( _x( 'Updated!', 'plugin' ) );
 530  
 531          wp.a11y.speak( __( 'Update completed successfully.' ) );
 532  
 533          wp.updates.decrementCount( 'plugin' );
 534  
 535          $document.trigger( 'wp-plugin-update-success', response );
 536      };
 537  
 538      /**
 539       * Updates the UI appropriately after a failed plugin update.
 540       *
 541       * @since 4.2.0
 542       * @since 4.6.0 More accurately named `updatePluginError`.
 543       *
 544       * @param {Object}  response              Response from the server.
 545       * @param {string}  response.slug         Slug of the plugin to be updated.
 546       * @param {string}  response.plugin       Basename of the plugin to be updated.
 547       * @param {string=} response.pluginName   Optional. Name of the plugin to be updated.
 548       * @param {string}  response.errorCode    Error code for the error that occurred.
 549       * @param {string}  response.errorMessage The error that occurred.
 550       */
 551      wp.updates.updatePluginError = function( response ) {
 552          var $card, $message, errorMessage;
 553  
 554          if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
 555              return;
 556          }
 557  
 558          if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
 559              return;
 560          }
 561  
 562          errorMessage = sprintf(
 563              /* translators: %s: Error string for a failed update. */
 564              __( 'Update failed: %s' ),
 565              response.errorMessage
 566          );
 567  
 568          if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
 569              if ( response.plugin ) {
 570                  $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
 571              } else {
 572                  $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
 573              }
 574              $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
 575  
 576              if ( response.pluginName ) {
 577                  $message.find( 'p' )
 578                      .attr(
 579                          'aria-label',
 580                          sprintf(
 581                              /* translators: %s: Plugin name and version. */
 582                              _x( '%s update failed.', 'plugin' ),
 583                              response.pluginName
 584                          )
 585                      );
 586              } else {
 587                  $message.find( 'p' ).removeAttr( 'aria-label' );
 588              }
 589          } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
 590              $card = $( '.plugin-card-' + response.slug )
 591                  .addClass( 'plugin-card-update-failed' )
 592                  .append( wp.updates.adminNotice( {
 593                      className: 'update-message notice-error notice-alt is-dismissible',
 594                      message:   errorMessage
 595                  } ) );
 596  
 597              $card.find( '.update-now' )
 598                  .text(  __( 'Update failed.' ) )
 599                  .removeClass( 'updating-message' );
 600  
 601              if ( response.pluginName ) {
 602                  $card.find( '.update-now' )
 603                      .attr(
 604                          'aria-label',
 605                          sprintf(
 606                              /* translators: %s: Plugin name and version. */
 607                              _x( '%s update failed.', 'plugin' ),
 608                              response.pluginName
 609                          )
 610                      );
 611              } else {
 612                  $card.find( '.update-now' ).removeAttr( 'aria-label' );
 613              }
 614  
 615              $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
 616  
 617                  // Use same delay as the total duration of the notice fadeTo + slideUp animation.
 618                  setTimeout( function() {
 619                      $card
 620                          .removeClass( 'plugin-card-update-failed' )
 621                          .find( '.column-name a' ).focus();
 622  
 623                      $card.find( '.update-now' )
 624                          .attr( 'aria-label', false )
 625                          .text( __( 'Update Now' ) );
 626                  }, 200 );
 627              } );
 628          }
 629  
 630          wp.a11y.speak( errorMessage, 'assertive' );
 631  
 632          $document.trigger( 'wp-plugin-update-error', response );
 633      };
 634  
 635      /**
 636       * Sends an Ajax request to the server to install a plugin.
 637       *
 638       * @since 4.6.0
 639       *
 640       * @param {Object}                args         Arguments.
 641       * @param {string}                args.slug    Plugin identifier in the WordPress.org Plugin repository.
 642       * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
 643       * @param {installPluginError=}   args.error   Optional. Error callback. Default: wp.updates.installPluginError
 644       * @return {$.promise} A jQuery promise that represents the request,
 645       *                     decorated with an abort() method.
 646       */
 647      wp.updates.installPlugin = function( args ) {
 648          var $card    = $( '.plugin-card-' + args.slug ),
 649              $message = $card.find( '.install-now' );
 650  
 651          args = _.extend( {
 652              success: wp.updates.installPluginSuccess,
 653              error: wp.updates.installPluginError
 654          }, args );
 655  
 656          if ( 'import' === pagenow ) {
 657              $message = $( '[data-slug="' + args.slug + '"]' );
 658          }
 659  
 660          if ( $message.html() !== __( 'Installing...' ) ) {
 661              $message.data( 'originaltext', $message.html() );
 662          }
 663  
 664          $message
 665              .addClass( 'updating-message' )
 666              .attr(
 667                  'aria-label',
 668                  sprintf(
 669                      /* translators: %s: Plugin name and version. */
 670                      _x( 'Installing %s...', 'plugin' ),
 671                      $message.data( 'name' )
 672                  )
 673              )
 674              .text( __( 'Installing...' ) );
 675  
 676          wp.a11y.speak( __( 'Installing... please wait.' ) );
 677  
 678          // Remove previous error messages, if any.
 679          $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
 680  
 681          $document.trigger( 'wp-plugin-installing', args );
 682  
 683          return wp.updates.ajax( 'install-plugin', args );
 684      };
 685  
 686      /**
 687       * Updates the UI appropriately after a successful plugin install.
 688       *
 689       * @since 4.6.0
 690       *
 691       * @param {Object} response             Response from the server.
 692       * @param {string} response.slug        Slug of the installed plugin.
 693       * @param {string} response.pluginName  Name of the installed plugin.
 694       * @param {string} response.activateUrl URL to activate the just installed plugin.
 695       */
 696      wp.updates.installPluginSuccess = function( response ) {
 697          var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
 698  
 699          $message
 700              .removeClass( 'updating-message' )
 701              .addClass( 'updated-message installed button-disabled' )
 702              .attr(
 703                  'aria-label',
 704                  sprintf(
 705                      /* translators: %s: Plugin name and version. */
 706                      _x( '%s installed!', 'plugin' ),
 707                      response.pluginName
 708                  )
 709              )
 710              .text( _x( 'Installed!', 'plugin' ) );
 711  
 712          wp.a11y.speak( __( 'Installation completed successfully.' ) );
 713  
 714          $document.trigger( 'wp-plugin-install-success', response );
 715  
 716          if ( response.activateUrl ) {
 717              setTimeout( function() {
 718  
 719                  // Transform the 'Install' button into an 'Activate' button.
 720                  $message.removeClass( 'install-now installed button-disabled updated-message' )
 721                      .addClass( 'activate-now button-primary' )
 722                      .attr( 'href', response.activateUrl );
 723  
 724                  if ( 'plugins-network' === pagenow ) {
 725                      $message
 726                          .attr(
 727                              'aria-label',
 728                              sprintf(
 729                                  /* translators: %s: Plugin name. */
 730                                  _x( 'Network Activate %s', 'plugin' ),
 731                                  response.pluginName
 732                              )
 733                          )
 734                          .text( __( 'Network Activate' ) );
 735                  } else {
 736                      $message
 737                          .attr(
 738                              'aria-label',
 739                              sprintf(
 740                                  /* translators: %s: Plugin name. */
 741                                  _x( 'Activate %s', 'plugin' ),
 742                                  response.pluginName
 743                              )
 744                          )
 745                          .text( __( 'Activate' ) );
 746                  }
 747              }, 1000 );
 748          }
 749      };
 750  
 751      /**
 752       * Updates the UI appropriately after a failed plugin install.
 753       *
 754       * @since 4.6.0
 755       *
 756       * @param {Object}  response              Response from the server.
 757       * @param {string}  response.slug         Slug of the plugin to be installed.
 758       * @param {string=} response.pluginName   Optional. Name of the plugin to be installed.
 759       * @param {string}  response.errorCode    Error code for the error that occurred.
 760       * @param {string}  response.errorMessage The error that occurred.
 761       */
 762      wp.updates.installPluginError = function( response ) {
 763          var $card   = $( '.plugin-card-' + response.slug ),
 764              $button = $card.find( '.install-now' ),
 765              errorMessage;
 766  
 767          if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
 768              return;
 769          }
 770  
 771          if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
 772              return;
 773          }
 774  
 775          errorMessage = sprintf(
 776              /* translators: %s: Error string for a failed installation. */
 777              __( 'Installation failed: %s' ),
 778              response.errorMessage
 779          );
 780  
 781          $card
 782              .addClass( 'plugin-card-update-failed' )
 783              .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
 784  
 785          $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
 786  
 787              // Use same delay as the total duration of the notice fadeTo + slideUp animation.
 788              setTimeout( function() {
 789                  $card
 790                      .removeClass( 'plugin-card-update-failed' )
 791                      .find( '.column-name a' ).focus();
 792              }, 200 );
 793          } );
 794  
 795          $button
 796              .removeClass( 'updating-message' ).addClass( 'button-disabled' )
 797              .attr(
 798                  'aria-label',
 799                  sprintf(
 800                      /* translators: %s: Plugin name and version. */
 801                      _x( '%s installation failed', 'plugin' ),
 802                      $button.data( 'name' )
 803                  )
 804              )
 805              .text( __( 'Installation failed.' ) );
 806  
 807          wp.a11y.speak( errorMessage, 'assertive' );
 808  
 809          $document.trigger( 'wp-plugin-install-error', response );
 810      };
 811  
 812      /**
 813       * Updates the UI appropriately after a successful importer install.
 814       *
 815       * @since 4.6.0
 816       *
 817       * @param {Object} response             Response from the server.
 818       * @param {string} response.slug        Slug of the installed plugin.
 819       * @param {string} response.pluginName  Name of the installed plugin.
 820       * @param {string} response.activateUrl URL to activate the just installed plugin.
 821       */
 822      wp.updates.installImporterSuccess = function( response ) {
 823          wp.updates.addAdminNotice( {
 824              id:        'install-success',
 825              className: 'notice-success is-dismissible',
 826              message:   sprintf(
 827                  /* translators: %s: Activation URL. */
 828                  __( 'Importer installed successfully. <a href="%s">Run importer</a>' ),
 829                  response.activateUrl + '&from=import'
 830              )
 831          } );
 832  
 833          $( '[data-slug="' + response.slug + '"]' )
 834              .removeClass( 'install-now updating-message' )
 835              .addClass( 'activate-now' )
 836              .attr({
 837                  'href': response.activateUrl + '&from=import',
 838                  'aria-label':sprintf(
 839                      /* translators: %s: Importer name. */
 840                      __( 'Run %s' ),
 841                      response.pluginName
 842                  )
 843              })
 844              .text( __( 'Run Importer' ) );
 845  
 846          wp.a11y.speak( __( 'Installation completed successfully.' ) );
 847  
 848          $document.trigger( 'wp-importer-install-success', response );
 849      };
 850  
 851      /**
 852       * Updates the UI appropriately after a failed importer install.
 853       *
 854       * @since 4.6.0
 855       *
 856       * @param {Object}  response              Response from the server.
 857       * @param {string}  response.slug         Slug of the plugin to be installed.
 858       * @param {string=} response.pluginName   Optional. Name of the plugin to be installed.
 859       * @param {string}  response.errorCode    Error code for the error that occurred.
 860       * @param {string}  response.errorMessage The error that occurred.
 861       */
 862      wp.updates.installImporterError = function( response ) {
 863          var errorMessage = sprintf(
 864                  /* translators: %s: Error string for a failed installation. */
 865                  __( 'Installation failed: %s' ),
 866                  response.errorMessage
 867              ),
 868              $installLink = $( '[data-slug="' + response.slug + '"]' ),
 869              pluginName = $installLink.data( 'name' );
 870  
 871          if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
 872              return;
 873          }
 874  
 875          if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
 876              return;
 877          }
 878  
 879          wp.updates.addAdminNotice( {
 880              id:        response.errorCode,
 881              className: 'notice-error is-dismissible',
 882              message:   errorMessage
 883          } );
 884  
 885          $installLink
 886              .removeClass( 'updating-message' )
 887              .attr(
 888                  'aria-label',
 889                  sprintf(
 890                      /* translators: %s: Plugin name. */
 891                      _x( 'Install %s now', 'plugin' ),
 892                      pluginName
 893                  )
 894              )
 895              .text( __( 'Install Now' ) );
 896  
 897          wp.a11y.speak( errorMessage, 'assertive' );
 898  
 899          $document.trigger( 'wp-importer-install-error', response );
 900      };
 901  
 902      /**
 903       * Sends an Ajax request to the server to delete a plugin.
 904       *
 905       * @since 4.6.0
 906       *
 907       * @param {Object}               args         Arguments.
 908       * @param {string}               args.plugin  Basename of the plugin to be deleted.
 909       * @param {string}               args.slug    Slug of the plugin to be deleted.
 910       * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
 911       * @param {deletePluginError=}   args.error   Optional. Error callback. Default: wp.updates.deletePluginError
 912       * @return {$.promise} A jQuery promise that represents the request,
 913       *                     decorated with an abort() method.
 914       */
 915      wp.updates.deletePlugin = function( args ) {
 916          var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
 917  
 918          args = _.extend( {
 919              success: wp.updates.deletePluginSuccess,
 920              error: wp.updates.deletePluginError
 921          }, args );
 922  
 923          if ( $link.html() !== __( 'Deleting...' ) ) {
 924              $link
 925                  .data( 'originaltext', $link.html() )
 926                  .text( __( 'Deleting...' ) );
 927          }
 928  
 929          wp.a11y.speak( __( 'Deleting...' ) );
 930  
 931          $document.trigger( 'wp-plugin-deleting', args );
 932  
 933          return wp.updates.ajax( 'delete-plugin', args );
 934      };
 935  
 936      /**
 937       * Updates the UI appropriately after a successful plugin deletion.
 938       *
 939       * @since 4.6.0
 940       *
 941       * @param {Object} response            Response from the server.
 942       * @param {string} response.slug       Slug of the plugin that was deleted.
 943       * @param {string} response.plugin     Base name of the plugin that was deleted.
 944       * @param {string} response.pluginName Name of the plugin that was deleted.
 945       */
 946      wp.updates.deletePluginSuccess = function( response ) {
 947  
 948          // Removes the plugin and updates rows.
 949          $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
 950              var $form            = $( '#bulk-action-form' ),
 951                  $views           = $( '.subsubsub' ),
 952                  $pluginRow       = $( this ),
 953                  columnCount      = $form.find( 'thead th:not(.hidden), thead td' ).length,
 954                  pluginDeletedRow = wp.template( 'item-deleted-row' ),
 955                  /**
 956                   * Plugins Base names of plugins in their different states.
 957                   *
 958                   * @type {Object}
 959                   */
 960                  plugins          = settings.plugins;
 961  
 962              // Add a success message after deleting a plugin.
 963              if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
 964                  $pluginRow.after(
 965                      pluginDeletedRow( {
 966                          slug:    response.slug,
 967                          plugin:  response.plugin,
 968                          colspan: columnCount,
 969                          name:    response.pluginName
 970                      } )
 971                  );
 972              }
 973  
 974              $pluginRow.remove();
 975  
 976              // Remove plugin from update count.
 977              if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
 978                  plugins.upgrade = _.without( plugins.upgrade, response.plugin );
 979                  wp.updates.decrementCount( 'plugin' );
 980              }
 981  
 982              // Remove from views.
 983              if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
 984                  plugins.inactive = _.without( plugins.inactive, response.plugin );
 985                  if ( plugins.inactive.length ) {
 986                      $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
 987                  } else {
 988                      $views.find( '.inactive' ).remove();
 989                  }
 990              }
 991  
 992              if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
 993                  plugins.active = _.without( plugins.active, response.plugin );
 994                  if ( plugins.active.length ) {
 995                      $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
 996                  } else {
 997                      $views.find( '.active' ).remove();
 998                  }
 999              }
1000  
1001              if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
1002                  plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
1003                  if ( plugins.recently_activated.length ) {
1004                      $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
1005                  } else {
1006                      $views.find( '.recently_activated' ).remove();
1007                  }
1008              }
1009  
1010              plugins.all = _.without( plugins.all, response.plugin );
1011  
1012              if ( plugins.all.length ) {
1013                  $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
1014              } else {
1015                  $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
1016                  $views.find( '.all' ).remove();
1017  
1018                  if ( ! $form.find( 'tr.no-items' ).length ) {
1019                      $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + __( 'No plugins are currently available.' ) + '</td></tr>' );
1020                  }
1021              }
1022          } );
1023  
1024          wp.a11y.speak( _x( 'Deleted!', 'plugin' ) );
1025  
1026          $document.trigger( 'wp-plugin-delete-success', response );
1027      };
1028  
1029      /**
1030       * Updates the UI appropriately after a failed plugin deletion.
1031       *
1032       * @since 4.6.0
1033       *
1034       * @param {Object}  response              Response from the server.
1035       * @param {string}  response.slug         Slug of the plugin to be deleted.
1036       * @param {string}  response.plugin       Base name of the plugin to be deleted
1037       * @param {string=} response.pluginName   Optional. Name of the plugin to be deleted.
1038       * @param {string}  response.errorCode    Error code for the error that occurred.
1039       * @param {string}  response.errorMessage The error that occurred.
1040       */
1041      wp.updates.deletePluginError = function( response ) {
1042          var $plugin, $pluginUpdateRow,
1043              pluginUpdateRow  = wp.template( 'item-update-row' ),
1044              noticeContent    = wp.updates.adminNotice( {
1045                  className: 'update-message notice-error notice-alt',
1046                  message:   response.errorMessage
1047              } );
1048  
1049          if ( response.plugin ) {
1050              $plugin          = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
1051              $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
1052          } else {
1053              $plugin          = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
1054              $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
1055          }
1056  
1057          if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
1058              return;
1059          }
1060  
1061          if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
1062              return;
1063          }
1064  
1065          // Add a plugin update row if it doesn't exist yet.
1066          if ( ! $pluginUpdateRow.length ) {
1067              $plugin.addClass( 'update' ).after(
1068                  pluginUpdateRow( {
1069                      slug:    response.slug,
1070                      plugin:  response.plugin || response.slug,
1071                      colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1072                      content: noticeContent
1073                  } )
1074              );
1075          } else {
1076  
1077              // Remove previous error messages, if any.
1078              $pluginUpdateRow.find( '.notice-error' ).remove();
1079  
1080              $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
1081          }
1082  
1083          $document.trigger( 'wp-plugin-delete-error', response );
1084      };
1085  
1086      /**
1087       * Sends an Ajax request to the server to update a theme.
1088       *
1089       * @since 4.6.0
1090       *
1091       * @param {Object}              args         Arguments.
1092       * @param {string}              args.slug    Theme stylesheet.
1093       * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
1094       * @param {updateThemeError=}   args.error   Optional. Error callback. Default: wp.updates.updateThemeError
1095       * @return {$.promise} A jQuery promise that represents the request,
1096       *                     decorated with an abort() method.
1097       */
1098      wp.updates.updateTheme = function( args ) {
1099          var $notice;
1100  
1101          args = _.extend( {
1102              success: wp.updates.updateThemeSuccess,
1103              error: wp.updates.updateThemeError
1104          }, args );
1105  
1106          if ( 'themes-network' === pagenow ) {
1107              $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
1108  
1109          } else if ( 'customize' === pagenow ) {
1110  
1111              // Update the theme details UI.
1112              $notice = $( '[data-slug="' + args.slug + '"].notice' ).removeClass( 'notice-large' );
1113  
1114              $notice.find( 'h3' ).remove();
1115  
1116              // Add the top-level UI, and update both.
1117              $notice = $notice.add( $( '#customize-control-installed_theme_' + args.slug ).find( '.update-message' ) );
1118              $notice = $notice.addClass( 'updating-message' ).find( 'p' );
1119  
1120          } else {
1121              $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
1122  
1123              $notice.find( 'h3' ).remove();
1124  
1125              $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
1126              $notice = $notice.addClass( 'updating-message' ).find( 'p' );
1127          }
1128  
1129          if ( $notice.html() !== __( 'Updating...' ) ) {
1130              $notice.data( 'originaltext', $notice.html() );
1131          }
1132  
1133          wp.a11y.speak( __( 'Updating... please wait.' ) );
1134          $notice.text( __( 'Updating...' ) );
1135  
1136          $document.trigger( 'wp-theme-updating', args );
1137  
1138          return wp.updates.ajax( 'update-theme', args );
1139      };
1140  
1141      /**
1142       * Updates the UI appropriately after a successful theme update.
1143       *
1144       * @since 4.6.0
1145       * @since 5.5.0 Auto-update "time to next update" text cleared.
1146       *
1147       * @param {Object} response
1148       * @param {string} response.slug       Slug of the theme to be updated.
1149       * @param {Object} response.theme      Updated theme.
1150       * @param {string} response.oldVersion Old version of the theme.
1151       * @param {string} response.newVersion New version of the theme.
1152       */
1153      wp.updates.updateThemeSuccess = function( response ) {
1154          var isModalOpen    = $( 'body.modal-open' ).length,
1155              $theme         = $( '[data-slug="' + response.slug + '"]' ),
1156              updatedMessage = {
1157                  className: 'updated-message notice-success notice-alt',
1158                  message:   _x( 'Updated!', 'theme' )
1159              },
1160              $notice, newText;
1161  
1162          if ( 'customize' === pagenow ) {
1163              $theme = $( '.updating-message' ).siblings( '.theme-name' );
1164  
1165              if ( $theme.length ) {
1166  
1167                  // Update the version number in the row.
1168                  newText = $theme.html().replace( response.oldVersion, response.newVersion );
1169                  $theme.html( newText );
1170              }
1171  
1172              $notice = $( '.theme-info .notice' ).add( wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ).find( '.update-message' ) );
1173          } else if ( 'themes-network' === pagenow ) {
1174              $notice = $theme.find( '.update-message' );
1175  
1176              // Update the version number in the row.
1177              newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
1178              $theme.find( '.theme-version-author-uri' ).html( newText );
1179  
1180              // Clear the "time to next auto-update" text.
1181              $theme.find( '.auto-update-time' ).empty();
1182          } else {
1183              $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
1184  
1185              // Focus on Customize button after updating.
1186              if ( isModalOpen ) {
1187                  $( '.load-customize:visible' ).focus();
1188                  $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty();
1189              } else {
1190                  $theme.find( '.load-customize' ).focus();
1191              }
1192          }
1193  
1194          wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
1195          wp.a11y.speak( __( 'Update completed successfully.' ) );
1196  
1197          wp.updates.decrementCount( 'theme' );
1198  
1199          $document.trigger( 'wp-theme-update-success', response );
1200  
1201          // Show updated message after modal re-rendered.
1202          if ( isModalOpen && 'customize' !== pagenow ) {
1203              $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
1204          }
1205      };
1206  
1207      /**
1208       * Updates the UI appropriately after a failed theme update.
1209       *
1210       * @since 4.6.0
1211       *
1212       * @param {Object} response              Response from the server.
1213       * @param {string} response.slug         Slug of the theme to be updated.
1214       * @param {string} response.errorCode    Error code for the error that occurred.
1215       * @param {string} response.errorMessage The error that occurred.
1216       */
1217      wp.updates.updateThemeError = function( response ) {
1218          var $theme       = $( '[data-slug="' + response.slug + '"]' ),
1219              errorMessage = sprintf(
1220                  /* translators: %s: Error string for a failed update. */
1221                   __( 'Update failed: %s' ),
1222                  response.errorMessage
1223              ),
1224              $notice;
1225  
1226          if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
1227              return;
1228          }
1229  
1230          if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
1231              return;
1232          }
1233  
1234          if ( 'customize' === pagenow ) {
1235              $theme = wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' );
1236          }
1237  
1238          if ( 'themes-network' === pagenow ) {
1239              $notice = $theme.find( '.update-message ' );
1240          } else {
1241              $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
1242  
1243              $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).focus() : $theme.find( '.load-customize' ).focus();
1244          }
1245  
1246          wp.updates.addAdminNotice( {
1247              selector:  $notice,
1248              className: 'update-message notice-error notice-alt is-dismissible',
1249              message:   errorMessage
1250          } );
1251  
1252          wp.a11y.speak( errorMessage );
1253  
1254          $document.trigger( 'wp-theme-update-error', response );
1255      };
1256  
1257      /**
1258       * Sends an Ajax request to the server to install a theme.
1259       *
1260       * @since 4.6.0
1261       *
1262       * @param {Object}               args
1263       * @param {string}               args.slug    Theme stylesheet.
1264       * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
1265       * @param {installThemeError=}   args.error   Optional. Error callback. Default: wp.updates.installThemeError
1266       * @return {$.promise} A jQuery promise that represents the request,
1267       *                     decorated with an abort() method.
1268       */
1269      wp.updates.installTheme = function( args ) {
1270          var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
1271  
1272          args = _.extend( {
1273              success: wp.updates.installThemeSuccess,
1274              error: wp.updates.installThemeError
1275          }, args );
1276  
1277          $message.addClass( 'updating-message' );
1278          $message.parents( '.theme' ).addClass( 'focus' );
1279          if ( $message.html() !== __( 'Installing...' ) ) {
1280              $message.data( 'originaltext', $message.html() );
1281          }
1282  
1283          $message
1284              .attr(
1285                  'aria-label',
1286                  sprintf(
1287                      /* translators: %s: Theme name and version. */
1288                      _x( 'Installing %s...', 'theme' ),
1289                      $message.data( 'name' )
1290                  )
1291              )
1292              .text( __( 'Installing...' ) );
1293  
1294          wp.a11y.speak( __( 'Installing... please wait.' ) );
1295  
1296          // Remove previous error messages, if any.
1297          $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
1298  
1299          $document.trigger( 'wp-theme-installing', args );
1300  
1301          return wp.updates.ajax( 'install-theme', args );
1302      };
1303  
1304      /**
1305       * Updates the UI appropriately after a successful theme install.
1306       *
1307       * @since 4.6.0
1308       *
1309       * @param {Object} response              Response from the server.
1310       * @param {string} response.slug         Slug of the theme to be installed.
1311       * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
1312       * @param {string} response.activateUrl  URL to activate the just installed theme.
1313       */
1314      wp.updates.installThemeSuccess = function( response ) {
1315          var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
1316              $message;
1317  
1318          $document.trigger( 'wp-theme-install-success', response );
1319  
1320          $message = $card.find( '.button-primary' )
1321              .removeClass( 'updating-message' )
1322              .addClass( 'updated-message disabled' )
1323              .attr(
1324                  'aria-label',
1325                  sprintf(
1326                      /* translators: %s: Theme name and version. */
1327                      _x( '%s installed!', 'theme' ),
1328                      response.themeName
1329                  )
1330              )
1331              .text( _x( 'Installed!', 'theme' ) );
1332  
1333          wp.a11y.speak( __( 'Installation completed successfully.' ) );
1334  
1335          setTimeout( function() {
1336  
1337              if ( response.activateUrl ) {
1338  
1339                  // Transform the 'Install' button into an 'Activate' button.
1340                  $message
1341                      .attr( 'href', response.activateUrl )
1342                      .removeClass( 'theme-install updated-message disabled' )
1343                      .addClass( 'activate' );
1344  
1345                  if ( 'themes-network' === pagenow ) {
1346                      $message
1347                          .attr(
1348                              'aria-label',
1349                              sprintf(
1350                                  /* translators: %s: Theme name. */
1351                                  _x( 'Network Activate %s', 'theme' ),
1352                                  response.themeName
1353                              )
1354                          )
1355                          .text( __( 'Network Enable' ) );
1356                  } else {
1357                      $message
1358                          .attr(
1359                              'aria-label',
1360                              sprintf(
1361                                  /* translators: %s: Theme name. */
1362                                  _x( 'Activate %s', 'theme' ),
1363                                  response.themeName
1364                              )
1365                          )
1366                          .text( __( 'Activate' ) );
1367                  }
1368              }
1369  
1370              if ( response.customizeUrl ) {
1371  
1372                  // Transform the 'Preview' button into a 'Live Preview' button.
1373                  $message.siblings( '.preview' ).replaceWith( function () {
1374                      return $( '<a>' )
1375                          .attr( 'href', response.customizeUrl )
1376                          .addClass( 'button load-customize' )
1377                          .text( __( 'Live Preview' ) );
1378                  } );
1379              }
1380          }, 1000 );
1381      };
1382  
1383      /**
1384       * Updates the UI appropriately after a failed theme install.
1385       *
1386       * @since 4.6.0
1387       *
1388       * @param {Object} response              Response from the server.
1389       * @param {string} response.slug         Slug of the theme to be installed.
1390       * @param {string} response.errorCode    Error code for the error that occurred.
1391       * @param {string} response.errorMessage The error that occurred.
1392       */
1393      wp.updates.installThemeError = function( response ) {
1394          var $card, $button,
1395              errorMessage = sprintf(
1396                  /* translators: %s: Error string for a failed installation. */
1397                  __( 'Installation failed: %s' ),
1398                  response.errorMessage
1399              ),
1400              $message     = wp.updates.adminNotice( {
1401                  className: 'update-message notice-error notice-alt',
1402                  message:   errorMessage
1403              } );
1404  
1405          if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
1406              return;
1407          }
1408  
1409          if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
1410              return;
1411          }
1412  
1413          if ( 'customize' === pagenow ) {
1414              if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) {
1415                  $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1416                  $card   = $( '.theme-overlay .theme-info' ).prepend( $message );
1417              } else {
1418                  $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1419                  $card   = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message );
1420              }
1421              wp.customize.notifications.remove( 'theme_installing' );
1422          } else {
1423              if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
1424                  $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
1425                  $card   = $( '.install-theme-info' ).prepend( $message );
1426              } else {
1427                  $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
1428                  $button = $card.find( '.theme-install' );
1429              }
1430          }
1431  
1432          $button
1433              .removeClass( 'updating-message' )
1434              .attr(
1435                  'aria-label',
1436                  sprintf(
1437                      /* translators: %s: Theme name and version. */
1438                      _x( '%s installation failed', 'theme' ),
1439                      $button.data( 'name' )
1440                  )
1441              )
1442              .text( __( 'Installation failed.' ) );
1443  
1444          wp.a11y.speak( errorMessage, 'assertive' );
1445  
1446          $document.trigger( 'wp-theme-install-error', response );
1447      };
1448  
1449      /**
1450       * Sends an Ajax request to the server to delete a theme.
1451       *
1452       * @since 4.6.0
1453       *
1454       * @param {Object}              args
1455       * @param {string}              args.slug    Theme stylesheet.
1456       * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
1457       * @param {deleteThemeError=}   args.error   Optional. Error callback. Default: wp.updates.deleteThemeError
1458       * @return {$.promise} A jQuery promise that represents the request,
1459       *                     decorated with an abort() method.
1460       */
1461      wp.updates.deleteTheme = function( args ) {
1462          var $button;
1463  
1464          if ( 'themes' === pagenow ) {
1465              $button = $( '.theme-actions .delete-theme' );
1466          } else if ( 'themes-network' === pagenow ) {
1467              $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' );
1468          }
1469  
1470          args = _.extend( {
1471              success: wp.updates.deleteThemeSuccess,
1472              error: wp.updates.deleteThemeError
1473          }, args );
1474  
1475          if ( $button && $button.html() !== __( 'Deleting...' ) ) {
1476              $button
1477                  .data( 'originaltext', $button.html() )
1478                  .text( __( 'Deleting...' ) );
1479          }
1480  
1481          wp.a11y.speak( __( 'Deleting...' ) );
1482  
1483          // Remove previous error messages, if any.
1484          $( '.theme-info .update-message' ).remove();
1485  
1486          $document.trigger( 'wp-theme-deleting', args );
1487  
1488          return wp.updates.ajax( 'delete-theme', args );
1489      };
1490  
1491      /**
1492       * Updates the UI appropriately after a successful theme deletion.
1493       *
1494       * @since 4.6.0
1495       *
1496       * @param {Object} response      Response from the server.
1497       * @param {string} response.slug Slug of the theme that was deleted.
1498       */
1499      wp.updates.deleteThemeSuccess = function( response ) {
1500          var $themeRows = $( '[data-slug="' + response.slug + '"]' );
1501  
1502          if ( 'themes-network' === pagenow ) {
1503  
1504              // Removes the theme and updates rows.
1505              $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
1506                  var $views     = $( '.subsubsub' ),
1507                      $themeRow  = $( this ),
1508                      totals     = settings.themes,
1509                      deletedRow = wp.template( 'item-deleted-row' );
1510  
1511                  if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
1512                      $themeRow.after(
1513                          deletedRow( {
1514                              slug:    response.slug,
1515                              colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1516                              name:    $themeRow.find( '.theme-title strong' ).text()
1517                          } )
1518                      );
1519                  }
1520  
1521                  $themeRow.remove();
1522  
1523                  // Remove theme from update count.
1524                  if ( $themeRow.hasClass( 'update' ) ) {
1525                      totals.upgrade--;
1526                      wp.updates.decrementCount( 'theme' );
1527                  }
1528  
1529                  // Remove from views.
1530                  if ( $themeRow.hasClass( 'inactive' ) ) {
1531                      totals.disabled--;
1532                      if ( totals.disabled ) {
1533                          $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' );
1534                      } else {
1535                          $views.find( '.disabled' ).remove();
1536                      }
1537                  }
1538  
1539                  // There is always at least one theme available.
1540                  $views.find( '.all .count' ).text( '(' + --totals.all + ')' );
1541              } );
1542          }
1543  
1544          wp.a11y.speak( _x( 'Deleted!', 'theme' ) );
1545  
1546          $document.trigger( 'wp-theme-delete-success', response );
1547      };
1548  
1549      /**
1550       * Updates the UI appropriately after a failed theme deletion.
1551       *
1552       * @since 4.6.0
1553       *
1554       * @param {Object} response              Response from the server.
1555       * @param {string} response.slug         Slug of the theme to be deleted.
1556       * @param {string} response.errorCode    Error code for the error that occurred.
1557       * @param {string} response.errorMessage The error that occurred.
1558       */
1559      wp.updates.deleteThemeError = function( response ) {
1560          var $themeRow    = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
1561              $button      = $( '.theme-actions .delete-theme' ),
1562              updateRow    = wp.template( 'item-update-row' ),
1563              $updateRow   = $themeRow.siblings( '#' + response.slug + '-update' ),
1564              errorMessage = sprintf(
1565                  /* translators: %s: Error string for a failed deletion. */
1566                  __( 'Deletion failed: %s' ),
1567                  response.errorMessage
1568              ),
1569              $message     = wp.updates.adminNotice( {
1570                  className: 'update-message notice-error notice-alt',
1571                  message:   errorMessage
1572              } );
1573  
1574          if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
1575              return;
1576          }
1577  
1578          if ( 'themes-network' === pagenow ) {
1579              if ( ! $updateRow.length ) {
1580                  $themeRow.addClass( 'update' ).after(
1581                      updateRow( {
1582                          slug: response.slug,
1583                          colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
1584                          content: $message
1585                      } )
1586                  );
1587              } else {
1588                  // Remove previous error messages, if any.
1589                  $updateRow.find( '.notice-error' ).remove();
1590                  $updateRow.find( '.plugin-update' ).append( $message );
1591              }
1592          } else {
1593              $( '.theme-info .theme-description' ).before( $message );
1594          }
1595  
1596          $button.html( $button.data( 'originaltext' ) );
1597  
1598          wp.a11y.speak( errorMessage, 'assertive' );
1599  
1600          $document.trigger( 'wp-theme-delete-error', response );
1601      };
1602  
1603      /**
1604       * Adds the appropriate callback based on the type of action and the current page.
1605       *
1606       * @since 4.6.0
1607       * @private
1608       *
1609       * @param {Object} data   Ajax payload.
1610       * @param {string} action The type of request to perform.
1611       * @return {Object} The Ajax payload with the appropriate callbacks.
1612       */
1613      wp.updates._addCallbacks = function( data, action ) {
1614          if ( 'import' === pagenow && 'install-plugin' === action ) {
1615              data.success = wp.updates.installImporterSuccess;
1616              data.error   = wp.updates.installImporterError;
1617          }
1618  
1619          return data;
1620      };
1621  
1622      /**
1623       * Pulls available jobs from the queue and runs them.
1624       *
1625       * @since 4.2.0
1626       * @since 4.6.0 Can handle multiple job types.
1627       */
1628      wp.updates.queueChecker = function() {
1629          var job;
1630  
1631          if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
1632              return;
1633          }
1634  
1635          job = wp.updates.queue.shift();
1636  
1637          // Handle a queue job.
1638          switch ( job.action ) {
1639              case 'install-plugin':
1640                  wp.updates.installPlugin( job.data );
1641                  break;
1642  
1643              case 'update-plugin':
1644                  wp.updates.updatePlugin( job.data );
1645                  break;
1646  
1647              case 'delete-plugin':
1648                  wp.updates.deletePlugin( job.data );
1649                  break;
1650  
1651              case 'install-theme':
1652                  wp.updates.installTheme( job.data );
1653                  break;
1654  
1655              case 'update-theme':
1656                  wp.updates.updateTheme( job.data );
1657                  break;
1658  
1659              case 'delete-theme':
1660                  wp.updates.deleteTheme( job.data );
1661                  break;
1662  
1663              default:
1664                  break;
1665          }
1666      };
1667  
1668      /**
1669       * Requests the users filesystem credentials if they aren't already known.
1670       *
1671       * @since 4.2.0
1672       *
1673       * @param {Event=} event Optional. Event interface.
1674       */
1675      wp.updates.requestFilesystemCredentials = function( event ) {
1676          if ( false === wp.updates.filesystemCredentials.available ) {
1677              /*
1678               * After exiting the credentials request modal,
1679               * return the focus to the element triggering the request.
1680               */
1681              if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
1682                  wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
1683              }
1684  
1685              wp.updates.ajaxLocked = true;
1686              wp.updates.requestForCredentialsModalOpen();
1687          }
1688      };
1689  
1690      /**
1691       * Requests the users filesystem credentials if needed and there is no lock.
1692       *
1693       * @since 4.6.0
1694       *
1695       * @param {Event=} event Optional. Event interface.
1696       */
1697      wp.updates.maybeRequestFilesystemCredentials = function( event ) {
1698          if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
1699              wp.updates.requestFilesystemCredentials( event );
1700          }
1701      };
1702  
1703      /**
1704       * Keydown handler for the request for credentials modal.
1705       *
1706       * Closes the modal when the escape key is pressed and
1707       * constrains keyboard navigation to inside the modal.
1708       *
1709       * @since 4.2.0
1710       *
1711       * @param {Event} event Event interface.
1712       */
1713      wp.updates.keydown = function( event ) {
1714          if ( 27 === event.keyCode ) {
1715              wp.updates.requestForCredentialsModalCancel();
1716          } else if ( 9 === event.keyCode ) {
1717  
1718              // #upgrade button must always be the last focus-able element in the dialog.
1719              if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
1720                  $( '#hostname' ).focus();
1721  
1722                  event.preventDefault();
1723              } else if ( 'hostname' === event.target.id && event.shiftKey ) {
1724                  $( '#upgrade' ).focus();
1725  
1726                  event.preventDefault();
1727              }
1728          }
1729      };
1730  
1731      /**
1732       * Opens the request for credentials modal.
1733       *
1734       * @since 4.2.0
1735       */
1736      wp.updates.requestForCredentialsModalOpen = function() {
1737          var $modal = $( '#request-filesystem-credentials-dialog' );
1738  
1739          $( 'body' ).addClass( 'modal-open' );
1740          $modal.show();
1741          $modal.find( 'input:enabled:first' ).focus();
1742          $modal.on( 'keydown', wp.updates.keydown );
1743      };
1744  
1745      /**
1746       * Closes the request for credentials modal.
1747       *
1748       * @since 4.2.0
1749       */
1750      wp.updates.requestForCredentialsModalClose = function() {
1751          $( '#request-filesystem-credentials-dialog' ).hide();
1752          $( 'body' ).removeClass( 'modal-open' );
1753  
1754          if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
1755              wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
1756          }
1757      };
1758  
1759      /**
1760       * Takes care of the steps that need to happen when the modal is canceled out.
1761       *
1762       * @since 4.2.0
1763       * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
1764       */
1765      wp.updates.requestForCredentialsModalCancel = function() {
1766  
1767          // Not ajaxLocked and no queue means we already have cleared things up.
1768          if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
1769              return;
1770          }
1771  
1772          _.each( wp.updates.queue, function( job ) {
1773              $document.trigger( 'credential-modal-cancel', job );
1774          } );
1775  
1776          // Remove the lock, and clear the queue.
1777          wp.updates.ajaxLocked = false;
1778          wp.updates.queue = [];
1779  
1780          wp.updates.requestForCredentialsModalClose();
1781      };
1782  
1783      /**
1784       * Displays an error message in the request for credentials form.
1785       *
1786       * @since 4.2.0
1787       *
1788       * @param {string} message Error message.
1789       */
1790      wp.updates.showErrorInCredentialsForm = function( message ) {
1791          var $filesystemForm = $( '#request-filesystem-credentials-form' );
1792  
1793          // Remove any existing error.
1794          $filesystemForm.find( '.notice' ).remove();
1795          $filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
1796      };
1797  
1798      /**
1799       * Handles credential errors and runs events that need to happen in that case.
1800       *
1801       * @since 4.2.0
1802       *
1803       * @param {Object} response Ajax response.
1804       * @param {string} action   The type of request to perform.
1805       */
1806      wp.updates.credentialError = function( response, action ) {
1807  
1808          // Restore callbacks.
1809          response = wp.updates._addCallbacks( response, action );
1810  
1811          wp.updates.queue.unshift( {
1812              action: action,
1813  
1814              /*
1815               * Not cool that we're depending on response for this data.
1816               * This would feel more whole in a view all tied together.
1817               */
1818              data: response
1819          } );
1820  
1821          wp.updates.filesystemCredentials.available = false;
1822          wp.updates.showErrorInCredentialsForm( response.errorMessage );
1823          wp.updates.requestFilesystemCredentials();
1824      };
1825  
1826      /**
1827       * Handles credentials errors if it could not connect to the filesystem.
1828       *
1829       * @since 4.6.0
1830       *
1831       * @param {Object} response              Response from the server.
1832       * @param {string} response.errorCode    Error code for the error that occurred.
1833       * @param {string} response.errorMessage The error that occurred.
1834       * @param {string} action                The type of request to perform.
1835       * @return {boolean} Whether there is an error that needs to be handled or not.
1836       */
1837      wp.updates.maybeHandleCredentialError = function( response, action ) {
1838          if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
1839              wp.updates.credentialError( response, action );
1840              return true;
1841          }
1842  
1843          return false;
1844      };
1845  
1846      /**
1847       * Validates an Ajax response to ensure it's a proper object.
1848       *
1849       * If the response deems to be invalid, an admin notice is being displayed.
1850       *
1851       * @param {(Object|string)} response              Response from the server.
1852       * @param {function=}       response.always       Optional. Callback for when the Deferred is resolved or rejected.
1853       * @param {string=}         response.statusText   Optional. Status message corresponding to the status code.
1854       * @param {string=}         response.responseText Optional. Request response as text.
1855       * @param {string}          action                Type of action the response is referring to. Can be 'delete',
1856       *                                                'update' or 'install'.
1857       */
1858      wp.updates.isValidResponse = function( response, action ) {
1859          var error = __( 'Something went wrong.' ),
1860              errorMessage;
1861  
1862          // Make sure the response is a valid data object and not a Promise object.
1863          if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
1864              return true;
1865          }
1866  
1867          if ( _.isString( response ) && '-1' === response ) {
1868              error = __( 'An error has occurred. Please reload the page and try again.' );
1869          } else if ( _.isString( response ) ) {
1870              error = response;
1871          } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
1872              error = __( 'Connection lost or the server is busy. Please try again later.' );
1873          } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
1874              error = response.responseText;
1875          } else if ( _.isString( response.statusText ) ) {
1876              error = response.statusText;
1877          }
1878  
1879          switch ( action ) {
1880              case 'update':
1881                  /* translators: %s: Error string for a failed update. */
1882                  errorMessage = __( 'Update failed: %s' );
1883                  break;
1884  
1885              case 'install':
1886                  /* translators: %s: Error string for a failed installation. */
1887                  errorMessage = __( 'Installation failed: %s' );
1888                  break;
1889  
1890              case 'delete':
1891                  /* translators: %s: Error string for a failed deletion. */
1892                  errorMessage = __( 'Deletion failed: %s' );
1893                  break;
1894          }
1895  
1896          // Messages are escaped, remove HTML tags to make them more readable.
1897          error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
1898          errorMessage = errorMessage.replace( '%s', error );
1899  
1900          // Add admin notice.
1901          wp.updates.addAdminNotice( {
1902              id:        'unknown_error',
1903              className: 'notice-error is-dismissible',
1904              message:   _.escape( errorMessage )
1905          } );
1906  
1907          // Remove the lock, and clear the queue.
1908          wp.updates.ajaxLocked = false;
1909          wp.updates.queue      = [];
1910  
1911          // Change buttons of all running updates.
1912          $( '.button.updating-message' )
1913              .removeClass( 'updating-message' )
1914              .removeAttr( 'aria-label' )
1915              .prop( 'disabled', true )
1916              .text( __( 'Update failed.' ) );
1917  
1918          $( '.updating-message:not(.button):not(.thickbox)' )
1919              .removeClass( 'updating-message notice-warning' )
1920              .addClass( 'notice-error' )
1921              .find( 'p' )
1922                  .removeAttr( 'aria-label' )
1923                  .text( errorMessage );
1924  
1925          wp.a11y.speak( errorMessage, 'assertive' );
1926  
1927          return false;
1928      };
1929  
1930      /**
1931       * Potentially adds an AYS to a user attempting to leave the page.
1932       *
1933       * If an update is on-going and a user attempts to leave the page,
1934       * opens an "Are you sure?" alert.
1935       *
1936       * @since 4.2.0
1937       */
1938      wp.updates.beforeunload = function() {
1939          if ( wp.updates.ajaxLocked ) {
1940              return __( 'Updates may not complete if you navigate away from this page.' );
1941          }
1942      };
1943  
1944      $( function() {
1945          var $pluginFilter        = $( '#plugin-filter' ),
1946              $bulkActionForm      = $( '#bulk-action-form' ),
1947              $filesystemForm      = $( '#request-filesystem-credentials-form' ),
1948              $filesystemModal     = $( '#request-filesystem-credentials-dialog' ),
1949              $pluginSearch        = $( '.plugins-php .wp-filter-search' ),
1950              $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
1951  
1952          settings = _.extend( settings, window._wpUpdatesItemCounts || {} );
1953  
1954          if ( settings.totals ) {
1955              wp.updates.refreshCount();
1956          }
1957  
1958          /*
1959           * Whether a user needs to submit filesystem credentials.
1960           *
1961           * This is based on whether the form was output on the page server-side.
1962           *
1963           * @see {wp_print_request_filesystem_credentials_modal() in PHP}
1964           */
1965          wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
1966  
1967          /**
1968           * File system credentials form submit noop-er / handler.
1969           *
1970           * @since 4.2.0
1971           */
1972          $filesystemModal.on( 'submit', 'form', function( event ) {
1973              event.preventDefault();
1974  
1975              // Persist the credentials input by the user for the duration of the page load.
1976              wp.updates.filesystemCredentials.ftp.hostname       = $( '#hostname' ).val();
1977              wp.updates.filesystemCredentials.ftp.username       = $( '#username' ).val();
1978              wp.updates.filesystemCredentials.ftp.password       = $( '#password' ).val();
1979              wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
1980              wp.updates.filesystemCredentials.ssh.publicKey      = $( '#public_key' ).val();
1981              wp.updates.filesystemCredentials.ssh.privateKey     = $( '#private_key' ).val();
1982              wp.updates.filesystemCredentials.fsNonce            = $( '#_fs_nonce' ).val();
1983              wp.updates.filesystemCredentials.available          = true;
1984  
1985              // Unlock and invoke the queue.
1986              wp.updates.ajaxLocked = false;
1987              wp.updates.queueChecker();
1988  
1989              wp.updates.requestForCredentialsModalClose();
1990          } );
1991  
1992          /**
1993           * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
1994           *
1995           * @since 4.2.0
1996           */
1997          $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
1998  
1999          /**
2000           * Hide SSH fields when not selected.
2001           *
2002           * @since 4.2.0
2003           */
2004          $filesystemForm.on( 'change', 'input[name="connection_type"]', function() {
2005              $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
2006          } ).change();
2007  
2008          /**
2009           * Handles events after the credential modal was closed.
2010           *
2011           * @since 4.6.0
2012           *
2013           * @param {Event}  event Event interface.
2014           * @param {string} job   The install/update.delete request.
2015           */
2016          $document.on( 'credential-modal-cancel', function( event, job ) {
2017              var $updatingMessage = $( '.updating-message' ),
2018                  $message, originalText;
2019  
2020              if ( 'import' === pagenow ) {
2021                  $updatingMessage.removeClass( 'updating-message' );
2022              } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
2023                  if ( 'update-plugin' === job.action ) {
2024                      $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
2025                  } else if ( 'delete-plugin' === job.action ) {
2026                      $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
2027                  }
2028              } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
2029                  if ( 'update-theme' === job.action ) {
2030                      $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
2031                  } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
2032                      $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
2033                  } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
2034                      $message = $( '.theme-actions .delete-theme' );
2035                  }
2036              } else {
2037                  $message = $updatingMessage;
2038              }
2039  
2040              if ( $message && $message.hasClass( 'updating-message' ) ) {
2041                  originalText = $message.data( 'originaltext' );
2042  
2043                  if ( 'undefined' === typeof originalText ) {
2044                      originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
2045                  }
2046  
2047                  $message
2048                      .removeClass( 'updating-message' )
2049                      .html( originalText );
2050  
2051                  if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
2052                      if ( 'update-plugin' === job.action ) {
2053                          $message.attr(
2054                              'aria-label',
2055                              sprintf(
2056                                  /* translators: %s: Plugin name and version. */
2057                                  _x( 'Update %s now', 'plugin' ),
2058                                  $message.data( 'name' )
2059                              )
2060                          );
2061                      } else if ( 'install-plugin' === job.action ) {
2062                          $message.attr(
2063                              'aria-label',
2064                              sprintf(
2065                                  /* translators: %s: Plugin name. */
2066                                  _x( 'Install %s now', 'plugin' ),
2067                                  $message.data( 'name' )
2068                              )
2069                          );
2070                      }
2071                  }
2072              }
2073  
2074              wp.a11y.speak( __( 'Update canceled.' ) );
2075          } );
2076  
2077          /**
2078           * Click handler for plugin updates in List Table view.
2079           *
2080           * @since 4.2.0
2081           *
2082           * @param {Event} event Event interface.
2083           */
2084          $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
2085              var $message   = $( event.target ),
2086                  $pluginRow = $message.parents( 'tr' );
2087  
2088              event.preventDefault();
2089  
2090              if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
2091                  return;
2092              }
2093  
2094              wp.updates.maybeRequestFilesystemCredentials( event );
2095  
2096              // Return the user to the input box of the plugin's table row after closing the modal.
2097              wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
2098              wp.updates.updatePlugin( {
2099                  plugin: $pluginRow.data( 'plugin' ),
2100                  slug:   $pluginRow.data( 'slug' )
2101              } );
2102          } );
2103  
2104          /**
2105           * Click handler for plugin updates in plugin install view.
2106           *
2107           * @since 4.2.0
2108           *
2109           * @param {Event} event Event interface.
2110           */
2111          $pluginFilter.on( 'click', '.update-now', function( event ) {
2112              var $button = $( event.target );
2113              event.preventDefault();
2114  
2115              if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
2116                  return;
2117              }
2118  
2119              wp.updates.maybeRequestFilesystemCredentials( event );
2120  
2121              wp.updates.updatePlugin( {
2122                  plugin: $button.data( 'plugin' ),
2123                  slug:   $button.data( 'slug' )
2124              } );
2125          } );
2126  
2127          /**
2128           * Click handler for plugin installs in plugin install view.
2129           *
2130           * @since 4.6.0
2131           *
2132           * @param {Event} event Event interface.
2133           */
2134          $pluginFilter.on( 'click', '.install-now', function( event ) {
2135              var $button = $( event.target );
2136              event.preventDefault();
2137  
2138              if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
2139                  return;
2140              }
2141  
2142              if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
2143                  wp.updates.requestFilesystemCredentials( event );
2144  
2145                  $document.on( 'credential-modal-cancel', function() {
2146                      var $message = $( '.install-now.updating-message' );
2147  
2148                      $message
2149                          .removeClass( 'updating-message' )
2150                          .text( __( 'Install Now' ) );
2151  
2152                      wp.a11y.speak( __( 'Update canceled.' ) );
2153                  } );
2154              }
2155  
2156              wp.updates.installPlugin( {
2157                  slug: $button.data( 'slug' )
2158              } );
2159          } );
2160  
2161          /**
2162           * Click handler for importer plugins installs in the Import screen.
2163           *
2164           * @since 4.6.0
2165           *
2166           * @param {Event} event Event interface.
2167           */
2168          $document.on( 'click', '.importer-item .install-now', function( event ) {
2169              var $button = $( event.target ),
2170                  pluginName = $( this ).data( 'name' );
2171  
2172              event.preventDefault();
2173  
2174              if ( $button.hasClass( 'updating-message' ) ) {
2175                  return;
2176              }
2177  
2178              if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
2179                  wp.updates.requestFilesystemCredentials( event );
2180  
2181                  $document.on( 'credential-modal-cancel', function() {
2182  
2183                      $button
2184                          .removeClass( 'updating-message' )
2185                          .attr(
2186                              'aria-label',
2187                              sprintf(
2188                                  /* translators: %s: Plugin name. */
2189                                  _x( 'Install %s now', 'plugin' ),
2190                                  pluginName
2191                              )
2192                          )
2193                          .text( __( 'Install Now' ) );
2194  
2195                      wp.a11y.speak( __( 'Update canceled.' ) );
2196                  } );
2197              }
2198  
2199              wp.updates.installPlugin( {
2200                  slug:    $button.data( 'slug' ),
2201                  pagenow: pagenow,
2202                  success: wp.updates.installImporterSuccess,
2203                  error:   wp.updates.installImporterError
2204              } );
2205          } );
2206  
2207          /**
2208           * Click handler for plugin deletions.
2209           *
2210           * @since 4.6.0
2211           *
2212           * @param {Event} event Event interface.
2213           */
2214          $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
2215              var $pluginRow = $( event.target ).parents( 'tr' ),
2216                  confirmMessage;
2217  
2218              if ( $pluginRow.hasClass( 'is-uninstallable' ) ) {
2219                  confirmMessage = sprintf(
2220                      /* translators: %s: Plugin name. */
2221                      __( 'Are you sure you want to delete %s and its data?' ),
2222                      $pluginRow.find( '.plugin-title strong' ).text()
2223                  );
2224              } else {
2225                  confirmMessage = sprintf(
2226                      /* translators: %s: Plugin name. */
2227                      __( 'Are you sure you want to delete %s?' ),
2228                      $pluginRow.find( '.plugin-title strong' ).text()
2229                  );
2230              }
2231  
2232              event.preventDefault();
2233  
2234              if ( ! window.confirm( confirmMessage ) ) {
2235                  return;
2236              }
2237  
2238              wp.updates.maybeRequestFilesystemCredentials( event );
2239  
2240              wp.updates.deletePlugin( {
2241                  plugin: $pluginRow.data( 'plugin' ),
2242                  slug:   $pluginRow.data( 'slug' )
2243              } );
2244  
2245          } );
2246  
2247          /**
2248           * Click handler for theme updates.
2249           *
2250           * @since 4.6.0
2251           *
2252           * @param {Event} event Event interface.
2253           */
2254          $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
2255              var $message  = $( event.target ),
2256                  $themeRow = $message.parents( 'tr' );
2257  
2258              event.preventDefault();
2259  
2260              if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
2261                  return;
2262              }
2263  
2264              wp.updates.maybeRequestFilesystemCredentials( event );
2265  
2266              // Return the user to the input box of the theme's table row after closing the modal.
2267              wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
2268              wp.updates.updateTheme( {
2269                  slug: $themeRow.data( 'slug' )
2270              } );
2271          } );
2272  
2273          /**
2274           * Click handler for theme deletions.
2275           *
2276           * @since 4.6.0
2277           *
2278           * @param {Event} event Event interface.
2279           */
2280          $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
2281              var $themeRow = $( event.target ).parents( 'tr' ),
2282                  confirmMessage = sprintf(
2283                      /* translators: %s: Theme name. */
2284                      __( 'Are you sure you want to delete %s?' ),
2285                      $themeRow.find( '.theme-title strong' ).text()
2286                  );
2287  
2288              event.preventDefault();
2289  
2290              if ( ! window.confirm( confirmMessage ) ) {
2291                  return;
2292              }
2293  
2294              wp.updates.maybeRequestFilesystemCredentials( event );
2295  
2296              wp.updates.deleteTheme( {
2297                  slug: $themeRow.data( 'slug' )
2298              } );
2299          } );
2300  
2301          /**
2302           * Bulk action handler for plugins and themes.
2303           *
2304           * Handles both deletions and updates.
2305           *
2306           * @since 4.6.0
2307           *
2308           * @param {Event} event Event interface.
2309           */
2310          $bulkActionForm.on( 'click', '[type="submit"]:not([name="clear-recent-list"])', function( event ) {
2311              var bulkAction    = $( event.target ).siblings( 'select' ).val(),
2312                  itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
2313                  success       = 0,
2314                  error         = 0,
2315                  errorMessages = [],
2316                  type, action;
2317  
2318              // Determine which type of item we're dealing with.
2319              switch ( pagenow ) {
2320                  case 'plugins':
2321                  case 'plugins-network':
2322                      type = 'plugin';
2323                      break;
2324  
2325                  case 'themes-network':
2326                      type = 'theme';
2327                      break;
2328  
2329                  default:
2330                      return;
2331              }
2332  
2333              // Bail if there were no items selected.
2334              if ( ! itemsSelected.length ) {
2335                  event.preventDefault();
2336                  $( 'html, body' ).animate( { scrollTop: 0 } );
2337  
2338                  return wp.updates.addAdminNotice( {
2339                      id:        'no-items-selected',
2340                      className: 'notice-error is-dismissible',
2341                      message:   __( 'Please select at least one item to perform this action on.' )
2342                  } );
2343              }
2344  
2345              // Determine the type of request we're dealing with.
2346              switch ( bulkAction ) {
2347                  case 'update-selected':
2348                      action = bulkAction.replace( 'selected', type );
2349                      break;
2350  
2351                  case 'delete-selected':
2352                      var confirmMessage = 'plugin' === type ?
2353                          __( 'Are you sure you want to delete the selected plugins and their data?' ) :
2354                          __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' );
2355  
2356                      if ( ! window.confirm( confirmMessage ) ) {
2357                          event.preventDefault();
2358                          return;
2359                      }
2360  
2361                      action = bulkAction.replace( 'selected', type );
2362                      break;
2363  
2364                  default:
2365                      return;
2366              }
2367  
2368              wp.updates.maybeRequestFilesystemCredentials( event );
2369  
2370              event.preventDefault();
2371  
2372              // Un-check the bulk checkboxes.
2373              $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
2374  
2375              $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
2376  
2377              // Find all the checkboxes which have been checked.
2378              itemsSelected.each( function( index, element ) {
2379                  var $checkbox = $( element ),
2380                      $itemRow = $checkbox.parents( 'tr' );
2381  
2382                  // Only add update-able items to the update queue.
2383                  if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
2384  
2385                      // Un-check the box.
2386                      $checkbox.prop( 'checked', false );
2387                      return;
2388                  }
2389  
2390                  // Add it to the queue.
2391                  wp.updates.queue.push( {
2392                      action: action,
2393                      data:   {
2394                          plugin: $itemRow.data( 'plugin' ),
2395                          slug:   $itemRow.data( 'slug' )
2396                      }
2397                  } );
2398              } );
2399  
2400              // Display bulk notification for updates of any kind.
2401              $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
2402                  var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
2403                      $bulkActionNotice, itemName;
2404  
2405                  if ( 'wp-' + response.update + '-update-success' === event.type ) {
2406                      success++;
2407                  } else {
2408                      itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
2409  
2410                      error++;
2411                      errorMessages.push( itemName + ': ' + response.errorMessage );
2412                  }
2413  
2414                  $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
2415  
2416                  wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
2417  
2418                  wp.updates.addAdminNotice( {
2419                      id:            'bulk-action-notice',
2420                      className:     'bulk-action-notice',
2421                      successes:     success,
2422                      errors:        error,
2423                      errorMessages: errorMessages,
2424                      type:          response.update
2425                  } );
2426  
2427                  $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
2428                      // $( this ) is the clicked button, no need to get it again.
2429                      $( this )
2430                          .toggleClass( 'bulk-action-errors-collapsed' )
2431                          .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
2432                      // Show the errors list.
2433                      $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
2434                  } );
2435  
2436                  if ( error > 0 && ! wp.updates.queue.length ) {
2437                      $( 'html, body' ).animate( { scrollTop: 0 } );
2438                  }
2439              } );
2440  
2441              // Reset admin notice template after #bulk-action-notice was added.
2442              $document.on( 'wp-updates-notice-added', function() {
2443                  wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
2444              } );
2445  
2446              // Check the queue, now that the event handlers have been added.
2447              wp.updates.queueChecker();
2448          } );
2449  
2450          if ( $pluginInstallSearch.length ) {
2451              $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
2452          }
2453  
2454          /**
2455           * Handles changes to the plugin search box on the new-plugin page,
2456           * searching the repository dynamically.
2457           *
2458           * @since 4.6.0
2459           */
2460          $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
2461              var $searchTab = $( '.plugin-install-search' ), data, searchLocation;
2462  
2463              data = {
2464                  _ajax_nonce: wp.updates.ajaxNonce,
2465                  s:           event.target.value,
2466                  tab:         'search',
2467                  type:        $( '#typeselector' ).val(),
2468                  pagenow:     pagenow
2469              };
2470              searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
2471  
2472              // Clear on escape.
2473              if ( 'keyup' === event.type && 27 === event.which ) {
2474                  event.target.value = '';
2475              }
2476  
2477              if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
2478                  return;
2479              } else {
2480                  $pluginFilter.empty();
2481                  wp.updates.searchTerm = data.s;
2482              }
2483  
2484              if ( window.history && window.history.replaceState ) {
2485                  window.history.replaceState( null, '', searchLocation );
2486              }
2487  
2488              if ( ! $searchTab.length ) {
2489                  $searchTab = $( '<li class="plugin-install-search" />' )
2490                      .append( $( '<a />', {
2491                          'class': 'current',
2492                          'href': searchLocation,
2493                          'text': __( 'Search Results' )
2494                      } ) );
2495  
2496                  $( '.wp-filter .filter-links .current' )
2497                      .removeClass( 'current' )
2498                      .parents( '.filter-links' )
2499                      .prepend( $searchTab );
2500  
2501                  $pluginFilter.prev( 'p' ).remove();
2502                  $( '.plugins-popular-tags-wrapper' ).remove();
2503              }
2504  
2505              if ( 'undefined' !== typeof wp.updates.searchRequest ) {
2506                  wp.updates.searchRequest.abort();
2507              }
2508              $( 'body' ).addClass( 'loading-content' );
2509  
2510              wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
2511                  $( 'body' ).removeClass( 'loading-content' );
2512                  $pluginFilter.append( response.items );
2513                  delete wp.updates.searchRequest;
2514  
2515                  if ( 0 === response.count ) {
2516                      wp.a11y.speak( __( 'You do not appear to have any plugins available at this time.' ) );
2517                  } else {
2518                      wp.a11y.speak(
2519                          sprintf(
2520                              /* translators: %s: Number of plugins. */
2521                              __( 'Number of plugins found: %d' ),
2522                              response.count
2523                          )
2524                      );
2525                  }
2526              } );
2527          }, 1000 ) );
2528  
2529          if ( $pluginSearch.length ) {
2530              $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
2531          }
2532  
2533          /**
2534           * Handles changes to the plugin search box on the Installed Plugins screen,
2535           * searching the plugin list dynamically.
2536           *
2537           * @since 4.6.0
2538           */
2539          $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
2540              var data = {
2541                  _ajax_nonce:   wp.updates.ajaxNonce,
2542                  s:             event.target.value,
2543                  pagenow:       pagenow,
2544                  plugin_status: 'all'
2545              },
2546              queryArgs;
2547  
2548              // Clear on escape.
2549              if ( 'keyup' === event.type && 27 === event.which ) {
2550                  event.target.value = '';
2551              }
2552  
2553              if ( wp.updates.searchTerm === data.s ) {
2554                  return;
2555              } else {
2556                  wp.updates.searchTerm = data.s;
2557              }
2558  
2559              queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) {
2560                  if ( item ) return item.split( '=' );
2561              } ) ) );
2562  
2563              data.plugin_status = queryArgs.plugin_status || 'all';
2564  
2565              if ( window.history && window.history.replaceState ) {
2566                  window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status );
2567              }
2568  
2569              if ( 'undefined' !== typeof wp.updates.searchRequest ) {
2570                  wp.updates.searchRequest.abort();
2571              }
2572  
2573              $bulkActionForm.empty();
2574              $( 'body' ).addClass( 'loading-content' );
2575              $( '.subsubsub .current' ).removeClass( 'current' );
2576  
2577              wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
2578  
2579                  // Can we just ditch this whole subtitle business?
2580                  var $subTitle    = $( '<span />' ).addClass( 'subtitle' ).html(
2581                      sprintf(
2582                          /* translators: %s: Search query. */
2583                          __( 'Search results for: %s' ),
2584                          '<strong>' + _.escape( data.s ) + '</strong>'
2585                      ) ),
2586                      $oldSubTitle = $( '.wrap .subtitle' );
2587  
2588                  if ( ! data.s.length ) {
2589                      $oldSubTitle.remove();
2590                      $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' );
2591                  } else if ( $oldSubTitle.length ) {
2592                      $oldSubTitle.replaceWith( $subTitle );
2593                  } else {
2594                      $( '.wp-header-end' ).before( $subTitle );
2595                  }
2596  
2597                  $( 'body' ).removeClass( 'loading-content' );
2598                  $bulkActionForm.append( response.items );
2599                  delete wp.updates.searchRequest;
2600  
2601                  if ( 0 === response.count ) {
2602                      wp.a11y.speak( __( 'No plugins found. Try a different search.'  ) );
2603                  } else {
2604                      wp.a11y.speak(
2605                          sprintf(
2606                              /* translators: %s: Number of plugins. */
2607                              __( 'Number of plugins found: %d' ),
2608                              response.count
2609                          )
2610                      );
2611                  }
2612              } );
2613          }, 500 ) );
2614  
2615          /**
2616           * Trigger a search event when the search form gets submitted.
2617           *
2618           * @since 4.6.0
2619           */
2620          $document.on( 'submit', '.search-plugins', function( event ) {
2621              event.preventDefault();
2622  
2623              $( 'input.wp-filter-search' ).trigger( 'input' );
2624          } );
2625  
2626          /**
2627           * Trigger a search event when the "Try Again" button is clicked.
2628           *
2629           * @since 4.9.0
2630           */
2631          $document.on( 'click', '.try-again', function( event ) {
2632              event.preventDefault();
2633              $pluginInstallSearch.trigger( 'input' );
2634          } );
2635  
2636          /**
2637           * Trigger a search event when the search type gets changed.
2638           *
2639           * @since 4.6.0
2640           */
2641          $( '#typeselector' ).on( 'change', function() {
2642              var $search = $( 'input[name="s"]' );
2643  
2644              if ( $search.val().length ) {
2645                  $search.trigger( 'input', 'typechange' );
2646              }
2647          } );
2648  
2649          /**
2650           * Click handler for updating a plugin from the details modal on `plugin-install.php`.
2651           *
2652           * @since 4.2.0
2653           *
2654           * @param {Event} event Event interface.
2655           */
2656          $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
2657              var target = window.parent === window ? null : window.parent,
2658                  update;
2659  
2660              $.support.postMessage = !! window.postMessage;
2661  
2662              if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
2663                  return;
2664              }
2665  
2666              event.preventDefault();
2667  
2668              update = {
2669                  action: 'update-plugin',
2670                  data:   {
2671                      plugin: $( this ).data( 'plugin' ),
2672                      slug:   $( this ).data( 'slug' )
2673                  }
2674              };
2675  
2676              target.postMessage( JSON.stringify( update ), window.location.origin );
2677          } );
2678  
2679          /**
2680           * Click handler for installing a plugin from the details modal on `plugin-install.php`.
2681           *
2682           * @since 4.6.0
2683           *
2684           * @param {Event} event Event interface.
2685           */
2686          $( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
2687              var target = window.parent === window ? null : window.parent,
2688                  install;
2689  
2690              $.support.postMessage = !! window.postMessage;
2691  
2692              if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
2693                  return;
2694              }
2695  
2696              event.preventDefault();
2697  
2698              install = {
2699                  action: 'install-plugin',
2700                  data:   {
2701                      slug: $( this ).data( 'slug' )
2702                  }
2703              };
2704  
2705              target.postMessage( JSON.stringify( install ), window.location.origin );
2706          } );
2707  
2708          /**
2709           * Handles postMessage events.
2710           *
2711           * @since 4.2.0
2712           * @since 4.6.0 Switched `update-plugin` action to use the queue.
2713           *
2714           * @param {Event} event Event interface.
2715           */
2716          $( window ).on( 'message', function( event ) {
2717              var originalEvent  = event.originalEvent,
2718                  expectedOrigin = document.location.protocol + '//' + document.location.host,
2719                  message;
2720  
2721              if ( originalEvent.origin !== expectedOrigin ) {
2722                  return;
2723              }
2724  
2725              try {
2726                  message = $.parseJSON( originalEvent.data );
2727              } catch ( e ) {
2728                  return;
2729              }
2730  
2731              if ( ! message || 'undefined' === typeof message.action ) {
2732                  return;
2733              }
2734  
2735              switch ( message.action ) {
2736  
2737                  // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
2738                  case 'decrementUpdateCount':
2739                      /** @property {string} message.upgradeType */
2740                      wp.updates.decrementCount( message.upgradeType );
2741                      break;
2742  
2743                  case 'install-plugin':
2744                  case 'update-plugin':
2745                      /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
2746                      window.tb_remove();
2747                      /* jscs:enable */
2748  
2749                      message.data = wp.updates._addCallbacks( message.data, message.action );
2750  
2751                      wp.updates.queue.push( message );
2752                      wp.updates.queueChecker();
2753                      break;
2754              }
2755          } );
2756  
2757          /**
2758           * Adds a callback to display a warning before leaving the page.
2759           *
2760           * @since 4.2.0
2761           */
2762          $( window ).on( 'beforeunload', wp.updates.beforeunload );
2763  
2764          /**
2765           * Prevents the page form scrolling when activating auto-updates with the Spacebar key.
2766           *
2767           * @since 5.5.0
2768           */
2769          $document.on( 'keydown', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) {
2770              if ( 32 === event.which ) {
2771                  event.preventDefault();
2772              }
2773          } );
2774  
2775          /**
2776           * Click and keyup handler for enabling and disabling plugin and theme auto-updates.
2777           *
2778           * These controls can be either links or buttons. When JavaScript is enabled,
2779           * we want them to behave like buttons. An ARIA role `button` is added via
2780           * the JavaScript that targets elements with the CSS class `aria-button-if-js`.
2781           *
2782           * @since 5.5.0
2783           */
2784          $document.on( 'click keyup', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) {
2785              var data, asset, type, $parent,
2786                  $toggler = $( this ),
2787                  action = $toggler.attr( 'data-wp-action' ),
2788                  $label = $toggler.find( '.label' );
2789  
2790              if ( 'keyup' === event.type && 32 !== event.which ) {
2791                  return;
2792              }
2793  
2794              if ( 'themes' !== pagenow ) {
2795                  $parent = $toggler.closest( '.column-auto-updates' );
2796              } else {
2797                  $parent = $toggler.closest( '.theme-autoupdate' );
2798              }
2799  
2800              event.preventDefault();
2801  
2802              // Prevent multiple simultaneous requests.
2803              if ( $toggler.attr( 'data-doing-ajax' ) === 'yes' ) {
2804                  return;
2805              }
2806  
2807              $toggler.attr( 'data-doing-ajax', 'yes' );
2808  
2809              switch ( pagenow ) {
2810                  case 'plugins':
2811                  case 'plugins-network':
2812                      type = 'plugin';
2813                      asset = $toggler.closest( 'tr' ).attr( 'data-plugin' );
2814                      break;
2815                  case 'themes-network':
2816                      type = 'theme';
2817                      asset = $toggler.closest( 'tr' ).attr( 'data-slug' );
2818                      break;
2819                  case 'themes':
2820                      type = 'theme';
2821                      asset = $toggler.attr( 'data-slug' );
2822                      break;
2823              }
2824  
2825              // Clear any previous errors.
2826              $parent.find( '.notice.notice-error' ).addClass( 'hidden' );
2827  
2828              // Show loading status.
2829              if ( 'enable' === action ) {
2830                  $label.text( __( 'Enabling...' ) );
2831              } else {
2832                  $label.text( __( 'Disabling...' ) );
2833              }
2834  
2835              $toggler.find( '.dashicons-update' ).removeClass( 'hidden' );
2836  
2837              data = {
2838                  action: 'toggle-auto-updates',
2839                  _ajax_nonce: settings.ajax_nonce,
2840                  state: action,
2841                  type: type,
2842                  asset: asset
2843              };
2844  
2845              $.post( window.ajaxurl, data )
2846                  .done( function( response ) {
2847                      var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage,
2848                          href = $toggler.attr( 'href' );
2849  
2850                      if ( ! response.success ) {
2851                          // if WP returns 0 for response (which can happen in a few cases),
2852                          // output the general error message since we won't have response.data.error.
2853                          if ( response.data && response.data.error ) {
2854                              errorMessage = response.data.error;
2855                          } else {
2856                              errorMessage = __( 'The request could not be completed.' );
2857                          }
2858  
2859                          $parent.find( '.notice.notice-error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage );
2860                          wp.a11y.speak( errorMessage, 'assertive' );
2861                          return;
2862                      }
2863  
2864                      // Update the counts in the enabled/disabled views if on a screen
2865                      // with a list table.
2866                      if ( 'themes' !== pagenow ) {
2867                          $enabled       = $( '.auto-update-enabled span' );
2868                          $disabled      = $( '.auto-update-disabled span' );
2869                          enabledNumber  = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
2870                          disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0;
2871  
2872                          switch ( action ) {
2873                              case 'enable':
2874                                  ++enabledNumber;
2875                                  --disabledNumber;
2876                                  break;
2877                              case 'disable':
2878                                  --enabledNumber;
2879                                  ++disabledNumber;
2880                                  break;
2881                          }
2882  
2883                          enabledNumber = Math.max( 0, enabledNumber );
2884                          disabledNumber = Math.max( 0, disabledNumber );
2885  
2886                          $enabled.text( '(' + enabledNumber + ')' );
2887                          $disabled.text( '(' + disabledNumber + ')' );
2888                      }
2889  
2890                      if ( 'enable' === action ) {
2891                          // The toggler control can be either a link or a button.
2892                          if ( $toggler[ 0 ].hasAttribute( 'href' ) ) {
2893                              href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' );
2894                              $toggler.attr( 'href', href );
2895                          }
2896                          $toggler.attr( 'data-wp-action', 'disable' );
2897  
2898                          $label.text( __( 'Disable auto-updates' ) );
2899                          $parent.find( '.auto-update-time' ).removeClass( 'hidden' );
2900                          wp.a11y.speak( __( 'Auto-updates enabled' ) );
2901                      } else {
2902                          // The toggler control can be either a link or a button.
2903                          if ( $toggler[ 0 ].hasAttribute( 'href' ) ) {
2904                              href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' );
2905                              $toggler.attr( 'href', href );
2906                          }
2907                          $toggler.attr( 'data-wp-action', 'enable' );
2908  
2909                          $label.text( __( 'Enable auto-updates' ) );
2910                          $parent.find( '.auto-update-time' ).addClass( 'hidden' );
2911                          wp.a11y.speak( __( 'Auto-updates disabled' ) );
2912                      }
2913  
2914                      $document.trigger( 'wp-auto-update-setting-changed', { state: action, type: type, asset: asset } );
2915                  } )
2916                  .fail( function() {
2917                      $parent.find( '.notice.notice-error' )
2918                          .removeClass( 'hidden' )
2919                          .find( 'p' )
2920                          .text( __( 'The request could not be completed.' ) );
2921  
2922                      wp.a11y.speak( __( 'The request could not be completed.' ), 'assertive' );
2923                  } )
2924                  .always( function() {
2925                      $toggler.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' );
2926                  } );
2927              }
2928          );
2929      } );
2930  })( jQuery, window.wp, window._wpUpdatesSettings );


Generated : Wed Oct 28 08:20:01 2020 Cross-referenced by PHPXref