Skip to content

Commit

Permalink
Experiment: Add full page client-side navigation experiment setting (#…
Browse files Browse the repository at this point in the history
…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 <mmczaplinski@gmail.com>

* 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 <santosguillamot@git.wordpress.org>
Co-authored-by: cbravobernal <cbravobernal@git.wordpress.org>
Co-authored-by: gziolo <gziolo@git.wordpress.org>
Co-authored-by: DAreRodz <darerodz@git.wordpress.org>
Co-authored-by: luisherranz <luisherranz@git.wordpress.org>
Co-authored-by: michalczaplinski <czapla@git.wordpress.org>
Co-authored-by: felixarntz <flixos90@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>
  • Loading branch information
9 people authored Apr 22, 2024
1 parent 1791f14 commit e16ea9f
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 26 deletions.
3 changes: 3 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ function gutenberg_enable_experiments() {
if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) {
wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' );
}
if ( gutenberg_is_experiment_enabled( 'gutenberg-full-page-client-side-navigation' ) ) {
wp_add_inline_script( 'wp-block-library', 'window.__experimentalFullPageClientSideNavigation = true', 'before' );
}
}

add_action( 'admin_init', 'gutenberg_enable_experiments' );
Expand Down
57 changes: 57 additions & 0 deletions lib/experimental/full-page-client-side-navigation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php
/**
* Registers full page client-side navigation option using the Interactivity API and adds the necessary directives.
*/

/**
* Enqueue the interactivity router script.
*/
function _gutenberg_enqueue_interactivity_router() {
// Set the navigation mode to full page client-side navigation.
wp_interactivity_config( 'core/router', array( 'navigationMode' => 'fullPage' ) );
wp_enqueue_script_module( '@wordpress/interactivity-router' );
}

add_action( 'wp_enqueue_scripts', '_gutenberg_enqueue_interactivity_router' );

/**
* Set enhancedPagination attribute for query loop when the experiment is enabled.
*
* @param array $parsed_block The parsed block.
*
* @return array The same parsed block with the modified attribute.
*/
function _gutenberg_add_enhanced_pagination_to_query_block( $parsed_block ) {
if ( 'core/query' !== $parsed_block['blockName'] ) {
return $parsed_block;
}

$parsed_block['attrs']['enhancedPagination'] = true;
return $parsed_block;
}

add_filter( 'render_block_data', '_gutenberg_add_enhanced_pagination_to_query_block' );

/**
* Add directives to all links.
*
* Note: This should probably be done per site, not by default when this option is enabled.
*
* @param array $content The block content.
*
* @return array The same block content with the directives needed.
*/
function _gutenberg_add_client_side_navigation_directives( $content ) {
$p = new WP_HTML_Tag_Processor( $content );
// Hack to add the necessary directives to the body tag.
// TODO: Find a proper way to add directives to the body tag.
static $body_interactive_added;
if ( ! $body_interactive_added ) {
$body_interactive_added = true;
return (string) $p . '<body data-wp-interactive="core/experimental" data-wp-context="{}">';
}
return (string) $p;
}

// TODO: Explore moving this to the server directive processing.
add_filter( 'render_block', '_gutenberg_add_client_side_navigation_directives' );
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ function gutenberg_initialize_experiments_settings() {
)
);

