From 16ea028b0378f30d1fe9113503071ae44cc97590 Mon Sep 17 00:00:00 2001 From: Luigi <gigitux@gmail.com> Date: Mon, 12 Sep 2022 12:02:04 +0200 Subject: [PATCH 01/11] Product Query: Fix pagination issue --- src/BlockTypes/ProductQuery.php | 43 +++++++++------------------------ 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 2b39c231988..8fc72d91657 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -37,27 +37,6 @@ protected function initialize() { } - /** - * Remove the query block filter and parse the custom query - * - * This function is supposed to be called by the `query_loop_block_query_vars` - * filter. It de-registers the filter to make sure it runs only once and doesn't end - * up hi-jacking future Query Loop blocks. - * - * It needs unfortunately to be `public` or otherwise the filter can't call it. - * - * @param WP_Query $query The WordPress Query. - * @return array - */ - public function get_query_by_attributes_once( $query ) { - remove_filter( - 'query_loop_block_query_vars', - array( $this, 'get_query_by_attributes_once' ), - 10 - ); - - return $this->get_query_by_attributes( $query, $this->parsed_block ); - } /** * Update the query for the product query block. @@ -66,28 +45,30 @@ public function get_query_by_attributes_once( $query ) { * @param array $parsed_block The block being rendered. */ public function update_query( $pre_render, $parsed_block ) { - if ( 'core/query' !== $parsed_block['blockName'] || ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { + if ( 'core/query' !== $parsed_block['blockName'] ) { return; } $this->parsed_block = $parsed_block; - add_filter( - 'query_loop_block_query_vars', - array( $this, 'get_query_by_attributes_once' ), - 10, - 1 - ); + if ( isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { + add_filter( + 'query_loop_block_query_vars', + array( $this, 'get_query_by_attributes' ), + 10, + 1 + ); + } } /** * Return a custom query based on the attributes. * - * @param WP_Query $query The WordPress Query. - * @param WP_Block $parsed_block The block being rendered. + * @param WP_Query $query The WordPress Query. * @return array */ - public function get_query_by_attributes( $query, $parsed_block ) { + public function get_query_by_attributes( $query ) { + $parsed_block = $this->parsed_block; if ( ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { return $query; } From 6e93dd02c77f91dfe99f6459cc66825b7782550f Mon Sep 17 00:00:00 2001 From: Luigi <gigitux@gmail.com> Date: Thu, 15 Sep 2022 14:10:47 +0200 Subject: [PATCH 02/11] Product Query - Add support for the Filter By Price Block #6790 Product Query - Add support for the Filter By Price Block --- src/BlockTypes/PriceFilter.php | 2 + src/BlockTypes/ProductQuery.php | 109 ++++++++++++++++++++++++++++---- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/BlockTypes/PriceFilter.php b/src/BlockTypes/PriceFilter.php index 0c9ab501c8e..6274d326375 100644 --- a/src/BlockTypes/PriceFilter.php +++ b/src/BlockTypes/PriceFilter.php @@ -12,4 +12,6 @@ class PriceFilter extends AbstractBlock { * @var string */ protected $block_name = 'price-filter'; + const MIN_PRICE_QUERY_VAR = 'min_price'; + const MAX_PRICE_QUERY_VAR = 'max_price'; } diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 8fc72d91657..b03b4ec347a 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -27,6 +27,10 @@ class ProductQuery extends AbstractBlock { * - Hook into pre_render_block to update the query. */ protected function initialize() { + add_filter( 'query_vars', array( $this, 'set_query_vars' ) ); + // Set this so that our product filters can detect if it's a PHP template. + $this->asset_data_registry->add( 'has_filterable_products', true, true ); + $this->asset_data_registry->add( 'is_rendering_php_template', true, true ); parent::initialize(); add_filter( 'pre_render_block', @@ -37,7 +41,6 @@ protected function initialize() { } - /** * Update the query for the product query block. * @@ -54,7 +57,7 @@ public function update_query( $pre_render, $parsed_block ) { if ( isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { add_filter( 'query_loop_block_query_vars', - array( $this, 'get_query_by_attributes' ), + array( $this, 'build_query' ), 10, 1 ); @@ -62,12 +65,12 @@ public function update_query( $pre_render, $parsed_block ) { } /** - * Return a custom query based on the attributes. + * Return a custom query based on attributes, filters and global WP_Query. * * @param WP_Query $query The WordPress Query. * @return array */ - public function get_query_by_attributes( $query ) { + public function build_query( $query ) { $parsed_block = $this->parsed_block; if ( ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { return $query; @@ -79,24 +82,39 @@ public function get_query_by_attributes( $query ) { 'post_status' => 'publish', 'posts_per_page' => $query['posts_per_page'], 'orderby' => $query['orderby'], + 'orderby' => $query['orderby'], 'order' => $query['order'], + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array(), + ); + + $queries_attributes = $this->get_queries_by_attributes( $variation_props ); + $queries_filters = $this->get_queries_by_applied_filters(); + + return array_reduce( + array_merge( + $queries_attributes, + $queries_filters + ), + function( $acc, $query ) { + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; + return $acc; + }, + $common_query_values ); - $on_sale_query = $this->get_on_sale_products_query( $variation_props ); - return array_merge( $query, $common_query_values, $on_sale_query ); } + /** * Return a query for on sale products. * - * @param array $variation_props Dedicated attributes for the variation. * @return array */ - private function get_on_sale_products_query( $variation_props ) { - if ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) { - return array(); - } - + private function get_on_sale_products_query() { return array( // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query @@ -117,4 +135,71 @@ private function get_on_sale_products_query( $variation_props ) { ), ); } + + /** + * Set the query vars that are used by filter blocks. + * + * @param array $qvars Public query vars. + * @return array + */ + public function set_query_vars( $qvars ) { + $filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ); + return array_merge( $qvars, $filter_query_args ); + } + + /** + * Return queries that are generated by query args + * + * @return array + */ + private function get_queries_by_applied_filters() { + return array( 'price_filter' => $this->get_filter_by_price_query() ); + } + + /** + * Return queries that are generated by attributes + * + * @param array $variation_props Dedicated attributes for the variation. + * @return array + */ + private function get_queries_by_attributes( $variation_props ) { + return array( + 'on_sale' => ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) ? array() : $this->get_on_sale_products_query(), + ); + } + + /** + * Return a query that filters products by price. + * + * @return array + */ + private function get_filter_by_price_query() { + $min_price = get_query_var( PriceFilter::MIN_PRICE_QUERY_VAR ); + $max_price = get_query_var( PriceFilter::MAX_PRICE_QUERY_VAR ); + + $max_price_query = empty( $max_price ) ? array() : [ + 'key' => '_price', + 'value' => $max_price, + 'compare' => '<=', + 'type' => 'numeric', + ]; + + $min_price_query = empty( $min_price ) ? array() : [ + 'key' => '_price', + 'value' => $min_price, + 'compare' => '>=', + 'type' => 'numeric', + ]; + + return array( + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + 'relation' => 'OR', + $max_price_query, + $min_price_query, + ), + ); + } + } From b243528f2dc703917063a352e1bfe4cb8c8a2545 Mon Sep 17 00:00:00 2001 From: Luigi <gigitux@gmail.com> Date: Fri, 16 Sep 2022 17:49:18 +0200 Subject: [PATCH 03/11] fix query relation --- src/BlockTypes/ProductQuery.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index b03b4ec347a..ac89c03a6da 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -82,7 +82,6 @@ public function build_query( $query ) { 'post_status' => 'publish', 'posts_per_page' => $query['posts_per_page'], 'orderby' => $query['orderby'], - 'orderby' => $query['orderby'], 'order' => $query['order'], // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query @@ -163,8 +162,9 @@ private function get_queries_by_applied_filters() { * @return array */ private function get_queries_by_attributes( $variation_props ) { + $on_sale_enabled = isset( $variation_props['attributes']['query']['onSale'] ) && true === $variation_props['attributes']['query']['onSale']; return array( - 'on_sale' => ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) ? array() : $this->get_on_sale_products_query(), + 'on_sale' => ( $on_sale_enabled ? $this->get_on_sale_products_query() : array() ), ); } @@ -195,7 +195,7 @@ private function get_filter_by_price_query() { // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( - 'relation' => 'OR', + 'relation' => 'AND', $max_price_query, $min_price_query, ), From e55a41dcf2f3bd366edb7837b1d8d8eca0c39c1b Mon Sep 17 00:00:00 2001 From: Luigi <gigitux@gmail.com> Date: Fri, 16 Sep 2022 18:52:33 +0200 Subject: [PATCH 04/11] fix on sale query --- src/BlockTypes/ProductQuery.php | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index ac89c03a6da..cdf4e15b78c 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -115,23 +115,7 @@ function( $acc, $query ) { */ private function get_on_sale_products_query() { return array( - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - 'relation' => 'OR', - array( - 'key' => '_sale_price', - 'value' => 0, - 'compare' => '>', - 'type' => 'numeric', - ), - array( - 'key' => '_min_variation_sale_price', - 'value' => 0, - 'compare' => '>', - 'type' => 'numeric', - ), - ), + 'post_in' => wc_get_product_ids_on_sale(), ); } From 6d0de4dfeed961c077f77db25333c9c9342ca41d Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Tue, 20 Sep 2022 12:18:06 +0200 Subject: [PATCH 05/11] Initial client-side navigation --- assets/js/blocks/product-query/directives.js | 39 +++++++ assets/js/blocks/product-query/frontend.js | 70 +++++++++++++ assets/js/blocks/product-query/router.js | 104 +++++++++++++++++++ assets/js/blocks/product-query/style.scss | 21 ++++ assets/js/blocks/product-query/vdom.js | 43 ++++++++ package-lock.json | 15 +++ package.json | 1 + src/BlockTypes/ProductQuery.php | 16 +++ 8 files changed, 309 insertions(+) create mode 100644 assets/js/blocks/product-query/directives.js create mode 100644 assets/js/blocks/product-query/frontend.js create mode 100644 assets/js/blocks/product-query/router.js create mode 100644 assets/js/blocks/product-query/style.scss create mode 100644 assets/js/blocks/product-query/vdom.js diff --git a/assets/js/blocks/product-query/directives.js b/assets/js/blocks/product-query/directives.js new file mode 100644 index 00000000000..72a6fcb64c9 --- /dev/null +++ b/assets/js/blocks/product-query/directives.js @@ -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.class; + 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 ); +}; diff --git a/assets/js/blocks/product-query/frontend.js b/assets/js/blocks/product-query/frontend.js new file mode 100644 index 00000000000..5716a62232c --- /dev/null +++ b/assets/js/blocks/product-query/frontend.js @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import { options } from 'preact'; +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; + + const url = href.startsWith( '/' ) ? href : window.location.pathname + href; + + useEffect( () => { + // Prefetch the page if it is in the directive options. + if ( clientNavigation?.prefetch ) { + prefetch( url ); + } + } ); + + // 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( url ); + + // 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 ); + } + }; + } +} ); + +// Manually add the `wp-client-navigation` directive to the virtual nodes. +// TODO: Move this to the HTML. +const clientNavigationClassNames = [ + 'wp-block-query-pagination-next', + 'wp-block-query-pagination-previous', + 'page-numbers', +]; +const old = options.vnode; +options.vnode = ( vnode ) => { + if ( vnode.type === 'a' ) { + clientNavigationClassNames.forEach( ( className ) => { + if ( vnode.props.class?.includes( className ) ) { + vnode.props.wp = { + clientNavigation: { + prefetch: + className === 'page-numbers' ? false : 'eager', + scroll: false, + }, + }; + } + } ); + } + if ( old ) old( vnode ); +}; diff --git a/assets/js/blocks/product-query/router.js b/assets/js/blocks/product-query/router.js new file mode 100644 index 00000000000..b70b8fb894b --- /dev/null +++ b/assets/js/blocks/product-query/router.js @@ -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 ) ); + } +}; + +window.prefetch = prefetch; + +// 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( {}, '', href ); +}; + +window.navigate = navigate; + +// Listen to the back and forward buttons and restore the page if it's in +// the cache. +window.addEventListener( 'popstate', async () => { + const previousUrl = cleanUrl( window.location ); // Remove hash. + if ( pages.has( previousUrl ) ) { + const vdom = await pages.get( previousUrl ); + render( vdom, rootFragment ); + } else { + window.location.reload(); + } +} ); + +// Initialize the router with the initial DOM. +document.addEventListener( 'DOMContentLoaded', async () => { + // Create the root fragment to hydrate everything. + rootFragment = createRootFragment( + document.documentElement, + document.body + ); + const url = cleanUrl( window.location ); // Remove hash. + + 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 ); +} ); diff --git a/assets/js/blocks/product-query/style.scss b/assets/js/blocks/product-query/style.scss new file mode 100644 index 00000000000..d8757619ada --- /dev/null +++ b/assets/js/blocks/product-query/style.scss @@ -0,0 +1,21 @@ +p.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; + } +} \ No newline at end of file diff --git a/assets/js/blocks/product-query/vdom.js b/assets/js/blocks/product-query/vdom.js new file mode 100644 index 00000000000..452bc34c33a --- /dev/null +++ b/assets/js/blocks/product-query/vdom.js @@ -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', null ); + + 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; diff --git a/package-lock.json b/package-lock.json index 97c6a4cc201..36ee42de712 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "dinero.js": "1.9.1", "downshift": "6.1.7", "html-react-parser": "0.14.3", + "preact": "^10.11.0", "react-number-format": "4.9.3", "reakit": "1.3.11", "snakecase-keys": "5.4.2", @@ -44169,6 +44170,15 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/preact": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.0.tgz", + "integrity": "sha512-Fk6+vB2kb6mSJfDgODq0YDhMfl0HNtK5+Uc9QqECO4nlyPAQwCI+BKyWO//idA7ikV7o+0Fm6LQmNuQi1wXI1w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -88343,6 +88353,11 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "preact": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.0.tgz", + "integrity": "sha512-Fk6+vB2kb6mSJfDgODq0YDhMfl0HNtK5+Uc9QqECO4nlyPAQwCI+BKyWO//idA7ikV7o+0Fm6LQmNuQi1wXI1w==" + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index c3174880ba6..59ec30dfc35 100644 --- a/package.json +++ b/package.json @@ -232,6 +232,7 @@ "dinero.js": "1.9.1", "downshift": "6.1.7", "html-react-parser": "0.14.3", + "preact": "^10.11.0", "react-number-format": "4.9.3", "reakit": "1.3.11", "snakecase-keys": "5.4.2", diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 8fc72d91657..b24edd6de99 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -35,6 +35,22 @@ protected function initialize() { 2 ); + add_filter( + 'render_block_core/query', + array( $this, 'enqueue_script' ), + 10, + 3 + ); + + } + + public function enqueue_script( $block_content, $block, $instance ) { + if ( + isset($block['attrs']['__woocommerceVariationProps']['name']) && + $block['attrs']['__woocommerceVariationProps']['name'] === 'product-query') { + $this->enqueue_scripts(); + } + return $block_content; } From 1508400f13ec7b2839ed05d75bf4af2611cd35dd Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Tue, 20 Sep 2022 13:49:13 +0200 Subject: [PATCH 06/11] Add query-id class --- src/BlockTypes/ProductQuery.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index b24edd6de99..bfb4d8f7c08 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -48,8 +48,19 @@ public function enqueue_script( $block_content, $block, $instance ) { if ( isset($block['attrs']['__woocommerceVariationProps']['name']) && $block['attrs']['__woocommerceVariationProps']['name'] === 'product-query') { + + // Enqueue the full vdom scripts. $this->enqueue_scripts(); + + // Add "query-id-${query_id}" class. + // TODO: Replace with WP_HTML_Walker when available. + $block_content = preg_replace( + '/([ "])wp-block-query([ "])/', + '$1wp-block-query query-id-' . $block['attrs']['queryId'] . '$2', + $block_content + ); } + return $block_content; } From 49ac769b0a6819696c0d65b0080cba9fb3c47823 Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Tue, 20 Sep 2022 14:14:04 +0200 Subject: [PATCH 07/11] Move full-vdom files to its own folder --- assets/js/blocks/product-query/frontend.js | 5 ++--- assets/js/blocks/product-query/{ => full-vdom}/directives.js | 0 assets/js/blocks/product-query/full-vdom/index.js | 2 ++ assets/js/blocks/product-query/{ => full-vdom}/router.js | 0 assets/js/blocks/product-query/{ => full-vdom}/vdom.js | 0 5 files changed, 4 insertions(+), 3 deletions(-) rename assets/js/blocks/product-query/{ => full-vdom}/directives.js (100%) create mode 100644 assets/js/blocks/product-query/full-vdom/index.js rename assets/js/blocks/product-query/{ => full-vdom}/router.js (100%) rename assets/js/blocks/product-query/{ => full-vdom}/vdom.js (100%) diff --git a/assets/js/blocks/product-query/frontend.js b/assets/js/blocks/product-query/frontend.js index 5716a62232c..f1e4665adc8 100644 --- a/assets/js/blocks/product-query/frontend.js +++ b/assets/js/blocks/product-query/frontend.js @@ -7,8 +7,7 @@ import { useEffect } from 'preact/hooks'; /** * Internal dependencies */ -import { prefetch, navigate } from './router'; -import { directive } from './directives'; +import { prefetch, navigate, directive } from './full-vdom'; // The `wp-client-navigation` directive. directive( 'clientNavigation', ( props ) => { @@ -45,7 +44,7 @@ directive( 'clientNavigation', ( props ) => { } ); // Manually add the `wp-client-navigation` directive to the virtual nodes. -// TODO: Move this to the HTML. +// TODO: Move this to the HTML once WP_HTML_Walker is available. const clientNavigationClassNames = [ 'wp-block-query-pagination-next', 'wp-block-query-pagination-previous', diff --git a/assets/js/blocks/product-query/directives.js b/assets/js/blocks/product-query/full-vdom/directives.js similarity index 100% rename from assets/js/blocks/product-query/directives.js rename to assets/js/blocks/product-query/full-vdom/directives.js diff --git a/assets/js/blocks/product-query/full-vdom/index.js b/assets/js/blocks/product-query/full-vdom/index.js new file mode 100644 index 00000000000..7ca826040be --- /dev/null +++ b/assets/js/blocks/product-query/full-vdom/index.js @@ -0,0 +1,2 @@ +export { navigate, prefetch } from './router'; +export { directive } from './directives'; diff --git a/assets/js/blocks/product-query/router.js b/assets/js/blocks/product-query/full-vdom/router.js similarity index 100% rename from assets/js/blocks/product-query/router.js rename to assets/js/blocks/product-query/full-vdom/router.js diff --git a/assets/js/blocks/product-query/vdom.js b/assets/js/blocks/product-query/full-vdom/vdom.js similarity index 100% rename from assets/js/blocks/product-query/vdom.js rename to assets/js/blocks/product-query/full-vdom/vdom.js From 4fc131456fa19b475dc9eb5d9b49b913e45f94da Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Tue, 20 Sep 2022 16:33:16 +0200 Subject: [PATCH 08/11] Hydrate only the Product Query blocks --- assets/js/blocks/product-query/frontend.js | 16 +-- .../product-query/full-vdom/router-helpers.js | 94 +++++++++++++++ .../blocks/product-query/full-vdom/router.js | 108 ++++++------------ src/BlockTypes/ProductQuery.php | 6 +- 4 files changed, 137 insertions(+), 87 deletions(-) create mode 100644 assets/js/blocks/product-query/full-vdom/router-helpers.js diff --git a/assets/js/blocks/product-query/frontend.js b/assets/js/blocks/product-query/frontend.js index f1e4665adc8..5bbf2a9c4e3 100644 --- a/assets/js/blocks/product-query/frontend.js +++ b/assets/js/blocks/product-query/frontend.js @@ -15,7 +15,6 @@ directive( 'clientNavigation', ( props ) => { wp: { clientNavigation }, href, } = props; - const url = href.startsWith( '/' ) ? href : window.location.pathname + href; useEffect( () => { @@ -23,22 +22,15 @@ directive( 'clientNavigation', ( props ) => { if ( clientNavigation?.prefetch ) { prefetch( url ); } - } ); + }, [ url ] ); // Don't do anything if it's falsy. if ( clientNavigation !== false ) { props.onclick = async ( event ) => { + // Stop server-side navigation. event.preventDefault(); - - // Fetch the page (or return it from cache). - await navigate( url ); - - // 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 ); - } + // Start client-side navigation. + await navigate( url, { scroll: clientNavigation?.scroll } ); }; } } ); diff --git a/assets/js/blocks/product-query/full-vdom/router-helpers.js b/assets/js/blocks/product-query/full-vdom/router-helpers.js new file mode 100644 index 00000000000..34ce563e841 --- /dev/null +++ b/assets/js/blocks/product-query/full-vdom/router-helpers.js @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import { hydrate, render } from 'preact'; + +/** + * Internal dependencies + */ +import { toVdom } from './vdom'; + +// Remove domain and hash from the URL. We are only interesting in the path and +// the query. +export const cleanUrl = ( url ) => { + const u = new URL( url, 'http://a.bc' ); + return u.pathname + u.search; +}; + +// Helper to await until the CPU is idle. +export const idle = () => + new Promise( ( resolve ) => window.requestIdleCallback( resolve ) ); + +// Get the id class from a Product Query element. +const getQueryId = ( query ) => + Array.from( query.classList.values() ).find( ( className ) => + className.startsWith( 'query-id-' ) + ); + +// Root fragments where we will render the Product Query blocks. +const rootFragments = new Map(); + +// Create root fragments for each Product Query block. +export const createRootFragments = () => { + document.querySelectorAll( '.woo-product-query' ).forEach( ( query ) => { + rootFragments.set( + getQueryId( query ), + createRootFragment( query.parentElement, query ) + ); + } ); +}; + +// Fetch a URL and return the HTML string. +const fetchUrl = async ( url ) => { + return await window.fetch( url ).then( ( res ) => res.text() ); +}; + +// Parse a DOM from an HTML string. +const parseDom = ( html ) => { + return new window.DOMParser().parseFromString( html, 'text/html' ); +}; + +// Fetch a page and return the virtual DOM of each Product Query block. +export const fetchPage = async ( url ) => { + const html = await fetchUrl( url ); + const dom = parseDom( html ); + return toVdoms( dom ); +}; + +// Build a virtual DOM for each Product Query block found in a document. +export const toVdoms = ( dom ) => { + const vdoms = new Map(); + dom.querySelectorAll( '.woo-product-query' ).forEach( ( query ) => { + vdoms.set( getQueryId( query ), toVdom( query ) ); + } ); + return vdoms; +}; + +// Render the virtual DOM of each Product Query block. +export const renderVdoms = ( vdoms, initial = false ) => { + const r = initial ? hydrate : render; + vdoms.forEach( ( vdom, id ) => { + r( vdom, rootFragments.get( id ) ); + } ); +}; + +// We use this for wrapperless hydration. +// 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 ); + }, + } ); +}; diff --git a/assets/js/blocks/product-query/full-vdom/router.js b/assets/js/blocks/product-query/full-vdom/router.js index b70b8fb894b..69d1d65bcf4 100644 --- a/assets/js/blocks/product-query/full-vdom/router.js +++ b/assets/js/blocks/product-query/full-vdom/router.js @@ -1,59 +1,17 @@ -/** - * External dependencies - */ -import { hydrate, render } from 'preact'; - /** * Internal dependencies */ -import { toVdom } from './vdom'; +import { + createRootFragments, + cleanUrl, + fetchPage, + toVdoms, + renderVdoms, +} from './router-helpers'; -// The root to render the vdom (document.body). -let rootFragment; - -// The cache of visited and prefetched pages. +// The cache of all the visited or 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 ) => { @@ -63,42 +21,48 @@ export const prefetch = ( url ) => { } }; -window.prefetch = prefetch; - // Navigate to a new page. -export const navigate = async ( href ) => { - const url = cleanUrl( href ); +export const navigate = async ( href, { scroll } = { scroll: false } ) => { + const url = cleanUrl( href ); // Remove hash. + + // Get the new page and render each Product Query block. prefetch( url ); - const vdom = await pages.get( url ); - render( vdom, rootFragment ); + const vdoms = await pages.get( url ); + renderVdoms( vdoms ); + + // Change the history. window.history.pushState( {}, '', href ); -}; -window.navigate = navigate; + // Update the scroll, depending on the option. True by default. + if ( scroll === 'smooth' ) { + window.scrollTo( { top: 0, left: 0, behavior: 'smooth' } ); + } else if ( scroll !== false ) { + window.scrollTo( 0, 0 ); + } +}; // Listen to the back and forward buttons and restore the page if it's in -// the cache. +// the cache. If not, refresh the page. window.addEventListener( 'popstate', async () => { const previousUrl = cleanUrl( window.location ); // Remove hash. if ( pages.has( previousUrl ) ) { - const vdom = await pages.get( previousUrl ); - render( vdom, rootFragment ); + const vdoms = await pages.get( previousUrl ); + renderVdoms( vdoms ); } else { window.location.reload(); } } ); -// Initialize the router with the initial DOM. +// Initialize the router. document.addEventListener( 'DOMContentLoaded', async () => { - // Create the root fragment to hydrate everything. - rootFragment = createRootFragment( - document.documentElement, - document.body - ); - const url = cleanUrl( window.location ); // Remove hash. + // Create root fragments for each Product Query block. + createRootFragments(); + + // Get the virtual DOM of each Product Query block and store them in the + // cache. + const vdoms = toVdoms( document.body ); + pages.set( cleanUrl( window.location ), Promise.resolve( vdoms ) ); - 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 ); + // Render the virtual DOM of each Product Query block. + renderVdoms( vdoms, true ); } ); diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index bfb4d8f7c08..a0a693ff19d 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -52,11 +52,11 @@ public function enqueue_script( $block_content, $block, $instance ) { // Enqueue the full vdom scripts. $this->enqueue_scripts(); - // Add "query-id-${query_id}" class. - // TODO: Replace with WP_HTML_Walker when available. + // Add the "woo-product-query" and "query-id-${query_id}" class names. + // TODO: Replace regular expression with WP_HTML_Walker when available. $block_content = preg_replace( '/([ "])wp-block-query([ "])/', - '$1wp-block-query query-id-' . $block['attrs']['queryId'] . '$2', + '$1wp-block-query woo-product-query query-id-' . $block['attrs']['queryId'] . '$2', $block_content ); } From 926f62c56f92778780eb49313307769d9c6f73d8 Mon Sep 17 00:00:00 2001 From: Luigi <gigitux@gmail.com> Date: Tue, 20 Sep 2022 15:30:28 +0200 Subject: [PATCH 09/11] Product Query - Add support for the Filter By Attributes block #6790 Product Query - Add support for the Filter By Attributes block --- src/BlockTypes/AttributeFilter.php | 4 +- src/BlockTypes/ProductQuery.php | 112 +++++++++++++++++++++++++++-- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/BlockTypes/AttributeFilter.php b/src/BlockTypes/AttributeFilter.php index ec2d0bf3441..4b9d06ee7e9 100644 --- a/src/BlockTypes/AttributeFilter.php +++ b/src/BlockTypes/AttributeFilter.php @@ -10,7 +10,9 @@ class AttributeFilter extends AbstractBlock { * * @var string */ - protected $block_name = 'attribute-filter'; + protected $block_name = 'attribute-filter'; + const FILTER_QUERY_VAR = 'filter_'; + const QUERY_TYPE_QUERY_VAR = 'query_type_'; /** * Extra data passed through from server to client for block. diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index cdf4e15b78c..a34d8045765 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -19,6 +19,13 @@ class ProductQuery extends AbstractBlock { */ protected $parsed_block; + /** + * All the query args related to the filter by attributes block. + * + * @var array + */ + protected $attributes_filter_query_args = array(); + /** * Initialize this block type. * @@ -86,6 +93,8 @@ public function build_query( $query ) { // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array(), + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'tax_query' => array(), ); $queries_attributes = $this->get_queries_by_attributes( $variation_props ); @@ -100,6 +109,9 @@ function( $acc, $query ) { // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + $acc['tax_query'] = isset( $query['tax_query'] ) ? array_merge( $acc['tax_query'], array( $query['tax_query'] ) ) : $acc['tax_query']; + return $acc; }, $common_query_values @@ -126,21 +138,58 @@ private function get_on_sale_products_query() { * @return array */ public function set_query_vars( $qvars ) { - $filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ); - return array_merge( $qvars, $filter_query_args ); + $attributes_filter_query_args = array_reduce( + array_values( $this->get_filter_by_attributes_query_vars() ), + function( $acc, $array ) { + return array_merge( array_values( $array ), $acc ); + }, + array() + ); + + $price_filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ); + return array_merge( $qvars, $price_filter_query_args, $attributes_filter_query_args ); + } + + /** + * Get all the query args related to the filter by attributes block. + * + * @return array + */ + private function get_filter_by_attributes_query_vars() { + + if ( ! empty( $this->attributes_filter_query_args ) ) { + return $this->attributes_filter_query_args; + } + + $this->attributes_filter_query_args = array_reduce( + wc_get_attribute_taxonomies(), + function( $acc, $attribute ) { + $acc[ $attribute->attribute_name ] = array( + 'filter' => AttributeFilter::FILTER_QUERY_VAR . $attribute->attribute_name, + 'query_type' => AttributeFilter::QUERY_TYPE_QUERY_VAR . $attribute->attribute_name, + ); + return $acc; + }, + array() + ); + + return $this->attributes_filter_query_args; } /** - * Return queries that are generated by query args + * Return queries that are generated by query args. * * @return array */ private function get_queries_by_applied_filters() { - return array( 'price_filter' => $this->get_filter_by_price_query() ); + return array( + 'price_filter' => $this->get_filter_by_price_query(), + 'attributes_filter' => $this->get_filter_by_attributes_query(), + ); } /** - * Return queries that are generated by attributes + * Return queries that are generated by block attributes. * * @param array $variation_props Dedicated attributes for the variation. * @return array @@ -175,6 +224,10 @@ private function get_filter_by_price_query() { 'type' => 'numeric', ]; + if ( empty( $min_price_query ) && empty( $max_price_query ) ) { + return array(); + } + return array( // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query @@ -186,4 +239,53 @@ private function get_filter_by_price_query() { ); } + /** + * Return a query that filters products by attributes. + * + * @return array + */ + private function get_filter_by_attributes_query() { + $attributes_filter_query_args = $this->get_filter_by_attributes_query_vars(); + + $queries = array_reduce( + $attributes_filter_query_args, + function( $acc, $query_args ) { + $attribute_name = $query_args['filter']; + $attribute_query_type = $query_args['query_type']; + + $attribute_name_value = get_query_var( $attribute_name ); + $attribute_query_type_value = get_query_var( $attribute_query_type ); + + if ( empty( $attribute_name_value ) ) { + return $acc; + } + + $attribute_name_value = explode( ',', $attribute_name_value ); + + $acc[] = array( + 'taxonomy' => str_replace( AttributeFilter::FILTER_QUERY_VAR, 'pa_', $attribute_name ), + 'field' => 'slug', + 'terms' => $attribute_name_value, + 'operator' => 'and' === $attribute_query_type_value ? 'AND' : 'IN', + ); + + return $acc; + }, + array() + ); + + if ( empty( $filters ) ) { + return array(); + } + + return array( + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'tax_query' => array( + 'relation' => 'AND', + $queries, + ), + ); + } + } From 1d56a2e90d0ef49330eef6b0133f117237d70e22 Mon Sep 17 00:00:00 2001 From: Luigi <gigitux@gmail.com> Date: Wed, 21 Sep 2022 18:56:49 +0200 Subject: [PATCH 10/11] fix bugged pagination and on-sale filter after refactor --- src/BlockTypes/ProductQuery.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index cdf4e15b78c..1ba8a4ad281 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -83,6 +83,7 @@ public function build_query( $query ) { 'posts_per_page' => $query['posts_per_page'], 'orderby' => $query['orderby'], 'order' => $query['order'], + 'offset' => $query['offset'], // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array(), @@ -97,10 +98,13 @@ public function build_query( $query ) { $queries_filters ), function( $acc, $query ) { - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; - return $acc; + if ( isset( $query['post__in'] ) ) { + $acc['post__in'] = isset( $acc['post__in'] ) ? array_merge( $acc['post__in'], $query['post__in'] ) : $query['post__in']; + } + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; + return $acc; }, $common_query_values ); @@ -115,7 +119,7 @@ function( $acc, $query ) { */ private function get_on_sale_products_query() { return array( - 'post_in' => wc_get_product_ids_on_sale(), + 'post__in' => wc_get_product_ids_on_sale(), ); } From 1eea9681e797f12dad0976afe8e9eb28087a9142 Mon Sep 17 00:00:00 2001 From: Luigi <gigitux@gmail.com> Date: Thu, 22 Sep 2022 11:43:44 +0200 Subject: [PATCH 11/11] add compatibility with filters --- assets/js/blocks/attribute-filter/block.tsx | 80 +++++++++++---------- assets/js/blocks/price-filter/block.tsx | 46 ++++++------ assets/js/utils/filters.ts | 7 +- 3 files changed, 72 insertions(+), 61 deletions(-) diff --git a/assets/js/blocks/attribute-filter/block.tsx b/assets/js/blocks/attribute-filter/block.tsx index a7b2a13d266..43253f0c4fe 100644 --- a/assets/js/blocks/attribute-filter/block.tsx +++ b/assets/js/blocks/attribute-filter/block.tsx @@ -275,48 +275,54 @@ const AttributeFilterBlock = ( { * @param {boolean} allFiltersRemoved If there are active filters or not. */ const updateFilterUrl = useCallback( - ( query, allFiltersRemoved = false ) => { - if ( allFiltersRemoved ) { - if ( ! attributeObject?.taxonomy ) { - return; - } - const currentQueryArgKeys = Object.keys( - getQueryArgs( window.location.href ) - ); - - const parsedTaxonomy = parseTaxonomyToGenerateURL( - attributeObject.taxonomy - ); - - const url = currentQueryArgKeys.reduce( - ( currentUrl, queryArg ) => - queryArg.includes( - PREFIX_QUERY_ARG_QUERY_TYPE + parsedTaxonomy - ) || - queryArg.includes( - PREFIX_QUERY_ARG_FILTER_TYPE + parsedTaxonomy - ) - ? removeQueryArgs( currentUrl, queryArg ) - : currentUrl, - window.location.href - ); - - const newUrl = formatParams( url, query ); - changeUrl( newUrl ); - } else { - const newUrl = formatParams( pageUrl, query ); - const currentQueryArgs = getQueryArgs( window.location.href ); - const newUrlQueryArgs = getQueryArgs( newUrl ); - - if ( ! isQueryArgsEqual( currentQueryArgs, newUrlQueryArgs ) ) { + async ( query, allFiltersRemoved = false ) => { + return await new Promise( () => { + if ( allFiltersRemoved ) { + if ( ! attributeObject?.taxonomy ) { + return; + } + const currentQueryArgKeys = Object.keys( + getQueryArgs( window.location.href ) + ); + + const parsedTaxonomy = parseTaxonomyToGenerateURL( + attributeObject.taxonomy + ); + + const url = currentQueryArgKeys.reduce( + ( currentUrl, queryArg ) => + queryArg.includes( + PREFIX_QUERY_ARG_QUERY_TYPE + parsedTaxonomy + ) || + queryArg.includes( + PREFIX_QUERY_ARG_FILTER_TYPE + parsedTaxonomy + ) + ? removeQueryArgs( currentUrl, queryArg ) + : currentUrl, + window.location.href + ); + + const newUrl = formatParams( url, query ); changeUrl( newUrl ); + } else { + const newUrl = formatParams( pageUrl, query ); + const currentQueryArgs = getQueryArgs( + window.location.href + ); + const newUrlQueryArgs = getQueryArgs( newUrl ); + + if ( + ! isQueryArgsEqual( currentQueryArgs, newUrlQueryArgs ) + ) { + changeUrl( newUrl ); + } } - } + } ); }, [ pageUrl, attributeObject?.taxonomy ] ); - const onSubmit = ( checkedFilters: string[] ) => { + const onSubmit = async ( checkedFilters: string[] ) => { const query = updateAttributeFilter( productAttributesQuery, setProductAttributesQuery, @@ -325,7 +331,7 @@ const AttributeFilterBlock = ( { blockAttributes.queryType === 'or' ? 'in' : 'and' ); - updateFilterUrl( query, checkedFilters.length === 0 ); + await updateFilterUrl( query, checkedFilters.length === 0 ); }; const updateCheckedFilters = useCallback( diff --git a/assets/js/blocks/price-filter/block.tsx b/assets/js/blocks/price-filter/block.tsx index 4e191b55658..2762665e03e 100644 --- a/assets/js/blocks/price-filter/block.tsx +++ b/assets/js/blocks/price-filter/block.tsx @@ -169,30 +169,32 @@ const PriceFilterBlock = ( { // Updates the query based on slider values. const onSubmit = useCallback( - ( newMinPrice, newMaxPrice ) => { - const finalMaxPrice = - newMaxPrice >= Number( maxConstraint ) - ? undefined - : newMaxPrice; - const finalMinPrice = - newMinPrice <= Number( minConstraint ) - ? undefined - : newMinPrice; - - if ( window ) { - const newUrl = formatParams( window.location.href, { - min_price: finalMinPrice / 10 ** currency.minorUnit, - max_price: finalMaxPrice / 10 ** currency.minorUnit, - } ); - - // If the params have changed, lets update the filter URL. - if ( window.location.href !== newUrl ) { - changeUrl( newUrl ); + async ( newMinPrice, newMaxPrice ) => { + return await new Promise( () => { + const finalMaxPrice = + newMaxPrice >= Number( maxConstraint ) + ? undefined + : newMaxPrice; + const finalMinPrice = + newMinPrice <= Number( minConstraint ) + ? undefined + : newMinPrice; + + if ( window ) { + const newUrl = formatParams( window.location.href, { + min_price: finalMinPrice / 10 ** currency.minorUnit, + max_price: finalMaxPrice / 10 ** currency.minorUnit, + } ); + + // If the params have changed, lets update the filter URL. + if ( window.location.href !== newUrl ) { + changeUrl( newUrl ); + } } - } - setMinPriceQuery( finalMinPrice ); - setMaxPriceQuery( finalMaxPrice ); + setMinPriceQuery( finalMinPrice ); + setMaxPriceQuery( finalMaxPrice ); + } ); }, [ minConstraint, diff --git a/assets/js/utils/filters.ts b/assets/js/utils/filters.ts index 98548288fba..5180860e127 100644 --- a/assets/js/utils/filters.ts +++ b/assets/js/utils/filters.ts @@ -5,6 +5,8 @@ import { getQueryArg } from '@wordpress/url'; import { getSettingWithCoercion } from '@woocommerce/settings'; import { isBoolean } from '@woocommerce/types'; +import { navigate } from '../blocks/product-query/full-vdom'; + const filteringForPhpTemplate = getSettingWithCoercion( 'is_rendering_php_template', false, @@ -32,10 +34,11 @@ export function getUrlParameter( name: string ) { * * @param {string} newUrl New URL to be set. */ -export function changeUrl( newUrl: string ) { - if ( filteringForPhpTemplate ) { +export async function changeUrl( newUrl: string ) { + if ( false ) { window.location.href = newUrl; } else { window.history.replaceState( {}, '', newUrl ); + await navigate( newUrl ); } }