]> scripts.mit.edu Git - autoinstalls/wordpress.git/blobdiff - wp-admin/js/updates.js
Wordpress 4.6-scripts
[autoinstalls/wordpress.git] / wp-admin / js / updates.js
index 35ae6c7d9b5b367fae315cc2ec4ec9c83abeea7b..239829fe3252294c2c0f8b7077d90966f836adf4 100644 (file)
@@ -1,7 +1,43 @@
-/* global tb_remove */
-window.wp = window.wp || {};
+/**
+ * Functions for ajaxified updates, deletions and installs inside the WordPress admin.
+ *
+ * @version 4.2.0
+ *
+ * @package WordPress
+ * @subpackage Administration
+ */
+
+/* global pagenow */
+
+/**
+ * @param {jQuery}  $                                   jQuery object.
+ * @param {object}  wp                                  WP object.
+ * @param {object}  settings                            WP Updates settings.
+ * @param {string}  settings.ajax_nonce                 AJAX nonce.
+ * @param {object}  settings.l10n                       Translation strings.
+ * @param {object=} settings.plugins                    Base names of plugins in their different states.
+ * @param {Array}   settings.plugins.all                Base names of all plugins.
+ * @param {Array}   settings.plugins.active             Base names of active plugins.
+ * @param {Array}   settings.plugins.inactive           Base names of inactive plugins.
+ * @param {Array}   settings.plugins.upgrade            Base names of plugins with updates available.
+ * @param {Array}   settings.plugins.recently_activated Base names of recently activated plugins.
+ * @param {object=} settings.totals                     Plugin/theme status information or null.
+ * @param {number}  settings.totals.all                 Amount of all plugins or themes.
+ * @param {number}  settings.totals.upgrade             Amount of plugins or themes with updates available.
+ * @param {number}  settings.totals.disabled            Amount of disabled themes.
+ */
+(function( $, wp, settings ) {
+       var $document = $( document );
+
+       wp = wp || {};
 
-(function( $, wp, pagenow ) {
+       /**
+        * The WP Updates object.
+        *
+        * @since 4.2.0
+        *
+        * @type {object}
+        */
        wp.updates = {};
 
        /**
@@ -9,588 +45,2336 @@ window.wp = window.wp || {};
         *
         * @since 4.2.0
         *
-        * @var string
+        * @type {string}
         */
-       wp.updates.ajaxNonce = window._wpUpdatesSettings.ajax_nonce;
+       wp.updates.ajaxNonce = settings.ajax_nonce;
 
        /**
         * Localized strings.
         *
         * @since 4.2.0
         *
-        * @var object
+        * @type {object}
+        */
+       wp.updates.l10n = settings.l10n;
+
+       /**
+        * Current search term.
+        *
+        * @since 4.6.0
+        *
+        * @type {string}
         */
-       wp.updates.l10n = window._wpUpdatesSettings.l10n;
+       wp.updates.searchTerm = '';
 
        /**
         * Whether filesystem credentials need to be requested from the user.
         *
         * @since 4.2.0
         *
-        * @var bool
+        * @type {bool}
         */
-       wp.updates.shouldRequestFilesystemCredentials = null;
+       wp.updates.shouldRequestFilesystemCredentials = false;
 
        /**
         * Filesystem credentials to be packaged along with the request.
         *
         * @since 4.2.0
+        * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
         *
-        * @var object
+        * @type {object} filesystemCredentials                    Holds filesystem credentials.
+        * @type {object} filesystemCredentials.ftp                Holds FTP credentials.
+        * @type {string} filesystemCredentials.ftp.host           FTP host. Default empty string.
+        * @type {string} filesystemCredentials.ftp.username       FTP user name. Default empty string.
+        * @type {string} filesystemCredentials.ftp.password       FTP password. Default empty string.
+        * @type {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
+        *                                                         Default empty string.
+        * @type {object} filesystemCredentials.ssh                Holds SSH credentials.
+        * @type {string} filesystemCredentials.ssh.publicKey      The public key. Default empty string.
+        * @type {string} filesystemCredentials.ssh.privateKey     The private key. Default empty string.
+        * @type {bool}   filesystemCredentials.available          Whether filesystem credentials have been provided.
+        *                                                         Default 'false'.
         */
        wp.updates.filesystemCredentials = {
-               ftp: {
-                       host: null,
-                       username: null,
-                       password: null,
-                       connectionType: null
+               ftp:       {
+                       host:           '',
+                       username:       '',
+                       password:       '',
+                       connectionType: ''
                },
-               ssh: {
-                       publicKey: null,
-                       privateKey: null
-               }
+               ssh:       {
+                       publicKey:  '',
+                       privateKey: ''
+               },
+               available: false
        };
 
        /**
-        * Flag if we're waiting for an update to complete.
+        * Whether we're waiting for an Ajax request to complete.
         *
         * @since 4.2.0
+        * @since 4.6.0 More accurately named `ajaxLocked`.
         *
-        * @var bool
+        * @type {bool}
         */
-       wp.updates.updateLock = false;
+       wp.updates.ajaxLocked = false;
 
        /**
-        * * Flag if we've done an update successfully.
+        * Admin notice template.
         *
-        * @since 4.2.0
+        * @since 4.6.0
         *
-        * @var bool
+        * @type {function} A function that lazily-compiles the template requested.
         */
-       wp.updates.updateDoneSuccessfully = false;
+       wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
 
        /**
+        * Update queue.
+        *
         * If the user tries to update a plugin while an update is
         * already happening, it can be placed in this queue to perform later.
         *
         * @since 4.2.0
+        * @since 4.6.0 More accurately named `queue`.
         *
-        * @var array
+        * @type {Array.object}
         */
-       wp.updates.updateQueue = [];
+       wp.updates.queue = [];
 
        /**
-        * Store a jQuery reference to return focus to when exiting the request credentials modal.
+        * Holds a jQuery reference to return focus to when exiting the request credentials modal.
         *
         * @since 4.2.0
         *
-        * @var jQuery object
+        * @type {jQuery}
+        */
+       wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
+
+       /**
+        * Adds or updates an admin notice.
+        *
+        * @since 4.6.0
+        *
+        * @param {object}  data
+        * @param {*=}      data.selector      Optional. Selector of an element to be replaced with the admin notice.
+        * @param {string=} data.id            Optional. Unique id that will be used as the notice's id attribute.
+        * @param {string=} data.className     Optional. Class names that will be used in the admin notice.
+        * @param {string=} data.message       Optional. The message displayed in the notice.
+        * @param {number=} data.successes     Optional. The amount of successful operations.
+        * @param {number=} data.errors        Optional. The amount of failed operations.
+        * @param {Array=}  data.errorMessages Optional. Error messages of failed operations.
+        *
+        */
+       wp.updates.addAdminNotice = function( data ) {
+               var $notice = $( data.selector ), $adminNotice;
+
+               delete data.selector;
+               $adminNotice = wp.updates.adminNotice( data );
+
+               // Check if this admin notice already exists.
+               if ( ! $notice.length ) {
+                       $notice = $( '#' + data.id );
+               }
+
+               if ( $notice.length ) {
+                       $notice.replaceWith( $adminNotice );
+               } else {
+                       $( '.wrap' ).find( '> h1' ).after( $adminNotice );
+               }
+
+               $document.trigger( 'wp-updates-notice-added' );
+       };
+
+       /**
+        * Handles Ajax requests to WordPress.
+        *
+        * @since 4.6.0
+        *
+        * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
+        * @param {object} data   Data that needs to be passed to the ajax callback.
+        * @return {$.promise}    A jQuery promise that represents the request,
+        *                        decorated with an abort() method.
         */
-       wp.updates.$elToReturnFocusToFromCredentialsModal = null;
+       wp.updates.ajax = function( action, data ) {
+               var options = {};
+
+               if ( wp.updates.ajaxLocked ) {
+                       wp.updates.queue.push( {
+                               action: action,
+                               data:   data
+                       } );
+
+                       // Return a Deferred object so callbacks can always be registered.
+                       return $.Deferred();
+               }
+
+               wp.updates.ajaxLocked = true;
+
+               if ( data.success ) {
+                       options.success = data.success;
+                       delete data.success;
+               }
+
+               if ( data.error ) {
+                       options.error = data.error;
+                       delete data.error;
+               }
+
+               options.data = _.extend( data, {
+                       action:          action,
+                       _ajax_nonce:     wp.updates.ajaxNonce,
+                       username:        wp.updates.filesystemCredentials.ftp.username,
+                       password:        wp.updates.filesystemCredentials.ftp.password,
+                       hostname:        wp.updates.filesystemCredentials.ftp.hostname,
+                       connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
+                       public_key:      wp.updates.filesystemCredentials.ssh.publicKey,
+                       private_key:     wp.updates.filesystemCredentials.ssh.privateKey
+               } );
+
+               return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
+       };
+
+       /**
+        * Actions performed after every Ajax request.
+        *
+        * @since 4.6.0
+        *
+        * @param {object}  response
+        * @param {array=}  response.debug     Optional. Debug information.
+        * @param {string=} response.errorCode Optional. Error code for an error that occurred.
+        */
+       wp.updates.ajaxAlways = function( response ) {
+               if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) {
+                       wp.updates.ajaxLocked = false;
+                       wp.updates.queueChecker();
+               }
+
+               if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) {
+                       _.map( response.debug, function( message ) {
+                               window.console.log( $( '<p />' ).html( message ).text() );
+                       } );
+               }
+       };
 
        /**
-        * Decrement update counts throughout the various menus.
+        * Decrements the update counts throughout the various menus.
+        *
+        * This includes the toolbar, the "Updates" menu item and the menu items
+        * for plugins and themes.
         *
         * @since 3.9.0
         *
-        * @param {string} upgradeType
+        * @param {string} type The type of item that was updated or deleted.
+        *                      Can be 'plugin', 'theme'.
         */
-       wp.updates.decrementCount = function( upgradeType ) {
-               var count,
-                       pluginCount,
-                       $adminBarUpdateCount = $( '#wp-admin-bar-updates .ab-label' ),
+       wp.updates.decrementCount = function( type ) {
+               var $adminBarUpdates             = $( '#wp-admin-bar-updates' ),
                        $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ),
-                       $pluginsMenuItem = $( '#menu-plugins' );
+                       count                        = $adminBarUpdates.find( '.ab-label' ).text(),
+                       $menuItem, $itemCount, itemCount;
 
-
-               count = $adminBarUpdateCount.text();
                count = parseInt( count, 10 ) - 1;
+
                if ( count < 0 || isNaN( count ) ) {
                        return;
                }
-               $( '#wp-admin-bar-updates .ab-item' ).removeAttr( 'title' );
-               $adminBarUpdateCount.text( count );
 
+               $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
+               $adminBarUpdates.find( '.ab-label' ).text( count );
+
+               // Remove the update count from the toolbar if it's zero.
+               if ( ! count ) {
+                       $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
+               }
 
-               $dashboardNavMenuUpdateCount.each( function( index, elem ) {
-                       elem.className = elem.className.replace( /count-\d+/, 'count-' + count );
+               // Update the "Updates" menu item.
+               $dashboardNavMenuUpdateCount.each( function( index, element ) {
+                       element.className = element.className.replace( /count-\d+/, 'count-' + count );
                } );
+
                $dashboardNavMenuUpdateCount.removeAttr( 'title' );
                $dashboardNavMenuUpdateCount.find( '.update-count' ).text( count );
 
-               if ( 'plugin' === upgradeType ) {
-                       pluginCount = $pluginsMenuItem.find( '.plugin-count' ).eq(0).text();
-                       pluginCount = parseInt( pluginCount, 10 ) - 1;
-                       if ( pluginCount < 0 || isNaN( pluginCount ) ) {
-                               return;
-                       }
-                       $pluginsMenuItem.find( '.plugin-count' ).text( pluginCount );
-                       $pluginsMenuItem.find( '.update-plugins' ).each( function( index, elem ) {
-                               elem.className = elem.className.replace( /count-\d+/, 'count-' + pluginCount );
-                       } );
+               if ( 'plugin' === type ) {
+                       $menuItem  = $( '#menu-plugins' );
+                       $itemCount = $menuItem.find( '.plugin-count' );
+               } else if ( 'theme' === type ) {
+                       $menuItem  = $( '#menu-appearance' );
+                       $itemCount = $menuItem.find( '.theme-count' );
+               }
 
-                       if (pluginCount > 0 ) {
-                               $( '.subsubsub .upgrade .count' ).text( '(' + pluginCount + ')' );
-                       } else {
-                               $( '.subsubsub .upgrade' ).remove();
-                       }
+               // Decrement the counter of the other menu items.
+               if ( $itemCount ) {
+                       itemCount = $itemCount.eq( 0 ).text();
+                       itemCount = parseInt( itemCount, 10 ) - 1;
+               }
+
+               if ( itemCount < 0 || isNaN( itemCount ) ) {
+                       return;
+               }
+
+               if ( itemCount > 0 ) {
+                       $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
+
+                       $itemCount.text( itemCount );
+                       $menuItem.find( '.update-plugins' ).each( function( index, element ) {
+                               element.className = element.className.replace( /count-\d+/, 'count-' + itemCount );
+                       } );
+               } else {
+                       $( '.subsubsub .upgrade' ).remove();
+                       $menuItem.find( '.update-plugins' ).remove();
                }
        };
 
        /**
-        * Send an Ajax request to the server to update a plugin.
+        * Sends an Ajax request to the server to update a plugin.
         *
         * @since 4.2.0
+        * @since 4.6.0 More accurately named `updatePlugin`.
         *
-        * @param {string} plugin
-        * @param {string} slug
+        * @param {object}               args         Arguments.
+        * @param {string}               args.plugin  Plugin basename.
+        * @param {string}               args.slug    Plugin slug.
+        * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess
+        * @param {updatePluginError=}   args.error   Optional. Error callback. Default: wp.updates.updatePluginError
+        * @return {$.promise} A jQuery promise that represents the request,
+        *                     decorated with an abort() method.
         */
-       wp.updates.updatePlugin = function( plugin, slug ) {
-               var $message, name,
-                       $card = $( '.plugin-card-' + slug );
+       wp.updates.updatePlugin = function( args ) {
+               var $updateRow, $card, $message, message;
+
+               args = _.extend( {
+                       success: wp.updates.updatePluginSuccess,
+                       error: wp.updates.updatePluginError
+               }, args );
 
                if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
-                       $message = $( '[data-plugin="' + plugin + '"]' ).next().find( '.update-message' );
-               } else if ( 'plugin-install' === pagenow ) {
-                       $message = $card.find( '.update-now' );
-                       name = $message.data( 'name' );
-                       $message.attr( 'aria-label', wp.updates.l10n.updatingLabel.replace( '%s', name ) );
+                       $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
+                       $message   = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
+                       message    = wp.updates.l10n.updatingLabel.replace( '%s', $updateRow.find( '.plugin-title strong' ).text() );
+               } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
+                       $card    = $( '.plugin-card-' + args.slug );
+                       $message = $card.find( '.update-now' ).addClass( 'updating-message' );
+                       message  = wp.updates.l10n.updatingLabel.replace( '%s', $message.data( 'name' ) );
+
                        // Remove previous error messages, if any.
                        $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
                }
 
-               $message.addClass( 'updating-message' );
-               if ( $message.html() !== wp.updates.l10n.updating ){
+               if ( $message.html() !== wp.updates.l10n.updating ) {
                        $message.data( 'originaltext', $message.html() );
                }
 
-               $message.text( wp.updates.l10n.updating );
-               wp.a11y.speak( wp.updates.l10n.updatingMsg );
-
-               if ( wp.updates.updateLock ) {
-                       wp.updates.updateQueue.push( {
-                               type: 'update-plugin',
-                               data: {
-                                       plugin: plugin,
-                                       slug: slug
-                               }
-                       } );
-                       return;
-               }
-
-               wp.updates.updateLock = true;
+               $message
+                       .attr( 'aria-label', message )
+                       .text( wp.updates.l10n.updating );
 
-               var data = {
-                       _ajax_nonce:     wp.updates.ajaxNonce,
-                       plugin:          plugin,
-                       slug:            slug,
-                       username:        wp.updates.filesystemCredentials.ftp.username,
-                       password:        wp.updates.filesystemCredentials.ftp.password,
-                       hostname:        wp.updates.filesystemCredentials.ftp.hostname,
-                       connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
-                       public_key:      wp.updates.filesystemCredentials.ssh.publicKey,
-                       private_key:     wp.updates.filesystemCredentials.ssh.privateKey
-               };
+               $document.trigger( 'wp-plugin-updating', args );
 
-               wp.ajax.post( 'update-plugin', data )
-                       .done( wp.updates.updateSuccess )
-                       .fail( wp.updates.updateError );
+               return wp.updates.ajax( 'update-plugin', args );
        };
 
        /**
-        * On a successful plugin update, update the UI with the result.
+        * Updates the UI appropriately after a successful plugin update.
         *
         * @since 4.2.0
+        * @since 4.6.0 More accurately named `updatePluginSuccess`.
         *
-        * @param {object} response
+        * @typedef {object} updatePluginSuccess
+        * @param {object} response            Response from the server.
+        * @param {string} response.slug       Slug of the plugin to be updated.
+        * @param {string} response.plugin     Basename of the plugin to be updated.
+        * @param {string} response.pluginName Name of the plugin to be updated.
+        * @param {string} response.oldVersion Old version of the plugin.
+        * @param {string} response.newVersion New version of the plugin.
         */
-       wp.updates.updateSuccess = function( response ) {
-               var $updateMessage, name, $pluginRow, newText;
+       wp.updates.updatePluginSuccess = function( response ) {
+               var $pluginRow, $updateMessage, newText;
+
                if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
-                       $pluginRow = $( '[data-plugin="' + response.plugin + '"]' ).first();
-                       $updateMessage = $pluginRow.next().find( '.update-message' );
-                       $pluginRow.addClass( 'updated' ).removeClass( 'update' );
+                       $pluginRow     = $( 'tr[data-plugin="' + response.plugin + '"]' )
+                               .removeClass( 'update' )
+                               .addClass( 'updated' );
+                       $updateMessage = $pluginRow.find( '.update-message' )
+                               .removeClass( 'updating-message notice-warning' )
+                               .addClass( 'updated-message notice-success' ).find( 'p' );
 
                        // Update the version number in the row.
-                       newText = $pluginRow.find('.plugin-version-author-uri').html().replace( response.oldVersion, response.newVersion );
-                       $pluginRow.find('.plugin-version-author-uri').html( newText );
-
-                       // Add updated class to update message parent tr
-                       $pluginRow.next().addClass( 'updated' );
-               } else if ( 'plugin-install' === pagenow ) {
-                       $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' );
-                       $updateMessage.addClass( 'button-disabled' );
-                       name = $updateMessage.data( 'name' );
-                       $updateMessage.attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', name ) );
+                       newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
+                       $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
+               } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
+                       $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
+                               .removeClass( 'updating-message' )
+                               .addClass( 'button-disabled updated-message' );
                }
 
-               $updateMessage.removeClass( 'updating-message' ).addClass( 'updated-message' );
-               $updateMessage.text( wp.updates.l10n.updated );
-               wp.a11y.speak( wp.updates.l10n.updatedMsg );
-
-               wp.updates.decrementCount( 'plugin' );
-
-               wp.updates.updateDoneSuccessfully = true;
+               $updateMessage
+                       .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) )
+                       .text( wp.updates.l10n.updated );
 
-               /*
-                * The lock can be released since the update was successful,
-                * and any other updates can commence.
-                */
-               wp.updates.updateLock = false;
+               wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
 
-               $(document).trigger( 'wp-plugin-update-success', response );
+               wp.updates.decrementCount( 'plugin' );
 
-               wp.updates.queueChecker();
+               $document.trigger( 'wp-plugin-update-success', response );
        };
 
-
        /**
-        * On a plugin update error, update the UI appropriately.
+        * Updates the UI appropriately after a failed plugin update.
         *
         * @since 4.2.0
+        * @since 4.6.0 More accurately named `updatePluginError`.
         *
-        * @param {object} response
+        * @typedef {object} updatePluginError
+        * @param {object}  response              Response from the server.
+        * @param {string}  response.slug         Slug of the plugin to be updated.
+        * @param {string}  response.plugin       Basename of the plugin to be updated.
+        * @param {string=} response.pluginName   Optional. Name of the plugin to be updated.
+        * @param {string}  response.errorCode    Error code for the error that occurred.
+        * @param {string}  response.errorMessage The error that occurred.
         */
-       wp.updates.updateError = function( response ) {
-               var $card = $( '.plugin-card-' + response.slug ),
-                       $message,
-                       $button,
-                       name,
-                       error_message;
+       wp.updates.updatePluginError = function( response ) {
+               var $card, $message, errorMessage;
 
-               wp.updates.updateDoneSuccessfully = false;
+               if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
+                       return;
+               }
 
-               if ( response.errorCode && response.errorCode == 'unable_to_connect_to_filesystem' && wp.updates.shouldRequestFilesystemCredentials ) {
-                       wp.updates.credentialError( response, 'update-plugin' );
+               if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
                        return;
                }
 
-               error_message = wp.updates.l10n.updateFailed.replace( '%s', response.error );
+               errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage );
 
                if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
-                       $message = $( '[data-plugin="' + response.plugin + '"]' ).next().find( '.update-message' );
-                       $message.html( error_message ).removeClass( 'updating-message' );
-               } else if ( 'plugin-install' === pagenow ) {
-                       $button = $card.find( '.update-now' );
-                       name = $button.data( 'name' );
+                       if ( response.plugin ) {
+                               $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
+                       } else {
+                               $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
+                       }
+                       $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
 
-                       $card
+                       if ( response.pluginName ) {
+                               $message.find( 'p' )
+                                       .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
+                       } else {
+                               $message.find( 'p' ).removeAttr( 'aria-label' );
+                       }
+               } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
+                       $card = $( '.plugin-card-' + response.slug )
                                .addClass( 'plugin-card-update-failed' )
-                               .append( '<div class="notice notice-error is-dismissible"><p>' + error_message + '</p></div>' );
+                               .append( wp.updates.adminNotice( {
+                                       className: 'update-message notice-error notice-alt is-dismissible',
+                                       message:   errorMessage
+                               } ) );
 
-                       $button
-                               .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', name ) )
-                               .html( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' );
+                       $card.find( '.update-now' )
+                               .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' );
+
+                       if ( response.pluginName ) {
+                               $card.find( '.update-now' )
+                                       .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
+                       } else {
+                               $card.find( '.update-now' ).removeAttr( 'aria-label' );
+                       }
 
                        $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
+
                                // Use same delay as the total duration of the notice fadeTo + slideUp animation.
                                setTimeout( function() {
                                        $card
                                                .removeClass( 'plugin-card-update-failed' )
                                                .find( '.column-name a' ).focus();
+
+                                       $card.find( '.update-now' )
+                                               .attr( 'aria-label', false )
+                                               .text( wp.updates.l10n.updateNow );
                                }, 200 );
-                       });
+                       } );
                }
 
