WordPress.org

WordPress Developer Blog

Getting started with the Command Palette API

WordPress 6.3 introduced the Command Palette to millions of users along with an API for developers to extend its capabilities with additional commands.

This type of feature is common in other apps and programs. In WordPress, you hit Cmd + K on Mac or Ctrl + K on Windows, and a modal appears on your screen. From there, simply type or search for commands:

As of WordPress 6.4, the Command Palette is only available from the Post Editor and Site Editor screens. It is likely to be an admin-wide feature in a future release.

Version 6.4 polished the interface and added more options to its built-in command catalog. Some existing commands let you:

  • Add a new post/page
  • Edit an existing post/page
  • Access the Patterns library
  • Toggle various user preferences

These are just some baseline commands you would expect for navigating the WordPress interface via the keyboard. But what makes the Command Palette truly powerful is the API that lets you add custom commands with minimal JavaScript code.

There are two types of commands you can register: static and dynamic. In this tutorial, you will learn how to register static commands, but you can learn more about dynamic commands (command loaders) in the Command Palette API’s dev note.

Prerequisites

While you’ll learn the basics of the Command Palette API in this post, it is still an advanced-level tutorial. This means you should be familiar with using the @wordpress/scripts package before proceeding. This tutorial expects that you understand how to run the start and build commands from that package to process the JavaScript code required.

You will likely add custom commands via plugins. However, it is also possible to add them via a theme. To learn how to set up @wordpress/scripts in a theme, check out Beyond block styles, part 1: using the WordPress scripts package with themes.

Reviewing the @wordpress/commands documentation will deepen your understanding of the code that follows, but it is not necessary to get started with this post.

Registering static commands

There are two ways to register static commands:

  • The wp.data.dispatch( wp.commands.store ).registerCommand() action
  • The wp.commands.useCommand() React hook

They essentially work in the same way, accepting the same parameters. The only difference is that the useCommand() hook must be used inside a React component. Outside of a component, stick with registerCommand(). Both accept a single parameter of an object that lets you define several properties:

  • name: A unique namespace and slug for the command.
  • label: The label used to show the command in the Command Palette.
  • searchLabel: An optional label that is checked against what the user is searching (the label argument is what gets shown).
  • context: An optional context to show the command in the default list for a particular screen. The current contexts are:
    • site-editor: The main Site Editor screen.
    • site-editor-edit: When editing a template in the Site Editor.
  • icon: An SVG icon (React component) to display next to the label in the Command Palette.
  • callback: A callback function that fires when a user selects the command.

Take a quick look at an example of what calling registerCommand() looks like:

wp.data.dispatch( wp.commands.store ).registerCommand( {
	name:  'your-namespace/your-command-slug',
	label: 'Command label',
	searchLabel: 'Command search label',
	icon:  icon,
	context: 'site-editor',
	callback: ( { close } ) => {
		close();
	}
} );

The code should be pretty straightforward for most JavaScript developers working within the WordPress environment. The most complex part is deciding what to do within your callback function, which is the actual command that you’re executing.

Building an example commands plugin

Let’s complete an exercise together and build a plugin that adds a few custom commands.

Setting up the plugin

First, create a new plugin in your wp-content/plugins folder of your WordPress installation with the following file structure:

  • build/
    • WordPress will output the compiled files to this folder.
  • src/
    • index.js
  • index.php
  • package.json

Make sure that you have at least your start and build scripts defined in package.json:

{
	"scripts": {
		"start": "wp-scripts start",
		"build": "wp-scripts build"
	}
}

Next, install the @wordpress/scripts package via the CLI command:

npm install @wordpress/scripts --saveDev

You’ll also need the @wordpress/icons package for this plugin, so add that too:

npm install @wordpress/icons

Then, add your Plugin File Header in index.php, which should look similar to this:

<?php
/**
 * Plugin Name:       Dev Blog: Command Palette API
 * Plugin URI:        https://developer.wordpress.org/news/
 * Description:       Showcases examples of adding static commands via the Command Palette API.
 * Version:           1.0.0
 * Requires at least: 6.4
 * Requires PHP:      7.4
 * Author:            WordPress Developer Blog
 * Author URI:        https://developer.wordpress.org/news/
 * Text Domain:       dev-blog
 */

