@siratik/wp-component-library 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -0
- package/README.md +51 -5
- package/components/attachment-image/README.md +118 -0
- package/components/attachment-image/index.js +105 -0
- package/components/attributes-panel/README.md +92 -0
- package/components/attributes-panel/index.js +324 -0
- package/components/badge/README.md +61 -0
- package/components/badge/index.js +105 -0
- package/components/block-appender/README.md +158 -0
- package/components/block-appender/index.js +135 -0
- package/components/block-extension/index.js +214 -0
- package/components/business-hours/SpecialHoursControl.js +211 -0
- package/components/business-hours/WeeklyHoursControl.js +419 -0
- package/components/business-hours/index.js +8 -0
- package/components/button/README.md +172 -0
- package/components/button/index.js +84 -0
- package/components/captcha-placeholder/CloudflareTurnstile.js +111 -0
- package/components/captcha-placeholder/GoogleRecaptcha.js +132 -0
- package/components/captcha-placeholder/HCaptcha.js +99 -0
- package/components/captcha-placeholder/README.md +228 -0
- package/components/captcha-placeholder/index.js +39 -0
- package/components/captcha-placeholder/styles.js +33 -0
- package/components/geocoder/AddressEntryControl.js +390 -0
- package/components/geocoder/GeocoderSearchControl.js +353 -0
- package/components/geocoder/countryOptions.js +66 -0
- package/components/geocoder/geocoder.js +400 -0
- package/components/geocoder/index.js +16 -0
- package/components/icon-picker/Icon.js +53 -0
- package/components/icon-picker/IconPicker.js +70 -0
- package/components/icon-picker/IconPickerModal.js +474 -0
- package/components/icon-picker/IconPickerToolbarButton.js +60 -0
- package/components/icon-picker/InlineIconPicker.js +92 -0
- package/components/icon-picker/api.js +181 -0
- package/components/icon-picker/config-bootstrap.js +108 -0
- package/components/icon-picker/hooks.js +157 -0
- package/components/icon-picker/index.js +39 -0
- package/components/icon-picker/load-script.js +33 -0
- package/components/icon-picker/loaders.js +79 -0
- package/components/icon-picker/store.js +223 -0
- package/components/icon-picker/styles.js +232 -0
- package/components/icon-picker/utils.js +138 -0
- package/components/layout/README.md +268 -0
- package/components/link-tools-panel/README.md +49 -0
- package/components/link-tools-panel/index.js +198 -0
- package/components/link-tools-panel/styles.js +90 -0
- package/components/media/README.md +288 -0
- package/components/media/SettingsMediaUpload.js +187 -0
- package/components/media/editor-media-upload.js +65 -0
- package/components/media/image-controls.js +196 -0
- package/components/media/index.js +13 -0
- package/components/media/inspector-media-upload.js +290 -0
- package/components/media/toolbar-media-upload.js +53 -0
- package/components/media/utils/aspect-ratios.js +118 -0
- package/components/meta/MetaPanel.js +75 -0
- package/components/meta/README.md +46 -0
- package/components/meta/index.js +6 -0
- package/components/meta/meta-gated-editor.js +282 -0
- package/components/query/README.md +340 -0
- package/components/query/cardTemplatePartPanel.js +69 -0
- package/components/query/cardTemplatePartSelect.js +165 -0
- package/components/query/cardTemplatePartUtils.js +119 -0
- package/components/query/columnCountControl.js +63 -0
- package/components/query/displayTypeSelect.js +52 -0
- package/components/query/examples.js +166 -0
- package/components/query/hideCurrentPostControl.js +47 -0
- package/components/query/index.js +20 -0
- package/components/query/manualPostSelector.js +125 -0
- package/components/query/manualTermSelector.js +141 -0
- package/components/query/orderBySelect.js +95 -0
- package/components/query/postTypeSelect.js +67 -0
- package/components/query/postsPerPageControl.js +51 -0
- package/components/query/selectionModeControl.js +61 -0
- package/components/query/taxonomySelect.js +139 -0
- package/components/query/utils.js +75 -0
- package/components/repeater/README.md +159 -0
- package/components/repeater/index.js +829 -0
- package/components/segmented-bar/README.md +30 -0
- package/components/segmented-bar/index.js +68 -0
- package/components/sortable-select/README.md +153 -0
- package/components/sortable-select/custom-sortable-token.js +127 -0
- package/components/sortable-select/index.js +693 -0
- package/components/style-picker/README.md +36 -0
- package/components/style-picker/index.js +92 -0
- package/components/variable-field/README.md +299 -0
- package/components/variable-field/index.js +592 -0
- package/components/variable-field/value-to-chip-html.js +38 -0
- package/components/variable-inserter/README.md +68 -0
- package/components/variable-inserter/index.js +168 -0
- package/constants/statusColors.js +16 -6
- package/editor.js +16 -0
- package/index.js +127 -0
- package/package.json +27 -16
- package/utils/kitaris-localize.js +128 -0
- package/utils/slug-aware-color-setter.js +27 -0
- package/build/index.asset.php +0 -20
- package/build/index.js +0 -1233
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeocoderSearchControl
|
|
3
|
+
*
|
|
4
|
+
* Reusable address/place search input wrapping the shared `geocodeAddress`
|
|
5
|
+
* (Nominatim) helper.
|
|
6
|
+
*
|
|
7
|
+
* Explicit-submit pattern: search fires only when the user presses Enter or
|
|
8
|
+
* clicks the search button — never as-you-type. This is required by the
|
|
9
|
+
* Nominatim usage policy, which forbids autocomplete-style request streams
|
|
10
|
+
* regardless of debounce: see https://operations.osmfoundation.org/policies/nominatim/
|
|
11
|
+
*
|
|
12
|
+
* The search input + suggestions dropdown live in their own
|
|
13
|
+
* `position: relative` container so the dropdown butts directly
|
|
14
|
+
* against the bottom of the input. Attribution sits in a sibling
|
|
15
|
+
* below that container, in normal flow, and is overlaid by the
|
|
16
|
+
* dropdown when results are visible.
|
|
17
|
+
*
|
|
18
|
+
* @package Siratik/WP-Component-Library
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Button, SearchControl } from "@wordpress/components";
|
|
22
|
+
import {
|
|
23
|
+
useCallback,
|
|
24
|
+
useEffect,
|
|
25
|
+
useRef,
|
|
26
|
+
useState,
|
|
27
|
+
} from "@wordpress/element";
|
|
28
|
+
import { __ } from "@wordpress/i18n";
|
|
29
|
+
import { search as searchIcon } from "@wordpress/icons";
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
geocodeAddress,
|
|
33
|
+
GEOCODER_ATTRIBUTION_HTML,
|
|
34
|
+
} from "./geocoder";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {Object} props
|
|
38
|
+
* @param {(result:GeocodeResult)=>void} props.onSelect Called when the user picks a suggestion.
|
|
39
|
+
* @param {string} [props.id] Stable id for the underlying SearchControl.
|
|
40
|
+
* @param {string} [props.placeholder]
|
|
41
|
+
* @param {number} [props.limit=8] Max suggestions returned.
|
|
42
|
+
* @param {string[]} [props.countryCodes] ISO 3166-1 alpha-2 list to bias results to.
|
|
43
|
+
* @param {number} [props.minLength=3]
|
|
44
|
+
* @param {boolean} [props.clearOnSelect=true]
|
|
45
|
+
* @param {boolean} [props.showAttribution=true]
|
|
46
|
+
*/
|
|
47
|
+
export function GeocoderSearchControl({
|
|
48
|
+
onSelect,
|
|
49
|
+
id = "geocoder-search",
|
|
50
|
+
placeholder = __("Search address, city, or ZIP — then press Enter", "wp-component-library"),
|
|
51
|
+
limit = 8,
|
|
52
|
+
countryCodes,
|
|
53
|
+
minLength = 3,
|
|
54
|
+
clearOnSelect = true,
|
|
55
|
+
showAttribution = true,
|
|
56
|
+
}) {
|
|
57
|
+
const [value, setValue] = useState("");
|
|
58
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
59
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
60
|
+
const [hasSearched, setHasSearched] = useState(false);
|
|
61
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
62
|
+
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
63
|
+
|
|
64
|
+
const containerRef = useRef(null);
|
|
65
|
+
const abortRef = useRef(null);
|
|
66
|
+
|
|
67
|
+
const runSearch = useCallback(
|
|
68
|
+
async (query) => {
|
|
69
|
+
const trimmed = (query || "").trim();
|
|
70
|
+
if (trimmed.length < minLength) {
|
|
71
|
+
setSuggestions([]);
|
|
72
|
+
setShowSuggestions(false);
|
|
73
|
+
setHasSearched(false);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// User mashed Enter twice — cancel the prior request so the
|
|
78
|
+
// later one always wins.
|
|
79
|
+
if (abortRef.current) {
|
|
80
|
+
abortRef.current.abort();
|
|
81
|
+
}
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
abortRef.current = controller;
|
|
84
|
+
|
|
85
|
+
setIsSearching(true);
|
|
86
|
+
setHasSearched(true);
|
|
87
|
+
try {
|
|
88
|
+
const results = await geocodeAddress({
|
|
89
|
+
query: trimmed,
|
|
90
|
+
limit,
|
|
91
|
+
countryCodes,
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
});
|
|
94
|
+
if (controller.signal.aborted) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setSuggestions(results);
|
|
98
|
+
setShowSuggestions(true);
|
|
99
|
+
setSelectedIndex(-1);
|
|
100
|
+
} finally {
|
|
101
|
+
if (abortRef.current === controller) {
|
|
102
|
+
abortRef.current = null;
|
|
103
|
+
setIsSearching(false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[limit, countryCodes, minLength],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Typing only updates local state. Editing after a search clears the
|
|
111
|
+
// stale dropdown so it's obvious the user needs to re-submit.
|
|
112
|
+
const handleChange = useCallback((next) => {
|
|
113
|
+
setValue(next);
|
|
114
|
+
setShowSuggestions(false);
|
|
115
|
+
setSuggestions([]);
|
|
116
|
+
setSelectedIndex(-1);
|
|
117
|
+
setHasSearched(false);
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const handleSelect = useCallback(
|
|
121
|
+
(suggestion) => {
|
|
122
|
+
onSelect?.(suggestion);
|
|
123
|
+
if (clearOnSelect) {
|
|
124
|
+
setValue("");
|
|
125
|
+
}
|
|
126
|
+
setSuggestions([]);
|
|
127
|
+
setShowSuggestions(false);
|
|
128
|
+
setSelectedIndex(-1);
|
|
129
|
+
setHasSearched(false);
|
|
130
|
+
},
|
|
131
|
+
[onSelect, clearOnSelect],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const handleKeyDown = useCallback(
|
|
135
|
+
(e) => {
|
|
136
|
+
// Enter picks the highlighted suggestion when the dropdown is
|
|
137
|
+
// open; otherwise it submits the search.
|
|
138
|
+
if (e.key === "Enter") {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
if (
|
|
141
|
+
showSuggestions &&
|
|
142
|
+
selectedIndex >= 0 &&
|
|
143
|
+
suggestions[selectedIndex]
|
|
144
|
+
) {
|
|
145
|
+
handleSelect(suggestions[selectedIndex]);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
runSearch(value);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (e.key === "Escape") {
|
|
153
|
+
setShowSuggestions(false);
|
|
154
|
+
setSelectedIndex(-1);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!showSuggestions || suggestions.length === 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (e.key === "ArrowDown") {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
setSelectedIndex((prev) =>
|
|
165
|
+
prev < suggestions.length - 1 ? prev + 1 : prev,
|
|
166
|
+
);
|
|
167
|
+
} else if (e.key === "ArrowUp") {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
[
|
|
173
|
+
showSuggestions,
|
|
174
|
+
suggestions,
|
|
175
|
+
selectedIndex,
|
|
176
|
+
handleSelect,
|
|
177
|
+
runSearch,
|
|
178
|
+
value,
|
|
179
|
+
],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Click-outside to dismiss.
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!showSuggestions) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const handler = (event) => {
|
|
188
|
+
if (
|
|
189
|
+
containerRef.current &&
|
|
190
|
+
!containerRef.current.contains(event.target)
|
|
191
|
+
) {
|
|
192
|
+
setShowSuggestions(false);
|
|
193
|
+
setSelectedIndex(-1);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
document.addEventListener("mousedown", handler);
|
|
197
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
198
|
+
}, [showSuggestions]);
|
|
199
|
+
|
|
200
|
+
// Cancel any in-flight request on unmount so React doesn't try to
|
|
201
|
+
// set state on a torn-down component.
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
return () => {
|
|
204
|
+
if (abortRef.current) {
|
|
205
|
+
abortRef.current.abort();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
const canSearch = value.trim().length >= minLength && !isSearching;
|
|
211
|
+
|
|
212
|
+
const showNoResults =
|
|
213
|
+
showSuggestions &&
|
|
214
|
+
!isSearching &&
|
|
215
|
+
hasSearched &&
|
|
216
|
+
suggestions.length === 0 &&
|
|
217
|
+
value.trim().length >= minLength;
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div ref={containerRef} className="geocoder-search">
|
|
221
|
+
{/* Positioning context for the dropdown only — keeps the
|
|
222
|
+
suggestions visually flush with the bottom of the input. */}
|
|
223
|
+
<div style={{ position: "relative" }}>
|
|
224
|
+
<div style={{ display: "flex", gap: "6px", alignItems: "stretch" }}>
|
|
225
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
226
|
+
<SearchControl
|
|
227
|
+
__nextHasNoMarginBottom
|
|
228
|
+
id={id}
|
|
229
|
+
value={value}
|
|
230
|
+
onChange={handleChange}
|
|
231
|
+
onKeyDown={handleKeyDown}
|
|
232
|
+
placeholder={placeholder}
|
|
233
|
+
autoComplete="off"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<Button
|
|
237
|
+
__next40pxDefaultSize
|
|
238
|
+
variant="secondary"
|
|
239
|
+
icon={searchIcon}
|
|
240
|
+
label={__("Search", "wp-component-library")}
|
|
241
|
+
showTooltip
|
|
242
|
+
disabled={!canSearch}
|
|
243
|
+
onClick={() => runSearch(value)}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{isSearching && (
|
|
248
|
+
<DropdownPanel>
|
|
249
|
+
<div
|
|
250
|
+
style={{
|
|
251
|
+
padding: "8px 12px",
|
|
252
|
+
fontSize: "13px",
|
|
253
|
+
color: "#646970",
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
{__("Searching…", "wp-component-library")}
|
|
257
|
+
</div>
|
|
258
|
+
</DropdownPanel>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{!isSearching && showSuggestions && suggestions.length > 0 && (
|
|
262
|
+
<DropdownPanel>
|
|
263
|
+
{suggestions.map((s, idx) => (
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
266
|
+
key={s.id}
|
|
267
|
+
onMouseEnter={() => setSelectedIndex(idx)}
|
|
268
|
+
onClick={() => handleSelect(s)}
|
|
269
|
+
style={{
|
|
270
|
+
display: "block",
|
|
271
|
+
width: "100%",
|
|
272
|
+
textAlign: "left",
|
|
273
|
+
padding: "8px 12px",
|
|
274
|
+
fontSize: "13px",
|
|
275
|
+
border: "none",
|
|
276
|
+
borderBottom:
|
|
277
|
+
idx < suggestions.length - 1
|
|
278
|
+
? "1px solid #f0f0f1"
|
|
279
|
+
: "none",
|
|
280
|
+
background:
|
|
281
|
+
idx === selectedIndex
|
|
282
|
+
? "#f0f0f1"
|
|
283
|
+
: "#fff",
|
|
284
|
+
cursor: "pointer",
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{s.displayName}
|
|
288
|
+
</button>
|
|
289
|
+
))}
|
|
290
|
+
</DropdownPanel>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{showNoResults && (
|
|
294
|
+
<DropdownPanel>
|
|
295
|
+
<div
|
|
296
|
+
style={{
|
|
297
|
+
padding: "8px 12px",
|
|
298
|
+
fontSize: "13px",
|
|
299
|
+
color: "#646970",
|
|
300
|
+
fontStyle: "italic",
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
{__("No matches found.", "wp-component-library")}
|
|
304
|
+
</div>
|
|
305
|
+
</DropdownPanel>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{showAttribution && (
|
|
310
|
+
<div
|
|
311
|
+
style={{
|
|
312
|
+
marginTop: "4px",
|
|
313
|
+
fontSize: "11px",
|
|
314
|
+
color: "#757575",
|
|
315
|
+
lineHeight: 1.4,
|
|
316
|
+
}}
|
|
317
|
+
dangerouslySetInnerHTML={{
|
|
318
|
+
__html: GEOCODER_ATTRIBUTION_HTML,
|
|
319
|
+
}}
|
|
320
|
+
/>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Absolute-positioned panel that hangs directly below the search input.
|
|
328
|
+
*/
|
|
329
|
+
function DropdownPanel({ children }) {
|
|
330
|
+
return (
|
|
331
|
+
<div
|
|
332
|
+
style={{
|
|
333
|
+
position: "absolute",
|
|
334
|
+
top: "100%",
|
|
335
|
+
left: 0,
|
|
336
|
+
right: 0,
|
|
337
|
+
marginTop: "-1px",
|
|
338
|
+
background: "#fff",
|
|
339
|
+
border: "1px solid #ddd",
|
|
340
|
+
borderTop: "none",
|
|
341
|
+
borderRadius: "0 0 2px 2px",
|
|
342
|
+
maxHeight: "260px",
|
|
343
|
+
overflowY: "auto",
|
|
344
|
+
zIndex: 1000,
|
|
345
|
+
boxShadow: "0 2px 6px rgba(0,0,0,0.08)",
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
{children}
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export default GeocoderSearchControl;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Country options for the address fields. Codes match Siratik\Utility\Utilities\CountryList;
|
|
3
|
+
* names come from the browser (Intl.DisplayNames) so they follow the admin's language.
|
|
4
|
+
*
|
|
5
|
+
* @package Siratik/WP-Component-Library
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const COUNTRY_CODES = [
|
|
9
|
+
"AF", "AL", "DZ", "AD", "AO", "AG", "AR", "AM", "AU", "AT", "AZ", "BS", "BH", "BD", "BB", "BY",
|
|
10
|
+
"BE", "BZ", "BJ", "BT", "BO", "BA", "BW", "BR", "BN", "BG", "BF", "BI", "CV", "KH", "CM", "CA",
|
|
11
|
+
"CF", "TD", "CL", "CN", "CO", "KM", "CD", "CG", "CR", "HR", "CU", "CY", "CZ", "DK", "DJ", "DM",
|
|
12
|
+
"DO", "EC", "EG", "SV", "GQ", "ER", "EE", "SZ", "ET", "FJ", "FI", "FR", "GA", "GM", "GE", "DE",
|
|
13
|
+
"GH", "GR", "GD", "GT", "GN", "GW", "GY", "HT", "HN", "HU", "IS", "IN", "ID", "IR", "IQ", "IE",
|
|
14
|
+
"IL", "IT", "JM", "JP", "JO", "KZ", "KE", "KI", "KP", "KR", "XK", "KW", "KG", "LA", "LV", "LB",
|
|
15
|
+
"LS", "LR", "LY", "LI", "LT", "LU", "MG", "MW", "MY", "MV", "ML", "MT", "MH", "MR", "MU", "MX",
|
|
16
|
+
"FM", "MD", "MC", "MN", "ME", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "NZ", "NI", "NE", "NG",
|
|
17
|
+
"MK", "NO", "OM", "PK", "PW", "PS", "PA", "PG", "PY", "PE", "PH", "PL", "PT", "QA", "RO", "RU",
|
|
18
|
+
"RW", "KN", "LC", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SK", "SI", "SB",
|
|
19
|
+
"SO", "ZA", "SS", "ES", "LK", "SD", "SR", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", "TG",
|
|
20
|
+
"TO", "TT", "TN", "TR", "TM", "TV", "UG", "UA", "AE", "GB", "US", "UY", "UZ", "VU", "VA", "VE",
|
|
21
|
+
"VN", "YE", "ZM", "ZW",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Display name for a country code, falling back to the code itself.
|
|
26
|
+
*
|
|
27
|
+
* @param {Intl.DisplayNames|null} names
|
|
28
|
+
* @param {string} code
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function countryName(names, code) {
|
|
32
|
+
try {
|
|
33
|
+
return names?.of(code) || code;
|
|
34
|
+
} catch {
|
|
35
|
+
return code;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* SelectControl options sorted by name. A saved code that isn't in the list
|
|
41
|
+
* (e.g. a territory a search returned) is kept as an option so it isn't lost.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} [currentCode] The saved country code.
|
|
44
|
+
* @returns {{label:string,value:string}[]}
|
|
45
|
+
*/
|
|
46
|
+
export function getCountryOptions(currentCode = "") {
|
|
47
|
+
let names = null;
|
|
48
|
+
try {
|
|
49
|
+
names = new Intl.DisplayNames(
|
|
50
|
+
[document.documentElement.lang || "en"],
|
|
51
|
+
{ type: "region" },
|
|
52
|
+
);
|
|
53
|
+
} catch {
|
|
54
|
+
names = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const codes = [...COUNTRY_CODES];
|
|
58
|
+
const current = String(currentCode || "").toUpperCase();
|
|
59
|
+
if (/^[A-Z]{2}$/.test(current) && !codes.includes(current)) {
|
|
60
|
+
codes.push(current);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return codes
|
|
64
|
+
.map((code) => ({ value: code, label: countryName(names, code) }))
|
|
65
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
66
|
+
}
|