-               wp.a11y.speak( error_message, 'assertive' );
-
-               /*
-                * The lock can be released since this failure was
-                * after the credentials form.
-                */
-               wp.updates.updateLock = false;
-
-               $(document).trigger( 'wp-plugin-update-error', response );
+               wp.a11y.speak( errorMessage, 'assertive' );
 
-               wp.updates.queueChecker();
+               $document.trigger( 'wp-plugin-update-error', response );
        };
 
        /**
-        * Show an error message in the request for credentials form.
+        * Sends an Ajax request to the server to install a plugin.
         *
-        * @param {string} message
-        * @since 4.2.0
+        * @since 4.6.0
+        *
+        * @param {object}                args         Arguments.
+        * @param {string}                args.slug    Plugin identifier in the WordPress.org Plugin repository.
+        * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
+        * @param {installPluginError=}   args.error   Optional. Error callback. Default: wp.updates.installPluginError
+        * @return {$.promise} A jQuery promise that represents the request,
+        *                     decorated with an abort() method.
         */
-       wp.updates.showErrorInCredentialsForm = function( message ) {
-               var $modal = $( '.notification-dialog' );
+       wp.updates.installPlugin = function( args ) {
+               var $card    = $( '.plugin-card-' + args.slug ),
+                       $message = $card.find( '.install-now' );
 
-               // Remove any existing error.
-               $modal.find( '.error' ).remove();
+               args = _.extend( {
+                       success: wp.updates.installPluginSuccess,
+                       error: wp.updates.installPluginError
+               }, args );
+
+               if ( 'import' === pagenow ) {
+                       $message = $( '[data-slug="' + args.slug + '"]' );
+               }
+
+               if ( $message.html() !== wp.updates.l10n.installing ) {
+                       $message.data( 'originaltext', $message.html() );
+               }
+
+               $message
+                       .addClass( 'updating-message' )
+                       .attr( 'aria-label', wp.updates.l10n.pluginInstallingLabel.replace( '%s', $message.data( 'name' ) ) )
+                       .text( wp.updates.l10n.installing );
 
-               $modal.find( 'h3' ).after( '<div class="error">' + message + '</div>' );
+               wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
+
+               // Remove previous error messages, if any.
+               $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
+
+               $document.trigger( 'wp-plugin-installing', args );
+
+               return wp.updates.ajax( 'install-plugin', args );
        };
 
        /**
-        * Events that need to happen when there is a credential error
+        * Updates the UI appropriately after a successful plugin install.
         *
-        * @since 4.2.0
+        * @since 4.6.0
+        *
+        * @typedef {object} installPluginSuccess
+        * @param {object} response             Response from the server.
+        * @param {string} response.slug        Slug of the installed plugin.
+        * @param {string} response.pluginName  Name of the installed plugin.
+        * @param {string} response.activateUrl URL to activate the just installed plugin.
         */
-       wp.updates.credentialError = function( response, type ) {
-               wp.updates.updateQueue.push( {
-                       'type': type,
-                       'data': {
-                               // Not cool that we're depending on response for this data.
-                               // This would feel more whole in a view all tied together.
-                               plugin: response.plugin,
-                               slug: response.slug
-                       }
-               } );
-               wp.updates.showErrorInCredentialsForm( response.error );
-               wp.updates.requestFilesystemCredentials();
+       wp.updates.installPluginSuccess = function( response ) {
+               var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
+
+               $message
+                       .removeClass( 'updating-message' )
+                       .addClass( 'updated-message installed button-disabled' )
+                       .attr( 'aria-label', wp.updates.l10n.pluginInstalledLabel.replace( '%s', response.pluginName ) )
+                       .text( wp.updates.l10n.installed );
+
+               wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
+
+               $document.trigger( 'wp-plugin-install-success', response );
+
+               if ( response.activateUrl ) {
+                       setTimeout( function() {
+
+                               // Transform the 'Install' button into an 'Activate' button.
+                               $message.removeClass( 'install-now installed button-disabled updated-message' ).addClass( 'activate-now button-primary' )
+                                       .attr( 'href', response.activateUrl )
+                                       .attr( 'aria-label', wp.updates.l10n.activatePluginLabel.replace( '%s', response.pluginName ) )
+                                       .text( wp.updates.l10n.activatePlugin );
+                       }, 1000 );
+               }
        };
 
        /**
-        * If an update job has been placed in the queue, queueChecker pulls it out and runs it.
+        * Updates the UI appropriately after a failed plugin install.
         *
-        * @since 4.2.0
+        * @since 4.6.0
+        *
+        * @typedef {object} installPluginError
+        * @param {object}  response              Response from the server.
+        * @param {string}  response.slug         Slug of the plugin to be installed.
+        * @param {string=} response.pluginName   Optional. Name of the plugin to be installed.
+        * @param {string}  response.errorCode    Error code for the error that occurred.
+        * @param {string}  response.errorMessage The error that occurred.
         */
-       wp.updates.queueChecker = function() {
-               if ( wp.updates.updateLock || wp.updates.updateQueue.length <= 0 ) {
+       wp.updates.installPluginError = function( response ) {
+               var $card   = $( '.plugin-card-' + response.slug ),
+                       $button = $card.find( '.install-now' ),
+                       errorMessage;
+
+               if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
                        return;
                }
 
-               var job = wp.updates.updateQueue.shift();
+               if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
+                       return;
+               }
 
-               wp.updates.updatePlugin( job.data.plugin, job.data.slug );
-       };
+               errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage );
 
+               $card
+                       .addClass( 'plugin-card-update-failed' )
+                       .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
 
-       /**
-        * Request the users filesystem credentials if we don't have them already.
-        *
-        * @since 4.2.0
-        */
-       wp.updates.requestFilesystemCredentials = function( event ) {
-               if ( wp.updates.updateDoneSuccessfully === false ) {
-                       /*
-                        * For the plugin install screen, return the focus to the install button
-                        * after exiting the credentials request modal.
-                        */
-                       if ( 'plugin-install' === pagenow && event ) {
-                               wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
-                       }
+               $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
 
-                       wp.updates.updateLock = true;
+                       // Use same delay as the total duration of the notice fadeTo + slideUp animation.
+                       setTimeout( function() {
+                               $card
+                                       .removeClass( 'plugin-card-update-failed' )
+                                       .find( '.column-name a' ).focus();
+                       }, 200 );
+               } );
 
-                       wp.updates.requestForCredentialsModalOpen();
-               }
+               $button
+                       .removeClass( 'updating-message' ).addClass( 'button-disabled' )
+                       .attr( 'aria-label', wp.updates.l10n.pluginInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
+                       .text( wp.updates.l10n.installFailedShort );
+
+               wp.a11y.speak( errorMessage, 'assertive' );
+
+               $document.trigger( 'wp-plugin-install-error', response );
        };
 
        /**
-        * Keydown handler for the request for credentials modal.
+        * Updates the UI appropriately after a successful importer install.
         *
-        * Close the modal when the escape key is pressed.
-        * Constrain keyboard navigation to inside the modal.
+        * @since 4.6.0
         *
-        * @since 4.2.0
+        * @typedef {object} installImporterSuccess
+        * @param {object} response             Response from the server.
+        * @param {string} response.slug        Slug of the installed plugin.
+        * @param {string} response.pluginName  Name of the installed plugin.
+        * @param {string} response.activateUrl URL to activate the just installed plugin.
         */