Once you’ve activated your example plugin, don’t forget to run the start CLI command:

npm run start

Enqueue the editor script

Because this isn’t a custom block and WordPress isn’t loading your JavaScript automatically, you’ll need to enqueue your build/index.js file on the enqueue_block_editor_scripts hook.

Add this code to your plugin’s index.php file:

add_action( 'enqueue_block_editor_assets', 'devblog_command_api_editor_assets' );

function devblog_command_api_editor_assets() {
	$asset_file = trailingslashit( __DIR__ ) . 'build/index.asset.php';

	if ( file_exists( $asset_file ) ) {
		$asset = include $asset_file;

		wp_enqueue_script(
			'devblog-command-api',
			trailingslashit( plugin_dir_url( __FILE__ ) ) . 'build/index.js',
			$asset['dependencies'],
			$asset['version'],
			true
		);
	}
}

Import dependencies

Now open your plugin’s src/index.js file. You’ll use it to import dependencies needed to add commands:

To import the dependencies, add this to the top of your src/index.js file:

import { store as commandsStore } from '@wordpress/commands';
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { settings, comment, button } from '@wordpress/icons';

Now you can begin working with the Command Palette API. Try some of the examples that follow.

Example #1: Navigate to admin page command

For the first example, let’s add a simple command to visit a page elsewhere in the WordPress admin, as shown below:

I’ve chosen a command for navigating to the Gutenberg Experiments settings screen (requires the Gutenberg plugin to be active). But you can change the URL in the code below to point to any webpage that you’d like.

To add a command that navigates to another page, you can use the document.location JavaScript object and set its href property. Try this by adding this code to your src/index.js file:

dispatch( commandsStore ).registerCommand( {
	name:  'dev-blog/gutenberg-experiments',
	label: __( 'Gutenberg Experiments', 'dev-blog' ),
	icon:  settings,
	context: 'site-editor',
	callback: ( { close } ) => {
		document.location.href = 'admin.php?page=gutenberg-experiments';
		close();
	}
} );

To test, open the Command Palette and begin typing the “Gutenberg Experiments” label. You should see it appear as an option, and when selected, you should be taken to the correct admin screen.

Example #2: Toggle panel command

In this example, you will add a toggle command that either enables or disables the Discussion panel in the post editor:

You can do this with any panel as long as you know its name. In the case of the Discussion panel, it is discussion-panel.

To toggle a panel in the post editor, you’ll need to pass the panel name to the toggleEditorPanelEnabled() action from core/edit-post.

Add this code to your src/index.js file:

dispatch( commandsStore ).registerCommand( {
	name:  'dev-blog/discussion-panel',
	label: __( 'Toggle discussion panel', 'dev-blog' ),
	icon:  comment,
	callback: ( { close } ) => {
		dispatch( 'core/edit-post' ).toggleEditorPanelEnabled(
			'discussion-panel'
		);
		close();
	}
} );

Some panels may not be available for every editor. For example, the Discussion panel is shown in the Post Editor but not the Site Editor. So, if you use the “Toggle Discussion Panel” command from the Site Editor, you will not actually see the result of the command.

I’m not sure that this is the optimal user experience. An alternative is to only register the command when on the edit post screen. You can check if you are on that screen by testing whether the wp.editPost object is defined.

Try wrapping your previous code in a conditional so that the command is only loaded for the Post Editor:

if ( undefined !== wp.editPost ) {
	dispatch( commandsStore ).registerCommand( {
		name:  'dev-blog/discussion-panel',
		label: __( 'Toggle discussion panel', 'dev-blog' ),
		icon:  comment,
		callback: ( { close } ) => {
			dispatch( 'core/edit-post' ).toggleEditorPanelEnabled(
				'discussion-panel'
			);
			close();
		}
	} );
}

I’m also on the fence on conditional commands and whether they offer a better user experience. Let me know how you would handle it in the comments.

Example #3: Toggle user preference command

For the final example, let’s build a command to toggle the user preference between icons or labels for editor UI buttons:

