/** * Functions for ajaxified updates, deletions and installs inside the WordPress admin. * * @version 4.2.0 * * @package WordPress * @subpackage Administration */ /* global pagenow */ /** * @param {jQuery} $ jQuery object. * @param {object} wp WP object. * @param {object} settings WP Updates settings. * @param {string} settings.ajax_nonce AJAX nonce. * @param {object} settings.l10n Translation strings. * @param {object=} settings.plugins Base names of plugins in their different states. * @param {Array} settings.plugins.all Base names of all plugins. * @param {Array} settings.plugins.active Base names of active plugins. * @param {Array} settings.plugins.inactive Base names of inactive plugins. * @param {Array} settings.plugins.upgrade Base names of plugins with updates available. * @param {Array} settings.plugins.recently_activated Base names of recently activated plugins. * @param {object=} settings.totals Plugin/theme status information or null. * @param {number} settings.totals.all Amount of all plugins or themes. * @param {number} settings.totals.upgrade Amount of plugins or themes with updates available. * @param {number} settings.totals.disabled Amount of disabled themes. */ (function( $, wp, settings ) { var $document = $( document ); wp = wp || {}; /** * The WP Updates object. * * @since 4.2.0 * * @type {object} */ wp.updates = {}; /** * User nonce for ajax calls. * * @since 4.2.0 * * @type {string} */ wp.updates.ajaxNonce = settings.ajax_nonce; /** * Localized strings. * * @since 4.2.0 * * @type {object} */ wp.updates.l10n = settings.l10n; /** * Current search term. * * @since 4.6.0 * * @type {string} */ wp.updates.searchTerm = ''; /** * Whether filesystem credentials need to be requested from the user. * * @since 4.2.0 * * @type {bool} */ wp.updates.shouldRequestFilesystemCredentials = false; /** * Filesystem credentials to be packaged along with the request. * * @since 4.2.0 * @since 4.6.0 Added `available` property to indicate whether credentials have been provided. * * @type {object} filesystemCredentials Holds filesystem credentials. * @type {object} filesystemCredentials.ftp Holds FTP credentials. * @type {string} filesystemCredentials.ftp.host FTP host. Default empty string. * @type {string} filesystemCredentials.ftp.username FTP user name. Default empty string. * @type {string} filesystemCredentials.ftp.password FTP password. Default empty string. * @type {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'. * Default empty string. * @type {object} filesystemCredentials.ssh Holds SSH credentials. * @type {string} filesystemCredentials.ssh.publicKey The public key. Default empty string. * @type {string} filesystemCredentials.ssh.privateKey The private key. Default empty string. * @type {bool} filesystemCredentials.available Whether filesystem credentials have been provided. * Default 'false'. */ wp.updates.filesystemCredentials = { ftp: { host: '', username: '', password: '', connectionType: '' }, ssh: { publicKey: '', privateKey: '' }, available: false }; /** * Whether we're waiting for an Ajax request to complete. * * @since 4.2.0 * @since 4.6.0 More accurately named `ajaxLocked`. * * @type {bool} */ wp.updates.ajaxLocked = false; /** * Admin notice template. * * @since 4.6.0 * * @type {function} A function that lazily-compiles the template requested. */ wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); /** * Update queue. * * If the user tries to update a plugin while an update is * already happening, it can be placed in this queue to perform later. * * @since 4.2.0 * @since 4.6.0 More accurately named `queue`. * * @type {Array.object} */ wp.updates.queue = []; /** * Holds a jQuery reference to return focus to when exiting the request credentials modal. * * @since 4.2.0 * * @type {jQuery} */ wp.updates.$elToReturnFocusToFromCredentialsModal = undefined; /** * Adds or updates an admin notice. * * @since 4.6.0 * * @param {object} data * @param {*=} data.selector Optional. Selector of an element to be replaced with the admin notice. * @param {string=} data.id Optional. Unique id that will be used as the notice's id attribute. * @param {string=} data.className Optional. Class names that will be used in the admin notice. * @param {string=} data.message Optional. The message displayed in the notice. * @param {number=} data.successes Optional. The amount of successful operations. * @param {number=} data.errors Optional. The amount of failed operations. * @param {Array=} data.errorMessages Optional. Error messages of failed operations. * */ wp.updates.addAdminNotice = function( data ) { var $notice = $( data.selector ), $adminNotice; delete data.selector; $adminNotice = wp.updates.adminNotice( data ); // Check if this admin notice already exists. if ( ! $notice.length ) { $notice = $( '#' + data.id ); } if ( $notice.length ) { $notice.replaceWith( $adminNotice ); } else { $( '.wrap' ).find( '> h1' ).after( $adminNotice ); } $document.trigger( 'wp-updates-notice-added' ); }; /** * Handles Ajax requests to WordPress. * * @since 4.6.0 * * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc). * @param {object} data Data that needs to be passed to the ajax callback. * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.ajax = function( action, data ) { var options = {}; if ( wp.updates.ajaxLocked ) { wp.updates.queue.push( { action: action, data: data } ); // Return a Deferred object so callbacks can always be registered. return $.Deferred(); } wp.updates.ajaxLocked = true; if ( data.success ) { options.success = data.success; delete data.success; } if ( data.error ) { options.error = data.error; delete data.error; } options.data = _.extend( data, { action: action, _ajax_nonce: wp.updates.ajaxNonce, username: wp.updates.filesystemCredentials.ftp.username, password: wp.updates.filesystemCredentials.ftp.password, hostname: wp.updates.filesystemCredentials.ftp.hostname, connection_type: wp.updates.filesystemCredentials.ftp.connectionType, public_key: wp.updates.filesystemCredentials.ssh.publicKey, private_key: wp.updates.filesystemCredentials.ssh.privateKey } ); return wp.ajax.send( options ).always( wp.updates.ajaxAlways ); }; /** * Actions performed after every Ajax request. * * @since 4.6.0 * * @param {object} response * @param {array=} response.debug Optional. Debug information. * @param {string=} response.errorCode Optional. Error code for an error that occurred. */ wp.updates.ajaxAlways = function( response ) { if ( ! response.errorCode && 'unable_to_connect_to_filesystem' !== response.errorCode ) { wp.updates.ajaxLocked = false; wp.updates.queueChecker(); } if ( 'undefined' !== typeof response.debug ) { _.map( response.debug, function( message ) { window.console.log( $( '
' ).html( message ).text() ); } ); } }; /** * Decrements the update counts throughout the various menus. * * This includes the toolbar, the "Updates" menu item and the menu items * for plugins and themes. * * @since 3.9.0 * * @param {string} type The type of item that was updated or deleted. * Can be 'plugin', 'theme'. */ wp.updates.decrementCount = function( type ) { var $adminBarUpdates = $( '#wp-admin-bar-updates' ), $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ), count = $adminBarUpdates.find( '.ab-label' ).text(), $menuItem, $itemCount, itemCount; count = parseInt( count, 10 ) - 1; if ( count < 0 || isNaN( count ) ) { return; } $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' ); $adminBarUpdates.find( '.ab-label' ).text( count ); // Remove the update count from the toolbar if it's zero. if ( ! count ) { $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove(); } // Update the "Updates" menu item. $dashboardNavMenuUpdateCount.each( function( index, element ) { element.className = element.className.replace( /count-\d+/, 'count-' + count ); } ); $dashboardNavMenuUpdateCount.removeAttr( 'title' ); $dashboardNavMenuUpdateCount.find( '.update-count' ).text( count ); switch ( type ) { case 'plugin': $menuItem = $( '#menu-plugins' ); $itemCount = $menuItem.find( '.plugin-count' ); break; case 'theme': $menuItem = $( '#menu-appearance' ); $itemCount = $menuItem.find( '.theme-count' ); break; default: window.console.error( '"%s" is not white-listed to have its count decremented.', type ); return; } // Decrement the counter of the other menu items. if ( $itemCount ) { itemCount = $itemCount.eq( 0 ).text(); itemCount = parseInt( itemCount, 10 ) - 1; } if ( itemCount < 0 || isNaN( itemCount ) ) { return; } if ( itemCount > 0 ) { $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' ); $itemCount.text( itemCount ); $menuItem.find( '.update-plugins' ).each( function( index, element ) { element.className = element.className.replace( /count-\d+/, 'count-' + itemCount ); } ); } else { $( '.subsubsub .upgrade' ).remove(); $menuItem.find( '.update-plugins' ).remove(); } }; /** * Sends an Ajax request to the server to update a plugin. * * @since 4.2.0 * @since 4.6.0 More accurately named `updatePlugin`. * * @param {object} args Arguments. * @param {string} args.plugin Plugin basename. * @param {string} args.slug Plugin slug. * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess * @param {updatePluginError=} args.error Optional. Error callback. Default: wp.updates.updatePluginError * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.updatePlugin = function( args ) { var $updateRow, $card, $message, message; args = _.extend( { success: wp.updates.updatePluginSuccess, error: wp.updates.updatePluginError }, args ); if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' ); $message = $updateRow.find( '.update-message' ).addClass( 'updating-message' ).find( 'p' ); message = wp.updates.l10n.updatingLabel.replace( '%s', $updateRow.find( '.plugin-title strong' ).text() ); } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { $card = $( '.plugin-card-' + args.slug ); $message = $card.find( '.update-now' ).addClass( 'updating-message' ); message = wp.updates.l10n.updatingLabel.replace( '%s', $message.data( 'name' ) ); // Remove previous error messages, if any. $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove(); } if ( $message.html() !== wp.updates.l10n.updating ) { $message.data( 'originaltext', $message.html() ); } $message .attr( 'aria-label', message ) .text( wp.updates.l10n.updating ); $document.trigger( 'wp-plugin-updating' ); return wp.updates.ajax( 'update-plugin', args ); }; /** * Updates the UI appropriately after a successful plugin update. * * @since 4.2.0 * @since 4.6.0 More accurately named `updatePluginSuccess`. * * @typedef {object} updatePluginSuccess * @param {object} response Response from the server. * @param {string} response.slug Slug of the plugin to be updated. * @param {string} response.plugin Basename of the plugin to be updated. * @param {string} response.pluginName Name of the plugin to be updated. * @param {string} response.oldVersion Old version of the plugin. * @param {string} response.newVersion New version of the plugin. */ wp.updates.updatePluginSuccess = function( response ) { var $pluginRow, $updateMessage, newText; if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ) .removeClass( 'update' ) .addClass( 'updated' ); $updateMessage = $pluginRow.find( '.update-message' ) .removeClass( 'updating-message notice-warning' ) .addClass( 'updated-message notice-success' ).find( 'p' ); // Update the version number in the row. newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); $pluginRow.find( '.plugin-version-author-uri' ).html( newText ); } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' ) .removeClass( 'updating-message' ) .addClass( 'button-disabled updated-message' ); } $updateMessage .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) ) .text( wp.updates.l10n.updated ); wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' ); wp.updates.decrementCount( 'plugin' ); $document.trigger( 'wp-plugin-update-success', response ); }; /** * Updates the UI appropriately after a failed plugin update. * * @since 4.2.0 * @since 4.6.0 More accurately named `updatePluginError`. * * @typedef {object} updatePluginError * @param {object} response Response from the server. * @param {string} response.slug Slug of the plugin to be updated. * @param {string} response.plugin Basename of the plugin to be updated. * @param {string=} response.pluginName Optional. Name of the plugin to be updated. * @param {string} response.errorCode Error code for the error that occurred. * @param {string} response.errorMessage The error that occurred. */ wp.updates.updatePluginError = function( response ) { var $card, $message, errorMessage; if ( ! wp.updates.isValidResponse( response, 'update' ) ) { return; } if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) { return; } errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ); if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' ); $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage ); } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { $card = $( '.plugin-card-' + response.slug ) .addClass( 'plugin-card-update-failed' ) .append( wp.updates.adminNotice( { className: 'update-message notice-error notice-alt is-dismissible', message: errorMessage } ) ); $card.find( '.update-now' ) .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) ) .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' ); $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { // Use same delay as the total duration of the notice fadeTo + slideUp animation. setTimeout( function() { $card .removeClass( 'plugin-card-update-failed' ) .find( '.column-name a' ).focus(); $card.find( '.update-now' ) .attr( 'aria-label', false ) .text( wp.updates.l10n.updateNow ); }, 200 ); } ); } wp.a11y.speak( errorMessage, 'assertive' ); $document.trigger( 'wp-plugin-update-error', response ); }; /** * Sends an Ajax request to the server to install a plugin. * * @since 4.6.0 * * @param {object} args Arguments. * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess * @param {installPluginError=} args.error Optional. Error callback. Default: wp.updates.installPluginError * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ wp.updates.installPlugin = function( args ) { var $card = $( '.plugin-card-' + args.slug ), $message = $card.find( '.install-now' ); args = _.extend( { success: wp.updates.installPluginSuccess, error: wp.updates.installPluginError }, args ); if ( 'import' === pagenow ) { $message = $( 'a[href*="' + args.slug + '"]' ); } else { $message.text( wp.updates.l10n.installing ); } $message.addClass( 'updating-message' ); wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' ); // Remove previous error messages, if any. $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove(); return wp.updates.ajax( 'install-plugin', args ); }; /** * Updates the UI appropriately after a successful plugin install. * * @since 4.6.0 * * @typedef {object} installPluginSuccess * @param {object} response Response from the server. * @param {string} response.slug Slug of the installed plugin. * @param {string} response.pluginName Name of the installed plugin. * @param {string} response.activateUrl URL to activate the just installed plugin. */ wp.updates.installPluginSuccess = function( response ) { var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' ); $message .removeClass( 'updating-message' ) .addClass( 'updated-message installed button-disabled' ) .text( wp.updates.l10n.installed ); wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' ); $document.trigger( 'wp-plugin-install-success', response ); if ( response.activateUrl ) { setTimeout( function() { // Transform the 'Install' button into an 'Activate' button. $message.removeClass( 'install-now installed button-disabled updated-message' ).addClass( 'activate-now button-primary' ) .attr( 'href', response.activateUrl ) .text( wp.updates.l10n.activatePlugin ); }, 1000 ); } }; /** * Updates the UI appropriately after a failed plugin install. * * @since 4.6.0 * * @typedef {object} installPluginError * @param {object} response Response from the server. * @param {string} response.slug Slug of the plugin to be installed. * @param {string=} response.pluginName Optional. Name of the plugin to be installed. * @param {string} response.errorCode Error code for the error that occurred. * @param {string} response.errorMessage The error that occurred. */ wp.updates.installPluginError = function( response ) { var $card = $( '.plugin-card-' + response.slug ), $button = $card.find( '.install-now' ), errorMessage; if ( ! wp.updates.isValidResponse( response, 'install' ) ) { return; } if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { return; } errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ); $card .addClass( 'plugin-card-update-failed' ) .append( '' + errorMessage + '
' + message + '
' ).html( $message.find( 'p' ).data( 'originaltext' ) ); } $message .removeClass( 'updating-message' ) .html( originalText ); } wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' ); } ); /** * Click handler for plugin updates in List Table view. * * @since 4.2.0 * * @param {Event} event Event interface. */ $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) { var $message = $( event.target ), $pluginRow = $message.parents( 'tr' ); event.preventDefault(); if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { return; } wp.updates.maybeRequestFilesystemCredentials( event ); // Return the user to the input box of the plugin's table row after closing the modal. wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' ); wp.updates.updatePlugin( { plugin: $pluginRow.data( 'plugin' ), slug: $pluginRow.data( 'slug' ) } ); } ); /** * Click handler for plugin updates in plugin install view. * * @since 4.2.0 * * @param {Event} event Event interface. */ $pluginFilter.on( 'click', '.update-now', function( event ) { var $button = $( event.target ); event.preventDefault(); if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { return; } wp.updates.maybeRequestFilesystemCredentials( event ); wp.updates.updatePlugin( { plugin: $button.data( 'plugin' ), slug: $button.data( 'slug' ) } ); } ); /** * Click handler for plugin installs in plugin install view. * * @since 4.6.0 * * @param {Event} event Event interface. */ $pluginFilter.on( 'click', '.install-now', function( event ) { var $button = $( event.target ); event.preventDefault(); if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { return; } if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { wp.updates.requestFilesystemCredentials( event ); $document.on( 'credential-modal-cancel', function() { var $message = $( '.install-now.updating-message' ); $message .removeClass( 'updating-message' ) .text( wp.updates.l10n.installNow ); wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' ); } ); } wp.updates.installPlugin( { slug: $button.data( 'slug' ) } ); } ); /** * Click handler for plugin deletions. * * @since 4.6.0 * * @param {Event} event Event interface. */ $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) { var $pluginRow = $( event.target ).parents( 'tr' ); event.preventDefault(); if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) { return; } wp.updates.maybeRequestFilesystemCredentials( event ); wp.updates.deletePlugin( { plugin: $pluginRow.data( 'plugin' ), slug: $pluginRow.data( 'slug' ) } ); } ); /** * Click handler for theme updates. * * @since 4.6.0 * * @param {Event} event Event interface. */ $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) { var $message = $( event.target ), $themeRow = $message.parents( 'tr' ); event.preventDefault(); if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { return; } wp.updates.maybeRequestFilesystemCredentials( event ); // Return the user to the input box of the theme's table row after closing the modal. wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' ); wp.updates.updateTheme( { slug: $themeRow.data( 'slug' ) } ); } ); /** * Click handler for theme deletions. * * @since 4.6.0 * * @param {Event} event Event interface. */ $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) { var $themeRow = $( event.target ).parents( 'tr' ); event.preventDefault(); if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) { return; } wp.updates.maybeRequestFilesystemCredentials( event ); wp.updates.deleteTheme( { slug: $themeRow.data( 'slug' ) } ); } ); /** * Bulk action handler for plugins and themes. * * Handles both deletions and updates. * * @since 4.6.0 * * @param {Event} event Event interface. */ $bulkActionForm.on( 'click', '[type="submit"]', function( event ) { var bulkAction = $( event.target ).siblings( 'select' ).val(), itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ), success = 0, error = 0, errorMessages = [], type, action; // Determine which type of item we're dealing with. switch ( pagenow ) { case 'plugins': case 'plugins-network': type = 'plugin'; break; case 'themes-network': type = 'theme'; break; default: window.console.error( 'The page "%s" is not white-listed for bulk action handling.', pagenow ); return; } // Bail if there were no items selected. if ( ! itemsSelected.length ) { event.preventDefault(); $( 'html, body' ).animate( { scrollTop: 0 } ); return wp.updates.addAdminNotice( { id: 'no-items-selected', className: 'notice-error is-dismissible', message: wp.updates.l10n.noItemsSelected } ); } // Determine the type of request we're dealing with. switch ( bulkAction ) { case 'update-selected': action = bulkAction.replace( 'selected', type ); break; case 'delete-selected': if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) { event.preventDefault(); return; } action = bulkAction.replace( 'selected', type ); break; default: window.console.error( 'Failed to identify bulk action: %s', bulkAction ); return; } wp.updates.maybeRequestFilesystemCredentials( event ); event.preventDefault(); // Un-check the bulk checkboxes. $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false ); // Find all the checkboxes which have been checked. itemsSelected.each( function( index, element ) { var $checkbox = $( element ), $itemRow = $checkbox.parents( 'tr' ); // Un-check the box. $checkbox.prop( 'checked', false ); // Only add update-able items to the update queue. if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) { return; } // Add it to the queue. wp.updates.queue.push( { action: action, data: { plugin: $itemRow.data( 'plugin' ), slug: $itemRow.data( 'slug' ) } } ); } ); // Display bulk notification for updates of any kind. $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) { var $bulkActionNotice, itemName; if ( 'wp-' + response.update + '-update-success' === event.type ) { success++; } else { itemName = response.pluginName ? response.pluginName : $( '[data-slug="' + response.slug + '"]' ).find( '.theme-title strong' ).text(); error++; errorMessages.push( itemName + ': ' + response.errorMessage ); } wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' ); wp.updates.addAdminNotice( { id: 'bulk-action-notice', successes: success, errors: error, errorMessages: errorMessages, type: response.update } ); $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() { $bulkActionNotice.find( 'ul' ).toggleClass( 'hidden' ); } ); if ( error > 0 && ! wp.updates.queue.length ) { $( 'html, body' ).animate( { scrollTop: 0 } ); } } ); // Reset admin notice template after #bulk-action-notice was added. $document.on( 'wp-updates-notice-added', function() { wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); } ); // Check the queue, now that the event handlers have been added. wp.updates.queueChecker(); } ); /** * Handles changes to the plugin search box on the new-plugin page, * searching the repository dynamically. * * @since 4.6.0 */ $( 'input.wp-filter-search, .wp-filter input[name="s"]' ).on( 'keyup search', _.debounce( function() { var $form = $( '#plugin-filter' ).empty(), data = _.extend( { _ajax_nonce: wp.updates.ajaxNonce, s: $( '
' ).html( $( this ).val() ).text(), tab: 'search', type: $( '#typeselector' ).val() }, { type: 'term' } ); if ( wp.updates.searchTerm === data.s ) { return; } else { wp.updates.searchTerm = data.s; } history.pushState( null, '', location.href.split( '?' )[0] + '?' + $.param( _.omit( data, '_ajax_nonce' ) ) ); if ( 'undefined' !== typeof wp.updates.searchRequest ) { wp.updates.searchRequest.abort(); } $( 'body' ).addClass( 'loading-content' ); wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) { $( 'body' ).removeClass( 'loading-content' ); $form.append( response.items ); delete wp.updates.searchRequest; } ); }, 500 ) ); /** * Handles changes to the plugin search box on the Installed Plugins screen, * searching the plugin list dynamically. * * @since 4.6.0 */ $( '#plugin-search-input' ).on( 'keyup search', _.debounce( function() { var data = { _ajax_nonce: wp.updates.ajaxNonce, s: $( '' ).html( $( this ).val() ).text() }; if ( wp.updates.searchTerm === data.s ) { return; } else { wp.updates.searchTerm = data.s; } history.pushState( null, '', location.href.split( '?' )[0] + '?s=' + data.s ); if ( 'undefined' !== typeof wp.updates.searchRequest ) { wp.updates.searchRequest.abort(); } $bulkActionForm.empty(); $( 'body' ).addClass( 'loading-content' ); wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) { // Can we just ditch this whole subtitle business? var $subTitle = $( '' ).addClass( 'subtitle' ).html( wp.updates.l10n.searchResults.replace( '%s', data.s ) ), $oldSubTitle = $( '.wrap .subtitle' ); if ( ! data.s.length ) { $oldSubTitle.remove(); } else if ( $oldSubTitle.length ) { $oldSubTitle.replaceWith( $subTitle ); } else { $( '.wrap h1' ).append( $subTitle ); } $( 'body' ).removeClass( 'loading-content' ); $bulkActionForm.append( response.items ); delete wp.updates.searchRequest; } ); }, 500 ) ); /** * Trigger a search event when the search form gets submitted. * * @since 4.6.0 */ $document.on( 'submit', '.search-plugins', function( event ) { event.preventDefault(); $( 'input.wp-filter-search' ).trigger( 'search' ); } ); /** * Trigger a search event when the search type gets changed. * * @since 4.6.0 */ $( '#typeselector' ).on( 'change', function() { $( 'input[name="s"]' ).trigger( 'search' ); } ); /** * Click handler for updating a plugin from the details modal on `plugin-install.php`. * * @since 4.2.0 * * @param {Event} event Event interface. */ $( '#plugin_update_from_iframe' ).on( 'click', function( event ) { var target = window.parent === window ? null : window.parent, update; $.support.postMessage = !! window.postMessage; if ( false === $.support.postMessage || null === target ) { return; } event.preventDefault(); update = { action: 'update-plugin', data: { plugin: $( this ).data( 'plugin' ), slug: $( this ).data( 'slug' ) } }; target.postMessage( JSON.stringify( update ), window.location.origin ); } ); /** * Click handler for installing a plugin from the details modal on `plugin-install.php`. * * @since 4.6.0 * * @param {Event} event Event interface. */ $( '#plugin_install_from_iframe' ).on( 'click', function( event ) { var target = window.parent === window ? null : window.parent, install; $.support.postMessage = !! window.postMessage; if ( false === $.support.postMessage || null === target ) { return; } event.preventDefault(); install = { action: 'install-plugin', data: { slug: $( this ).data( 'slug' ) } }; target.postMessage( JSON.stringify( install ), window.location.origin ); } ); /** * Handles postMessage events. * * @since 4.2.0 * @since 4.6.0 Switched `update-plugin` action to use the queue. * * @param {Event} event Event interface. */ $( window ).on( 'message', function( event ) { var originalEvent = event.originalEvent, expectedOrigin = document.location.protocol + '//' + document.location.hostname, message; if ( originalEvent.origin !== expectedOrigin ) { return; } message = $.parseJSON( originalEvent.data ); if ( 'undefined' === typeof message.action ) { return; } switch ( message.action ) { // Called from `wp-admin/includes/class-wp-upgrader-skins.php`. case 'decrementUpdateCount': /** @property {string} message.upgradeType */ wp.updates.decrementCount( message.upgradeType ); break; case 'install-plugin': case 'update-plugin': /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ window.tb_remove(); /* jscs:enable */ message.data = wp.updates._addCallbacks( message.data, message.action ); wp.updates.queue.push( message ); wp.updates.queueChecker(); break; } } ); /** * Adds a callback to display a warning before leaving the page. * * @since 4.2.0 */ $( window ).on( 'beforeunload', wp.updates.beforeunload ); } ); })( jQuery, window.wp, _.extend( window._wpUpdatesSettings, window._wpUpdatesItemCounts || {} ) );