Experimental Collection Rating Filter (#41999)
Introduce Experimental Collection Rating Filter, CheckboxList interactivity component.
This commit is contained in:
parent
bf29119032
commit
6d4c69850c
|
@ -25,6 +25,14 @@ const template = [
|
|||
},
|
||||
],
|
||||
[ 'woocommerce/collection-stock-filter', {} ],
|
||||
[
|
||||
'core/heading',
|
||||
{
|
||||
content: __( 'Filter by Rating', 'woocommerce' ),
|
||||
level: 3,
|
||||
},
|
||||
],
|
||||
[ 'woocommerce/collection-rating-filter', {} ],
|
||||
];
|
||||
|
||||
const firstAttribute = ATTRIBUTES.find( Boolean );
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import {
|
||||
PanelBody,
|
||||
ToggleControl,
|
||||
// @ts-expect-error - no types.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalToggleGroupControl as ToggleGroupControl,
|
||||
// @ts-expect-error - no types.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
||||
} from '@wordpress/components';
|
||||
|
@ -16,9 +19,12 @@ import {
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { AttributeSelectControls } from './attribute-select-controls';
|
||||
import { EditProps } from '../types';
|
||||
import { BlockAttributes } from '../types';
|
||||
|
||||
export const Inspector = ( { attributes, setAttributes }: EditProps ) => {
|
||||
export const Inspector = ( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
}: BlockEditProps< BlockAttributes > ) => {
|
||||
const { attributeId, showCounts, queryType, displayStyle, selectType } =
|
||||
attributes;
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "woocommerce/collection-rating-filter",
|
||||
"version": "1.0.0",
|
||||
"title": "Collection Rating Filter",
|
||||
"description": "Enable customers to filter the product collection by rating.",
|
||||
"category": "woocommerce",
|
||||
"keywords": [ "WooCommerce" ],
|
||||
"supports": {
|
||||
"interactivity": true
|
||||
},
|
||||
"ancestor": [ "woocommerce/collection-filters" ],
|
||||
"usesContext": [ "collectionData" ],
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"showCounts": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"displayStyle": {
|
||||
"type": "string",
|
||||
"default": "list"
|
||||
},
|
||||
"selectType": {
|
||||
"type": "string",
|
||||
"default": "multiple"
|
||||
},
|
||||
"isPreview": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"queryParam": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"calculate_rating_counts": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
"textdomain": "woocommerce",
|
||||
"apiVersion": 2,
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
PanelBody,
|
||||
ToggleControl,
|
||||
// @ts-expect-error - no types.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalToggleGroupControl as ToggleGroupControl,
|
||||
// @ts-expect-error - no types.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Attributes } from '../types';
|
||||
|
||||
export const Inspector = ( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
}: BlockEditProps< Attributes > ) => {
|
||||
const { showCounts, displayStyle } = attributes;
|
||||
return (
|
||||
<InspectorControls key="inspector">
|
||||
<PanelBody title={ __( 'Display Settings', 'woocommerce' ) }>
|
||||
<ToggleControl
|
||||
label={ __( 'Display product count', 'woocommerce' ) }
|
||||
checked={ showCounts }
|
||||
onChange={ () =>
|
||||
setAttributes( {
|
||||
showCounts: ! showCounts,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
<ToggleGroupControl
|
||||
label={ __( 'Display Style', 'woocommerce' ) }
|
||||
value={ displayStyle }
|
||||
onChange={ ( value: string ) =>
|
||||
setAttributes( {
|
||||
displayStyle: value,
|
||||
} )
|
||||
}
|
||||
className="wc-block-attribute-filter__display-toggle"
|
||||
>
|
||||
<ToggleGroupControlOption
|
||||
value="list"
|
||||
label={ __( 'List', 'woocommerce' ) }
|
||||
/>
|
||||
<ToggleGroupControlOption
|
||||
value="dropdown"
|
||||
label={ __( 'Dropdown', 'woocommerce' ) }
|
||||
/>
|
||||
</ToggleGroupControl>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,316 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon, chevronDown } from '@wordpress/icons';
|
||||
import classnames from 'classnames';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import type { BlockEditProps } from '@wordpress/blocks';
|
||||
import Rating, {
|
||||
RatingValues,
|
||||
} from '@woocommerce/base-components/product-rating';
|
||||
import {
|
||||
useQueryStateByKey,
|
||||
useQueryStateByContext,
|
||||
useCollectionData,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { getSettingWithCoercion } from '@woocommerce/settings';
|
||||
import { isBoolean, isObject, objectHasProp } from '@woocommerce/types';
|
||||
import { useState, useMemo, useEffect } from '@wordpress/element';
|
||||
import { CheckboxList } from '@woocommerce/blocks-components';
|
||||
import FormTokenField from '@woocommerce/base-components/form-token-field';
|
||||
import { Disabled, Notice, withSpokenMessages } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { previewOptions } from './preview';
|
||||
import './style.scss';
|
||||
import { Attributes } from './types';
|
||||
import { formatSlug, getActiveFilters, generateUniqueId } from './utils';
|
||||
import { useSetWraperVisibility } from '../../../filter-wrapper/context';
|
||||
import './editor.scss';
|
||||
import { Inspector } from '../attribute-filter/components/inspector-controls';
|
||||
|
||||
const NoRatings = () => (
|
||||
<Notice status="warning" isDismissible={ false }>
|
||||
<p>
|
||||
{ __(
|
||||
"Your store doesn't have any products with ratings yet. This filter option will display when a product receives a review.",
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
</Notice>
|
||||
);
|
||||
|
||||
const Edit = ( props: BlockEditProps< Attributes > ) => {
|
||||
const { className } = props.attributes;
|
||||
const blockAttributes = props.attributes;
|
||||
|
||||
const blockProps = useBlockProps( {
|
||||
className: classnames( 'wc-block-rating-filter', className ),
|
||||
} );
|
||||
|
||||
const isEditor = true;
|
||||
|
||||
const setWrapperVisibility = useSetWraperVisibility();
|
||||
const [ queryState ] = useQueryStateByContext();
|
||||
|
||||
const { results: filteredCounts, isLoading: filteredCountsLoading } =
|
||||
useCollectionData( {
|
||||
queryRating: true,
|
||||
queryState,
|
||||
isEditor,
|
||||
} );
|
||||
|
||||
const [ displayedOptions, setDisplayedOptions ] = useState(
|
||||
blockAttributes.isPreview ? previewOptions : []
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
! blockAttributes.isPreview &&
|
||||
filteredCountsLoading &&
|
||||
displayedOptions.length === 0;
|
||||
|
||||
const isDisabled = ! blockAttributes.isPreview && filteredCountsLoading;
|
||||
|
||||
const initialFilters = useMemo(
|
||||
() => getActiveFilters( 'rating_filter' ),
|
||||
[]
|
||||
);
|
||||
|
||||
const [ checked ] = useState( initialFilters );
|
||||
|
||||
const [ productRatingsQuery ] = useQueryStateByKey(
|
||||
'rating',
|
||||
initialFilters
|
||||
);
|
||||
|
||||
/*
|
||||
FormTokenField forces the dropdown to reopen on reset, so we create a unique ID to use as the components key.
|
||||
This will force the component to remount on reset when we change this value.
|
||||
More info: https://github.com/woocommerce/woocommerce-blocks/pull/6920#issuecomment-1222402482
|
||||
*/
|
||||
const [ remountKey, setRemountKey ] = useState( generateUniqueId() );
|
||||
const [ displayNoProductRatingsNotice, setDisplayNoProductRatingsNotice ] =
|
||||
useState( false );
|
||||
|
||||
const multiple = blockAttributes.selectType !== 'single';
|
||||
|
||||
const showChevron = multiple
|
||||
? ! isLoading && checked.length < displayedOptions.length
|
||||
: ! isLoading && checked.length === 0;
|
||||
|
||||
/**
|
||||
* Compare intersection of all ratings and filtered counts to get a list of options to display.
|
||||
*/
|
||||
useEffect( () => {
|
||||
/**
|
||||
* Checks if a status slug is in the query state.
|
||||
*
|
||||
* @param {string} queryStatus The status slug to check.
|
||||
*/
|
||||
|
||||
if ( filteredCountsLoading || blockAttributes.isPreview ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orderedRatings =
|
||||
! filteredCountsLoading &&
|
||||
objectHasProp( filteredCounts, 'rating_counts' ) &&
|
||||
Array.isArray( filteredCounts.rating_counts )
|
||||
? [ ...filteredCounts.rating_counts ].reverse()
|
||||
: [];
|
||||
|
||||
if ( orderedRatings.length === 0 ) {
|
||||
setDisplayedOptions( previewOptions );
|
||||
setDisplayNoProductRatingsNotice( true );
|
||||
return;
|
||||
}
|
||||
|
||||
const newOptions = orderedRatings
|
||||
.filter(
|
||||
( item ) => isObject( item ) && Object.keys( item ).length > 0
|
||||
)
|
||||
.map( ( item ) => {
|
||||
return {
|
||||
label: (
|
||||
<Rating
|
||||
key={ item?.rating }
|
||||
rating={ item?.rating }
|
||||
ratedProductsCount={
|
||||
blockAttributes.showCounts ? item?.count : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
value: item?.rating?.toString(),
|
||||
};
|
||||
} );
|
||||
|
||||
setDisplayedOptions( newOptions );
|
||||
setRemountKey( generateUniqueId() );
|
||||
}, [
|
||||
blockAttributes.showCounts,
|
||||
blockAttributes.isPreview,
|
||||
filteredCounts,
|
||||
filteredCountsLoading,
|
||||
productRatingsQuery,
|
||||
] );
|
||||
|
||||
if ( ! filteredCountsLoading && displayedOptions.length === 0 ) {
|
||||
setWrapperVisibility( false );
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasFilterableProducts = getSettingWithCoercion(
|
||||
'hasFilterableProducts',
|
||||
false,
|
||||
isBoolean
|
||||
);
|
||||
|
||||
if ( ! hasFilterableProducts ) {
|
||||
setWrapperVisibility( false );
|
||||
return null;
|
||||
}
|
||||
|
||||
setWrapperVisibility( true );
|
||||
|
||||
return (
|
||||
<>
|
||||
<Inspector { ...props } />
|
||||
<div { ...blockProps }>
|
||||
<Disabled>
|
||||
{ displayNoProductRatingsNotice && <NoRatings /> }
|
||||
<div
|
||||
className={ classnames(
|
||||
'wc-block-rating-filter',
|
||||
`style-${ blockAttributes.displayStyle }`,
|
||||
{
|
||||
'is-loading': isLoading,
|
||||
}
|
||||
) }
|
||||
>
|
||||
{ blockAttributes.displayStyle === 'dropdown' ? (
|
||||
<>
|
||||
<FormTokenField
|
||||
key={ remountKey }
|
||||
className={ classnames( {
|
||||
'single-selection': ! multiple,
|
||||
'is-loading': isLoading,
|
||||
} ) }
|
||||
style={ {
|
||||
borderStyle: 'none',
|
||||
} }
|
||||
suggestions={ displayedOptions
|
||||
.filter(
|
||||
( option ) =>
|
||||
! checked.includes(
|
||||
option.value
|
||||
)
|
||||
)
|
||||
.map( ( option ) => option.value ) }
|
||||
disabled={ isLoading }
|
||||
placeholder={ __(
|
||||
'Select Rating',
|
||||
'woocommerce'
|
||||
) }
|
||||
onChange={ () => {
|
||||
// noop
|
||||
} }
|
||||
value={ checked }
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - FormTokenField doesn't accept custom components, forcing it here to display component
|
||||
displayTransform={ ( value ) => {
|
||||
const resultWithZeroCount = {
|
||||
value,
|
||||
label: (
|
||||
<Rating
|
||||
key={
|
||||
Number(
|
||||
value
|
||||
) as RatingValues
|
||||
}
|
||||
rating={
|
||||
Number(
|
||||
value
|
||||
) as RatingValues
|
||||
}
|
||||
ratedProductsCount={ 0 }
|
||||
/>
|
||||
),
|
||||
};
|
||||
const resultWithNonZeroCount =
|
||||
displayedOptions.find(
|
||||
( option ) =>
|
||||
option.value === value
|
||||
);
|
||||
|
||||
const displayedResult =
|
||||
resultWithNonZeroCount ||
|
||||
resultWithZeroCount;
|
||||
|
||||
const { label, value: rawValue } =
|
||||
displayedResult;
|
||||
|
||||
// A label - JSX component - is extended with faked string methods to allow using JSX element as an option in FormTokenField
|
||||
const extendedLabel = Object.assign(
|
||||
{},
|
||||
label,
|
||||
{
|
||||
toLocaleLowerCase: () =>
|
||||
rawValue,
|
||||
substring: (
|
||||
start: number,
|
||||
end: number
|
||||
) =>
|
||||
start === 0 && end === 1
|
||||
? label
|
||||
: '',
|
||||
}
|
||||
);
|
||||
return extendedLabel;
|
||||
} }
|
||||
saveTransform={ formatSlug }
|
||||
messages={ {
|
||||
added: __(
|
||||
'Rating filter added.',
|
||||
'woocommerce'
|
||||
),
|
||||
removed: __(
|
||||
'Rating filter removed.',
|
||||
'woocommerce'
|
||||
),
|
||||
remove: __(
|
||||
'Remove rating filter.',
|
||||
'woocommerce'
|
||||
),
|
||||
__experimentalInvalid: __(
|
||||
'Invalid rating filter.',
|
||||
'woocommerce'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
{ showChevron && (
|
||||
<Icon icon={ chevronDown } size={ 30 } />
|
||||
) }
|
||||
</>
|
||||
) : (
|
||||
<CheckboxList
|
||||
className={ 'wc-block-rating-filter-list' }
|
||||
options={ displayedOptions }
|
||||
checked={ checked }
|
||||
onChange={ () => {
|
||||
// noop
|
||||
} }
|
||||
isLoading={ isLoading }
|
||||
isDisabled={ isDisabled }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</Disabled>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withSpokenMessages( Edit );
|
|
@ -0,0 +1,3 @@
|
|||
.wc-block-rating-filter .components-notice__content {
|
||||
color: $black;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getContext, navigate, store } from '@woocommerce/interactivity';
|
||||
import { CheckboxListContext } from '@woocommerce/interactivity-components/checkbox-list';
|
||||
import { DropdownContext } from '@woocommerce/interactivity-components/dropdown';
|
||||
|
||||
store( 'woocommerce/collection-rating-filter', {
|
||||
actions: {
|
||||
onCheckboxChange: () => {
|
||||
const checkboxContext = getContext< CheckboxListContext >(
|
||||
'woocommerce/interactivity-checkbox-list'
|
||||
);
|
||||
|
||||
const filters = checkboxContext.items
|
||||
.filter( ( item ) => {
|
||||
return item.checked;
|
||||
} )
|
||||
.map( ( item ) => {
|
||||
return item.value;
|
||||
} );
|
||||
|
||||
const url = new URL( window.location.href );
|
||||
|
||||
if ( filters.length ) {
|
||||
// add filters to url
|
||||
url.searchParams.set( 'rating_filter', filters.join( ',' ) );
|
||||
} else {
|
||||
// remove filters from url
|
||||
url.searchParams.delete( 'rating_filter' );
|
||||
}
|
||||
|
||||
navigate( url );
|
||||
},
|
||||
onDropdownChange: () => {
|
||||
const dropdownContext = getContext< DropdownContext >(
|
||||
'woocommerce/interactivity-dropdown'
|
||||
);
|
||||
|
||||
const filter = dropdownContext.selectedItem?.value;
|
||||
const url = new URL( window.location.href );
|
||||
|
||||
if ( filter ) {
|
||||
// add filter to url
|
||||
url.searchParams.set( 'rating_filter', filter );
|
||||
} else {
|
||||
// remove filter from url
|
||||
url.searchParams.delete( 'rating_filter' );
|
||||
}
|
||||
|
||||
navigate( url );
|
||||
},
|
||||
},
|
||||
} );
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { Icon, starEmpty } from '@wordpress/icons';
|
||||
import classNames from 'classnames';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import edit from './edit';
|
||||
import metadata from './block.json';
|
||||
import type { Attributes } from './types';
|
||||
|
||||
registerBlockType( metadata, {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ starEmpty }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
attributes: {
|
||||
...metadata.attributes,
|
||||
},
|
||||
edit,
|
||||
// Save the props to post content.
|
||||
save( { attributes }: { attributes: Attributes } ) {
|
||||
const { className } = attributes;
|
||||
|
||||
return (
|
||||
<div
|
||||
{ ...useBlockProps.save( {
|
||||
className: classNames( 'is-loading', className ),
|
||||
} ) }
|
||||
/>
|
||||
);
|
||||
},
|
||||
} );
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import Rating from '@woocommerce/base-components/product-rating';
|
||||
|
||||
export const previewOptions = [
|
||||
{
|
||||
label: <Rating key={ 5 } rating={ 5 } ratedProductsCount={ null } />,
|
||||
value: '5',
|
||||
},
|
||||
{
|
||||
label: <Rating key={ 4 } rating={ 4 } ratedProductsCount={ null } />,
|
||||
value: '4',
|
||||
},
|
||||
{
|
||||
label: <Rating key={ 3 } rating={ 3 } ratedProductsCount={ null } />,
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
label: <Rating key={ 2 } rating={ 2 } ratedProductsCount={ null } />,
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: <Rating key={ 1 } rating={ 1 } ratedProductsCount={ null } />,
|
||||
value: '1',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,175 @@
|
|||
@import "../../../shared/styles/style";
|
||||
|
||||
.wc-block-rating-filter {
|
||||
&.is-loading {
|
||||
@include placeholder();
|
||||
margin-top: $gap;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&.style-dropdown {
|
||||
@include includeFormTokenFieldFix();
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
align-items: flex-start;
|
||||
|
||||
.wc-block-components-filter-submit-button {
|
||||
height: 36px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
> svg {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-product-rating__stars {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wc-blocks-components-form-token-field-wrapper {
|
||||
flex-grow: 1;
|
||||
max-width: unset;
|
||||
width: 0;
|
||||
height: max-content;
|
||||
|
||||
&:not(.is-loading) {
|
||||
border: 1px solid $gray-700 !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
border-radius: em(4px);
|
||||
}
|
||||
|
||||
.components-form-token-field {
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-blocks-components-form-token-field-wrapper .components-form-token-field__input-container {
|
||||
@include reset-color();
|
||||
@include reset-typography();
|
||||
border: 0;
|
||||
padding: $gap-smaller;
|
||||
border-radius: inherit;
|
||||
|
||||
.components-form-token-field__input {
|
||||
@include font-size(small);
|
||||
|
||||
&::placeholder {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.components-form-token-field__suggestions-list {
|
||||
border: 1px solid $gray-700;
|
||||
border-radius: 4px;
|
||||
margin-top: $gap-smaller;
|
||||
max-height: 21em;
|
||||
|
||||
.components-form-token-field__suggestion {
|
||||
color: $black;
|
||||
border: 1px solid $gray-400;
|
||||
border-radius: 4px;
|
||||
margin: $gap-small;
|
||||
padding: $gap-small;
|
||||
}
|
||||
}
|
||||
|
||||
.components-form-token-field__token,
|
||||
.components-form-token-field__suggestion {
|
||||
@include font-size(small);
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-product-rating {
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.wc-block-components-product-rating-count {
|
||||
margin-left: $gap-smallest;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-blocks-components-form-token-field-wrapper:not(.single-selection) .components-form-token-field__input-container {
|
||||
padding: $gap-smallest 30px $gap-smallest $gap-smaller;
|
||||
|
||||
.components-form-token-field__token-text {
|
||||
background-color: $white;
|
||||
border: 1px solid;
|
||||
border-right: 0;
|
||||
border-radius: 25px 0 0 25px;
|
||||
padding: em($gap-smallest) em($gap-smaller) em($gap-smallest) em($gap-small);
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
> .components-form-token-field__input {
|
||||
margin: em($gap-smallest) 0;
|
||||
}
|
||||
|
||||
.components-button.components-form-token-field__remove-token {
|
||||
background-color: $white;
|
||||
border: 1px solid;
|
||||
border-left: 0;
|
||||
border-radius: 0 25px 25px 0;
|
||||
padding: 1px em($gap-smallest) 0 0;
|
||||
|
||||
&.has-icon svg {
|
||||
background-color: $gray-200;
|
||||
border-radius: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-rating-filter {
|
||||
margin-bottom: $gap-large;
|
||||
|
||||
.wc-block-rating-filter .wc-block-rating-filter-list li input,
|
||||
.wc-block-rating-filter .wc-block-rating-filter-list li label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-rating-filter__actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
justify-content: flex-end;
|
||||
margin-top: $gap;
|
||||
|
||||
.wc-block-components-filter-submit-button {
|
||||
margin-left: 0;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// The specificity here is needed to overwrite the margin-top that is inherited on WC block template pages such as Shop.
|
||||
button[type="submit"]:not(.wp-block-search__button).wc-block-components-filter-submit-button {
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
@include font-size(small);
|
||||
}
|
||||
|
||||
.wc-block-rating-filter__button {
|
||||
margin-top: em($gap-smaller);
|
||||
padding: em($gap-smaller) em($gap);
|
||||
@include font-size(small);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-styles-wrapper .wc-block-rating-filter .wc-block-rating-filter__button {
|
||||
margin-top: em($gap-smaller);
|
||||
padding: em($gap-smaller) em($gap);
|
||||
@include font-size(small);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export interface Attributes {
|
||||
className?: string;
|
||||
displayStyle: string;
|
||||
selectType: string;
|
||||
showCounts: boolean;
|
||||
showFilterButton: boolean;
|
||||
isPreview?: boolean;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isString } from '@woocommerce/types';
|
||||
import { getUrlParameter } from '@woocommerce/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import metadata from './block.json';
|
||||
|
||||
export const getActiveFilters = ( queryParamKey = 'filter_rating' ) => {
|
||||
const params = getUrlParameter( queryParamKey );
|
||||
|
||||
if ( ! params ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsedParams = isString( params )
|
||||
? params.split( ',' )
|
||||
: ( params as string[] );
|
||||
|
||||
return parsedParams;
|
||||
};
|
||||
|
||||
export function generateUniqueId() {
|
||||
return Math.floor( Math.random() * Date.now() );
|
||||
}
|
||||
|
||||
export const formatSlug = ( slug: string ) =>
|
||||
slug
|
||||
.trim()
|
||||
.replace( /\s/g, '-' )
|
||||
.replace( /_/g, '-' )
|
||||
.replace( /-+/g, '-' )
|
||||
.replace( /[^a-zA-Z0-9-]/g, '' );
|
||||
|
||||
export const parseAttributes = ( data: Record< string, unknown > ) => {
|
||||
return {
|
||||
showFilterButton: data?.showFilterButton === 'true',
|
||||
showCounts: data?.showCounts === 'true',
|
||||
isPreview: false,
|
||||
displayStyle:
|
||||
( isString( data?.displayStyle ) && data.displayStyle ) ||
|
||||
metadata.attributes.displayStyle.default,
|
||||
selectType:
|
||||
( isString( data?.selectType ) && data.selectType ) ||
|
||||
metadata.attributes.selectType.default,
|
||||
};
|
||||
};
|
|
@ -109,6 +109,10 @@ const blocks = {
|
|||
customDir: 'collection-filters/inner-blocks/attribute-filter',
|
||||
isExperimental: true,
|
||||
},
|
||||
'collection-rating-filter': {
|
||||
customDir: 'collection-filters/inner-blocks/rating-filter',
|
||||
isExperimental: true,
|
||||
},
|
||||
'order-confirmation-summary': {
|
||||
customDir: 'order-confirmation/summary',
|
||||
},
|
||||
|
@ -191,6 +195,12 @@ const entries = {
|
|||
'./assets/js/atomic/blocks/product-elements/add-to-cart-form/index.tsx',
|
||||
...getBlockEntries( '{index,block,frontend}.{t,j}s{,x}' ),
|
||||
|
||||
// Interactivity component styling
|
||||
'wc-interactivity-checkbox-list':
|
||||
'./packages/interactivity-components/checkbox-list/index.ts',
|
||||
'wc-interactivity-dropdown':
|
||||
'./packages/interactivity-components/dropdown/index.ts',
|
||||
|
||||
// Templates
|
||||
'wc-blocks-classic-template-revert-button-style':
|
||||
'./assets/js/templates/revert-button/index.tsx',
|
||||
|
@ -209,6 +219,8 @@ const entries = {
|
|||
// interactivity components, exported as separate entries for now
|
||||
'wc-interactivity-dropdown':
|
||||
'./packages/interactivity-components/dropdown/index.ts',
|
||||
'wc-interactivity-checkbox-list':
|
||||
'./packages/interactivity-components/checkbox-list/index.ts',
|
||||
},
|
||||
main: {
|
||||
// Shared blocks code
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getContext, store } from '@woocommerce/interactivity';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { HTMLElementEvent } from '../../../assets/js/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
export type CheckboxListContext = {
|
||||
items: {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
checked: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
store( 'woocommerce/interactivity-checkbox-list', {
|
||||
state: {},
|
||||
actions: {
|
||||
selectCheckboxItem: ( event: HTMLElementEvent< HTMLInputElement > ) => {
|
||||
const context = getContext< CheckboxListContext >();
|
||||
const value = event.target.value;
|
||||
|
||||
context.items = context.items.map( ( item ) => {
|
||||
if ( item.value.toString() === value ) {
|
||||
return {
|
||||
...item,
|
||||
checked: ! item.checked,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
} );
|
||||
},
|
||||
},
|
||||
} );
|
|
@ -0,0 +1,3 @@
|
|||
// Import styles we need to render the checkbox list and checkbox control.
|
||||
@import "../../../packages/components/checkbox-list/style";
|
||||
@import "../../../packages/components/checkbox-control/style";
|
|
@ -3,6 +3,11 @@
|
|||
*/
|
||||
import { getContext, store } from '@woocommerce/interactivity';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
export type DropdownContext = {
|
||||
currentItem: {
|
||||
label: string;
|
||||
|
@ -19,74 +24,74 @@ export type DropdownContext = {
|
|||
isOpen: boolean;
|
||||
};
|
||||
|
||||
store( 'woocommerce/interactivity-dropdown', {
|
||||
type DropdownStore = {
|
||||
state: {
|
||||
get placeholderText() {
|
||||
const context = getContext< DropdownContext >();
|
||||
const { selectedItem } = context;
|
||||
selectedItem?: {
|
||||
label: string | null;
|
||||
value: string | null;
|
||||
};
|
||||
placeholderText: string;
|
||||
isSelected: boolean;
|
||||
};
|
||||
|
||||
return selectedItem.label || 'Select an option';
|
||||
},
|
||||
get isSelected() {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
const {
|
||||
currentItem: { value },
|
||||
} = context;
|
||||
|
||||
return (
|
||||
context.selectedItem.value === value ||
|
||||
context.hoveredItem.value === value
|
||||
);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleIsOpen: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
toggleIsOpen: () => void;
|
||||
selectDropdownItem: ( event: MouseEvent ) => void;
|
||||
};
|
||||
};
|
||||
|
||||
context.isOpen = ! context.isOpen;
|
||||
const { state } = store< DropdownStore >(
|
||||
'woocommerce/interactivity-dropdown',
|
||||
{
|
||||
state: {
|
||||
get placeholderText(): string {
|
||||
const { selectedItem } = state;
|
||||
|
||||
return selectedItem?.label || 'Select an option';
|
||||
},
|
||||
|
||||
get isSelected(): boolean {
|
||||
const { currentItem } = getContext< DropdownContext >();
|
||||
const { selectedItem } = state;
|
||||
|
||||
return selectedItem?.value === currentItem.value;
|
||||
},
|
||||
},
|
||||
selectDropdownItem: ( event: MouseEvent ) => {
|
||||
const context = getContext< DropdownContext >();
|
||||
actions: {
|
||||
toggleIsOpen: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
const {
|
||||
currentItem: { label, value },
|
||||
} = context;
|
||||
context.isOpen = ! context.isOpen;
|
||||
},
|
||||
selectDropdownItem: ( event: MouseEvent ) => {
|
||||
const context = getContext< DropdownContext >();
|
||||
const { selectedItem } = state;
|
||||
|
||||
const { selectedItem } = context;
|
||||
const {
|
||||
currentItem: { label, value },
|
||||
} = context;
|
||||
|
||||
if (
|
||||
selectedItem.value === value &&
|
||||
selectedItem.label === label
|
||||
) {
|
||||
context.selectedItem = {
|
||||
label: null,
|
||||
value: null,
|
||||
};
|
||||
} else {
|
||||
context.selectedItem = { label, value };
|
||||
}
|
||||
if (
|
||||
selectedItem?.value === value &&
|
||||
selectedItem?.label === label
|
||||
) {
|
||||
state.selectedItem = {
|
||||
label: null,
|
||||
value: null,
|
||||
};
|
||||
context.selectedItem = {
|
||||
label: null,
|
||||
value: null,
|
||||
};
|
||||
} else {
|
||||
state.selectedItem = { label, value };
|
||||
context.selectedItem = { label, value };
|
||||
}
|
||||
|
||||
context.isOpen = false;
|
||||
context.isOpen = false;
|
||||
|
||||
event.stopPropagation();
|
||||
event.stopPropagation();
|
||||
},
|
||||
},
|
||||
addHoverClass: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
const {
|
||||
currentItem: { label, value },
|
||||
} = context;
|
||||
|
||||
context.hoveredItem = { label, value };
|
||||
},
|
||||
removeHoverClass: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
context.hoveredItem = {
|
||||
label: null,
|
||||
value: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
@import "../../../assets/js/blocks/shared/styles/style";
|
||||
|
||||
.wc-interactivity-dropdown {
|
||||
@include includeFormTokenFieldFix();
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
align-items: flex-start;
|
||||
|
||||
.wc-block-components-filter-submit-button {
|
||||
height: 36px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
> svg {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wc-blocks-components-form-token-field-wrapper {
|
||||
flex-grow: 1;
|
||||
max-width: unset;
|
||||
width: 0;
|
||||
height: max-content;
|
||||
|
||||
&:not(.is-loading) {
|
||||
border: 1px solid $gray-700 !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
border-radius: em(4px);
|
||||
}
|
||||
|
||||
.components-form-token-field {
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-blocks-components-form-token-field-wrapper .components-form-token-field__input-container {
|
||||
@include reset-color();
|
||||
@include reset-typography();
|
||||
border: 0;
|
||||
padding: $gap-smaller;
|
||||
border-radius: inherit;
|
||||
|
||||
.components-form-token-field__input {
|
||||
@include font-size(small);
|
||||
|
||||
&::placeholder {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.components-form-token-field__suggestions-list {
|
||||
border: 1px solid $gray-700;
|
||||
border-radius: 4px;
|
||||
margin-top: $gap-smaller;
|
||||
max-height: 21em;
|
||||
|
||||
.components-form-token-field__suggestion {
|
||||
color: $black;
|
||||
border: 1px solid $gray-400;
|
||||
border-radius: 4px;
|
||||
margin: $gap-small;
|
||||
padding: $gap-small;
|
||||
|
||||
&.is-selected,
|
||||
&:hover {
|
||||
background: $gray-100;
|
||||
color: $gray-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.components-form-token-field__token,
|
||||
.components-form-token-field__suggestion {
|
||||
@include font-size(small);
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-product-rating {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.wc-blocks-components-form-token-field-wrapper:not(.single-selection) .components-form-token-field__input-container {
|
||||
padding: $gap-smallest 30px $gap-smallest $gap-smaller;
|
||||
|
||||
.components-form-token-field__token-text {
|
||||
background-color: $white;
|
||||
border: 1px solid;
|
||||
border-right: 0;
|
||||
border-radius: 25px 0 0 25px;
|
||||
padding: em($gap-smallest) em($gap-smaller) em($gap-smallest) em($gap-small);
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
> .components-form-token-field__input {
|
||||
margin: em($gap-smallest) 0;
|
||||
}
|
||||
|
||||
.components-button.components-form-token-field__remove-token {
|
||||
background-color: $white;
|
||||
border: 1px solid;
|
||||
border-left: 0;
|
||||
border-radius: 0 25px 25px 0;
|
||||
padding: 1px em($gap-smallest) 0 0;
|
||||
|
||||
&.has-icon svg {
|
||||
background-color: $gray-200;
|
||||
border-radius: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-stock-filter__actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
justify-content: flex-end;
|
||||
margin-top: $gap;
|
||||
|
||||
// The specificity here is needed to overwrite the margin-top that is inherited on WC block template pages such as Shop.
|
||||
button[type="submit"]:not(.wp-block-search__button).wc-block-components-filter-submit-button {
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
@include font-size(small);
|
||||
}
|
||||
|
||||
.wc-block-stock-filter__button {
|
||||
margin-top: em($gap-smaller);
|
||||
padding: em($gap-smaller) em($gap);
|
||||
@include font-size(small);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-styles-wrapper .wc-block-stock-filter .wc-block-stock-filter__button {
|
||||
margin-top: em($gap-smaller);
|
||||
padding: em($gap-smaller) em($gap);
|
||||
@include font-size(small);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
Comment: Add experimental product rating filter block powered by interactivity API.
|
||||
|
|
@ -46,26 +46,29 @@ final class AssetsController {
|
|||
* Register block scripts & styles.
|
||||
*/
|
||||
public function register_assets() {
|
||||
$this->register_style( 'wc-blocks-packages-style', plugins_url( $this->api->get_block_asset_build_path( 'packages-style', 'css' ), dirname( __DIR__ ) ), [], 'all', true );
|
||||
$this->register_style( 'wc-blocks-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks', 'css' ), dirname( __DIR__ ) ), [], 'all', true );
|
||||
$this->register_style( 'wc-blocks-editor-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-editor-style', 'css' ), dirname( __DIR__ ) ), [ 'wp-edit-blocks' ], 'all', true );
|
||||
$this->register_style( 'wc-blocks-packages-style', plugins_url( $this->api->get_block_asset_build_path( 'packages-style', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
|
||||
$this->register_style( 'wc-blocks-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
|
||||
$this->register_style( 'wc-blocks-editor-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-editor-style', 'css' ), dirname( __DIR__ ) ), array( 'wp-edit-blocks' ), 'all', true );
|
||||
|
||||
$this->api->register_script( 'wc-blocks-middleware', 'assets/client/blocks/wc-blocks-middleware.js', [], false );
|
||||
$this->api->register_script( 'wc-blocks-data-store', 'assets/client/blocks/wc-blocks-data.js', [ 'wc-blocks-middleware' ] );
|
||||
$this->api->register_script( 'wc-blocks-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-vendors' ), [], false );
|
||||
$this->api->register_script( 'wc-blocks-registry', 'assets/client/blocks/wc-blocks-registry.js', [], false );
|
||||
$this->api->register_script( 'wc-blocks', $this->api->get_block_asset_build_path( 'wc-blocks' ), [ 'wc-blocks-vendors' ], false );
|
||||
$this->api->register_script( 'wc-blocks-middleware', 'assets/client/blocks/wc-blocks-middleware.js', array(), false );
|
||||
$this->api->register_script( 'wc-blocks-data-store', 'assets/client/blocks/wc-blocks-data.js', array( 'wc-blocks-middleware' ) );
|
||||
$this->api->register_script( 'wc-blocks-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-vendors' ), array(), false );
|
||||
$this->api->register_script( 'wc-blocks-registry', 'assets/client/blocks/wc-blocks-registry.js', array(), false );
|
||||
$this->api->register_script( 'wc-blocks', $this->api->get_block_asset_build_path( 'wc-blocks' ), array( 'wc-blocks-vendors' ), false );
|
||||
$this->api->register_script( 'wc-blocks-shared-context', 'assets/client/blocks/wc-blocks-shared-context.js' );
|
||||
$this->api->register_script( 'wc-blocks-shared-hocs', 'assets/client/blocks/wc-blocks-shared-hocs.js', [], false );
|
||||
$this->api->register_script( 'wc-blocks-shared-hocs', 'assets/client/blocks/wc-blocks-shared-hocs.js', array(), false );
|
||||
|
||||
// The price package is shared externally so has no blocks prefix.
|
||||
$this->api->register_script( 'wc-price-format', 'assets/client/blocks/price-format.js', [], false );
|
||||
$this->api->register_script( 'wc-price-format', 'assets/client/blocks/price-format.js', array(), false );
|
||||
|
||||
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', [] );
|
||||
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', [] );
|
||||
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', array() );
|
||||
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', array() );
|
||||
|
||||
// Register the interactivity components here for now.
|
||||
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', [] );
|
||||
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() );
|
||||
$this->api->register_script( 'wc-interactivity-checkbox-list', 'assets/client/blocks/wc-interactivity-checkbox-list.js', array() );
|
||||
$this->register_style( 'wc-interactivity-checkbox-list', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-checkbox-list', 'css' ), __DIR__ ), array(), 'all', true );
|
||||
$this->register_style( 'wc-interactivity-dropdown', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-dropdown', 'css' ), __DIR__ ), array(), 'all', true );
|
||||
|
||||
wp_add_inline_script(
|
||||
'wc-blocks-middleware',
|
||||
|
@ -103,7 +106,7 @@ final class AssetsController {
|
|||
* @return array URLs to print for resource hints.
|
||||
*/
|
||||
public function add_resource_hints( $urls, $relation_type ) {
|
||||
if ( ! in_array( $relation_type, [ 'prefetch', 'prerender' ], true ) || is_admin() ) {
|
||||
if ( ! in_array( $relation_type, array( 'prefetch', 'prerender' ), true ) || is_admin() ) {
|
||||
return $urls;
|
||||
}
|
||||
|
||||
|
@ -137,7 +140,7 @@ final class AssetsController {
|
|||
* @return array Array of URLs.
|
||||
*/
|
||||
private function get_prefetch_resource_hints() {
|
||||
$urls = [];
|
||||
$urls = array();
|
||||
|
||||
// Core page IDs.
|
||||
$cart_page_id = wc_get_page_id( 'cart' );
|
||||
|
@ -168,7 +171,7 @@ final class AssetsController {
|
|||
* @return array Array of URLs.
|
||||
*/
|
||||
private function get_prerender_resource_hints() {
|
||||
$urls = [];
|
||||
$urls = array();
|
||||
$is_block_cart = has_block( 'woocommerce/cart' );
|
||||
|
||||
if ( ! $is_block_cart ) {
|
||||
|
@ -193,13 +196,13 @@ final class AssetsController {
|
|||
*/
|
||||
private function get_block_asset_resource_hints( $filename = '' ) {
|
||||
if ( ! $filename ) {
|
||||
return [];
|
||||
return array();
|
||||
}
|
||||
$script_data = $this->api->get_script_data(
|
||||
$this->api->get_block_asset_build_path( $filename )
|
||||
);
|
||||
$resources = array_merge(
|
||||
[ esc_url( add_query_arg( 'ver', $script_data['version'], $script_data['src'] ) ) ],
|
||||
array( esc_url( add_query_arg( 'ver', $script_data['version'], $script_data['src'] ) ) ),
|
||||
$this->get_script_dependency_src_array( $script_data['dependencies'] )
|
||||
);
|
||||
return array_map(
|
||||
|
@ -230,7 +233,7 @@ final class AssetsController {
|
|||
}
|
||||
return $src;
|
||||
},
|
||||
[]
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -289,7 +292,7 @@ final class AssetsController {
|
|||
* 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
|
||||
* @param boolean $rtl Optional. Whether or not to register RTL styles.
|
||||
*/
|
||||
protected function register_style( $handle, $src, $deps = [], $media = 'all', $rtl = false ) {
|
||||
protected function register_style( $handle, $src, $deps = array(), $media = 'all', $rtl = false ) {
|
||||
$filename = str_replace( plugins_url( '/', dirname( __DIR__ ) ), '', $src );
|
||||
$ver = self::get_file_version( $filename );
|
||||
|
||||
|
@ -338,7 +341,7 @@ final class AssetsController {
|
|||
*/
|
||||
public function update_block_settings_dependencies() {
|
||||
$wp_scripts = wp_scripts();
|
||||
$known_packages = [ 'wc-settings', 'wc-blocks-checkout', 'wc-price-format' ];
|
||||
$known_packages = array( 'wc-settings', 'wc-blocks-checkout', 'wc-price-format' );
|
||||
|
||||
foreach ( $wp_scripts->registered as $handle => $script ) {
|
||||
// scripts that are loaded in the footer has extra->group = 1.
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
|
||||
/**
|
||||
* Collection Rating Filter Block
|
||||
*
|
||||
* @package Automattic\WooCommerce\Blocks\BlockTypes
|
||||
*/
|
||||
final class CollectionRatingFilter extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-rating-filter';
|
||||
|
||||
const RATING_FILTER_QUERY_VAR = 'rating_filter';
|
||||
|
||||
/**
|
||||
* Include and render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes. Default empty array.
|
||||
* @param string $content Block content. Default empty string.
|
||||
* @param WP_Block $block Block instance.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
// don't render if its admin, or ajax in progress.
|
||||
if ( is_admin() || wp_doing_ajax() ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$rating_counts = $block->context['collectionData']['rating_counts'] ?? array();
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
|
||||
$selected_ratings_query_param = isset( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ) : '';
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-interactive' => 'woocommerce/collection-rating-filter',
|
||||
'class' => 'wc-block-rating-filter',
|
||||
)
|
||||
);
|
||||
|
||||
$input = 'list' === $display_style ? CheckboxList::render(
|
||||
array(
|
||||
'items' => $this->get_checkbox_list_items( $rating_counts, $selected_ratings_query_param, $show_counts ),
|
||||
'on_change' => 'woocommerce/collection-rating-filter::actions.onCheckboxChange',
|
||||
)
|
||||
) : Dropdown::render(
|
||||
$this->get_dropdown_props( $rating_counts, $selected_ratings_query_param, $show_counts )
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
<div class="wc-block-rating-filter__controls">%2$s</div>
|
||||
<div class="wc-block-rating-filter__actions"></div>
|
||||
</div>',
|
||||
$wrapper_attributes,
|
||||
$input
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the rating label.
|
||||
*
|
||||
* @param int $rating The rating to render.
|
||||
* @param string $count_label The count label to render.
|
||||
* @return string|false
|
||||
*/
|
||||
private function render_rating_label( $rating, $count_label ) {
|
||||
$width = $rating * 20;
|
||||
|
||||
$rating_label = sprintf(
|
||||
/* translators: %1$d is referring to rating value. Example: Rated 4 out of 5. */
|
||||
__( 'Rated %1$d out of 5', 'woocommerce' ),
|
||||
$rating,
|
||||
);
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wc-block-components-product-rating">
|
||||
<div class="wc-block-components-product-rating__stars" role="img" aria-label="<?php echo esc_attr( $rating_label ); ?>">
|
||||
<span style="width: <?php echo esc_attr( $width ); ?>%" aria-hidden="true">
|
||||
</span>
|
||||
</div>
|
||||
<span class="wc-block-components-product-rating-count">
|
||||
<?php echo esc_html( $count_label ); ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkbox list items.
|
||||
*
|
||||
* @param array $rating_counts The rating counts.
|
||||
* @param string $selected_ratings_query The url query param for selected ratings.
|
||||
* @param bool $show_counts Whether to show the counts.
|
||||
* @return array
|
||||
*/
|
||||
private function get_checkbox_list_items( $rating_counts, $selected_ratings_query, $show_counts ) {
|
||||
$ratings_array = explode( ',', $selected_ratings_query );
|
||||
|
||||
return array_map(
|
||||
function( $rating ) use ( $ratings_array, $show_counts ) {
|
||||
$rating_str = (string) $rating['rating'];
|
||||
$count = $rating['count'];
|
||||
$count_label = $show_counts ? "($count)" : '';
|
||||
|
||||
return array(
|
||||
'id' => 'rating-' . $rating_str,
|
||||
'checked' => in_array( $rating_str, $ratings_array, true ),
|
||||
'label' => $this->render_rating_label( (int) $rating_str, $count_label ),
|
||||
'value' => $rating_str,
|
||||
);
|
||||
},
|
||||
$rating_counts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dropdown props.
|
||||
*
|
||||
* @param mixed $rating_counts The rating counts.
|
||||
* @param mixed $selected_ratings_query The url query param for selected ratings.
|
||||
* @param mixed $show_counts Whether to show the counts.
|
||||
* @return array<array-key, array>
|
||||
*/
|
||||
private function get_dropdown_props( $rating_counts, $selected_ratings_query, $show_counts ) {
|
||||
$ratings_array = explode( ',', $selected_ratings_query );
|
||||
|
||||
$selected_item = array_reduce(
|
||||
$rating_counts,
|
||||
function( $carry, $rating ) use ( $ratings_array, $show_counts ) {
|
||||
if ( in_array( (string) $rating['rating'], $ratings_array, true ) ) {
|
||||
$count = $rating['count'];
|
||||
$count_label = $show_counts ? "($count)" : '';
|
||||
$rating_str = (string) $rating['rating'];
|
||||
return array(
|
||||
/* translators: %d is referring to the average rating value. Example: Rated 4 out of 5. */
|
||||
'label' => sprintf( __( 'Rated %d out of 5', 'woocommerce' ), $rating_str ) . ' ' . $count_label,
|
||||
'value' => $rating['rating'],
|
||||
);
|
||||
}
|
||||
return $carry;
|
||||
},
|
||||
array()
|
||||
);
|
||||
|
||||
return array(
|
||||
'items' => array_map(
|
||||
function ( $rating ) use ( $show_counts ) {
|
||||
$count = $rating['count'];
|
||||
$count_label = $show_counts ? "($count)" : '';
|
||||
$rating_str = (string) $rating['rating'];
|
||||
return array(
|
||||
/* translators: %d is referring to the average rating value. Example: Rated 4 out of 5. */
|
||||
'label' => sprintf( __( 'Rated %d out of 5', 'woocommerce' ), $rating_str ) . ' ' . $count_label,
|
||||
'value' => $rating['rating'],
|
||||
);
|
||||
},
|
||||
$rating_counts
|
||||
),
|
||||
'selected_item' => $selected_item,
|
||||
'action' => 'woocommerce/collection-rating-filter::actions.onDropdownChange',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -127,7 +127,7 @@ final class BlockTypesController {
|
|||
*
|
||||
* @param array $allowed_namespaces List of namespaces.
|
||||
*/
|
||||
$allowed_namespaces = array_merge( [ 'woocommerce', 'woocommerce-checkout' ], (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_namespace', [] ) );
|
||||
$allowed_namespaces = array_merge( array( 'woocommerce', 'woocommerce-checkout' ), (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_namespace', array() ) );
|
||||
|
||||
/**
|
||||
* Filters the list of allowed Block Names
|
||||
|
@ -138,17 +138,17 @@ final class BlockTypesController {
|
|||
*
|
||||
* @param array $allowed_namespaces List of namespaces.
|
||||
*/
|
||||
$allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', [] );
|
||||
$allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', array() );
|
||||
|
||||
if ( ! in_array( $block_namespace, $allowed_namespaces, true ) && ! in_array( $block_name, $allowed_blocks, true ) ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$attributes = (array) $block['attrs'];
|
||||
$exclude_attributes = [ 'className', 'align' ];
|
||||
$escaped_data_attributes = [
|
||||
$exclude_attributes = array( 'className', 'align' );
|
||||
$escaped_data_attributes = array(
|
||||
'data-block-name="' . esc_attr( $block['blockName'] ) . '"',
|
||||
];
|
||||
);
|
||||
|
||||
foreach ( $attributes as $key => $value ) {
|
||||
if ( in_array( $key, $exclude_attributes, true ) ) {
|
||||
|
@ -215,7 +215,7 @@ final class BlockTypesController {
|
|||
protected function get_block_types() {
|
||||
global $pagenow;
|
||||
|
||||
$block_types = [
|
||||
$block_types = array(
|
||||
'ActiveFilters',
|
||||
'AddToCartForm',
|
||||
'AllProducts',
|
||||
|
@ -281,7 +281,7 @@ final class BlockTypesController {
|
|||
'OrderConfirmation\BillingWrapper',
|
||||
'OrderConfirmation\ShippingWrapper',
|
||||
'OrderConfirmation\AdditionalInformation',
|
||||
];
|
||||
);
|
||||
|
||||
$block_types = array_merge(
|
||||
$block_types,
|
||||
|
@ -300,29 +300,30 @@ final class BlockTypesController {
|
|||
$block_types[] = 'CollectionStockFilter';
|
||||
$block_types[] = 'CollectionPriceFilter';
|
||||
$block_types[] = 'CollectionAttributeFilter';
|
||||
$block_types[] = 'CollectionRatingFilter';
|
||||
}
|
||||
|
||||
/**
|
||||
* This disables specific blocks in Widget Areas by not registering them.
|
||||
*/
|
||||
if ( in_array( $pagenow, [ 'widgets.php', 'themes.php', 'customize.php' ], true ) && ( empty( $_GET['page'] ) || 'gutenberg-edit-site' !== $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
if ( in_array( $pagenow, array( 'widgets.php', 'themes.php', 'customize.php' ), true ) && ( empty( $_GET['page'] ) || 'gutenberg-edit-site' !== $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
$block_types = array_diff(
|
||||
$block_types,
|
||||
[
|
||||
array(
|
||||
'AllProducts',
|
||||
'Cart',
|
||||
'Checkout',
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This disables specific blocks in Post and Page editor by not registering them.
|
||||
*/
|
||||
if ( in_array( $pagenow, [ 'post.php', 'post-new.php' ], true ) ) {
|
||||
if ( in_array( $pagenow, array( 'post.php', 'post-new.php' ), true ) ) {
|
||||
$block_types = array_diff(
|
||||
$block_types,
|
||||
[
|
||||
array(
|
||||
'AddToCartForm',
|
||||
'Breadcrumbs',
|
||||
'CatalogSorting',
|
||||
|
@ -340,7 +341,7 @@ final class BlockTypesController {
|
|||
'OrderConfirmation\BillingWrapper',
|
||||
'OrderConfirmation\ShippingWrapper',
|
||||
'OrderConfirmation\AdditionalInformation',
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\InteractivityComponents;
|
||||
|
||||
/**
|
||||
* CheckboxList class. This is a component for reuse with interactivity API.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Blocks\InteractivityComponents
|
||||
*/
|
||||
class CheckboxList {
|
||||
/**
|
||||
* Render the checkbox list.
|
||||
*
|
||||
* @param mixed $props The properties to render the dropdown with.
|
||||
* items: array of objects with label and value properties.
|
||||
* - id: string of the id to use for the checkbox (optional).
|
||||
* - checked: boolean to indicate if the checkbox is checked.
|
||||
* - label: string of the label to display (plaintext or HTML).
|
||||
* - value: string of the value to use.
|
||||
* on_change: string of the action to perform when the dropdown changes.
|
||||
* @return string|false
|
||||
*/
|
||||
public static function render( $props ) {
|
||||
wp_enqueue_script( 'wc-interactivity-checkbox-list' );
|
||||
wp_enqueue_style( 'wc-interactivity-checkbox-list' );
|
||||
|
||||
$items = $props['items'] ?? array();
|
||||
|
||||
$checkbox_list_context = array( 'items' => $items );
|
||||
|
||||
// Items should be an array of objects with a label (which can be plaintext or HTML) and value property.
|
||||
$items = $props['items'] ?? array();
|
||||
|
||||
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ) );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div data-wc-interactive='<?php echo esc_attr( $namespace ); ?>'>
|
||||
<div data-wc-context='<?php echo esc_attr( wp_json_encode( $checkbox_list_context ) ); ?>' >
|
||||
<div class="wc-block-stock-filter style-list">
|
||||
<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">
|
||||
<?php foreach ( $items as $item ) { ?>
|
||||
<?php $item['id'] = $item['id'] ?? uniqid( 'checkbox-' ); ?>
|
||||
<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<label for="<?php echo esc_attr( $item['id'] ); ?>">
|
||||
<input
|
||||
id="<?php echo esc_attr( $item['id'] ); ?>"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change--select-item="actions.selectCheckboxItem"
|
||||
data-wc-on--change--parent-action="<?php echo esc_attr( $props['on_change'] ?? '' ); ?>"
|
||||
value="<?php echo esc_attr( $item['value'] ); ?>"
|
||||
<?php checked( $item['checked'], 1 ); ?>
|
||||
>
|
||||
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20">
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path>
|
||||
</svg>
|
||||
<span class="wc-block-components-checkbox__label">
|
||||
<?php // The label can be HTML, so we don't want to escape it. ?>
|
||||
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php echo $item['label']; ?>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ class Dropdown {
|
|||
*/
|
||||
public static function render( $props ) {
|
||||
wp_enqueue_script( 'wc-interactivity-dropdown' );
|
||||
wp_enqueue_style( 'wc-interactivity-dropdown' );
|
||||
|
||||
$selected_item = $props['selected_item'] ?? array(
|
||||
'label' => null,
|
||||
|
@ -31,15 +32,19 @@ class Dropdown {
|
|||
'isOpen' => false,
|
||||
);
|
||||
|
||||
wc_initial_state( 'woocommerce/interactivity-dropdown', array( 'selectedItem' => $selected_item ) );
|
||||
|
||||
$action = $props['action'] ?? '';
|
||||
|
||||
// Items should be an array of objects with a label and value property.
|
||||
$items = $props['items'] ?? [];
|
||||
$items = $props['items'] ?? array();
|
||||
|
||||
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ) );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div data-wc-interactive='<?php echo wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ) ); ?>'>
|
||||
<div class="wc-block-stock-filter style-dropdown" data-wc-context='<?php echo wp_json_encode( $dropdown_context ); ?>' >
|
||||
<div data-wc-interactive='<?php echo esc_attr( $namespace ); ?>'>
|
||||
<div class="wc-interactivity-dropdown" data-wc-context='<?php echo esc_attr( wp_json_encode( $dropdown_context ) ); ?>' >
|
||||
<div class="wc-blocks-components-form-token-field-wrapper single-selection" >
|
||||
<div class="components-form-token-field" tabindex="-1">
|
||||
<div class="components-form-token-field__input-container"
|
||||
|
@ -59,14 +64,14 @@ class Dropdown {
|
|||
role="option"
|
||||
data-wc-on--click--select-item="actions.selectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
data-wc-class--is-selected="state.isSelected"
|
||||
data-wc-on--mouseover="actions.addHoverClass"
|
||||
data-wc-on--mouseout="actions.removeHoverClass"
|
||||
data-wc-context='<?php echo wp_json_encode( $context ); ?>'
|
||||
data-wc-class--is-selected="state.isSelected"
|
||||
data-wc-context='<?php echo esc_attr( wp_json_encode( $context ) ); ?>'
|
||||
class="components-form-token-field__suggestion"
|
||||
data-wc-bind--aria-selected="state.isSelected"
|
||||
>
|
||||
<?php echo esc_html( $item['label'] ); ?>
|
||||
<?php // This attribute supports HTML so should be sanitized by caller. ?>
|
||||
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php echo $item['label']; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
|
Loading…
Reference in New Issue