add_settings_field(
'gutenberg-full-page-client-side-navigation',
__( 'Enable full page client-side navigation', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
array(
'label' => __( 'Enable full page client-side navigation using the Interactivity API', 'gutenberg' ),
'id' => 'gutenberg-full-page-client-side-navigation',
)
);

register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
Expand Down
3 changes: 3 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/demo.php';
require __DIR__ . '/experiments-page.php';
require __DIR__ . '/interactivity-api.php';
if ( gutenberg_is_experiment_enabled( 'gutenberg-full-page-client-side-navigation' ) ) {
require __DIR__ . '/experimental/full-page-client-side-navigation.php';
}

// Copied package PHP files.
if ( is_dir( __DIR__ . '/../build/style-engine' ) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ export default function EnhancedPaginationModal( {
useUnsupportedBlocks( clientId );

useEffect( () => {
if ( enhancedPagination && hasUnsupportedBlocks ) {
if (
enhancedPagination &&
hasUnsupportedBlocks &&
! window.__experimentalFullPageClientSideNavigation
) {
setAttributes( { enhancedPagination: false } );
setOpen( true );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ export default function EnhancedPaginationControl( {
clientId,
} ) {
const { hasUnsupportedBlocks } = useUnsupportedBlocks( clientId );
const fullPageClientSideNavigation =
window.__experimentalFullPageClientSideNavigation;

let help = __( 'Browsing between pages requires a full page reload.' );
if ( enhancedPagination ) {
if ( fullPageClientSideNavigation ) {
help = __(
'Experimental full-page client-side navigation setting enabled.'
);
} else if ( enhancedPagination ) {
help = __(
"Browsing between pages won't require a full page reload, unless non-compatible blocks are detected."
);
Expand All @@ -32,8 +38,12 @@ export default function EnhancedPaginationControl( {
<ToggleControl
label={ __( 'Force page reload' ) }
help={ help }
checked={ ! enhancedPagination }
disabled={ hasUnsupportedBlocks }
checked={
! enhancedPagination && ! fullPageClientSideNavigation
}
disabled={
hasUnsupportedBlocks || fullPageClientSideNavigation
}
onChange={ ( value ) => {
setAttributes( {
enhancedPagination: ! value,
Expand Down
2 changes: 1 addition & 1 deletion packages/block-library/src/query/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ function render_block_core_query( $attributes, $content, $block ) {
// Add the necessary directives.
$p->set_attribute( 'data-wp-interactive', 'core/query' );
$p->set_attribute( 'data-wp-router-region', 'query-' . $attributes['queryId'] );
$p->set_attribute( 'data-wp-init', 'callbacks.setQueryRef' );
$p->set_attribute( 'data-wp-context', '{}' );
$p->set_attribute( 'data-wp-key', $attributes['queryId'] );
$content = $p->get_updated_html();
}
}
Expand Down
96 changes: 96 additions & 0 deletions packages/interactivity-router/src/head.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Helper to update only the necessary tags in the head.
*
* @async
* @param {Array} newHead The head elements of the new page.
*
*/
export const updateHead = async ( newHead ) => {
// Helper to get the tag id store in the cache.
const getTagId = ( tag ) => tag.id || tag.outerHTML;

// Map incoming head tags by their content.
const newHeadMap = new Map();
for ( const child of newHead ) {
newHeadMap.set( getTagId( child ), child );
}

const toRemove = [];

// Detect nodes that should be added or removed.
for ( const child of document.head.children ) {
const id = getTagId( child );
// Always remove styles and links as they might change.
if ( child.nodeName === 'LINK' || child.nodeName === 'STYLE' )
toRemove.push( child );
else if ( newHeadMap.has( id ) ) newHeadMap.delete( id );
else if ( child.nodeName !== 'SCRIPT' && child.nodeName !== 'META' )
toRemove.push( child );
}

// Prepare new assets.
const toAppend = [ ...newHeadMap.values() ];

// Apply the changes.
toRemove.forEach( ( n ) => n.remove() );
document.head.append( ...toAppend );
};

/**
* Fetches and processes head assets (stylesheets and scripts) from a specified document.
*
* @async
* @param {Document} doc The document from which to fetch head assets. It should support standard DOM querying methods.
* @param {Map} headElements A map of head elements to modify tracking the URLs of already processed assets to avoid duplicates.
*
* @return {Promise<HTMLElement[]>} Returns an array of HTML elements representing the head assets.
*/
export const fetchHeadAssets = async ( doc, headElements ) => {
const headTags = [];
const assets = [
{
tagName: 'style',
selector: 'link[rel=stylesheet]',
attribute: 'href',
},
{ tagName: 'script', selector: 'script[src]', attribute: 'src' },
];
for ( const asset of assets ) {
const { tagName, selector, attribute } = asset;
const tags = doc.querySelectorAll( selector );

// Use Promise.all to wait for fetch to complete
await Promise.all(
Array.from( tags ).map( async ( tag ) => {
const attributeValue = tag.getAttribute( attribute );
if ( ! headElements.has( attributeValue ) ) {
try {
const response = await fetch( attributeValue );
const text = await response.text();
headElements.set( attributeValue, {
tag,
text,
} );
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( e );
}
}

const headElement = headElements.get( attributeValue );
const element = doc.createElement( tagName );
element.innerText = headElement.text;
for ( const attr of headElement.tag.attributes ) {
element.setAttribute( attr.name, attr.value );
}
headTags.push( element );
} )
);
}

return [
doc.querySelector( 'title' ),
...doc.querySelectorAll( 'style' ),
...headTags,
];
};
Loading

1 comment on commit e16ea9f

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in e16ea9f.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/8783373087
📝 Reported issues:

Please sign in to comment.