Skip to content

Commit e16ea9f

Browse files
SantosGuillamotcbravobernalgzioloDAreRodzluisherranz
authored
Experiment: Add full page client-side navigation experiment setting (#59707)
* Add Gutenberg experiment option * Add config option and directives in PHP * Load full CSN logic conditionally * Add `data-wp-interactive` root * Change variables names * Register different scripts if the experiment is enabled * Require experimental code once interactivity is loaded * Change experiment namespace * Move full-csn logic to interactivity-router * Add proper support for prefetch * Adapt query loop * Fix modules error after csn * Add initial page to cache * WIP: Fix scripts loading after csn * Simplify code * Adapt query loop block * Fix full CSN when queryID is not defined * Remove preload logic * Change full csn conditional in query * Use only one app in the body * Use getRegionRootFragment and initialVdom * Adapt all query loop blocks * Add key to query loop block * Add `yield` to query block actions * Revert conditional scripts depending on the experiment * Register `interactivity-router-full-client-side-navigation` in the experiment * Load router conditionally in query loop * Scroll to anchor * Remove unnecessary empty conditional * Fix back and forward buttons * Fix query loop * Remove unnecessary conditional * Use full page client-side navigation naming * Change comments * Use render_block_data to change query attribute * Refactor JavaScript logic * Remove unused variable * Revert changes in query block view.js file * Remove unnecessary export from interactivity * Move logic to the existing router * Use vdom.get document.body * Remove nextTick function * Only call getElement when it is an event * Allow instanceof URL in navigate * Fix full page client-side navigation * Use `wp_enqueue_scripts` hook * Clean PHP code and docs * Move internal dependencies after WordPress ones * Add initial JSDocs to head helper functions * Allow URL instance in prefetch function * Properly support prefetch * Fix JSDoc comments * Add Promise to JSDoc Co-authored-by: Michal <[email protected]> * Specify experimental in query help message * Wrap fullPage code in IS_GUTENBERG_PLUGIN check * Use static variable to add body directive once * Wrap fetch in try/catch * Rename document variable to doc * Prevent client navigation in admin links * Add event listeners for navigate and prefetch in JS * Add check for anchor links of the same page --------- Co-authored-by: SantosGuillamot <[email protected]> Co-authored-by: cbravobernal <[email protected]> Co-authored-by: gziolo <[email protected]> Co-authored-by: DAreRodz <[email protected]> Co-authored-by: luisherranz <[email protected]> Co-authored-by: michalczaplinski <[email protected]> Co-authored-by: felixarntz <[email protected]> Co-authored-by: westonruter <[email protected]>
1 parent 1791f14 commit e16ea9f

File tree

9 files changed

+313
-26
lines changed

9 files changed

+313
-26
lines changed

lib/experimental/editor-settings.php

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ function gutenberg_enable_experiments() {
2828
if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) {
2929
wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' );
3030
}
31+
if ( gutenberg_is_experiment_enabled( 'gutenberg-full-page-client-side-navigation' ) ) {
32+
wp_add_inline_script( 'wp-block-library', 'window.__experimentalFullPageClientSideNavigation = true', 'before' );
33+
}
3134
}
3235

