PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / assets / js / features / components / control.js

control.js in ElasticPress 5.3.5, at assets/js/features/components/control.js

305 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WordPress dependencies.
3 */
4 import {
5 CheckboxControl,
6 FormTokenField,
7 Notice,
8 RadioControl,
9 SelectControl,
10 TextControl,
11 TextareaControl,
12 ToggleControl,
13 } from '@wordpress/components';
14 import { safeHTML } from '@wordpress/dom';
15 import { RawHTML, WPElement } from '@wordpress/element';
16 import { _n, __, _x, sprintf } from '@wordpress/i18n';
17
18 /**
19 * Internal dependencies.
20 */
21 import { useFeatureSettings } from '../provider';
22
23 /**
24 * Control component.
25 *
26 * @param {object} props Component props.
27 * @param {boolean} props.disabled Whether the control is disabled.
28 * @param {string} props.help Control help text.
29 * @param {string} props.label Control label.
30 * @param {string} props.name Setting name.
31 * @param {Function} props.onChange Change event handler.
32 * @param {Array|null} props.options (optional) Control options.
33 * @param {false|string} props.requiresFeature Any features required by this setting.
34 * @param {boolean} props.requiresSync Whether setting changes require a sync.
35 * @param {boolean|string} props.syncedValue Setting value at last sync.
36 * @param {string} props.type Control type.
37 * @param {boolean|string} props.value Setting value.
38 * @returns {WPElement} Reports component.
39 */
40 const Control = ({
41 disabled,
42 help,
43 label,
44 name,
45 onChange,
46 options,
47 requiresFeature,
48 requiresSync,
49 syncedValue,
50 type,
51 value,
52 }) => {
53 const { getFeature, isBusy, settings, willSettingRequireSync } = useFeatureSettings();
54
55 // Convert single feature requirement to array for compatibility
56 let requiredFeaturesList = [];
57 if (requiresFeature) {
58 requiredFeaturesList = Array.isArray(requiresFeature) ? requiresFeature : [requiresFeature];
59 }
60
61 /**
62 * Get missing required features.
63 */
64 const missingRequiredFeatures = requiredFeaturesList
65 .map((featureSlug) => getFeature(featureSlug))
66 .filter((feature) => !feature.isAvailable || settings[feature.slug]?.active !== true);
67
68 /**
69 * Help text formatted to allow safe HTML.
70 */
71 const helpHtml = help ? (
72 <span dangerouslySetInnerHTML={{ __html: safeHTML(help) }} /> // eslint-disable-line react/no-danger
73 ) : null;
74
75 /**
76 * Options formatted for radio controls to allow safe HTML in labels.
77 */
78 const radioOptions = options
79 ? options.map((o) => {
80 return {
81 value: o.value,
82 label: <span dangerouslySetInnerHTML={{ __html: safeHTML(o.label) }} />, // eslint-disable-line react/no-danger
83 };
84 })
85 : [];
86
87 const titles = missingRequiredFeatures.map((f) => f.shortTitle);
88
89 /**
90 * The notice to display if a feature is required.
91 */
92 const requiredFeatureNotice =
93 name === 'active'
94 ? /* translators: %s: feature list */
95 _n(
96 'The %s feature must be enabled to use this feature.',
97 'The %s features must be enabled to use this feature.',
98 titles.length,
99 'elasticpress',
100 )
101 : /* translators: %s: feature list */
102 _n(
103 'The %s feature must be enabled to use the following setting.',
104 'The %s features must be enabled to use the following setting.',
105 titles.length,
106 'elasticpress',
107 );
108
109 /**
110 * The notice to display if a sync is required.
111 */
112 const syncNotice =
113 name === 'active'
114 ? __('Enabling this feature requires re-syncing your content.', 'elasticpress')
115 : __('A change to following setting requires re-syncing your content.', 'elasticpress');
116
117 /**
118 * Whether the control is disabled.
119 */
120 const isDisabled = isBusy || disabled || missingRequiredFeatures.length > 0;
121
122 /**
123 * Whether the selected value for this setting will require a sync.
124 */
125 const willRequireSync = willSettingRequireSync(value, syncedValue, requiresSync);
126
127 /**
128 * Handle change to checkbox values.
129 *
130 * @param {boolean} checked Whether checkbox is checked.
131 */
132 const onChangeCheckbox = (checked) => {
133 const value = checked ? '1' : '0';
134
135 onChange(value);
136 };
137
138 /**
139 * Handle change to token field values.
140 *
141 * The FormTokenField control does not support separate values and labels,
142 * so whenever a change is made we need to set the field value based on the
143 * selected label.
144 *
145 * @param {string[]} values Selected values.
146 */
147 const onChangeFormTokenField = (values) => {
148 const value = values
149 .map((v) => options.find((o) => o.label === v)?.value)
150 .filter(Boolean)
151 .join(',');
152
153 onChange(value);
154 };
155
156 const list = (() => {
157 if (titles.length === 0) {
158 return '';
159 }
160 if (titles.length === 1) {
161 return titles[0];
162 }
163 if (titles.length === 2) {
164 return sprintf(
165 /* translators: %1$s: feature name, %2$s: last feature name */
166 _x('%1$s and %2$s', 'two feature names', 'elasticpress'),
167 titles[0],
168 titles[1],
169 );
170 }
171 return sprintf(
172 /* translators: %1$s: feature names, %2$s: last feature name */
173 _x('%1$s and %2$s', 'multiple feature names', 'elasticpress'),
174 titles.slice(0, -1).join(__(', ', 'elasticpress')),
175 titles[titles.length - 1],
176 );
177 })();
178
179 return (
180 <>
181 {missingRequiredFeatures.length > 0 ? (
182 <Notice isDismissible={false} status={name === 'active' ? 'error' : 'warning'}>
183 {sprintf(requiredFeatureNotice, list)}
184 </Notice>
185 ) : null}
186 {willRequireSync ? (
187 <Notice isDismissible={false} status="warning">
188 {syncNotice}
189 </Notice>
190 ) : null}
191 <div className="ep-dashboard-control">
192 {(() => {
193 switch (type) {
194 case 'checkbox': {
195 return (
196 <CheckboxControl
197 checked={value === '1'}
198 help={helpHtml}
199 label={label}
200 onChange={onChangeCheckbox}
201 disabled={isDisabled}
202 __nextHasNoMarginBottom
203 />
204 );
205 }
206 case 'hidden': {
207 return null;
208 }
209 case 'markup': {
210 return <RawHTML>{safeHTML(label)}</RawHTML>;
211 }
212 case 'multiple': {
213 const suggestions = options.map((o) => o.label);
214 const values = value
215 .split(',')
216 .map((v) => options.find((o) => o.value === v)?.label)
217 .filter(Boolean);
218
219 return (
220 <FormTokenField
221 __experimentalExpandOnFocus
222 help=""
223 label={label}
224 onChange={onChangeFormTokenField}
225 disabled={isDisabled}
226 suggestions={suggestions}
227 value={values}
228 __nextHasNoMarginBottom
229 __next40pxDefaultSize
230 />
231 );
232 }
233 case 'radio': {
234 return (
235 <RadioControl
236 help={helpHtml}
237 label={label}
238 onChange={onChange}
239 options={radioOptions}
240 disabled={isDisabled}
241 selected={value}
242 />
243 );
244 }
245 case 'select': {
246 return (
247 <SelectControl
248 help={helpHtml}
249 label={label}
250 onChange={onChange}
251 options={options}
252 disabled={isDisabled}
253 value={value}
254 __nextHasNoMarginBottom
255 __next40pxDefaultSize
256 />
257 );
258 }
259 case 'toggle': {
260 return (
261 <ToggleControl
262 checked={value}
263 help={helpHtml}
264 label={label}
265 onChange={onChange}
266 disabled={isDisabled}
267 __nextHasNoMarginBottom
268 />
269 );
270 }
271 case 'textarea': {
272 return (
273 <TextareaControl
274 help={helpHtml}
275 label={label}
276 onChange={onChange}
277 disabled={isDisabled}
278 value={value}
279 __nextHasNoMarginBottom
280 />
281 );
282 }
283 default: {
284 return (
285 <TextControl
286 help={helpHtml}
287 label={label}
288 onChange={onChange}
289 disabled={isDisabled}
290 value={value}
291 type={type}
292 __nextHasNoMarginBottom
293 __next40pxDefaultSize
294 />
295 );
296 }
297 }
298 })()}
299 </div>
300 </>
301 );
302 };
303
304 export default Control;
305