-       wp.updates.keydown = function( event ) {
-               if ( 27 === event.keyCode ) {
-                       wp.updates.requestForCredentialsModalCancel();
-               } else if ( 9 === event.keyCode ) {
-                       // #upgrade button must always be the last focusable element in the dialog.
-                       if ( event.target.id === 'upgrade' && ! event.shiftKey ) {
-                               $( '#hostname' ).focus();
-                               event.preventDefault();
-                       } else if ( event.target.id === 'hostname' && event.shiftKey ) {
-                               $( '#upgrade' ).focus();
-                               event.preventDefault();
-                       }
-               }
-       };
+       wp.updates.installImporterSuccess = function( response ) {
+               wp.updates.addAdminNotice( {
+                       id:        'install-success',
+                       className: 'notice-success is-dismissible',
+                       message:   wp.updates.l10n.importerInstalledMsg.replace( '%s', response.activateUrl + '&from=import' )
+               } );
 
-       /**
-        * Open the request for credentials modal.
-        *
-        * @since 4.2.0
-        */
-       wp.updates.requestForCredentialsModalOpen = function() {
-               var $modal = $( '#request-filesystem-credentials-dialog' );
-               $( 'body' ).addClass( 'modal-open' );
-               $modal.show();
+               $( '[data-slug="' + response.slug + '"]' )
+                       .removeClass( 'install-now updating-message' )
+                       .addClass( 'activate-now' )
+                       .attr({
+                               'href': response.activateUrl + '&from=import',
+                               'aria-label': wp.updates.l10n.activateImporterLabel.replace( '%s', response.pluginName )
+                       })
+                       .text( wp.updates.l10n.activateImporter );
 
-               $modal.find( 'input:enabled:first' ).focus();
-               $modal.keydown( wp.updates.keydown );
-       };
+               wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
 
-       /**
-        * Close the request for credentials modal.
-        *
-        * @since 4.2.0
-        */
-       wp.updates.requestForCredentialsModalClose = function() {
-               $( '#request-filesystem-credentials-dialog' ).hide();
-               $( 'body' ).removeClass( 'modal-open' );
-               wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
+               $document.trigger( 'wp-importer-install-success', response );
        };
 
        /**
-        * The steps that need to happen when the modal is canceled out
+        * Updates the UI appropriately after a failed importer install.
         *
-        * @since 4.2.0
+        * @since 4.6.0
+        *
+        * @typedef {object} installImporterError
+        * @param {object}  response              Response from the server.
+        * @param {string}  response.slug         Slug of the plugin to be installed.
+        * @param {string=} response.pluginName   Optional. Name of the plugin to be installed.
+        * @param {string}  response.errorCode    Error code for the error that occurred.
+        * @param {string}  response.errorMessage The error that occurred.
         */
-       wp.updates.requestForCredentialsModalCancel = function() {
-               // no updateLock and no updateQueue means we already have cleared things up
-               var data, $message;
+       wp.updates.installImporterError = function( response ) {
+               var errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
+                       $installLink = $( '[data-slug="' + response.slug + '"]' ),
+                       pluginName = $installLink.data( 'name' );
+
+               if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
+                       return;
+               }
 
-               if( wp.updates.updateLock === false && wp.updates.updateQueue.length === 0 ){
+               if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
                        return;
                }
 
-               data = wp.updates.updateQueue[0].data;
+               wp.updates.addAdminNotice( {
+                       id:        response.errorCode,
+                       className: 'notice-error is-dismissible',
+                       message:   errorMessage
+               } );
 
-               // remove the lock, and clear the queue
-               wp.updates.updateLock = false;
-               wp.updates.updateQueue = [];
+               $installLink
+                       .removeClass( 'updating-message' )
+                       .text( wp.updates.l10n.installNow )
+                       .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
 
-               wp.updates.requestForCredentialsModalClose();
-               if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
-                       $message = $( '[data-plugin="' + data.plugin + '"]' ).next().find( '.update-message' );
-               } else if ( 'plugin-install' === pagenow ) {
-                       $message = $( '.plugin-card-' + data.slug ).find( '.update-now' );
-               }
+               wp.a11y.speak( errorMessage, 'assertive' );
 
-               $message.removeClass( 'updating-message' );
-               $message.html( $message.data( 'originaltext' ) );
-               wp.a11y.speak( wp.updates.l10n.updateCancel );
+               $document.trigger( 'wp-importer-install-error', response );
        };