3336
add_action( 'admin_init', 'gutenberg_enable_experiments' );
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
/**
3+
* Registers full page client-side navigation option using the Interactivity API and adds the necessary directives.
4+
*/
5+
6+
/**
7+
* Enqueue the interactivity router script.
8+
*/
9+
function _gutenberg_enqueue_interactivity_router() {
10+
// Set the navigation mode to full page client-side navigation.
11+
wp_interactivity_config( 'core/router', array( 'navigationMode' => 'fullPage' ) );
12+
wp_enqueue_script_module( '@wordpress/interactivity-router' );
13+
}
14+
15+
add_action( 'wp_enqueue_scripts', '_gutenberg_enqueue_interactivity_router' );
16+
17+
/**
18+
* Set enhancedPagination attribute for query loop when the experiment is enabled.
19+
*
20+
* @param array $parsed_block The parsed block.
21+
*
22+
* @return array The same parsed block with the modified attribute.
23+
*/
24+
function _gutenberg_add_enhanced_pagination_to_query_block( $parsed_block ) {
25+
if ( 'core/query' !== $parsed_block['blockName'] ) {
26+
return $parsed_block;
27+
}
28+
29+
$parsed_block['attrs']['enhancedPagination'] = true;
30+
return $parsed_block;
31+
}
32+
33+
add_filter( 'render_block_data', '_gutenberg_add_enhanced_pagination_to_query_block' );
34+
35+
/**
36+
* Add directives to all links.
37+
*
38+
* Note: This should probably be done per site, not by default when this option is enabled.
39+
*
40+
* @param array $content The block content.
41+
*
42+
* @return array The same block content with the directives needed.
43+
*/
44+
function _gutenberg_add_client_side_navigation_directives( $content ) {
45+
$p = new WP_HTML_Tag_Processor( $content );
46+
// Hack to add the necessary directives to the body tag.
47+
// TODO: Find a proper way to add directives to the body tag.
48+
static $body_interactive_added;
49+
if ( ! $body_interactive_added ) {
50+
$body_interactive_added = true;
51+
return (string) $p . '<body data-wp-interactive="core/experimental" data-wp-context="{}">';
52+
}
53+
return (string) $p;
54+
}
55+
56+
// TODO: Explore moving this to the server directive processing.
57+
add_filter( 'render_block', '_gutenberg_add_client_side_navigation_directives' );

lib/experiments-page.php

+12
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ function gutenberg_initialize_experiments_settings() {
127127
)
128128
);
129129

