UNPKG

@wordpress/block-library

Version:
190 lines (186 loc) 6.07 kB
/** * WordPress dependencies */ import { FormTokenField, __experimentalVStack as VStack } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { useState, useEffect } from '@wordpress/element'; import { useDebounce } from '@wordpress/compose'; import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies */ import { useTaxonomies } from '../../utils'; import { jsx as _jsx } from "react/jsx-runtime"; const EMPTY_ARRAY = []; const BASE_QUERY = { order: 'asc', _fields: 'id,name', context: 'view' }; // Helper function to get the term id based on user input in terms `FormTokenField`. const getTermIdByTermValue = (terms, termValue) => { // First we check for exact match by `term.id` or case sensitive `term.name` match. const termId = termValue?.id || terms?.find(term => term.name === termValue)?.id; if (termId) { return termId; } /** * Here we make an extra check for entered terms in a non case sensitive way, * to match user expectations, due to `FormTokenField` behaviour that shows * suggestions which are case insensitive. * * Although WP tries to discourage users to add terms with the same name (case insensitive), * it's still possible if you manually change the name, as long as the terms have different slugs. * In this edge case we always apply the first match from the terms list. */ const termValueLower = termValue.toLocaleLowerCase(); return terms?.find(term => term.name.toLocaleLowerCase() === termValueLower)?.id; }; export function TaxonomyControls({ onChange, query }) { const { postType, taxQuery } = query; const taxonomies = useTaxonomies(postType); if (!taxonomies || taxonomies.length === 0) { return null; } return /*#__PURE__*/_jsx(VStack, { spacing: 4, children: taxonomies.map(taxonomy => { const termIds = taxQuery?.[taxonomy.slug] || []; const handleChange = newTermIds => onChange({ taxQuery: { ...taxQuery, [taxonomy.slug]: newTermIds } }); return /*#__PURE__*/_jsx(TaxonomyItem, { taxonomy: taxonomy, termIds: termIds, onChange: handleChange }, taxonomy.slug); }) }); } /** * Renders a `FormTokenField` for a given taxonomy. * * @param {Object} props The props for the component. * @param {Object} props.taxonomy The taxonomy object. * @param {number[]} props.termIds An array with the block's term ids for the given taxonomy. * @param {Function} props.onChange Callback `onChange` function. * @return {JSX.Element} The rendered component. */ function TaxonomyItem({ taxonomy, termIds, onChange }) { const [search, setSearch] = useState(''); const [value, setValue] = useState(EMPTY_ARRAY); const [suggestions, setSuggestions] = useState(EMPTY_ARRAY); const debouncedSearch = useDebounce(setSearch, 250); const { searchResults, searchHasResolved } = useSelect(select => { if (!search) { return { searchResults: EMPTY_ARRAY, searchHasResolved: true }; } const { getEntityRecords, hasFinishedResolution } = select(coreStore); const selectorArgs = ['taxonomy', taxonomy.slug, { ...BASE_QUERY, search, orderby: 'name', exclude: termIds, per_page: 20 }]; return { searchResults: getEntityRecords(...selectorArgs), searchHasResolved: hasFinishedResolution('getEntityRecords', selectorArgs) }; }, [search, taxonomy.slug, termIds]); // `existingTerms` are the ones fetched from the API and their type is `{ id: number; name: string }`. // They are used to extract the terms' names to populate the `FormTokenField` properly // and to sanitize the provided `termIds`, by setting only the ones that exist. const existingTerms = useSelect(select => { if (!termIds?.length) { return EMPTY_ARRAY; } const { getEntityRecords } = select(coreStore); return getEntityRecords('taxonomy', taxonomy.slug, { ...BASE_QUERY, include: termIds, per_page: termIds.length }); }, [taxonomy.slug, termIds]); // Update the `value` state only after the selectors are resolved // to avoid emptying the input when we're changing terms. useEffect(() => { if (!termIds?.length) { setValue(EMPTY_ARRAY); } if (!existingTerms?.length) { return; } // Returns only the existing entity ids. This prevents the component // from crashing in the editor, when non existing ids are provided. const sanitizedValue = termIds.reduce((accumulator, id) => { const entity = existingTerms.find(term => term.id === id); if (entity) { accumulator.push({ id, value: entity.name }); } return accumulator; }, []); setValue(sanitizedValue); }, [termIds, existingTerms]); // Update suggestions only when the query has resolved. useEffect(() => { if (!searchHasResolved) { return; } setSuggestions(searchResults.map(result => result.name)); }, [searchResults, searchHasResolved]); const onTermsChange = newTermValues => { const newTermIds = new Set(); for (const termValue of newTermValues) { const termId = getTermIdByTermValue(searchResults, termValue); if (termId) { newTermIds.add(termId); } } setSuggestions(EMPTY_ARRAY); onChange(Array.from(newTermIds)); }; return /*#__PURE__*/_jsx("div", { className: "block-library-query-inspector__taxonomy-control", children: /*#__PURE__*/_jsx(FormTokenField, { label: taxonomy.name, value: value, onInputChange: debouncedSearch, suggestions: suggestions, displayTransform: decodeEntities, onChange: onTermsChange, __experimentalShowHowTo: false, __nextHasNoMarginBottom: true, __next40pxDefaultSize: true }) }); } //# sourceMappingURL=taxonomy-controls.js.map