Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment: Client-side navigation of the Query Loop block using directives #44034

Closed
wants to merge 14 commits into from
Closed
1 change: 1 addition & 0 deletions packages/block-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"lodash": "^4.17.21",
"memize": "^1.1.0",
"micromodal": "^0.4.10",
"preact": "^10.10.6",
"remove-accents": "^0.4.2"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/block-library/src/post-featured-image/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
if ( $is_link ) {
$link_target = $attributes['linkTarget'];
$rel = ! empty( $attributes['rel'] ) ? 'rel="' . esc_attr( $attributes['rel'] ) . '"' : '';
$featured_image = sprintf( '<a href="%1$s" target="%2$s" %3$s>%4$s</a>', get_the_permalink( $post_ID ), esc_attr( $link_target ), $rel, $featured_image );
$featured_image = sprintf( '<a wp-client-navigation=\'{"prefetch":true}\' href="%1$s" target="%2$s" %3$s>%4$s</a>', get_the_permalink( $post_ID ), esc_attr( $link_target ), $rel, $featured_image );
}

$has_width = ! empty( $attributes['width'] );
Expand Down
2 changes: 1 addition & 1 deletion packages/block-library/src/post-template/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function render_block_core_post_template( $attributes, $content, $block ) {
wp_reset_postdata();

return sprintf(
'<ul %1$s>%2$s</ul>',
'<ul %1$s>%2$s</ul><div class="animation"></div>',
$wrapper_attributes,
$content
);
Expand Down
24 changes: 23 additions & 1 deletion packages/block-library/src/post-template/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,31 @@
@include break-small {
@for $i from 2 through 6 {
&.is-flex-container.columns-#{ $i } > li {
width: calc((100% / #{ $i }) - 1.25em + (1.25em / #{ $i }));
width: calc((100% / #{$i}) - 1.25em + (1.25em / #{$i}));
}
}
}
}
}

div.animation {
width: 20px;
height: 20px;
background: #f00;
position: relative;
animation: animate 3s infinite;
animation-direction: alternate;
}

@keyframes animate {
0% {
background: #f00;
left: 0;
top: 0;
}
100% {
background: #ff0;
left: 300px;
top: 0;
}
}
2 changes: 1 addition & 1 deletion packages/block-library/src/post-title/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function render_block_core_post_title( $attributes, $content, $block ) {

if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) {
$rel = ! empty( $attributes['rel'] ) ? 'rel="' . esc_attr( $attributes['rel'] ) . '"' : '';
$title = sprintf( '<a href="%1$s" target="%2$s" %3$s>%4$s</a>', get_the_permalink( $post_ID ), esc_attr( $attributes['linkTarget'] ), $rel, $title );
$title = sprintf( '<a wp-client-navigation=\'{"prefetch":true}\' href="%1$s" target="%2$s" %3$s>%4$s</a>', get_the_permalink( $post_ID ), esc_attr( $attributes['linkTarget'] ), $rel, $title );
}
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $align_class_name ) );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function render_block_core_query_pagination_next( $attributes, $content, $block
$custom_query_max_pages = (int) $custom_query->max_num_pages;
if ( $custom_query_max_pages && $custom_query_max_pages !== $page ) {
$content = sprintf(
'<a href="%1$s" %2$s>%3$s</a>',
'<a key="next" wp-client-navigation=\'{"prefetch": "eager", "scroll": false}\' href="%1$s" %2$s>%3$s</a>',
esc_url( add_query_arg( $page_key, $page + 1 ) ),
$wrapper_attributes,
$label
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function render_block_core_query_pagination_numbers( $attributes, $content, $blo
return '';
}
return sprintf(
'<div %1$s>%2$s</div>',
'<div key="numbers" %1$s>%2$s</div>',
$wrapper_attributes,
$content
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl
remove_filter( 'previous_posts_link_attributes', $filter_link_attributes );
} elseif ( 1 !== $page ) {
$content = sprintf(
'<a href="%1$s" %2$s>%3$s</a>',
'<a key="previous" wp-client-navigation=\'{"prefetch": "eager", "scroll": false}\' href="%1$s" %2$s>%3$s</a>',
esc_url( add_query_arg( $page_key, $page - 1 ) ),
$wrapper_attributes,
$label
Expand Down
3 changes: 2 additions & 1 deletion packages/block-library/src/query-pagination/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
}
},
"editorStyle": "wp-block-query-pagination-editor",
"style": "wp-block-query-pagination"
"style": "wp-block-query-pagination",
"viewScript": "file:./view.min.js"
}
39 changes: 39 additions & 0 deletions packages/block-library/src/query-pagination/directives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { h, options } from 'preact';

// WordPress Directives.
const directives = {};

// Expose function to add directives.
export const directive = ( name, cb ) => {
directives[ name ] = cb;
};

const WpDirective = ( props ) => {
for ( const d in props.wp ) {
directives[ d ]?.( props );
}
props._wrapped = true;
const { wp, tag, children, ...rest } = props;
return h( tag, rest, children );
};

const old = options.vnode;

options.vnode = ( vnode ) => {
const wp = vnode.props.wp;
const wrapped = vnode.props._wrapped;

if ( wp ) {
if ( ! wrapped ) {
vnode.props.tag = vnode.type;
vnode.type = WpDirective;
}
} else if ( wrapped ) {
delete vnode.props._wrapped;
}

if ( old ) old( vnode );
};
2 changes: 2 additions & 0 deletions packages/block-library/src/query-pagination/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function render_block_core_query_pagination( $attributes, $content ) {
return '';
}

wp_enqueue_script( 'wp-block-query-pagination-view' );

$wrapper_attributes = get_block_wrapper_attributes(
array(
'aria-label' => __( 'Pagination' ),
Expand Down
104 changes: 104 additions & 0 deletions packages/block-library/src/query-pagination/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* External dependencies
*/
import { hydrate, render } from 'preact';

/**
* Internal dependencies
*/
import { toVdom } from './vdom';

// The root to render the vdom (document.body).
let rootFragment;

// The cache of visited and prefetched pages.
const pages = new Map();

// For wrapperless hydration of document.body.
// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
const createRootFragment = ( parent, replaceNode ) => {
replaceNode = [].concat( replaceNode );
const s = replaceNode[ replaceNode.length - 1 ].nextSibling;
function insert( c, r ) {
parent.insertBefore( c, r || s );
}
return ( parent.__k = {
nodeType: 1,
parentNode: parent,
firstChild: replaceNode[ 0 ],
childNodes: replaceNode,
insertBefore: insert,
appendChild: insert,
removeChild( c ) {
parent.removeChild( c );
},
} );
};

// Helper function to await until the CPU is idle.
const idle = () =>
new Promise( ( resolve ) => window.requestIdleCallback( resolve ) );

// Helper to remove domain and hash from the URL. We are only interesting in
// caching the path and the query.
const cleanUrl = ( url ) => {
const u = new URL( url, 'http://a.bc' );
return u.pathname + u.search;
};

// Fetch a new page and convert it to a static virtual DOM.
const fetchPage = async ( url ) => {
const html = await window.fetch( url ).then( ( res ) => res.text() );
await idle(); // Wait until CPU is idle to do the parsing and vdom.
const dom = new window.DOMParser().parseFromString( html, 'text/html' );
return toVdom( dom.body );
};

// Prefetch a page. We store the promise to avoid triggering a second fetch for
// a page if a fetching has already started.
export const prefetch = ( url ) => {
url = cleanUrl( url );
if ( ! pages.has( url ) ) {
pages.set( url, fetchPage( url ) );
}
};

// Navigate to a new page.
export const navigate = async ( href ) => {
const url = cleanUrl( href );
prefetch( url );
const vdom = await pages.get( url );
render( vdom, rootFragment );
window.history.pushState( { wp: { clientNavigation: true } }, '', href );
};

// Listen to the back and forward buttons and restore the page if it's in the
// cache.
window.addEventListener( 'popstate', async () => {
const url = cleanUrl( window.location ); // Remove hash.
if ( pages.has( url ) ) {
const vdom = await pages.get( url );
render( vdom, rootFragment );
} else {
window.location.reload();
}
} );

// Initialize the router with the initial DOM.
document.addEventListener( 'DOMContentLoaded', async () => {
const url = cleanUrl( window.location ); // Remove hash.

// Create the root fragment to hydrate everything.
rootFragment = createRootFragment(
document.documentElement,
document.body
);

await idle(); // Wait until the CPU is idle to do the hydration.
const vdom = toVdom( document.body );
pages.set( url, Promise.resolve( vdom ) );
hydrate( vdom, rootFragment );

// eslint-disable-next-line no-console
console.log( 'hydrated!' );
} );
43 changes: 43 additions & 0 deletions packages/block-library/src/query-pagination/vdom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* External dependencies
*/
import { h } from 'preact';

// Convert DOM nodes to static virtual DOM nodes.
export const toVdom = ( node ) => {
if ( node.nodeType === 3 ) return node.data;
if ( node.nodeType === 8 ) return null;
if ( node.localName === 'script' ) return h( 'script' );

const props = {},
a = node.attributes;

for ( let i = 0; i < a.length; i++ ) {
if ( a[ i ].name.startsWith( 'wp-' ) ) {
props.wp = props.wp || {};
let value = a[ i ].value;
try {
value = JSON.parse( value );
} catch ( e ) {}
props.wp[ renameDirective( a[ i ].name ) ] = value;
} else {
props[ a[ i ].name ] = a[ i ].value;
}
}

return h(
node.localName,
props,
[].map.call( node.childNodes, toVdom ).filter( exists )
);
};

// Rename WordPress Directives from `wp-some-directive` to `someDirective`.
const renameDirective = ( s ) =>
s
.toLowerCase()
.replace( /^wp-/, '' )
.replace( /-(.)/g, ( _, chr ) => chr.toUpperCase() );

// Filter the truthy.
const exists = ( i ) => i;
42 changes: 42 additions & 0 deletions packages/block-library/src/query-pagination/view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* External dependencies
*/
import { useEffect } from 'preact/hooks';

/**
* Internal dependencies
*/
import { prefetch, navigate } from './router';
import { directive } from './directives';

// The `wp-client-navigation` directive.
directive( 'clientNavigation', ( props ) => {
const {
wp: { clientNavigation },
href,
} = props;

useEffect( () => {
// Prefetch the page if it is in the directive options.
if ( clientNavigation?.prefetch ) {
prefetch( href );
}
} );

// Don't do anything if it's falsy.
if ( clientNavigation !== false ) {
props.onclick = async ( event ) => {
event.preventDefault();

// Fetch the page (or return it from cache).
await navigate( href );

// Update the scroll, depending on the option. True by default.
if ( clientNavigation?.scroll === 'smooth' ) {
window.scrollTo( { top: 0, left: 0, behavior: 'smooth' } );
} else if ( clientNavigation?.scroll !== false ) {
window.scrollTo( 0, 0 );
}
};
}
} );