130+
add_settings_field(
131+
'gutenberg-full-page-client-side-navigation',
132+
__( 'Enable full page client-side navigation', 'gutenberg' ),
133+
'gutenberg_display_experiment_field',
134+
'gutenberg-experiments',
135+
'gutenberg_experiments_section',
136+
array(
137+
'label' => __( 'Enable full page client-side navigation using the Interactivity API', 'gutenberg' ),
138+
'id' => 'gutenberg-full-page-client-side-navigation',
139+
)
140+
);
141+
130142
register_setting(
131143
'gutenberg-experiments',
132144
'gutenberg-experiments'

lib/load.php

+3
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ function gutenberg_is_experiment_enabled( $name ) {
199199
require __DIR__ . '/demo.php';
200200
require __DIR__ . '/experiments-page.php';
201201
require __DIR__ . '/interactivity-api.php';
202+
if ( gutenberg_is_experiment_enabled( 'gutenberg-full-page-client-side-navigation' ) ) {
203+
require __DIR__ . '/experimental/full-page-client-side-navigation.php';
204+
}
202205

203206
// Copied package PHP files.
204207
if ( is_dir( __DIR__ . '/../build/style-engine' ) ) {

packages/block-library/src/query/edit/enhanced-pagination-modal.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export default function EnhancedPaginationModal( {
2727
useUnsupportedBlocks( clientId );
2828

2929
useEffect( () => {
30-
if ( enhancedPagination && hasUnsupportedBlocks ) {
30+
if (
31+
enhancedPagination &&
32+
hasUnsupportedBlocks &&
33+
! window.__experimentalFullPageClientSideNavigation
34+
) {
3135
setAttributes( { enhancedPagination: false } );
3236
setOpen( true );
3337
}

packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@ export default function EnhancedPaginationControl( {
1515
clientId,
1616
} ) {
1717
const { hasUnsupportedBlocks } = useUnsupportedBlocks( clientId );
18+
const fullPageClientSideNavigation =
19+
window.__experimentalFullPageClientSideNavigation;
1820

1921
let help = __( 'Browsing between pages requires a full page reload.' );
20-
if ( enhancedPagination ) {
22+
if ( fullPageClientSideNavigation ) {
23+
help = __(
24+
'Experimental full-page client-side navigation setting enabled.'
25+
);
26+
} else if ( enhancedPagination ) {
2127
help = __(
2228
"Browsing between pages won't require a full page reload, unless non-compatible blocks are detected."
2329
);
@@ -32,8 +38,12 @@ export default function EnhancedPaginationControl( {
3238
<ToggleControl
3339
label={ __( 'Force page reload' ) }
3440
help={ help }
35-
checked={ ! enhancedPagination }
36-
disabled={ hasUnsupportedBlocks }
41+
checked={
42+
! enhancedPagination && ! fullPageClientSideNavigation
43+
}
44+
disabled={
45+
hasUnsupportedBlocks || fullPageClientSideNavigation
46+
}
3747
onChange={ ( value ) => {
3848
setAttributes( {
3949
enhancedPagination: ! value,

packages/block-library/src/query/index.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ function render_block_core_query( $attributes, $content, $block ) {
5151
// Add the necessary directives.
5252
$p->set_attribute( 'data-wp-interactive', 'core/query' );
5353
$p->set_attribute( 'data-wp-router-region', 'query-' . $attributes['queryId'] );
54-
$p->set_attribute( 'data-wp-init', 'callbacks.setQueryRef' );
5554
$p->set_attribute( 'data-wp-context', '{}' );
55+
$p->set_attribute( 'data-wp-key', $attributes['queryId'] );
5656
$content = $p->get_updated_html();
5757
}
5858
}
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Helper to update only the necessary tags in the head.
3+
*
4+
* @async
5+
* @param {Array} newHead The head elements of the new page.
6+
*
7+
*/
8+
export const updateHead = async ( newHead ) => {
9+
// Helper to get the tag id store in the cache.
10+
const getTagId = ( tag ) => tag.id || tag.outerHTML;
11+
12+
// Map incoming head tags by their content.
13+
const newHeadMap = new Map();
14+
for ( const child of newHead ) {
15+
newHeadMap.set( getTagId( child ), child );
16+
}
17+
18+
const toRemove = [];
19+
20+
// Detect nodes that should be added or removed.
21+
for ( const child of document.head.children ) {
22+
const id = getTagId( child );
23+
// Always remove styles and links as they might change.
24+
if ( child.nodeName === 'LINK' || child.nodeName === 'STYLE' )
25+
toRemove.push( child );
26+
else if ( newHeadMap.has( id ) ) newHeadMap.delete( id );
27+
else if ( child.nodeName !== 'SCRIPT' && child.nodeName !== 'META' )
28+
toRemove.push( child );
29+
}
30+
31+
// Prepare new assets.
32+
const toAppend = [ ...newHeadMap.values() ];
33+
34+
// Apply the changes.
35+
toRemove.forEach( ( n ) => n.remove() );
36+
document.head.append( ...toAppend );
37+
};
38+
39+
/**
40+
* Fetches and processes head assets (stylesheets and scripts) from a specified document.
41+
*
42+
* @async
43+
* @param {Document} doc The document from which to fetch head assets. It should support standard DOM querying methods.
44+
* @param {Map} headElements A map of head elements to modify tracking the URLs of already processed assets to avoid duplicates.
45+
*
46+
* @return {Promise<HTMLElement[]>} Returns an array of HTML elements representing the head assets.
47+
*/
48+
export const fetchHeadAssets = async ( doc, headElements ) => {
49+
const headTags = [];
50+
const assets = [
51+
{
52+
tagName: 'style',
53+
selector: 'link[rel=stylesheet]',
54+
attribute: 'href',
55+
},
56+
{ tagName: 'script', selector: 'script[src]', attribute: 'src' },
57+
];
58+
for ( const asset of assets ) {
59+
const { tagName, selector, attribute } = asset;
60+
const tags = doc.querySelectorAll( selector );
61+
62+
// Use Promise.all to wait for fetch to complete
63+
await Promise.all(
64+
Array.from( tags ).map( async ( tag ) => {
65+
const attributeValue = tag.getAttribute( attribute );
66+
if ( ! headElements.has( attributeValue ) ) {
67+
try {
68+
const response = await fetch( attributeValue );
69+
const text = await response.text();
70+
headElements.set( attributeValue, {
71+
tag,
72+
text,
73+
} );
74+
} catch ( e ) {
75+
// eslint-disable-next-line no-console
76+
console.error( e );
77+
}
78+
}
79+
80+
const headElement = headElements.get( attributeValue );
81+
const element = doc.createElement( tagName );
82+
element.innerText = headElement.text;
83+
for ( const attr of headElement.tag.attributes ) {
84+
element.setAttribute( attr.name, attr.value );
85+
}
86+
headTags.push( element );
87+
} )
88+
);
89+
}
90+
91+
return [
92+
doc.querySelector( 'title' ),
93+
...doc.querySelectorAll( 'style' ),
94+
...headTags,
95+
];
96+
};

0 commit comments

Comments
 (0)