This is another contextual command similar to the previous example. The difference in this case is that the preferences are stored based on the current editor. One for the Site Editor and one for the Post Editor.

You already know how to check wp.editPost to determine whether you are in the Post Editor. There’s an equivalent object for checking the Site Editor: wp.editSite.

To toggle a user preference, use the toggle() action from the core/preferences data module. You will need to know the editor name (core/edit-site or core/edit-post) and the preference name. In this case, showIconLabels is the preference that you’ll target.

Add this code to your plugin’s src/index.js file:

dispatch( commandsStore ).registerCommand( {
	name:  'dev-blog/toggle-button-labels',
	label: __( 'Toggle button labels', 'dev-blog' ),
	icon:  button,
	context: 'site-editor-edit',
	callback: ( { close } ) => {
		
		// Toggles preference for site editor.
		if ( undefined !== wp.editSite ) {
			dispatch( 'core/preferences' ).toggle(
				'core/edit-site',
				'showIconLabels'
			);
		}

		// Toggles preference for post editor.
		else if ( undefined !== wp.editPost ) {
			dispatch( 'core/preferences' ).toggle(
				'core/edit-post',
				'showIconLabels'
			);
		}

		close();
	}
} );

Whether to implement the conditionals is entirely up to you. You may decide that a better user experience is to toggle this preference for both editors, regardless of which one the user is currently using. You’ll still need both of your dispatch( 'core/preferences' ).toggle() calls, but you can remove the conditional checks around them.

Example #4: Working from within a component

The previous examples showed you how to add some simple commands from outside of a component. But you’ll often be working from within a component in your plugins.

Let’s step things up a notch and smooth out some rougher bits from the “Example #2: Toggle panel command” section.

In particular, you’ll wrap your command inside of a component. You’ll also improve the user experience by showing a conditional label and triggering a snackbar notification message in the bottom left of the screen when the panel’s state changes:

First, you need to update your imports to include a few other items:

  • useCommand from the @wordpress/commands package.
  • useDispatch and useSelect from the @wordpress/data package.
  • registerPlugin from the @wordpress/buttons package.

Update your imports in index.js so that they look like this:

import { store, useCommand } from '@wordpress/commands';
import { dispatch, useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { settings, search, comment, button } from '@wordpress/icons';
import { registerPlugin } from '@wordpress/plugins';

Now add this code to your index.js file:

if ( undefined !== wp.editPost ) {
	registerPlugin( 'dev-blog-command-palette', {
		render: () => {
			// Determine if the discussion panel is enabled.
			const discussionPanelEnabled = useSelect( ( select ) => {
				return select( 'core/edit-post' ).isEditorPanelEnabled( 
					'discussion-panel' 
				);
			}, [] );

			// Get functions for toggling panels and creating snackbars.
			const { toggleEditorPanelEnabled } = useDispatch( 'core/edit-post' );
			const { createInfoNotice }         = useDispatch( 'core/notices'   );

			// Register command to toggle discussion panel.
			useCommand( {
				name:  'dev-blog/discussion-show-hide',
				label: discussionPanelEnabled
				       ? __( 'Hide discussion panel', 'dev-blog' ) 
				       : __( 'Show discussion panel', 'dev-blog' ),
				icon:  comment,
				callback: ( { close } ) => {
					// Toggle the discussion panel.
					toggleEditorPanelEnabled( 'discussion-panel' );
					
					// Add a snackbar notice.
					createInfoNotice(
						discussionPanelEnabled
						? __( 'Discussion panel hidden.', 'dev-blog' )
						: __( 'Discussion panel displayed.', 'dev-blog' ),
						{
							id:   'dev-blog/toggle-discussion/notice',
							type: 'snackbar'
						}
					);
		
					close();
				}
			} );
		}
	} );
}

Much of this code probably looks different than the previous examples. Here is a quick rundown of what each of the function or hook calls do:

Now you should have everything you need to get started with the Command Palette API. I’d love to hear what you want to build with it, so please share your ideas in the comments.

Props to @dansoschin, @bph, @juanmaguitar, and @richtabor for feedback and review.

Categories: ,

Leave a Reply

Your email address will not be published. Required fields are marked *