+
        /**
-        * Potentially add an AYS to a user attempting to leave the page
+        * Sends an Ajax request to the server to delete a plugin.
         *
-        * If an update is on-going and a user attempts to leave the page,
-        * open an "Are you sure?" alert.
+        * @since 4.6.0
         *
-        * @since 4.2.0
+        * @param {object}               args         Arguments.
+        * @param {string}               args.plugin  Basename of the plugin to be deleted.
+        * @param {string}               args.slug    Slug of the plugin to be deleted.
+        * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
+        * @param {deletePluginError=}   args.error   Optional. Error callback. Default: wp.updates.deletePluginError
+        * @return {$.promise} A jQuery promise that represents the request,
+        *                     decorated with an abort() method.
         */
-
-       wp.updates.beforeunload = function() {
-               if ( wp.updates.updateLock ) {
-                       return wp.updates.l10n.beforeunload;
+       wp.updates.deletePlugin = function( args ) {
+               var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
+
+               args = _.extend( {
+                       success: wp.updates.deletePluginSuccess,
+                       error: wp.updates.deletePluginError
+               }, args );
+
+               if ( $link.html() !== wp.updates.l10n.deleting ) {
+                       $link
+                               .data( 'originaltext', $link.html() )
+                               .text( wp.updates.l10n.deleting );
                }
-       };
 
+               wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
 
-       $( document ).ready( function() {
-               /*
-                * Check whether a user needs to submit filesystem credentials based on whether
-                * the form was output on the page server-side.
-                *
-                * @see {wp_print_request_filesystem_credentials_modal() in PHP}
-                */
-               wp.updates.shouldRequestFilesystemCredentials = ( $( '#request-filesystem-credentials-dialog' ).length <= 0 ) ? false : true;
+               $document.trigger( 'wp-plugin-deleting', args );
 
-               // File system credentials form submit noop-er / handler.
-               $( '#request-filesystem-credentials-dialog form' ).on( 'submit', function() {
-                       // Persist the credentials input by the user for the duration of the page load.
-                       wp.updates.filesystemCredentials.ftp.hostname = $('#hostname').val();
-                       wp.updates.filesystemCredentials.ftp.username = $('#username').val();
-                       wp.updates.filesystemCredentials.ftp.password = $('#password').val();
-                       wp.updates.filesystemCredentials.ftp.connectionType = $('input[name="connection_type"]:checked').val();
-                       wp.updates.filesystemCredentials.ssh.publicKey = $('#public_key').val();
-                       wp.updates.filesystemCredentials.ssh.privateKey = $('#private_key').val();
+               return wp.updates.ajax( 'delete-plugin', args );
+       };
 
-                       wp.updates.requestForCredentialsModalClose();
+       /**
+        * Updates the UI appropriately after a successful plugin deletion.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} deletePluginSuccess
+        * @param {object} response            Response from the server.
+        * @param {string} response.slug       Slug of the plugin that was deleted.
+        * @param {string} response.plugin     Base name of the plugin that was deleted.
+        * @param {string} response.pluginName Name of the plugin that was deleted.
+        */
+       wp.updates.deletePluginSuccess = function( response ) {
+
+               // Removes the plugin and updates rows.
+               $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
+                       var $form            = $( '#bulk-action-form' ),
+                               $views           = $( '.subsubsub' ),
+                               $pluginRow       = $( this ),
+                               columnCount      = $form.find( 'thead th:not(.hidden), thead td' ).length,
+                               pluginDeletedRow = wp.template( 'item-deleted-row' ),
+                               /** @type {object} plugins Base names of plugins in their different states. */
+                               plugins          = settings.plugins;
+
+                       // Add a success message after deleting a plugin.
+                       if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
+                               $pluginRow.after(
+                                       pluginDeletedRow( {
+                                               slug:    response.slug,
+                                               plugin:  response.plugin,
+                                               colspan: columnCount,
+                                               name:    response.pluginName
+                                       } )
+                               );
+                       }
 
-                       // Unlock and invoke the queue.
-                       wp.updates.updateLock = false;
-                       wp.updates.queueChecker();
+                       $pluginRow.remove();
 
-                       return false;
-               });
+                       // Remove plugin from update count.
+                       if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
+                               plugins.upgrade = _.without( plugins.upgrade, response.plugin );
+                               wp.updates.decrementCount( 'plugin' );
+                       }
 
-               // Close the request credentials modal when
-               $( '#request-filesystem-credentials-dialog [data-js-action="close"], .notification-dialog-background' ).on( 'click', function() {
-                       wp.updates.requestForCredentialsModalCancel();
-               });
+                       // Remove from views.
+                       if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
+                               plugins.inactive = _.without( plugins.inactive, response.plugin );
+                               if ( plugins.inactive.length ) {
+                                       $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
+                               } else {
+                                       $views.find( '.inactive' ).remove();
+                               }
+                       }
 
-               // Hide SSH fields when not selected
-               $( '#request-filesystem-credentials-dialog input[name="connection_type"]' ).on( 'change', function() {
-                       $( this ).parents( 'form' ).find( '#private_key, #public_key' ).parents( 'label' ).toggle( ( 'ssh' == $( this ).val() ) );
-               }).change();
+                       if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
+                               plugins.active = _.without( plugins.active, response.plugin );
+                               if ( plugins.active.length ) {
+                                       $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
+                               } else {
+                                       $views.find( '.active' ).remove();
+                               }
+                       }
 
-               // Click handler for plugin updates in List Table view.
-               $( '.plugin-update-tr' ).on( 'click', '.update-link', function( e ) {
-                       e.preventDefault();
-                       if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.updateLock ) {
-                               wp.updates.requestFilesystemCredentials( e );
+                       if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
+                               plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
+                               if ( plugins.recently_activated.length ) {
+                                       $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
+                               } else {
+                                       $views.find( '.recently_activated' ).remove();
+                               }
                        }
-                       var updateRow = $( e.target ).parents( '.plugin-update-tr' );
-                       // Return the user to the input box of the plugin's table row after closing the modal.
-                       wp.updates.$elToReturnFocusToFromCredentialsModal = updateRow.prev().find( '.check-column input' );
-                       wp.updates.updatePlugin( updateRow.data( 'plugin' ), updateRow.data( 'slug' ) );
-               } );
 
-               $( '.plugin-card' ).on( 'click', '.update-now', function( e ) {
-                       e.preventDefault();
-                       var $button = $( e.target );
+                       plugins.all = _.without( plugins.all, response.plugin );
 
-                       // Do nothing while updating and when the button is disabled.
-                       if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
-                               return;
-                       }
+                       if ( plugins.all.length ) {
+                               $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
+                       } else {
+                               $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
+                               $views.find( '.all' ).remove();
 
-                       if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.updateLock ) {
-                               wp.updates.requestFilesystemCredentials( e );
+                               if ( ! $form.find( 'tr.no-items' ).length ) {
+                                       $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + wp.updates.l10n.noPlugins + '</td></tr>' );
+                               }
                        }
-
-                       wp.updates.updatePlugin( $button.data( 'plugin' ), $button.data( 'slug' ) );
                } );
 
