home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-admin / js / updates.js < prev    next >
Encoding:
JavaScript  |  2018-01-24  |  79.2 KB  |  2,474 lines

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