Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ module.exports = function(grunt) {
[ WORKING_DIR + 'wp-includes/js/wp-auth-check.js' ]: [ './src/js/_enqueues/lib/auth-check.js' ],
[ WORKING_DIR + 'wp-includes/js/wp-tooltip.js' ]: [ './src/js/_enqueues/wp/wp-tooltip.js' ],
[ WORKING_DIR + 'wp-includes/js/wp-backbone.js' ]: [ './src/js/_enqueues/wp/backbone.js' ],
[ WORKING_DIR + 'wp-includes/js/wp-command-palette-loader.js' ]: [ './src/js/_enqueues/wp/command-palette-loader.js' ],
[ WORKING_DIR + 'wp-includes/js/wp-custom-header.js' ]: [ './src/js/_enqueues/wp/custom-header.js' ],
[ WORKING_DIR + 'wp-includes/js/wp-embed-template.js' ]: [ './src/js/_enqueues/lib/embed-template.js' ],
[ WORKING_DIR + 'wp-includes/js/wp-embed.js' ]: [ './src/js/_enqueues/wp/embed.js' ],
Expand Down Expand Up @@ -1383,6 +1384,7 @@ module.exports = function(grunt) {
'src/wp-includes/js/wp-api.js': 'src/js/_enqueues/wp/api.js',
'src/wp-includes/js/wp-auth-check.js': 'src/js/_enqueues/lib/auth-check.js',
'src/wp-includes/js/wp-backbone.js': 'src/js/_enqueues/wp/backbone.js',
'src/wp-includes/js/wp-command-palette-loader.js': 'src/js/_enqueues/wp/command-palette-loader.js',
'src/wp-includes/js/wp-custom-header.js': 'src/js/_enqueues/wp/custom-header.js',
'src/wp-includes/js/wp-embed-template.js': 'src/js/_enqueues/lib/embed-template.js',
'src/wp-includes/js/wp-embed.js': 'src/js/_enqueues/wp/embed.js',
Expand Down
171 changes: 171 additions & 0 deletions src/js/_enqueues/wp/command-palette-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* @output wp-includes/js/wp-command-palette-loader.js
*/

/**
* Opens the Command Palette, loading its assets on first use.
*
* On screens that do not load the Command Palette up front, its scripts and
* styles are printed into an inert template by
* wp_print_command_palette_assets_template(). They are only added to the page
* when the palette is first requested, with the keyboard shortcut or the
* toolbar button, so that they do not delay every admin page load.
*
* @since 7.3.0
*/
( function ( window, document ) {
const TEMPLATE_ID = 'wp-command-palette-assets';
const TOOLBAR_LINK_SELECTOR = '#wp-admin-bar-command-palette > .ab-item';
const IS_APPLE_OS = /Mac|iPod|iPhone|iPad/.test( window.navigator.platform );

let isLoading = false;

/**
* Tells whether a key press is the shortcut that opens the Command Palette.
*
* @param {KeyboardEvent} event The key press.
* @return {boolean} Whether the key press is Cmd+K on Apple devices or Ctrl+K elsewhere.
*/
function isOpenPaletteShortcut( event ) {
const primaryModifier = IS_APPLE_OS ? event.metaKey : event.ctrlKey;

return primaryModifier && ! event.altKey && ! event.shiftKey && 'k' === event.key?.toLowerCase();
}

/**
* Tells whether the Command Palette assets are still waiting in their template.
*
* @return {boolean} Whether the assets have not been added to the page yet.
*/
function arePaletteAssetsPending() {
return isLoading || null !== document.getElementById( TEMPLATE_ID );
}

/**
* Adds a script to the page and waits until it has run.
*
* A script added with the DOM runs as soon as it is inserted when it is
* inline, but only after it has loaded when it has a source. Waiting for
* each one keeps the order in which they were printed.
*
* @param {HTMLScriptElement} templateScript The script as printed in the template.
* @return {Promise<void>} Resolves once the script has run or failed to load.
*/
function insertScript( templateScript ) {
const script = document.createElement( 'script' );

for ( const attribute of templateScript.attributes ) {
script.setAttribute( attribute.name, attribute.value );
}
script.text = templateScript.text;

if ( ! script.hasAttribute( 'src' ) ) {
document.body.appendChild( script );
return Promise.resolve();
}

return new Promise( ( resolve ) => {
script.onload = resolve;
script.onerror = resolve;
document.body.appendChild( script );
} );
}

/**
* Adds a stylesheet to the page and waits until it has loaded.
*
* @param {HTMLLinkElement|HTMLStyleElement} templateStyle The style as printed in the template.
* @return {Promise<void>} Resolves once the stylesheet has loaded or failed to load.
*/
function insertStyle( templateStyle ) {
const style = document.importNode( templateStyle, true );

if ( 'LINK' !== style.tagName ) {
document.head.appendChild( style );
return Promise.resolve();
}

return new Promise( ( resolve ) => {
style.onload = resolve;
style.onerror = resolve;
document.head.appendChild( style );
} );
}

/**
* Asks the browser to start downloading a script before it is inserted.
*
* @param {string} url The script URL.
*/
function preloadScript( url ) {
const link = document.createElement( 'link' );

link.rel = 'preload';
link.as = 'script';
link.href = url;
document.head.appendChild( link );
}

/**
* Adds the Command Palette's scripts and styles from the template to the page.
*
* All scripts are preloaded at once and then run one after the other.
*
* @param {DocumentFragment} assets The template content.
* @return {Promise<void>} Resolves once every script has run and every stylesheet has loaded.
*/
async function insertPaletteAssets( assets ) {
const scripts = Array.from( assets.querySelectorAll( 'script' ) );
const styles = Array.from( assets.querySelectorAll( 'link[rel="stylesheet"], style' ) );

scripts
.filter( ( script ) => script.hasAttribute( 'src' ) )
.forEach( ( script ) => preloadScript( script.getAttribute( 'src' ) ) );

const stylesLoaded = Promise.all( styles.map( insertStyle ) );

for ( const script of scripts ) {
await insertScript( script );
}

await stylesLoaded;
}

/**
* Opens the Command Palette, adding its assets to the page first if needed.
*/
async function openPalette() {
if ( isLoading ) {
return;
}

const template = document.getElementById( TEMPLATE_ID );

if ( template ) {
isLoading = true;
template.remove();
await insertPaletteAssets( template.content );
isLoading = false;
}

window.wp.data.dispatch( 'core/commands' ).open();
}

document.addEventListener( 'keydown', ( event ) => {
if ( event.defaultPrevented || ! isOpenPaletteShortcut( event ) || ! arePaletteAssetsPending() ) {
return;
}

event.preventDefault();
openPalette();
} );

document.addEventListener( 'click', ( event ) => {
if ( ! event.target.closest( TOOLBAR_LINK_SELECTOR ) ) {
return;
}

event.preventDefault();
openPalette();
} );
}( window, document ) );
8 changes: 4 additions & 4 deletions src/wp-includes/admin-bar.php
Original file line number Diff line number Diff line change
Expand Up @@ -1011,11 +1011,12 @@ function wp_admin_bar_edit_menu( $wp_admin_bar ) {
* for opening the command palette.
*
* @since 7.0.0
* @since 7.3.0 Opens the palette through the script enqueued by wp_enqueue_command_palette_loader().
*
* @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
*/
function wp_admin_bar_command_palette_menu( WP_Admin_Bar $wp_admin_bar ): void {
if ( ! is_admin() || ! wp_script_is( 'wp-core-commands', 'enqueued' ) ) {
if ( ! is_admin() || ! wp_script_is( 'wp-command-palette-loader', 'enqueued' ) ) {
return;
}

Expand Down Expand Up @@ -1064,9 +1065,8 @@ function wp_admin_bar_command_palette_menu( WP_Admin_Bar $wp_admin_bar ): void {
'title' => $title,
'href' => '#',
'meta' => array(
'class' => 'hide-if-no-js',
'onclick' => 'wp.data.dispatch( "core/commands" ).open(); return false;',
'html' => wp_get_inline_script_tag( $script ),
'class' => 'hide-if-no-js',
'html' => wp_get_inline_script_tag( $script ),
),
)
);
Expand Down
3 changes: 2 additions & 1 deletion src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,8 @@
add_action( 'wp_enqueue_scripts', 'wp_enqueue_classic_theme_styles' );
add_action( 'admin_enqueue_scripts', 'wp_localize_jquery_ui_datepicker', 1000 );
add_action( 'admin_enqueue_scripts', 'wp_common_block_scripts_and_styles' );
add_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
add_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_loader' );
add_action( 'admin_print_footer_scripts', 'wp_print_command_palette_assets_template', 20 );
add_action( 'admin_head', 'wp_print_view_transitions_render_blocking_link' );
add_action( 'enqueue_block_assets', 'wp_enqueue_classic_theme_styles' );
add_action( 'enqueue_block_assets', 'wp_enqueue_registered_block_scripts_and_styles' );
Expand Down
91 changes: 80 additions & 11 deletions src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,8 @@ function wp_default_scripts( $scripts ) {

$scripts->add( 'admin-bar', "/wp-includes/js/admin-bar$suffix.js", array( 'hoverintent-js' ), false, 1 );

$scripts->add( 'wp-command-palette-loader', "/wp-includes/js/wp-command-palette-loader$suffix.js", array(), false, 1 );

$scripts->add( 'wplink', "/wp-includes/js/wplink$suffix.js", array( 'common', 'jquery', 'wp-a11y', 'wp-i18n' ), false, 1 );
$scripts->set_translations( 'wplink' );
did_action( 'init' ) && $scripts->localize(
Expand Down Expand Up @@ -2498,9 +2500,9 @@ function wp_prefetch_admin_assets(): void {
*
* Scripts printed in the footer are left out even though they block DOMContentLoaded, since
* they do not hold back the first paint. They are also where the bulk of the admin's bytes
* are, largely the command palette's dependencies, and a prefetch of them still in flight
* when the login form is submitted competes with the admin screen's own render-blocking
* stylesheets and delays its first paint on a slow connection.
* are, and a prefetch of them still in flight when the login form is submitted competes with
* the admin screen's own render-blocking stylesheets and delays its first paint on a slow
* connection.
*
* These are roots rather than the full set: everything they depend on is pulled in with them
* below, so the set follows the dependencies declared in wp_default_scripts() and
Expand All @@ -2512,8 +2514,9 @@ function wp_prefetch_admin_assets(): void {
*
* Each root mirrors an enqueue elsewhere, which carries a note pointing back here: `common`
* (for `jquery`) in wp-admin/admin.php, `colors` (for `wp-admin` and `buttons`) and `utils` in
* wp-admin/admin-header.php, `admin-bar` in WP_Admin_Bar::initialize(), `wp-auth-check` in
* wp_auth_check_load(), and `wp-commands` in wp_enqueue_command_palette_assets().
* wp-admin/admin-header.php, `admin-bar` in WP_Admin_Bar::initialize(), and `wp-auth-check`
* in wp_auth_check_load(). The command palette's stylesheet is not among them: admin screens
* load it when the palette is first opened, see wp_enqueue_command_palette_loader().
*/
$script_roots = array(
'jquery',
Expand All @@ -2525,7 +2528,6 @@ function wp_prefetch_admin_assets(): void {
'buttons',
'admin-bar',
'wp-auth-check',
'wp-commands',
);
}

Expand Down Expand Up @@ -3830,12 +3832,32 @@ function wp_enqueue_classic_theme_styles() {
/**
* Enqueues the assets required for the Command Palette.
*
* Loads the whole palette up front. Admin screens use wp_enqueue_command_palette_loader()
* instead, which loads the palette when it is first opened.
*
* @since 6.9.0
* @since 7.3.0 Moved the palette's settings to wp_enqueue_command_palette_loader().
*/
function wp_enqueue_command_palette_assets() {
wp_enqueue_script( 'wp-commands' );
wp_enqueue_command_palette_loader();
}

/**
* Enqueues the Command Palette loader and the palette's settings.
*
* The loader adds the palette's scripts and styles to the page when the palette is first
* opened, with the keyboard shortcut or the toolbar button, so that they do not hold back
* every admin screen. Until then they wait in the template printed by
* wp_print_command_palette_assets_template(). A screen that already loads the commands
* package, such as the block editor, gets the rest of the palette up front instead.
*
* @since 7.3.0
*
* @global array $menu
* @global array $submenu
*/
function wp_enqueue_command_palette_assets() {
function wp_enqueue_command_palette_loader() {
global $menu, $submenu;

$command_palette_settings = array(
Expand Down Expand Up @@ -3946,10 +3968,7 @@ function wp_enqueue_command_palette_assets() {
$command_palette_settings['menu_commands'] = $menu_commands;
}

wp_enqueue_script( 'wp-commands' );
// Prefetched from the login screen by wp_prefetch_admin_assets(), which needs updating if this changes.
wp_enqueue_style( 'wp-commands' );
wp_enqueue_script( 'wp-core-commands' );
wp_enqueue_script( 'wp-command-palette-loader' );

wp_add_inline_script(
'wp-core-commands',
Expand All @@ -3958,6 +3977,56 @@ function wp_enqueue_command_palette_assets() {
wp_json_encode( $command_palette_settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
)
);

if ( wp_script_is( 'wp-commands', 'enqueued' ) ) {
wp_enqueue_style( 'wp-commands' );
wp_enqueue_script( 'wp-core-commands' );

/**
* Fires when the Command Palette's scripts and styles are enqueued.
*
* Scripts and styles that add commands to the palette should be enqueued on this
* action. On screens that load the palette when it is first opened, the action fires
* in the admin footer, and what is enqueued on it is loaded together with the palette.
*
* @since 7.3.0
*/
do_action( 'enqueue_command_palette_assets' );
}
}

/**
* Prints the Command Palette's scripts and styles into an inert template.
*
* The template holds what the palette needs and the page has not printed yet, including the
* scripts and styles enqueued on the {@see 'enqueue_command_palette_assets'} action. The script
* enqueued by wp_enqueue_command_palette_loader() adds it to the page when the palette is
* first opened. Nothing is printed when the palette is already on the page.
*
* @since 7.3.0
*/
function wp_print_command_palette_assets_template() {
if ( ! wp_script_is( 'wp-command-palette-loader', 'done' ) || wp_script_is( 'wp-core-commands', 'done' ) ) {
return;
}

$scripts = wp_scripts();
$styles = wp_styles();
$scripts_queued_by_page = $scripts->queue;
$styles_queued_by_page = $styles->queue;

/** This action is documented in wp-includes/script-loader.php */
do_action( 'enqueue_command_palette_assets' );

$palette_scripts = array_merge( array( 'wp-core-commands' ), array_diff( $scripts->queue, $scripts_queued_by_page ) );
$palette_styles = array_merge( array( 'wp-commands' ), array_diff( $styles->queue, $styles_queued_by_page ) );

ob_start();
$styles->do_items( $palette_styles );
$scripts->do_items( $palette_scripts );
$assets = ob_get_clean();

printf( "<template id=\"wp-command-palette-assets\">\n%s</template>\n", $assets );
}

/**
Expand Down
Loading
Loading