-               $( '#plugin_update_from_iframe' ).on( 'click' , function( e ) {
-                       var target, job;
+               wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
 
-                       target = window.parent == window ? null : window.parent,
-                       $.support.postMessage = !! window.postMessage;
+               $document.trigger( 'wp-plugin-delete-success', response );
+       };
 
-                       if ( $.support.postMessage === false || target === null || window.parent.location.pathname.indexOf( 'update-core.php' ) !== -1 )
-                               return;
+       /**
+        * Updates the UI appropriately after a failed plugin deletion.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} deletePluginError
+        * @param {object}  response              Response from the server.
+        * @param {string}  response.slug         Slug of the plugin to be deleted.
+        * @param {string}  response.plugin       Base name of the plugin to be deleted
+        * @param {string=} response.pluginName   Optional. Name of the plugin to be deleted.
+        * @param {string}  response.errorCode    Error code for the error that occurred.
+        * @param {string}  response.errorMessage The error that occurred.
+        */
+       wp.updates.deletePluginError = function( response ) {
+               var $plugin, $pluginUpdateRow,
+                       pluginUpdateRow  = wp.template( 'item-update-row' ),
+                       noticeContent    = wp.updates.adminNotice( {
+                               className: 'update-message notice-error notice-alt',
+                               message:   response.errorMessage
+                       } );
 
-                       e.preventDefault();
+               if ( response.plugin ) {
+                       $plugin          = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
+                       $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
+               } else {
+                       $plugin          = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
+                       $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
+               }
 
-                       job = {
-                               action: 'updatePlugin',
-                               type: 'update-plugin',
-                               data: {
-                                       plugin: $( this ).data( 'plugin' ),
-                                       slug: $( this ).data( 'slug' )
+               if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
+                       return;
+               }
+
+               if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
+                       return;
+               }
+
+               // Add a plugin update row if it doesn't exist yet.
+               if ( ! $pluginUpdateRow.length ) {
+                       $plugin.addClass( 'update' ).after(
+                               pluginUpdateRow( {
+                                       slug:    response.slug,
+                                       plugin:  response.plugin || response.slug,
+                                       colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
+                                       content: noticeContent
+                               } )
+                       );
+               } else {
+
+                       // Remove previous error messages, if any.
+                       $pluginUpdateRow.find( '.notice-error' ).remove();
+
+                       $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
+               }
+
+               $document.trigger( 'wp-plugin-delete-error', response );
+       };
+
+       /**
+        * Sends an Ajax request to the server to update a theme.
+        *
+        * @since 4.6.0
+        *
+        * @param {object}              args         Arguments.
+        * @param {string}              args.slug    Theme stylesheet.
+        * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
+        * @param {updateThemeError=}   args.error   Optional. Error callback. Default: wp.updates.updateThemeError
+        * @return {$.promise} A jQuery promise that represents the request,
+        *                     decorated with an abort() method.
+        */
+       wp.updates.updateTheme = function( args ) {
+               var $notice;
+
+               args = _.extend( {
+                       success: wp.updates.updateThemeSuccess,
+                       error: wp.updates.updateThemeError
+               }, args );
+
+               if ( 'themes-network' === pagenow ) {
+                       $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
+
+               } else {
+                       $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
+
+                       $notice.find( 'h3' ).remove();
+
+                       $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
+                       $notice = $notice.addClass( 'updating-message' ).find( 'p' );
+               }
+
+               if ( $notice.html() !== wp.updates.l10n.updating ) {
+                       $notice.data( 'originaltext', $notice.html() );
+               }
+
+               wp.a11y.speak( wp.updates.l10n.updatingMsg, 'polite' );
+               $notice.text( wp.updates.l10n.updating );
+
+               $document.trigger( 'wp-theme-updating', args );
+
+               return wp.updates.ajax( 'update-theme', args );
+       };
+
+       /**
+        * Updates the UI appropriately after a successful theme update.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} updateThemeSuccess
+        * @param {object} response
+        * @param {string} response.slug       Slug of the theme to be updated.
+        * @param {object} response.theme      Updated theme.
+        * @param {string} response.oldVersion Old version of the theme.
+        * @param {string} response.newVersion New version of the theme.
+        */
+       wp.updates.updateThemeSuccess = function( response ) {
+               var isModalOpen    = $( 'body.modal-open' ).length,
+                       $theme         = $( '[data-slug="' + response.slug + '"]' ),
+                       updatedMessage = {
+                               className: 'updated-message notice-success notice-alt',
+                               message:   wp.updates.l10n.updated
+                       },
+                       $notice, newText;
+
+               if ( 'themes-network' === pagenow ) {
+                       $notice = $theme.find( '.update-message' );
+
+                       // Update the version number in the row.
+                       newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
+                       $theme.find( '.theme-version-author-uri' ).html( newText );
+               } else {
+                       $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
+
+                       // Focus on Customize button after updating.
+                       if ( isModalOpen ) {
+                               $( '.load-customize:visible' ).focus();
+                       } else {
+                               $theme.find( '.load-customize' ).focus();
+                       }
+               }
+
+               wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
+               wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
+
+               wp.updates.decrementCount( 'theme' );
+
+               $document.trigger( 'wp-theme-update-success', response );
+
+               // Show updated message after modal re-rendered.
+               if ( isModalOpen ) {
+                       $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
+               }
+       };
+
+       /**
+        * Updates the UI appropriately after a failed theme update.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} updateThemeError
+        * @param {object} response              Response from the server.
+        * @param {string} response.slug         Slug of the theme to be updated.
+        * @param {string} response.errorCode    Error code for the error that occurred.
+        * @param {string} response.errorMessage The error that occurred.
+        */
+       wp.updates.updateThemeError = function( response ) {
+               var $theme       = $( '[data-slug="' + response.slug + '"]' ),
+                       errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ),
+                       $notice;
+
+               if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
+                       return;
+               }
+
+               if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
+                       return;
+               }
+
+               if ( 'themes-network' === pagenow ) {
+                       $notice = $theme.find( '.update-message ' );
+               } else {
+                       $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
+
+                       $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).focus() : $theme.find( '.load-customize' ).focus();
+               }
+
+               wp.updates.addAdminNotice( {
+                       selector:  $notice,
+                       className: 'update-message notice-error notice-alt is-dismissible',
+                       message:   errorMessage
+               } );
+
+               wp.a11y.speak( errorMessage, 'polite' );
+
+               $document.trigger( 'wp-theme-update-error', response );
+       };
+
+       /**
+        * Sends an Ajax request to the server to install a theme.
+        *
+        * @since 4.6.0
+        *
+        * @param {object}               args
+        * @param {string}               args.slug    Theme stylesheet.
+        * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
+        * @param {installThemeError=}   args.error   Optional. Error callback. Default: wp.updates.installThemeError
+        * @return {$.promise} A jQuery promise that represents the request,
+        *                     decorated with an abort() method.
+        */
+       wp.updates.installTheme = function( args ) {
+               var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
+
+               args = _.extend( {
+                       success: wp.updates.installThemeSuccess,
+                       error: wp.updates.installThemeError
+               }, args );
+
+               $message.addClass( 'updating-message' );
+               $message.parents( '.theme' ).addClass( 'focus' );
+               if ( $message.html() !== wp.updates.l10n.installing ) {
+                       $message.data( 'originaltext', $message.html() );
+               }
+
+               $message
+                       .text( wp.updates.l10n.installing )
+                       .attr( 'aria-label', wp.updates.l10n.themeInstallingLabel.replace( '%s', $message.data( 'name' ) ) );
+               wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
+
+               // Remove previous error messages, if any.
+               $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
+
+               $document.trigger( 'wp-theme-installing', args );
+
+               return wp.updates.ajax( 'install-theme', args );
+       };
+
+       /**
+        * Updates the UI appropriately after a successful theme install.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} installThemeSuccess
+        * @param {object} response              Response from the server.
+        * @param {string} response.slug         Slug of the theme to be installed.
+        * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
+        * @param {string} response.activateUrl  URL to activate the just installed theme.
+        */
+       wp.updates.installThemeSuccess = function( response ) {
+               var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
+                       $message;
+
+               $document.trigger( 'wp-theme-install-success', response );
+
+               $message = $card.find( '.button-primary' )
+                       .removeClass( 'updating-message' )
+                       .addClass( 'updated-message disabled' )
+                       .attr( 'aria-label', wp.updates.l10n.themeInstalledLabel.replace( '%s', response.themeName ) )
+                       .text( wp.updates.l10n.installed );
+
+               wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
+
+               setTimeout( function() {
+
+                       if ( response.activateUrl ) {
+
+                               // Transform the 'Install' button into an 'Activate' button.
+                               $message
+                                       .attr( 'href', response.activateUrl )
+                                       .removeClass( 'theme-install updated-message disabled' )
+                                       .addClass( 'activate' )
+                                       .attr( 'aria-label', wp.updates.l10n.activateThemeLabel.replace( '%s', response.themeName ) )
+                                       .text( wp.updates.l10n.activateTheme );
+                       }
+
+                       if ( response.customizeUrl ) {
+
+                               // Transform the 'Preview' button into a 'Live Preview' button.
+                               $message.siblings( '.preview' ).replaceWith( function () {
+                                       return $( '<a>' )
+                                               .attr( 'href', response.customizeUrl )
+                                               .addClass( 'button button-secondary load-customize' )
+                                               .text( wp.updates.l10n.livePreview );
+                               } );
+                       }
+               }, 1000 );
+       };
+
+       /**
+        * Updates the UI appropriately after a failed theme install.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} installThemeError
+        * @param {object} response              Response from the server.
+        * @param {string} response.slug         Slug of the theme to be installed.
+        * @param {string} response.errorCode    Error code for the error that occurred.
+        * @param {string} response.errorMessage The error that occurred.
+        */
+       wp.updates.installThemeError = function( response ) {
+               var $card, $button,
+                       errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
+                       $message     = wp.updates.adminNotice( {
+                               className: 'update-message notice-error notice-alt',
+                               message:   errorMessage
+                       } );
+
+               if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
+                       return;
+               }
+
+               if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
+                       return;
+               }
+
+               if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
+                       $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
+                       $card   = $( '.install-theme-info' ).prepend( $message );
+               } else {
+                       $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
+                       $button = $card.find( '.theme-install' );
+               }
+
+               $button
+                       .removeClass( 'updating-message' )
+                       .attr( 'aria-label', wp.updates.l10n.themeInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
+                       .text( wp.updates.l10n.installFailedShort );
+
+               wp.a11y.speak( errorMessage, 'assertive' );
+
+               $document.trigger( 'wp-theme-install-error', response );
+       };
+
+       /**
+        * Sends an Ajax request to the server to install a theme.
+        *
+        * @since 4.6.0
+        *
+        * @param {object}              args
+        * @param {string}              args.slug    Theme stylesheet.
+        * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
+        * @param {deleteThemeError=}   args.error   Optional. Error callback. Default: wp.updates.deleteThemeError
+        * @return {$.promise} A jQuery promise that represents the request,
+        *                     decorated with an abort() method.
+        */
+       wp.updates.deleteTheme = function( args ) {
+               var $button;
+
+               if ( 'themes' === pagenow ) {
+                       $button = $( '.theme-actions .delete-theme' );
+               } else if ( 'themes-network' === pagenow ) {
+                       $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' );
+               }
+
+               args = _.extend( {
+                       success: wp.updates.deleteThemeSuccess,
+                       error: wp.updates.deleteThemeError
+               }, args );
+
+               if ( $button && $button.html() !== wp.updates.l10n.deleting ) {
+                       $button
+                               .data( 'originaltext', $button.html() )
+                               .text( wp.updates.l10n.deleting );
+               }
+
+               wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
+
+               // Remove previous error messages, if any.
+               $( '.theme-info .update-message' ).remove();
+
+               $document.trigger( 'wp-theme-deleting', args );
+
+               return wp.updates.ajax( 'delete-theme', args );
+       };
+
+       /**
+        * Updates the UI appropriately after a successful theme deletion.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} deleteThemeSuccess
+        * @param {object} response      Response from the server.
+        * @param {string} response.slug Slug of the theme that was deleted.
+        */
+       wp.updates.deleteThemeSuccess = function( response ) {
+               var $themeRows = $( '[data-slug="' + response.slug + '"]' );
+
+               if ( 'themes-network' === pagenow ) {
+
+                       // Removes the theme and updates rows.
+                       $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
+                               var $views     = $( '.subsubsub' ),
+                                       $themeRow  = $( this ),
+                                       totals     = settings.totals,
+                                       deletedRow = wp.template( 'item-deleted-row' );
+
+                               if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
+                                       $themeRow.after(
+                                               deletedRow( {
+                                                       slug:    response.slug,
+                                                       colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
+                                                       name:    $themeRow.find( '.theme-title strong' ).text()
+                                               } )
+                                       );
                                }
-                       };
 
-                       target.postMessage( JSON.stringify( job ), window.location.origin );
-               });
+                               $themeRow.remove();
 
