File "block.js"
Full Path: /home/vantageo/public_html/cache/.wp-cli/wp-content/plugins/woocommerce/packages/woocommerce-blocks/assets/js/blocks/attribute-filter/block.js
File size: 9.64 KB
MIME-type: text/x-java
Charset: utf-8
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { speak } from '@wordpress/a11y';
import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks';
import {
useCollection,
useQueryStateByKey,
useQueryStateByContext,
useCollectionData,
} from '@woocommerce/base-context/hooks';
import { useCallback, useEffect, useState, useMemo } from '@wordpress/element';
import CheckboxList from '@woocommerce/base-components/checkbox-list';
import DropdownSelector from '@woocommerce/base-components/dropdown-selector';
import FilterSubmitButton from '@woocommerce/base-components/filter-submit-button';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
*/
import { getAttributeFromID } from '../../utils/attributes';
import { updateAttributeFilter } from '../../utils/attributes-query';
import Label from './label';
import { previewAttributeObject, previewOptions } from './preview';
import './style.scss';
/**
* Component displaying an attribute filter.
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.attributes Incoming block attributes.
* @param {boolean} props.isEditor
*/
const AttributeFilterBlock = ( {
attributes: blockAttributes,
isEditor = false,
} ) => {
const attributeObject =
blockAttributes.isPreview && ! blockAttributes.attributeId
? previewAttributeObject
: getAttributeFromID( blockAttributes.attributeId );
const [ checked, setChecked ] = useState( [] );
const [ displayedOptions, setDisplayedOptions ] = useState(
blockAttributes.isPreview && ! blockAttributes.attributeId
? previewOptions
: []
);
const [ queryState ] = useQueryStateByContext();
const [
productAttributesQuery,
setProductAttributesQuery,
] = useQueryStateByKey( 'attributes', [] );
const {
results: attributeTerms,
isLoading: attributeTermsLoading,
} = useCollection( {
namespace: '/wc/store',
resourceName: 'products/attributes/terms',
resourceValues: [ attributeObject.id ],
shouldSelect: blockAttributes.attributeId > 0,
} );
const filterAvailableTerms =
blockAttributes.displayStyle !== 'dropdown' &&
blockAttributes.queryType === 'and';
const {
results: filteredCounts,
isLoading: filteredCountsLoading,
} = useCollectionData( {
queryAttribute: {
taxonomy: attributeObject.taxonomy,
queryType: blockAttributes.queryType,
},
queryState: {
...queryState,
attributes: filterAvailableTerms ? queryState.attributes : null,
},
} );
/**
* Get count data about a given term by ID.
*/
const getFilteredTerm = useCallback(
( id ) => {
if ( ! filteredCounts.attribute_counts ) {
return null;
}
return filteredCounts.attribute_counts.find(
( { term } ) => term === id
);
},
[ filteredCounts ]
);
/**
* Compare intersection of all terms and filtered counts to get a list of options to display.
*/
useEffect( () => {
/**
* Checks if a term slug is in the query state.
*
* @param {string} termSlug The term of the slug to check.
*/
const isTermInQueryState = ( termSlug ) => {
if ( ! queryState?.attributes ) {
return false;
}
return queryState.attributes.some(
( { attribute, slug = [] } ) =>
attribute === attributeObject.taxonomy &&
slug.includes( termSlug )
);
};
if ( attributeTermsLoading || filteredCountsLoading ) {
return;
}
const newOptions = attributeTerms
.map( ( term ) => {
const filteredTerm = getFilteredTerm( term.id );
// If there is no match this term doesn't match the current product collection - only render if checked.
if (
! filteredTerm &&
! checked.includes( term.slug ) &&
! isTermInQueryState( term.slug )
) {
return null;
}
const count = filteredTerm ? filteredTerm.count : 0;
return {
value: term.slug,
name: decodeEntities( term.name ),
label: (
<Label
name={ decodeEntities( term.name ) }
count={ blockAttributes.showCounts ? count : null }
/>
),
};
} )
.filter( Boolean );
setDisplayedOptions( newOptions );
}, [
attributeObject.taxonomy,
attributeTerms,
attributeTermsLoading,
blockAttributes.showCounts,
filteredCountsLoading,
getFilteredTerm,
checked,
queryState.attributes,
] );
// Track checked STATE changes - if state changes, update the query.
useEffect( () => {
if ( ! blockAttributes.showFilterButton ) {
onSubmit( checked );
}
}, [ blockAttributes.showFilterButton, checked, onSubmit ] );
const checkedQuery = useMemo( () => {
return productAttributesQuery
.filter(
( { attribute } ) => attribute === attributeObject.taxonomy
)
.flatMap( ( { slug } ) => slug );
}, [ productAttributesQuery, attributeObject.taxonomy ] );
const currentCheckedQuery = useShallowEqual( checkedQuery );
const previousCheckedQuery = usePrevious( currentCheckedQuery );
// Track ATTRIBUTES QUERY changes so the block reflects current filters.
useEffect( () => {
if (
! isShallowEqual( previousCheckedQuery, currentCheckedQuery ) && // checked query changed
! isShallowEqual( checked, currentCheckedQuery ) // checked query doesn't match the UI
) {
setChecked( currentCheckedQuery );
}
}, [ checked, currentCheckedQuery, previousCheckedQuery ] );
/**
* Returns an array of term objects that have been chosen via the checkboxes.
*/
const getSelectedTerms = useCallback(
( newChecked ) => {
return attributeTerms.reduce( ( acc, term ) => {
if ( newChecked.includes( term.slug ) ) {
acc.push( term );
}
return acc;
}, [] );
},
[ attributeTerms ]
);
const onSubmit = useCallback(
( isChecked ) => {
if ( isEditor ) {
return;
}
updateAttributeFilter(
productAttributesQuery,
setProductAttributesQuery,
attributeObject,
getSelectedTerms( isChecked ),
blockAttributes.queryType === 'or' ? 'in' : 'and'
);
},
[
isEditor,
productAttributesQuery,
setProductAttributesQuery,
attributeObject,
getSelectedTerms,
blockAttributes.queryType,
]
);
const multiple =
blockAttributes.displayStyle !== 'dropdown' ||
blockAttributes.queryType === 'or';
/**
* When a checkbox in the list changes, update state.
*/
const onChange = useCallback(
( checkedValue ) => {
const getFilterNameFromValue = ( filterValue ) => {
const { name } = displayedOptions.find(
( option ) => option.value === filterValue
);
return name;
};
const announceFilterChange = ( { filterAdded, filterRemoved } ) => {
const filterAddedName = filterAdded
? getFilterNameFromValue( filterAdded )
: null;
const filterRemovedName = filterRemoved
? getFilterNameFromValue( filterRemoved )
: null;
if ( filterAddedName && filterRemovedName ) {
speak(
sprintf(
/* translators: %1$s and %2$s are attribute terms (for example: 'red', 'blue', 'large'...). */
__(
'%1$s filter replaced with %2$s.',
'woocommerce'
),
filterAddedName,
filterRemovedName
)
);
} else if ( filterAddedName ) {
speak(
sprintf(
/* translators: %s attribute term (for example: 'red', 'blue', 'large'...) */
__(
'%s filter added.',
'woocommerce'
),
filterAddedName
)
);
} else if ( filterRemovedName ) {
speak(
sprintf(
/* translators: %s attribute term (for example: 'red', 'blue', 'large'...) */
__(
'%s filter removed.',
'woocommerce'
),
filterRemovedName
)
);
}
};
const previouslyChecked = checked.includes( checkedValue );
let newChecked;
if ( ! multiple ) {
newChecked = previouslyChecked ? [] : [ checkedValue ];
const filterAdded = previouslyChecked ? null : checkedValue;
const filterRemoved =
checked.length === 1 ? checked[ 0 ] : null;
announceFilterChange( { filterAdded, filterRemoved } );
} else {
newChecked = checked.filter(
( value ) => value !== checkedValue
);
if ( ! previouslyChecked ) {
newChecked.push( checkedValue );
newChecked.sort();
announceFilterChange( { filterAdded: checkedValue } );
} else {
announceFilterChange( { filterRemoved: checkedValue } );
}
}
setChecked( newChecked );
},
[ checked, displayedOptions, multiple ]
);
if ( displayedOptions.length === 0 && ! attributeTermsLoading ) {
return null;
}
const TagName = `h${ blockAttributes.headingLevel }`;
const isLoading = ! blockAttributes.isPreview && attributeTermsLoading;
const isDisabled = ! blockAttributes.isPreview && filteredCountsLoading;
return (
<>
{ ! isEditor && blockAttributes.heading && (
<TagName>{ blockAttributes.heading }</TagName>
) }
<div className="wc-block-attribute-filter">
{ blockAttributes.displayStyle === 'dropdown' ? (
<DropdownSelector
attributeLabel={ attributeObject.label }
checked={ checked }
className={ 'wc-block-attribute-filter-dropdown' }
inputLabel={ blockAttributes.heading }
isLoading={ isLoading }
multiple={ multiple }
onChange={ onChange }
options={ displayedOptions }
/>
) : (
<CheckboxList
className={ 'wc-block-attribute-filter-list' }
options={ displayedOptions }
checked={ checked }
onChange={ onChange }
isLoading={ isLoading }
isDisabled={ isDisabled }
/>
) }
{ blockAttributes.showFilterButton && (
<FilterSubmitButton
className="wc-block-attribute-filter__button"
disabled={ isLoading || isDisabled }
onClick={ () => onSubmit( checked ) }
/>
) }
</div>
</>
);
};
export default AttributeFilterBlock;