-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
187 lines (177 loc) · 5.65 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import { FormTokenField } from '@wordpress/components';
import { useDebounce } from '@wordpress/compose';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { __, sprintf } from '@wordpress/i18n';
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`.
*
* @param {Array} terms Array of terms from the search results.
* @param {string|object} termValue Single term name or object.
*
* @returns {number} The term ID.
*/
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;
};
/**
* Renders a `FormTokenField` for a given taxonomy. Based on the Query Loop block taxonomy controls.
* https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/query/edit/inspector-controls/taxonomy-controls.js
*
* @param {object} props The props for the component.
* @param {string} props.label The label text for the search field.
* @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.
*
* @returns {Element} The rendered component.
*/
function TermSearchControl( { label, 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 taxObject = useSelect(
( select ) => {
return select( 'core' ).getTaxonomy( taxonomy );
},
[ taxonomy ]
);
const { searchResults, searchHasResolved } = useSelect(
( select ) => {
if ( ! search ) {
return {
searchResults: EMPTY_ARRAY,
searchHasResolved: true,
};
}
const { getEntityRecords, hasFinishedResolution } = select( coreStore );
const selectorArgs = [
'taxonomy',
taxonomy,
{
...BASE_QUERY,
search,
orderby: 'name',
exclude: termIds,
per_page: 20,
},
];
return {
searchResults: getEntityRecords( ...selectorArgs ),
searchHasResolved: hasFinishedResolution(
'getEntityRecords',
selectorArgs
),
};
},
[ search, 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, {
...BASE_QUERY,
include: termIds,
per_page: termIds.length,
} );
},
[ 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 ] );
/**
* Function to handle change of selected terms.
*
* @param {Array} newTermValues Array of new term values.
*/
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 (
<FormTokenField
displayTransform={ decodeEntities }
label={
label ||
sprintf(
__( 'Filter by %s', 'block-editor-components' ),
taxObject
? taxObject?.labels?.singular_name
: __( 'term', 'block-editor-components' )
)
}
suggestions={ suggestions }
value={ value }
onChange={ onTermsChange }
onInputChange={ debouncedSearch }
/>
);
}
export default TermSearchControl;