-       } );
+                               // Remove theme from update count.
+                               if ( $themeRow.hasClass( 'update' ) ) {
+                                       totals.upgrade--;
+                                       wp.updates.decrementCount( 'theme' );
+                               }
+
+                               // Remove from views.
+                               if ( $themeRow.hasClass( 'inactive' ) ) {
+                                       totals.disabled--;
+                                       if ( totals.disabled ) {
+                                               $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' );
+                                       } else {
+                                               $views.find( '.disabled' ).remove();
+                                       }
+                               }
+
+                               // There is always at least one theme available.
+                               $views.find( '.all .count' ).text( '(' + --totals.all + ')' );
+                       } );
+               }
+
+               wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
+
+               $document.trigger( 'wp-theme-delete-success', response );
+       };
 
-       $( window ).on( 'message', function( e ) {
-               var event = e.originalEvent,
-                       message,
-                       loc = document.location,
-                       expectedOrigin = loc.protocol + '//' + loc.hostname;
+       /**
+        * Updates the UI appropriately after a failed theme deletion.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} deleteThemeError
+        * @param {object} response              Response from the server.
+        * @param {string} response.slug         Slug of the theme to be deleted.
+        * @param {string} response.errorCode    Error code for the error that occurred.
+        * @param {string} response.errorMessage The error that occurred.
+        */
+       wp.updates.deleteThemeError = function( response ) {
+               var $themeRow    = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
+                       $button      = $( '.theme-actions .delete-theme' ),
+                       updateRow    = wp.template( 'item-update-row' ),
+                       $updateRow   = $themeRow.siblings( '#' + response.slug + '-update' ),
+                       errorMessage = wp.updates.l10n.deleteFailed.replace( '%s', response.errorMessage ),
+                       $message     = wp.updates.adminNotice( {
+                               className: 'update-message notice-error notice-alt',
+                               message:   errorMessage
+                       } );
 
-               if ( event.origin !== expectedOrigin ) {
+               if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
                        return;
                }
 
-               message = $.parseJSON( event.data );
+               if ( 'themes-network' === pagenow ) {
+                       if ( ! $updateRow.length ) {
+                               $themeRow.addClass( 'update' ).after(
+                                       updateRow( {
+                                               slug: response.slug,
+                                               colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
+                                               content: $message
+                                       } )
+                               );
+                       } else {
+                               // Remove previous error messages, if any.
+                               $updateRow.find( '.notice-error' ).remove();
+                               $updateRow.find( '.plugin-update' ).append( $message );
+                       }
+               } else {
+                       $( '.theme-info .theme-description' ).before( $message );
+               }
+
+               $button.html( $button.data( 'originaltext' ) );
+
+               wp.a11y.speak( errorMessage, 'assertive' );
+
+               $document.trigger( 'wp-theme-delete-error', response );
+       };
+
+       /**
+        * Adds the appropriate callback based on the type of action and the current page.
+        *
+        * @since 4.6.0
+        * @private
+        *
+        * @param {object} data   AJAX payload.
+        * @param {string} action The type of request to perform.
+        * @return {object} The AJAX payload with the appropriate callbacks.
+        */
+       wp.updates._addCallbacks = function( data, action ) {
+               if ( 'import' === pagenow && 'install-plugin' === action ) {
+                       data.success = wp.updates.installImporterSuccess;
+                       data.error   = wp.updates.installImporterError;
+               }
+
+               return data;
+       };
+
+       /**
+        * Pulls available jobs from the queue and runs them.
+        *
+        * @since 4.2.0
+        * @since 4.6.0 Can handle multiple job types.
+        */
+       wp.updates.queueChecker = function() {
+               var job;
 
-               if ( typeof message.action === 'undefined' ) {
+               if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
                        return;
                }
 
-               switch (message.action){
-                       case 'decrementUpdateCount' :
-                               wp.updates.decrementCount( message.upgradeType );
+               job = wp.updates.queue.shift();
+
+               // Handle a queue job.
+               switch ( job.action ) {
+                       case 'install-plugin':
+                               wp.updates.installPlugin( job.data );
                                break;
-                       case 'updatePlugin' :
-                               tb_remove();
 
-                               wp.updates.updateQueue.push( message );
-                               wp.updates.queueChecker();
+                       case 'update-plugin':
+                               wp.updates.updatePlugin( job.data );
+                               break;
+
+                       case 'delete-plugin':
+                               wp.updates.deletePlugin( job.data );
+                               break;
+
+                       case 'install-theme':
+                               wp.updates.installTheme( job.data );
+                               break;
+
+                       case 'update-theme':
+                               wp.updates.updateTheme( job.data );
+                               break;
+
+                       case 'delete-theme':
+                               wp.updates.deleteTheme( job.data );
+                               break;
+
+                       default:
                                break;
                }
+       };
 
-       } );
+       /**
+        * Requests the users filesystem credentials if they aren't already known.
+        *
+        * @since 4.2.0
+        *
+        * @param {Event=} event Optional. Event interface.
+        */
+       wp.updates.requestFilesystemCredentials = function( event ) {
+               if ( false === wp.updates.filesystemCredentials.available ) {
+                       /*
+                        * After exiting the credentials request modal,
+                        * return the focus to the element triggering the request.
+                        */
+                       if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
+                               wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
+                       }
+
+                       wp.updates.ajaxLocked = true;
+                       wp.updates.requestForCredentialsModalOpen();
+               }
+       };
+
+       /**
+        * Requests the users filesystem credentials if needed and there is no lock.
+        *
+        * @since 4.6.0
+        *
+        * @param {Event=} event Optional. Event interface.
+        */
+       wp.updates.maybeRequestFilesystemCredentials = function( event ) {
+               if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+                       wp.updates.requestFilesystemCredentials( event );
+               }
+       };
+
+       /**
+        * Keydown handler for the request for credentials modal.
+        *
+        * Closes the modal when the escape key is pressed and
+        * constrains keyboard navigation to inside the modal.
+        *
+        * @since 4.2.0
+        *
+        * @param {Event} event Event interface.
+        */
+       wp.updates.keydown = function( event ) {
+               if ( 27 === event.keyCode ) {
+                       wp.updates.requestForCredentialsModalCancel();
+               } else if ( 9 === event.keyCode ) {
+
+                       // #upgrade button must always be the last focus-able element in the dialog.
+                       if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
+                               $( '#hostname' ).focus();
+
+                               event.preventDefault();
+                       } else if ( 'hostname' === event.target.id && event.shiftKey ) {
+                               $( '#upgrade' ).focus();
+
+                               event.preventDefault();
+                       }
+               }
+       };
+
+       /**
+        * Opens the request for credentials modal.
+        *
+        * @since 4.2.0
+        */
+       wp.updates.requestForCredentialsModalOpen = function() {
+               var $modal = $( '#request-filesystem-credentials-dialog' );
+
+               $( 'body' ).addClass( 'modal-open' );
+               $modal.show();
+               $modal.find( 'input:enabled:first' ).focus();
+               $modal.on( 'keydown', wp.updates.keydown );
+       };
+
+       /**
+        * Closes the request for credentials modal.
+        *
+        * @since 4.2.0
+        */
+       wp.updates.requestForCredentialsModalClose = function() {
+               $( '#request-filesystem-credentials-dialog' ).hide();
+               $( 'body' ).removeClass( 'modal-open' );
 
-       $( window ).on( 'beforeunload', wp.updates.beforeunload );
+               if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
+                       wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
+               }
+       };
 
