diff --git a/.wp-env.json b/.wp-env.json index 8a150435888..3aca2bc3c53 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,6 +1,7 @@ { "core": null, "plugins": [ + "https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip", "https://downloads.wordpress.org/plugin/woocommerce.latest-stable.zip", "https://github.com/WP-API/Basic-Auth/archive/master.zip", "./tests/mocks/woo-test-helper", 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..48d173205ee --- /dev/null +++ b/assets/js/blocks/simple-price-filter/block.json @@ -0,0 +1,11 @@ +{ + "$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 +} 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/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js new file mode 100644 index 00000000000..fd7e72edf89 --- /dev/null +++ b/assets/js/blocks/simple-price-filter/view.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import { store } from '@woocommerce/interactivity/store'; +import { navigate } from '@woocommerce/interactivity/router'; + +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' ); + } + + return url.href; +}; + +store( { + state: { + filters: { + rangeStyle: ( { state } ) => { + const { minPrice, maxPrice, maxRange } = state.filters; + return { + '--low': `${ ( 100 * minPrice ) / maxRange }%`, + '--high': `${ ( 100 * maxPrice ) / maxRange }%`, + }; + }, + }, + }, + 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/bin/webpack-configs.js b/bin/webpack-configs.js index 1cae71dab42..2fadb1961fb 100644 --- a/bin/webpack-configs.js +++ b/bin/webpack-configs.js @@ -786,6 +786,8 @@ const getInteractivityAPIConfig = ( options = {} ) => { return { entry: { runtime: './assets/js/interactivity', + 'simple-price-filter': + './assets/js/blocks/simple-price-filter/view.js', }, output: { filename: 'woo-directives-[name].js', diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index 3b03f77d997..943050bbe0d 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -64,6 +64,7 @@ const blocks = { 'reviews-by-product': { customDir: 'reviews/reviews-by-product', }, + 'simple-price-filter': {}, 'single-product': { isExperimental: true, }, diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php new file mode 100644 index 00000000000..aeae0fdfef8 --- /dev/null +++ b/src/BlockTypes/SimplePriceFilter.php @@ -0,0 +1,116 @@ + array( + 'filters' => array( + 'minPrice' => $min_price, + 'maxPrice' => $max_price, + 'maxRange' => $max_range, + 'rangeStyle' => $range_style, + 'isMinActive' => true, + 'isMaxActive' => false, + ), + ), + ) + ); + + return " +
+

Filter by price

+
+
+ + +
+
+ + +
+ +
+ "; + } + + /** + * Get the frontend script handle for this block type. + * + * @param string $key Data to get, or default to everything. + * + * @return null + */ + public function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'simple-price-filter-view', + 'path' => 'build/woo-directives-simple-price-filter.js', + 'dependencies' => [ 'woo-directives-runtime' ], + ]; + return $key ? $script[ $key ] : $script; + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index 5ad32e5558a..87752e50dbc 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -215,6 +215,7 @@ protected function get_block_types() { 'RelatedProducts', 'ProductDetails', 'StockFilter', + 'SimplePriceFilter', ]; $block_types = array_merge( $block_types, Cart::get_cart_block_types(), Checkout::get_checkout_block_types() ); diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 2572935d2f6..45aa2bc4280 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -290,3 +290,10 @@ function woocommerce_blocks_plugin_outdated_notice() { // Include the Interactivity API. require_once __DIR__ . '/src/Interactivity/woo-directives.php'; +// Enable client-side navigation. +add_filter( + 'client_side_navigation', + function () { + return true; + } +);