diff --git a/assets/js/blocks/simple-price-filter/block.json b/assets/js/blocks/simple-price-filter/block.json new file mode 100644 index 00000000000..d7a9774666c --- /dev/null +++ b/assets/js/blocks/simple-price-filter/block.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "woocommerce/simple-price-filter", + "version": "1.0.0", + "title": "Simple Price filter", + "description": "Enable customers to filter the product grid by choosing a price range.", + "category": "woocommerce", + "keywords": [ "WooCommerce" ], + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2, + "viewScript": [ + "wc-simple-price-filter-block-frontend", + "wc-interactivity" + ], + "supports": { + "interactivity": true + } +} diff --git a/assets/js/blocks/simple-price-filter/frontend.js b/assets/js/blocks/simple-price-filter/frontend.js new file mode 100644 index 00000000000..275ec49e2a5 --- /dev/null +++ b/assets/js/blocks/simple-price-filter/frontend.js @@ -0,0 +1,75 @@ +/** + * External dependencies + */ +import { store, navigate } from '@woocommerce/interactivity'; + +const getHrefWithFilters = ( { state } ) => { + const { minPrice, maxPrice } = state.filters; + const url = new URL( window.location.href ); + const { searchParams } = url; + + if ( minPrice > 0 ) { + searchParams.set( 'min_price', minPrice ); + } else { + searchParams.delete( 'min_price' ); + } + + if ( maxPrice < state.filters.maxRange ) { + searchParams.set( 'max_price', maxPrice ); + } else { + searchParams.delete( 'max_price' ); + } + + searchParams.forEach( ( _, key ) => { + if ( /query-[0-9]+-page/.test( key ) ) searchParams.delete( key ); + } ); + + return url.href; +}; + +store( { + state: { + filters: { + rangeStyle: ( { state } ) => { + const { minPrice, maxPrice, maxRange } = state.filters; + return [ + `--low: ${ ( 100 * minPrice ) / maxRange }%`, + `--high: ${ ( 100 * maxPrice ) / maxRange }%`, + ].join( ';' ); + }, + }, + }, + actions: { + filters: { + setMinPrice: ( { state, event } ) => { + const value = parseFloat( event.target.value ) || 0; + state.filters.minPrice = value; + }, + setMaxPrice: ( { state, event } ) => { + const value = + parseFloat( event.target.value ) || state.filters.maxRange; + state.filters.maxPrice = value; + }, + updateProducts: ( { state } ) => { + navigate( getHrefWithFilters( { state } ) ); + }, + reset: ( { state } ) => { + state.filters.minPrice = 0; + state.filters.maxPrice = state.filters.maxRange; + navigate( getHrefWithFilters( { state } ) ); + }, + updateActiveHandle: ( { state, event } ) => { + const { minPrice, maxPrice, maxRange } = state.filters; + const { target, offsetX } = event; + const xPos = offsetX / target.offsetWidth; + const minPos = minPrice / maxRange; + const maxPos = maxPrice / maxRange; + + state.filters.isMinActive = + Math.abs( xPos - minPos ) < Math.abs( xPos - maxPos ); + + state.filters.isMaxActive = ! state.filters.isMinActive; + }, + }, + }, +} ); diff --git a/assets/js/blocks/simple-price-filter/index.js b/assets/js/blocks/simple-price-filter/index.js new file mode 100644 index 00000000000..de30e2ed56f --- /dev/null +++ b/assets/js/blocks/simple-price-filter/index.js @@ -0,0 +1,9 @@ +/** + * External dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +registerBlockType( 'woocommerce/simple-price-filter', { + edit: () =>
Simple price filter
, + save: () => null, +} ); diff --git a/assets/js/blocks/simple-price-filter/style.scss b/assets/js/blocks/simple-price-filter/style.scss new file mode 100644 index 00000000000..0a92f493293 --- /dev/null +++ b/assets/js/blocks/simple-price-filter/style.scss @@ -0,0 +1,67 @@ +.wp-block-woocommerce-simple-price-filter { + --low: 0%; + --high: 100%; + --range-color: currentColor; + + .range { + position: relative; + margin: 15px 0; + + .range-bar { + position: relative; + height: 4px; + background: linear-gradient(90deg, transparent var(--low), var(--range-color) 0, var(--range-color) var(--high), transparent 0) no-repeat 0 100% / 100% 100%; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: currentColor; + opacity: 0.2; + } + } + + input[type="range"] { + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: 0; + margin: 0; + padding: 0; + + &.active { + z-index: 10; + } + } + + input[type="range" i] { + color: -internal-light-dark(rgb(16, 16, 16), rgb(255, 255, 255)); + padding: initial; + } + } + + .text { + display: flex; + align-items: center; + justify-content: space-between; + margin: 16px 0; + gap: 8px; + + input[type="text"] { + padding: 8px; + margin: 0; + width: auto; + max-width: 60px; + min-width: 0; + font-size: 0.875em; + border-width: 1px; + border-style: solid; + border-color: currentColor; + border-radius: 4px; + } + } +} diff --git a/assets/js/interactivity/index.js b/assets/js/interactivity/index.js index 00008625110..4bddd1db71a 100644 --- a/assets/js/interactivity/index.js +++ b/assets/js/interactivity/index.js @@ -1,7 +1,6 @@ import registerDirectives from './directives'; import { init } from './router'; import { rawStore, afterLoads } from './store'; - export { navigate } from './router'; export { store } from './store'; diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index ac2499cd932..13b0c3c2fc5 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -78,6 +78,7 @@ const blocks = { 'reviews-by-product': { customDir: 'reviews/reviews-by-product', }, + 'simple-price-filter': {}, 'single-product': {}, 'stock-filter': {}, 'product-collection': { diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php new file mode 100644 index 00000000000..d8528620990 --- /dev/null +++ b/src/BlockTypes/SimplePriceFilter.php @@ -0,0 +1,107 @@ + array( + 'filters' => array( + 'minPrice' => $min_price, + 'maxPrice' => $max_price, + 'maxRange' => $max_range, + 'rangeStyle' => $range_style, + 'isMinActive' => true, + 'isMaxActive' => false, + ), + ), + ) + ); + + return << +

Filter by price

+
+
+ + +
+
+ + +
+ + +HTML; + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index 21d1a062335..edaa77cbfea 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -215,6 +215,7 @@ protected function get_block_types() { 'ReviewsByProduct', 'RelatedProducts', 'ProductDetails', + 'SimplePriceFilter', 'SingleProduct', 'StockFilter', ]; diff --git a/src/Interactivity/client-side-navigation.php b/src/Interactivity/client-side-navigation.php new file mode 100644 index 00000000000..ab550070f61 --- /dev/null +++ b/src/Interactivity/client-side-navigation.php @@ -0,0 +1,9 @@ +'; +} +add_action( 'wp_head', 'woocommerce_interactivity_add_client_side_navigation_meta_tag' ); diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 2c7257273ef..b08551a5869 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -317,3 +317,6 @@ function woocommerce_blocks_interactivity_setup() { } } add_action( 'plugins_loaded', 'woocommerce_blocks_interactivity_setup' ); + +// Enable the interactivity API. +add_filter( 'woocommerce_blocks_enable_interactivity_api', '__return_true' );