-})( jQuery, window.wp, window.pagenow, window.ajaxurl );
+       /**
+        * Takes care of the steps that need to happen when the modal is canceled out.
+        *
+        * @since 4.2.0
+        * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
+        */
+       wp.updates.requestForCredentialsModalCancel = function() {
+
+               // Not ajaxLocked and no queue means we already have cleared things up.
+               if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
+                       return;
+               }
+
+               _.each( wp.updates.queue, function( job ) {
+                       $document.trigger( 'credential-modal-cancel', job );
+               } );
+
+               // Remove the lock, and clear the queue.
+               wp.updates.ajaxLocked = false;
+               wp.updates.queue = [];
+
+               wp.updates.requestForCredentialsModalClose();
+       };
+
+       /**
+        * Displays an error message in the request for credentials form.
+        *
+        * @since 4.2.0
+        *
+        * @param {string} message Error message.
+        */
+       wp.updates.showErrorInCredentialsForm = function( message ) {
+               var $modal = $( '#request-filesystem-credentials-form' );
+
+               // Remove any existing error.
+               $modal.find( '.notice' ).remove();
+               $modal.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
+       };
+
+       /**
+        * Handles credential errors and runs events that need to happen in that case.
+        *
+        * @since 4.2.0
+        *
+        * @param {object} response Ajax response.
+        * @param {string} action   The type of request to perform.
+        */
+       wp.updates.credentialError = function( response, action ) {
+
+               // Restore callbacks.
+               response = wp.updates._addCallbacks( response, action );
+
+               wp.updates.queue.unshift( {
+                       action: action,
+
+                       /*
+                        * Not cool that we're depending on response for this data.
+                        * This would feel more whole in a view all tied together.
+                        */
+                       data: response
+               } );
+
+               wp.updates.filesystemCredentials.available = false;
+               wp.updates.showErrorInCredentialsForm( response.errorMessage );
+               wp.updates.requestFilesystemCredentials();
+       };
+
+       /**
+        * Handles credentials errors if it could not connect to the filesystem.
+        *
+        * @since 4.6.0
+        *
+        * @typedef {object} maybeHandleCredentialError
+        * @param {object} response              Response from the server.
+        * @param {string} response.errorCode    Error code for the error that occurred.
+        * @param {string} response.errorMessage The error that occurred.
+        * @param {string} action                The type of request to perform.
+        * @returns {boolean} Whether there is an error that needs to be handled or not.
+        */
+       wp.updates.maybeHandleCredentialError = function( response, action ) {
+               if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
+                       wp.updates.credentialError( response, action );
+                       return true;
+               }
+
+               return false;
+       };
+
+       /**
+        * Validates an AJAX response to ensure it's a proper object.
+        *
+        * If the response deems to be invalid, an admin notice is being displayed.
+        *
+        * @param {(object|string)} response              Response from the server.
+        * @param {function=}       response.always       Optional. Callback for when the Deferred is resolved or rejected.
+        * @param {string=}         response.statusText   Optional. Status message corresponding to the status code.
+        * @param {string=}         response.responseText Optional. Request response as text.
+        * @param {string}          action                Type of action the response is referring to. Can be 'delete',
+        *                                                'update' or 'install'.
+        */
+       wp.updates.isValidResponse = function( response, action ) {
+               var error = wp.updates.l10n.unknownError,
+                   errorMessage;
+
+               // Make sure the response is a valid data object and not a Promise object.
+               if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
+                       return true;
+               }
+
+               if ( _.isString( response ) && '-1' === response ) {
+                       error = wp.updates.l10n.nonceError;
+               } else if ( _.isString( response ) ) {
+                       error = response;
+               } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
+                       error = wp.updates.l10n.connectionError;
+               } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
+                       error = response.responseText;
+               } else if ( _.isString( response.statusText ) ) {
+                       error = response.statusText;
+               }
+
+               switch ( action ) {
+                       case 'update':
+                               errorMessage = wp.updates.l10n.updateFailed;
+                               break;
+
+                       case 'install':
+                               errorMessage = wp.updates.l10n.installFailed;
+                               break;
+
+                       case 'delete':
+                               errorMessage = wp.updates.l10n.deleteFailed;
+                               break;
+               }
+
+               // Messages are escaped, remove HTML tags to make them more readable.
+               error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
+               errorMessage = errorMessage.replace( '%s', error );
+
+               // Add admin notice.
+               wp.updates.addAdminNotice( {
+                       id:        'unknown_error',
+                       className: 'notice-error is-dismissible',
+                       message:   _.escape( errorMessage )
+               } );
+
+               // Remove the lock, and clear the queue.
+               wp.updates.ajaxLocked = false;
+               wp.updates.queue      = [];
+
+               // Change buttons of all running updates.
+               $( '.button.updating-message' )
+                       .removeClass( 'updating-message' )
+                       .removeAttr( 'aria-label' )
+                       .prop( 'disabled', true )
+                       .text( wp.updates.l10n.updateFailedShort );
+
+               $( '.updating-message:not(.button):not(.thickbox)' )
+                       .removeClass( 'updating-message notice-warning' )
+                       .addClass( 'notice-error' )
+                       .find( 'p' )
+                               .removeAttr( 'aria-label' )
+                               .text( errorMessage );
+
+               wp.a11y.speak( errorMessage, 'assertive' );
+
+               return false;
+       };
+
+       /**
+        * Potentially adds an AYS to a user attempting to leave the page.
+        *
+        * If an update is on-going and a user attempts to leave the page,
+        * opens an "Are you sure?" alert.
+        *
+        * @since 4.2.0
+        */
+       wp.updates.beforeunload = function() {
+               if ( wp.updates.ajaxLocked ) {
+                       return wp.updates.l10n.beforeunload;
+               }
+       };
+
+       $( function() {
+               var $pluginFilter        = $( '#plugin-filter' ),
+                       $bulkActionForm      = $( '#bulk-action-form' ),
+                       $filesystemModal     = $( '#request-filesystem-credentials-dialog' ),
+                       $pluginSearch        = $( '.plugins-php .wp-filter-search' ),
+                       $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
+
+               /*
+                * Whether a user needs to submit filesystem credentials.
+                *
+                * This is based on whether the form was output on the page server-side.
+                *
+                * @see {wp_print_request_filesystem_credentials_modal() in PHP}
+                */
+               wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
+
+               /**
+                * File system credentials form submit noop-er / handler.
+                *
+                * @since 4.2.0
+                */
+               $filesystemModal.on( 'submit', 'form', function( event ) {
+                       event.preventDefault();
+
+                       // Persist the credentials input by the user for the duration of the page load.
+                       wp.updates.filesystemCredentials.ftp.hostname       = $( '#hostname' ).val();
+                       wp.updates.filesystemCredentials.ftp.username       = $( '#username' ).val();
+                       wp.updates.filesystemCredentials.ftp.password       = $( '#password' ).val();
+                       wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
+                       wp.updates.filesystemCredentials.ssh.publicKey      = $( '#public_key' ).val();
+                       wp.updates.filesystemCredentials.ssh.privateKey     = $( '#private_key' ).val();
+                       wp.updates.filesystemCredentials.available          = true;
+
+                       // Unlock and invoke the queue.
+                       wp.updates.ajaxLocked = false;
+                       wp.updates.queueChecker();
+
+                       wp.updates.requestForCredentialsModalClose();
+               } );
+
+               /**
+                * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
+                *
+                * @since 4.2.0
+                */
+               $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
+
+               /**
+                * Hide SSH fields when not selected.
+                *
+                * @since 4.2.0
+                */
+               $filesystemModal.on( 'change', 'input[name="connection_type"]', function() {
+                       $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
+               } ).change();
+
+               /**
+                * Handles events after the credential modal was closed.
+                *
+                * @since 4.6.0
+                *
+                * @param {Event}  event Event interface.
+                * @param {string} job   The install/update.delete request.
+                */
+               $document.on( 'credential-modal-cancel', function( event, job ) {
+                       var $updatingMessage = $( '.updating-message' ),
+                               $message, originalText;
+
+                       if ( 'import' === pagenow ) {
+                               $updatingMessage.removeClass( 'updating-message' );
+                       } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
+                               if ( 'update-plugin' === job.action ) {
+                                       $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
+                               } else if ( 'delete-plugin' === job.action ) {
+                                       $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
+                               }
+                       } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
+                               if ( 'update-theme' === job.action ) {
+                                       $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
+                               } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
+                                       $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
+                               } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
+                                       $message = $( '.theme-actions .delete-theme' );
+                               }
+                       } else {
+                               $message = $updatingMessage;
+                       }
+
+                       if ( $message && $message.hasClass( 'updating-message' ) ) {
+                               originalText = $message.data( 'originaltext' );
+
+                               if ( 'undefined' === typeof originalText ) {
+                                       originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
+                               }
+
+                               $message
+                                       .removeClass( 'updating-message' )
+                                       .html( originalText );
+
+                               if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
+                                       if ( 'update-plugin' === job.action ) {
+                                               $message.attr( 'aria-label', wp.updates.l10n.updateNowLabel.replace( '%s', $message.data( 'name' ) ) );
+                                       } else if ( 'install-plugin' === job.action ) {
+                                               $message.attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', $message.data( 'name' ) ) );
+                                       }
+                               }
+                       }
+
+                       wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
+               } );
+
+               /**
+                * Click handler for plugin updates in List Table view.
+                *
+                * @since 4.2.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
+                       var $message   = $( event.target ),
+                               $pluginRow = $message.parents( 'tr' );
+
+                       event.preventDefault();
+
+                       if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
+                               return;
+                       }
+
+                       wp.updates.maybeRequestFilesystemCredentials( event );
+
+                       // Return the user to the input box of the plugin's table row after closing the modal.
+                       wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
+                       wp.updates.updatePlugin( {
+                               plugin: $pluginRow.data( 'plugin' ),
+                               slug:   $pluginRow.data( 'slug' )
+                       } );
+               } );
+
+               /**
+                * Click handler for plugin updates in plugin install view.
+                *
+                * @since 4.2.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $pluginFilter.on( 'click', '.update-now', function( event ) {
+                       var $button = $( event.target );
+                       event.preventDefault();
+
+                       if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
+                               return;
+                       }
+
+                       wp.updates.maybeRequestFilesystemCredentials( event );
+
+                       wp.updates.updatePlugin( {
+                               plugin: $button.data( 'plugin' ),
+                               slug:   $button.data( 'slug' )
+                       } );
+               } );
+
+               /**
+                * Click handler for plugin installs in plugin install view.
+                *
+                * @since 4.6.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $pluginFilter.on( 'click', '.install-now', function( event ) {
+                       var $button = $( event.target );
+                       event.preventDefault();
+
+                       if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
+                               return;
+                       }
+
+                       if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+                               wp.updates.requestFilesystemCredentials( event );
+
+                               $document.on( 'credential-modal-cancel', function() {
+                                       var $message = $( '.install-now.updating-message' );
+
+                                       $message
+                                               .removeClass( 'updating-message' )
+                                               .text( wp.updates.l10n.installNow );
+
+                                       wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
+                               } );
+                       }
+
+                       wp.updates.installPlugin( {
+                               slug: $button.data( 'slug' )
+                       } );
+               } );
+
+               /**
+                * Click handler for importer plugins installs in the Import screen.
+                *
+                * @since 4.6.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $document.on( 'click', '.importer-item .install-now', function( event ) {
+                       var $button = $( event.target ),
+                               pluginName = $( this ).data( 'name' );
+
+                       event.preventDefault();
+
+                       if ( $button.hasClass( 'updating-message' ) ) {
+                               return;
+                       }
+
+                       if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
+                               wp.updates.requestFilesystemCredentials( event );
+
+                               $document.on( 'credential-modal-cancel', function() {
+
+                                       $button
+                                               .removeClass( 'updating-message' )
+                                               .text( wp.updates.l10n.installNow )
+                                               .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
+
+                                       wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
+                               } );
+                       }
+
+                       wp.updates.installPlugin( {
+                               slug:    $button.data( 'slug' ),
+                               success: wp.updates.installImporterSuccess,
+                               error:   wp.updates.installImporterError
+                       } );
+               } );
+
+               /**
+                * Click handler for plugin deletions.
+                *
+                * @since 4.6.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
+                       var $pluginRow = $( event.target ).parents( 'tr' );
+
+                       event.preventDefault();
+
+                       if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) {
+                               return;
+                       }
+
+                       wp.updates.maybeRequestFilesystemCredentials( event );
+
+                       wp.updates.deletePlugin( {
+                               plugin: $pluginRow.data( 'plugin' ),
+                               slug:   $pluginRow.data( 'slug' )
+                       } );
+
+               } );
+
+               /**
+                * Click handler for theme updates.
+                *
+                * @since 4.6.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
+                       var $message  = $( event.target ),
+                               $themeRow = $message.parents( 'tr' );
+
+                       event.preventDefault();
+
+                       if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
+                               return;
+                       }
+
+                       wp.updates.maybeRequestFilesystemCredentials( event );
+
+                       // Return the user to the input box of the theme's table row after closing the modal.
+                       wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
+                       wp.updates.updateTheme( {
+                               slug: $themeRow.data( 'slug' )
+                       } );
+               } );
+
+               /**
+                * Click handler for theme deletions.
+                *
+                * @since 4.6.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
+                       var $themeRow = $( event.target ).parents( 'tr' );
+
+                       event.preventDefault();
+
+                       if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) {
+                               return;
+                       }
+
+                       wp.updates.maybeRequestFilesystemCredentials( event );
+
+                       wp.updates.deleteTheme( {
+                               slug: $themeRow.data( 'slug' )
+                       } );
+               } );
+
+               /**
+                * Bulk action handler for plugins and themes.
+                *
+                * Handles both deletions and updates.
+                *
+                * @since 4.6.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $bulkActionForm.on( 'click', '[type="submit"]', function( event ) {
+                       var bulkAction    = $( event.target ).siblings( 'select' ).val(),
+                               itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
+                               success       = 0,
+                               error         = 0,
+                               errorMessages = [],
+                               type, action;
+
+                       // Determine which type of item we're dealing with.
+                       switch ( pagenow ) {
+                               case 'plugins':
+                               case 'plugins-network':
+                                       type = 'plugin';
+                                       break;
+
+                               case 'themes-network':
+                                       type = 'theme';
+                                       break;
+
+                               default:
+                                       return;
+                       }
+
+                       // Bail if there were no items selected.
+                       if ( ! itemsSelected.length ) {
+                               event.preventDefault();
+                               $( 'html, body' ).animate( { scrollTop: 0 } );
+
+                               return wp.updates.addAdminNotice( {
+                                       id:        'no-items-selected',
+                                       className: 'notice-error is-dismissible',
+                                       message:   wp.updates.l10n.noItemsSelected
+                               } );
+                       }
+
+                       // Determine the type of request we're dealing with.
+                       switch ( bulkAction ) {
+                               case 'update-selected':
+                                       action = bulkAction.replace( 'selected', type );
+                                       break;
+
+                               case 'delete-selected':
+                                       if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) {
+                                               event.preventDefault();
+                                               return;
+                                       }
+
+                                       action = bulkAction.replace( 'selected', type );
+                                       break;
+
+                               default:
+                                       return;
+                       }
+
+                       wp.updates.maybeRequestFilesystemCredentials( event );
+
+                       event.preventDefault();
+
+                       // Un-check the bulk checkboxes.
+                       $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
+
+                       $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
+
+                       // Find all the checkboxes which have been checked.
+                       itemsSelected.each( function( index, element ) {
+                               var $checkbox = $( element ),
+                                       $itemRow = $checkbox.parents( 'tr' );
+
+                               // Only add update-able items to the update queue.
+                               if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
+
+                                       // Un-check the box.
+                                       $checkbox.prop( 'checked', false );
+                                       return;
+                               }
+
+                               // Add it to the queue.
+                               wp.updates.queue.push( {
+                                       action: action,
+                                       data:   {
+                                               plugin: $itemRow.data( 'plugin' ),
+                                               slug:   $itemRow.data( 'slug' )
+                                       }
+                               } );
+                       } );
+
+                       // Display bulk notification for updates of any kind.
+                       $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
+                               var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
+                                       $bulkActionNotice, itemName;
+
+                               if ( 'wp-' + response.update + '-update-success' === event.type ) {
+                                       success++;
+                               } else {
+                                       itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
+
+                                       error++;
+                                       errorMessages.push( itemName + ': ' + response.errorMessage );
+                               }
+
+                               $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
+
+                               wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
+
+                               wp.updates.addAdminNotice( {
+                                       id:            'bulk-action-notice',
+                                       className:     'bulk-action-notice',
+                                       successes:     success,
+                                       errors:        error,
+                                       errorMessages: errorMessages,
+                                       type:          response.update
+                               } );
+
+                               $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
+                                       // $( this ) is the clicked button, no need to get it again.
+                                       $( this )
+                                               .toggleClass( 'bulk-action-errors-collapsed' )
+                                               .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
+                                       // Show the errors list.
+                                       $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
+                               } );
+
+                               if ( error > 0 && ! wp.updates.queue.length ) {
+                                       $( 'html, body' ).animate( { scrollTop: 0 } );
+                               }
+                       } );
+
+                       // Reset admin notice template after #bulk-action-notice was added.
+                       $document.on( 'wp-updates-notice-added', function() {
+                               wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
+                       } );
+
+                       // Check the queue, now that the event handlers have been added.
+                       wp.updates.queueChecker();
+               } );
+
+               if ( $pluginInstallSearch.length ) {
+                       $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
+               }
+
+               /**
+                * Handles changes to the plugin search box on the new-plugin page,
+                * searching the repository dynamically.
+                *
+                * @since 4.6.0
+                */
+               $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
+                       var $searchTab = $( '.plugin-install-search' ), data, searchLocation;
+
+                       data = {
+                               _ajax_nonce: wp.updates.ajaxNonce,
+                               s:           event.target.value,
+                               tab:         'search',
+                               type:        $( '#typeselector' ).val(),
+                               pagenow:     pagenow
+                       };
+                       searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
+
+                       // Clear on escape.
+                       if ( 'keyup' === event.type && 27 === event.which ) {
+                               event.target.value = '';
+                       }
+
+                       if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
+                               return;
+                       } else {
+                               $pluginFilter.empty();
+                               wp.updates.searchTerm = data.s;
+                       }
+
+                       if ( window.history && window.history.replaceState ) {
+                               window.history.replaceState( null, '', searchLocation );
+                       }
+
+                       if ( ! $searchTab.length ) {
+                               $searchTab = $( '<li class="plugin-install-search" />' )
+                                       .append( $( '<a />', {
+                                               'class': 'current',
+                                               'href': searchLocation,
+                                               'text': wp.updates.l10n.searchResultsLabel
+                                       } ) );
+
+                               $( '.wp-filter .filter-links .current' )
+                                       .removeClass( 'current' )
+                                       .parents( '.filter-links' )
+                                       .prepend( $searchTab );
+
+                               $pluginFilter.prev( 'p' ).remove();
+                               $( '.plugins-popular-tags-wrapper' ).remove();
+                       }
+
+                       if ( 'undefined' !== typeof wp.updates.searchRequest ) {
+                               wp.updates.searchRequest.abort();
+                       }
+                       $( 'body' ).addClass( 'loading-content' );
+
+                       wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
+                               $( 'body' ).removeClass( 'loading-content' );
+                               $pluginFilter.append( response.items );
+                               delete wp.updates.searchRequest;
+
+                               if ( 0 === response.count ) {
+                                       wp.a11y.speak( wp.updates.l10n.noPluginsFound );
+                               } else {
+                                       wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
+                               }
+                       } );
+               }, 500 ) );
+
+               if ( $pluginSearch.length ) {
+                       $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
+               }
+
+               /**
+                * Handles changes to the plugin search box on the Installed Plugins screen,
+                * searching the plugin list dynamically.
+                *
+                * @since 4.6.0
+                */
+               $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
+                       var data = {
+                               _ajax_nonce: wp.updates.ajaxNonce,
+                               s:           event.target.value,
+                               pagenow:     pagenow
+                       };
+
+                       // Clear on escape.
+                       if ( 'keyup' === event.type && 27 === event.which ) {
+                               event.target.value = '';
+                       }
+
+                       if ( wp.updates.searchTerm === data.s ) {
+                               return;
+                       } else {
+                               wp.updates.searchTerm = data.s;
+                       }
+
+                       if ( window.history && window.history.replaceState ) {
+                               window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s );
+                       }
+
+                       if ( 'undefined' !== typeof wp.updates.searchRequest ) {
+                               wp.updates.searchRequest.abort();
+                       }
+
+                       $bulkActionForm.empty();
+                       $( 'body' ).addClass( 'loading-content' );
+
+                       wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
+
+                               // Can we just ditch this whole subtitle business?
+                               var $subTitle    = $( '<span />' ).addClass( 'subtitle' ).html( wp.updates.l10n.searchResults.replace( '%s', _.escape( data.s ) ) ),
+                                       $oldSubTitle = $( '.wrap .subtitle' );
+
+                               if ( ! data.s.length ) {
+                                       $oldSubTitle.remove();
+                               } else if ( $oldSubTitle.length ) {
+                                       $oldSubTitle.replaceWith( $subTitle );
+                               } else {
+                                       $( '.wrap h1' ).append( $subTitle );
+                               }
+
+                               $( 'body' ).removeClass( 'loading-content' );
+                               $bulkActionForm.append( response.items );
+                               delete wp.updates.searchRequest;
+
+                               if ( 0 === response.count ) {
+                                       wp.a11y.speak( wp.updates.l10n.noPluginsFound );
+                               } else {
+                                       wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
+                               }
+                       } );
+               }, 500 ) );
+
+               /**
+                * Trigger a search event when the search form gets submitted.
+                *
+                * @since 4.6.0
+                */
+               $document.on( 'submit', '.search-plugins', function( event ) {
+                       event.preventDefault();
+
+                       $( 'input.wp-filter-search' ).trigger( 'input' );
+               } );
+
+               /**
+                * Trigger a search event when the search type gets changed.
+                *
+                * @since 4.6.0
+                */
+               $( '#typeselector' ).on( 'change', function() {
+                       var $search = $( 'input[name="s"]' );
+
+                       if ( $search.val().length ) {
+                               $search.trigger( 'input', 'typechange' );
+                       }
+               } );
+
+               /**
+                * Click handler for updating a plugin from the details modal on `plugin-install.php`.
+                *
+                * @since 4.2.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
+                       var target = window.parent === window ? null : window.parent,
+                               update;
+
+                       $.support.postMessage = !! window.postMessage;
+
+                       if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
+                               return;
+                       }
+
+                       event.preventDefault();
+
+                       update = {
+                               action: 'update-plugin',
+                               data:   {
+                                       plugin: $( this ).data( 'plugin' ),
+                                       slug:   $( this ).data( 'slug' )
+                               }
+                       };
+
+                       target.postMessage( JSON.stringify( update ), window.location.origin );
+               } );
+
+               /**
+                * Click handler for installing a plugin from the details modal on `plugin-install.php`.
+                *
+                * @since 4.6.0
+                *
+                * @param {Event} event Event interface.
+                */
+               $( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
+                       var target = window.parent === window ? null : window.parent,
+                               install;
+
+                       $.support.postMessage = !! window.postMessage;
+
+                       if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
+                               return;
+                       }
+
+                       event.preventDefault();
+
+                       install = {
+                               action: 'install-plugin',
+                               data:   {
+                                       slug: $( this ).data( 'slug' )
+                               }
+                       };
+
+                       target.postMessage( JSON.stringify( install ), window.location.origin );
+               } );
+
+               /**
+                * Handles postMessage events.
+                *
+                * @since 4.2.0
+                * @since 4.6.0 Switched `update-plugin` action to use the queue.
+                *
+                * @param {Event} event Event interface.
+                */
+               $( window ).on( 'message', function( event ) {
+                       var originalEvent  = event.originalEvent,
+                               expectedOrigin = document.location.protocol + '//' + document.location.hostname,
+                               message;
+
+                       if ( originalEvent.origin !== expectedOrigin ) {
+                               return;
+                       }
+
+                       try {
+                               message = $.parseJSON( originalEvent.data );
+                       } catch ( e ) {
+                               return;
+                       }
+
+                       if ( 'undefined' === typeof message.action ) {
+                               return;
+                       }
+
+                       switch ( message.action ) {
+
+                               // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
+                               case 'decrementUpdateCount':
+                                       /** @property {string} message.upgradeType */
+                                       wp.updates.decrementCount( message.upgradeType );
+                                       break;
+
+                               case 'install-plugin':
+                               case 'update-plugin':
+                                       /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+                                       window.tb_remove();
+                                       /* jscs:enable */
+
+                                       message.data = wp.updates._addCallbacks( message.data, message.action );
+
+                                       wp.updates.queue.push( message );
+                                       wp.updates.queueChecker();
+                                       break;
+                       }
+               } );
+
+               /**
+                * Adds a callback to display a warning before leaving the page.
+                *
+                * @since 4.2.0
+                */
+               $( window ).on( 'beforeunload', wp.updates.beforeunload );
+       } );
+})( jQuery, window.wp, _.extend( window._wpUpdatesSettings, window._wpUpdatesItemCounts || {} ) );