@siratik/wp-component-library 0.1.1 → 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 +43 -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 +1 -1
- package/editor.js +16 -0
- package/index.js +127 -0
- package/package.json +21 -13
- 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 -1242
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared geocoding helper for admin and editor address search.
|
|
3
|
+
*
|
|
4
|
+
* Uses Nominatim (OpenStreetMap) — free, no API key required.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT — Nominatim usage policy:
|
|
7
|
+
* https://operations.osmfoundation.org/policies/nominatim/
|
|
8
|
+
*
|
|
9
|
+
* - Hard limit: 1 request/second. Requests from a page are spaced at
|
|
10
|
+
* least 2 seconds apart (MIN_REQUEST_INTERVAL_MS) as a safety margin.
|
|
11
|
+
* - **No autocomplete-style as-you-type queries.** Callers MUST be
|
|
12
|
+
* explicit-submit (Enter / button click) and NOT fire on every
|
|
13
|
+
* keystroke or debounced keystroke burst. Use
|
|
14
|
+
* `<GeocoderSearchControl>` which enforces this pattern.
|
|
15
|
+
* - Provide visible attribution (see `GEOCODER_ATTRIBUTION_HTML`).
|
|
16
|
+
*
|
|
17
|
+
* Returns normalized `GeocodeResult` objects so consumers don't have
|
|
18
|
+
* to care about the raw provider response shape — this keeps the door
|
|
19
|
+
* open to swapping in LocationIQ/MapTiler/Pelias later (those services
|
|
20
|
+
* DO support autocomplete) without touching the React components.
|
|
21
|
+
*
|
|
22
|
+
* @package Siratik/WP-Component-Library
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const NOMINATIM_BASE = "https://nominatim.openstreetmap.org/search";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Request spacing
|
|
29
|
+
//
|
|
30
|
+
// Nominatim allows at most one request a second. Network requests from this
|
|
31
|
+
// page are spaced at least two seconds apart, a safety margin: a search that
|
|
32
|
+
// comes sooner waits its turn. Cache hits don't wait. A search cancelled
|
|
33
|
+
// while it waits sends nothing.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
export const MIN_REQUEST_INTERVAL_MS = 2000;
|
|
36
|
+
|
|
37
|
+
let nextRequestAt = 0;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Reserve the next request slot and wait for it.
|
|
41
|
+
*
|
|
42
|
+
* @param {AbortSignal} [signal]
|
|
43
|
+
* @returns {Promise<void>} Rejects with an AbortError if cancelled first.
|
|
44
|
+
*/
|
|
45
|
+
function waitForRequestSlot(signal) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const startAt = Math.max(now, nextRequestAt);
|
|
48
|
+
nextRequestAt = startAt + MIN_REQUEST_INTERVAL_MS;
|
|
49
|
+
|
|
50
|
+
const delay = startAt - now;
|
|
51
|
+
if (delay <= 0) {
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const timer = setTimeout(resolve, delay);
|
|
57
|
+
signal?.addEventListener(
|
|
58
|
+
"abort",
|
|
59
|
+
() => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
62
|
+
},
|
|
63
|
+
{ once: true },
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Test helper: forget the last request time.
|
|
70
|
+
*/
|
|
71
|
+
export function resetGeocoderSpacing() {
|
|
72
|
+
nextRequestAt = 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Per-session geocode result cache
|
|
77
|
+
//
|
|
78
|
+
// Nominatim's own usage policy encourages result caching. sessionStorage is
|
|
79
|
+
// ideal: results are scoped to the browser tab (not persisted cross-session),
|
|
80
|
+
// which keeps data fresh while eliminating redundant round-trips when the
|
|
81
|
+
// user re-searches the same address within the same admin session.
|
|
82
|
+
//
|
|
83
|
+
// TTL: 10 minutes — protects against stale results after map updates while
|
|
84
|
+
// still providing meaningful cache hits during normal admin workflows.
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
const GEOCODE_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a stable sessionStorage key from the geocode parameters.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} query
|
|
92
|
+
* @param {number} limit
|
|
93
|
+
* @param {string[]} countryCodes
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
function buildCacheKey(query, limit, countryCodes) {
|
|
97
|
+
const payload = JSON.stringify({
|
|
98
|
+
q: query.toLowerCase().trim(),
|
|
99
|
+
l: limit,
|
|
100
|
+
cc: Array.isArray(countryCodes) ? [...countryCodes].sort() : [],
|
|
101
|
+
});
|
|
102
|
+
try {
|
|
103
|
+
return "geocode_" + btoa(encodeURIComponent(payload)).slice(0, 80);
|
|
104
|
+
} catch {
|
|
105
|
+
// btoa may throw on unusual unicode — fall back to raw (still unique).
|
|
106
|
+
return "geocode_" + encodeURIComponent(payload).slice(0, 80);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} key
|
|
112
|
+
* @returns {GeocodeResult[]|null}
|
|
113
|
+
*/
|
|
114
|
+
function readGeocodeCache(key) {
|
|
115
|
+
try {
|
|
116
|
+
const raw = sessionStorage.getItem(key);
|
|
117
|
+
if (!raw) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const { ts, data } = JSON.parse(raw);
|
|
121
|
+
if (Date.now() - ts > GEOCODE_CACHE_TTL_MS) {
|
|
122
|
+
sessionStorage.removeItem(key);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return data;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} key
|
|
133
|
+
* @param {GeocodeResult[]} data
|
|
134
|
+
*/
|
|
135
|
+
function writeGeocodeCache(key, data) {
|
|
136
|
+
try {
|
|
137
|
+
sessionStorage.setItem(key, JSON.stringify({ ts: Date.now(), data }));
|
|
138
|
+
} catch {
|
|
139
|
+
// sessionStorage may be full or unavailable (private-browsing mode,
|
|
140
|
+
// storage quota exceeded, etc.) — silently skip caching.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Countries we currently serve. Constrains Nominatim to these so US
|
|
146
|
+
* street searches don't compete with global noise (e.g. typing "600"
|
|
147
|
+
* returning Iceland / Lesotho / Taiwan before any US address).
|
|
148
|
+
*
|
|
149
|
+
* Override per-call by passing `countryCodes` to `geocodeAddress`,
|
|
150
|
+
* or pass `countryCodes={[]}` (or any custom list) to
|
|
151
|
+
* `<GeocoderSearchControl>` to search globally / a different set.
|
|
152
|
+
*/
|
|
153
|
+
export const DEFAULT_GEOCODER_COUNTRY_CODES = Object.freeze([
|
|
154
|
+
"us", // United States
|
|
155
|
+
"ca", // Canada
|
|
156
|
+
"au", // Australia
|
|
157
|
+
"gb", // United Kingdom
|
|
158
|
+
"ie", // Ireland
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @typedef {Object} GeocodeAddress
|
|
163
|
+
* @property {string} street House number + road (or just road).
|
|
164
|
+
* @property {string} houseNumber
|
|
165
|
+
* @property {string} road
|
|
166
|
+
* @property {string} city city || town || village || hamlet || suburb
|
|
167
|
+
* @property {string} state
|
|
168
|
+
* @property {string} postcode
|
|
169
|
+
* @property {string} country
|
|
170
|
+
* @property {string} countryCode ISO-3166-1 alpha-2, uppercase.
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @typedef {Object} GeocodeResult
|
|
175
|
+
* @property {string} id Stable key for React lists.
|
|
176
|
+
* @property {string} displayName Short, comma-separated label.
|
|
177
|
+
* @property {number} lat
|
|
178
|
+
* @property {number} lng
|
|
179
|
+
* @property {GeocodeAddress} address Structured address fields.
|
|
180
|
+
* @property {string} type OSM type/category (e.g. "house", "city").
|
|
181
|
+
* @property {number} importance Nominatim importance score (0..1).
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Run a forward geocode against Nominatim.
|
|
186
|
+
*
|
|
187
|
+
* @param {Object} options
|
|
188
|
+
* @param {string} options.query
|
|
189
|
+
* @param {number} [options.limit=8]
|
|
190
|
+
* @param {string} [options.acceptLanguage="en"]
|
|
191
|
+
* @param {string[]} [options.countryCodes] ISO 3166-1 alpha-2 list to constrain results to.
|
|
192
|
+
* Defaults to DEFAULT_GEOCODER_COUNTRY_CODES.
|
|
193
|
+
* Pass an empty array to search globally.
|
|
194
|
+
* @param {AbortSignal} [options.signal]
|
|
195
|
+
* @returns {Promise<GeocodeResult[]>}
|
|
196
|
+
*/
|
|
197
|
+
export async function geocodeAddress({
|
|
198
|
+
query,
|
|
199
|
+
limit = 8,
|
|
200
|
+
acceptLanguage = "en",
|
|
201
|
+
countryCodes = DEFAULT_GEOCODER_COUNTRY_CODES,
|
|
202
|
+
signal,
|
|
203
|
+
} = {}) {
|
|
204
|
+
const trimmed = (query || "").trim();
|
|
205
|
+
if (trimmed.length < 3) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check session cache before hitting the network.
|
|
210
|
+
const cacheKey = buildCacheKey(trimmed, limit, countryCodes);
|
|
211
|
+
const cached = readGeocodeCache(cacheKey);
|
|
212
|
+
if (cached) {
|
|
213
|
+
return cached;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const params = new URLSearchParams({
|
|
217
|
+
q: trimmed,
|
|
218
|
+
format: "jsonv2",
|
|
219
|
+
addressdetails: "1",
|
|
220
|
+
dedupe: "1",
|
|
221
|
+
limit: String(limit),
|
|
222
|
+
"accept-language": acceptLanguage,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (Array.isArray(countryCodes) && countryCodes.length > 0) {
|
|
226
|
+
params.set(
|
|
227
|
+
"countrycodes",
|
|
228
|
+
countryCodes
|
|
229
|
+
.filter((c) => typeof c === "string" && c.length === 2)
|
|
230
|
+
.map((c) => c.toLowerCase())
|
|
231
|
+
.join(","),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await waitForRequestSlot(signal);
|
|
237
|
+
if (signal?.aborted) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const response = await fetch(`${NOMINATIM_BASE}?${params.toString()}`, {
|
|
242
|
+
headers: { Accept: "application/json" },
|
|
243
|
+
signal,
|
|
244
|
+
});
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
const data = await response.json();
|
|
249
|
+
if (!Array.isArray(data)) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
const results = data.map(toGeocodeResult).filter(Boolean);
|
|
253
|
+
writeGeocodeCache(cacheKey, results);
|
|
254
|
+
return results;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
// Aborted requests are expected (explicit-submit UX can still abort
|
|
257
|
+
// an in-flight request if the user submits a new search quickly).
|
|
258
|
+
if (err?.name === "AbortError") {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
// eslint-disable-next-line no-console -- surfaces failed lookups, which otherwise return no results silently.
|
|
262
|
+
console.error("Nominatim geocoding error:", err);
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Normalize a raw Nominatim feature into a GeocodeResult.
|
|
269
|
+
*
|
|
270
|
+
* @param {Object} item
|
|
271
|
+
* @returns {GeocodeResult|null}
|
|
272
|
+
*/
|
|
273
|
+
function toGeocodeResult(item) {
|
|
274
|
+
const lat = Number(item.lat);
|
|
275
|
+
const lng = Number(item.lon);
|
|
276
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const a = item.address || {};
|
|
281
|
+
const houseNumber = a.house_number || "";
|
|
282
|
+
const road = a.road || a.pedestrian || a.footway || a.cycleway || "";
|
|
283
|
+
const street = houseNumber && road ? `${houseNumber} ${road}` : road;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
id: `${item.osm_type ?? "x"}-${item.osm_id ?? `${lng},${lat}`}-${item.place_id ?? ""}`,
|
|
287
|
+
displayName: formatDisplayName(item, { street, road }),
|
|
288
|
+
lat,
|
|
289
|
+
lng,
|
|
290
|
+
address: {
|
|
291
|
+
street,
|
|
292
|
+
houseNumber,
|
|
293
|
+
road,
|
|
294
|
+
city: a.city || a.town || a.village || a.hamlet || a.suburb || "",
|
|
295
|
+
state: a.state || a.region || a["ISO3166-2-lvl4"] || "",
|
|
296
|
+
postcode: a.postcode || "",
|
|
297
|
+
country: a.country || "",
|
|
298
|
+
countryCode: (a.country_code || "").toUpperCase(),
|
|
299
|
+
},
|
|
300
|
+
type: item.type || item.category || "",
|
|
301
|
+
importance: typeof item.importance === "number" ? item.importance : 0,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Build a short, readable label. Nominatim's own `display_name` is
|
|
307
|
+
* comma-heavy and very long ("Boston, Suffolk County, Massachusetts,
|
|
308
|
+
* United States, 02108, United States of America") so we rebuild it
|
|
309
|
+
* from the structured address fields.
|
|
310
|
+
*/
|
|
311
|
+
function formatDisplayName(item, { street, road }) {
|
|
312
|
+
const a = item.address || {};
|
|
313
|
+
const parts = [];
|
|
314
|
+
|
|
315
|
+
if (street) {
|
|
316
|
+
parts.push(street);
|
|
317
|
+
} else if (road) {
|
|
318
|
+
parts.push(road);
|
|
319
|
+
} else {
|
|
320
|
+
const poi =
|
|
321
|
+
a.amenity ||
|
|
322
|
+
a.shop ||
|
|
323
|
+
a.tourism ||
|
|
324
|
+
a.leisure ||
|
|
325
|
+
a.office ||
|
|
326
|
+
a.building ||
|
|
327
|
+
item.name;
|
|
328
|
+
if (poi) {
|
|
329
|
+
parts.push(poi);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const city = a.city || a.town || a.village || a.hamlet || a.suburb;
|
|
334
|
+
if (city) {
|
|
335
|
+
parts.push(city);
|
|
336
|
+
}
|
|
337
|
+
if (a.state) {
|
|
338
|
+
parts.push(a.state);
|
|
339
|
+
}
|
|
340
|
+
if (a.postcode) {
|
|
341
|
+
parts.push(a.postcode);
|
|
342
|
+
}
|
|
343
|
+
if (a.country_code) {
|
|
344
|
+
parts.push(a.country_code.toUpperCase());
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return parts.length ? parts.join(", ") : item.display_name || "";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Convert a GeocodeResult into the organization location
|
|
352
|
+
* field shape consumed by LocationMapField.
|
|
353
|
+
*
|
|
354
|
+
* @param {GeocodeResult} result
|
|
355
|
+
* @returns {{street:string,city:string,state:string,zip:string,country:string,lat:number,lng:number}|null}
|
|
356
|
+
*/
|
|
357
|
+
export function toOrganizationLocation(result) {
|
|
358
|
+
if (!result) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const a = result.address;
|
|
362
|
+
return {
|
|
363
|
+
street: a.street || "",
|
|
364
|
+
city: a.city || "",
|
|
365
|
+
state: a.state || "",
|
|
366
|
+
zip: a.postcode || "",
|
|
367
|
+
country: a.countryCode || "US",
|
|
368
|
+
lat: result.lat,
|
|
369
|
+
lng: result.lng,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* One-line lookup query for an organization-shaped address: street, city,
|
|
375
|
+
* "state ZIP". Line 2 (suite, unit, floor) is left out: it makes lookups fail.
|
|
376
|
+
*
|
|
377
|
+
* @param {Object} location Object with address_* keys.
|
|
378
|
+
* @returns {string}
|
|
379
|
+
*/
|
|
380
|
+
export function toAddressQuery(location) {
|
|
381
|
+
const value = location && typeof location === "object" ? location : {};
|
|
382
|
+
return [
|
|
383
|
+
value.address_street,
|
|
384
|
+
value.address_city,
|
|
385
|
+
[value.address_state, value.address_zip]
|
|
386
|
+
.map((part) => String(part || "").trim())
|
|
387
|
+
.filter(Boolean)
|
|
388
|
+
.join(" "),
|
|
389
|
+
]
|
|
390
|
+
.map((part) => String(part || "").trim())
|
|
391
|
+
.filter(Boolean)
|
|
392
|
+
.join(", ");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Visible attribution required by the Nominatim usage policy.
|
|
397
|
+
* Render this somewhere near the search control (small print is fine).
|
|
398
|
+
*/
|
|
399
|
+
export const GEOCODER_ATTRIBUTION_HTML =
|
|
400
|
+
'Search by <a href="https://nominatim.openstreetmap.org" target="_blank" rel="noopener noreferrer">Nominatim</a> / © <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Address search (Nominatim): the explicit-submit search control and the
|
|
3
|
+
* geocoding helper behind it. No API key; never searches as you type.
|
|
4
|
+
*
|
|
5
|
+
* @package Siratik/WP-Component-Library
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { GeocoderSearchControl } from "./GeocoderSearchControl";
|
|
9
|
+
export { AddressEntryControl } from "./AddressEntryControl";
|
|
10
|
+
export { getCountryOptions } from "./countryOptions";
|
|
11
|
+
export {
|
|
12
|
+
geocodeAddress,
|
|
13
|
+
toOrganizationLocation,
|
|
14
|
+
GEOCODER_ATTRIBUTION_HTML,
|
|
15
|
+
DEFAULT_GEOCODER_COUNTRY_CODES,
|
|
16
|
+
} from "./geocoder";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon Component
|
|
3
|
+
*
|
|
4
|
+
* Renders an SVG icon. Uses the stored `source` string directly (preferred,
|
|
5
|
+
* ensures frontend-saved icons always render even if the set is later removed),
|
|
6
|
+
* with a registry fallback for display-only usage.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { forwardRef } from "@wordpress/element";
|
|
10
|
+
import { useIcon } from "./hooks";
|
|
11
|
+
import { safeSvgSource } from "./utils";
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line no-shadow -- named for React DevTools.
|
|
14
|
+
const IconDisplay = forwardRef(function IconDisplay(
|
|
15
|
+
{ name, iconSet, source, onClick, className = "", style, ...rest },
|
|
16
|
+
ref,
|
|
17
|
+
) {
|
|
18
|
+
// Prefer the directly-passed source (from saved attributes) for reliability.
|
|
19
|
+
// Fall back to registry lookup for display-only usage (e.g. rendering by name).
|
|
20
|
+
const registryIcon = useIcon(iconSet, name);
|
|
21
|
+
const svgSource = safeSvgSource(source || registryIcon?.source || "");
|
|
22
|
+
|
|
23
|
+
if (!svgSource) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const clickProps = {};
|
|
28
|
+
if (typeof onClick === "function") {
|
|
29
|
+
clickProps.role = "button";
|
|
30
|
+
clickProps.tabIndex = 0;
|
|
31
|
+
clickProps["aria-label"] = "Change Icon";
|
|
32
|
+
clickProps.onClick = onClick;
|
|
33
|
+
clickProps.onKeyDown = (e) => {
|
|
34
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
onClick(e);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={`wpcl-icon${className ? ` ${className}` : ""}`}
|
|
45
|
+
dangerouslySetInnerHTML={{ __html: svgSource }}
|
|
46
|
+
style={style}
|
|
47
|
+
{...clickProps}
|
|
48
|
+
{...rest}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export { IconDisplay as Icon };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconPicker Component
|
|
3
|
+
*
|
|
4
|
+
* Inspector-panel icon picker. Shows the currently selected icon
|
|
5
|
+
* (or a placeholder) as a button — clicking opens the shared modal.
|
|
6
|
+
*
|
|
7
|
+
* Matches the 10up IconPicker API:
|
|
8
|
+
* <IconPicker value={icon} onChange={handleChange} label="Icon" />
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BaseControl, Button } from "@wordpress/components";
|
|
12
|
+
import { useState, useCallback } from "@wordpress/element";
|
|
13
|
+
import { __ } from "@wordpress/i18n";
|
|
14
|
+
|
|
15
|
+
import { IconPickerModal } from "./IconPickerModal";
|
|
16
|
+
import { PickerPreview, PickerName } from "./styles";
|
|
17
|
+
import { PLACEHOLDER_SVG, safeSvgSource } from "./utils";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} props
|
|
21
|
+
* @param {Object} [props.value] Current icon { name, iconSet, source }.
|
|
22
|
+
* @param {Function} props.onChange Called with new icon value.
|
|
23
|
+
* @param {string} [props.label] Label for the BaseControl.
|
|
24
|
+
* @param {string} [props.className]
|
|
25
|
+
*/
|
|
26
|
+
export function IconPicker({ value, onChange, label = "", className = "" }) {
|
|
27
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
28
|
+
|
|
29
|
+
const openModal = useCallback(() => setIsOpen(true), []);
|
|
30
|
+
const closeModal = useCallback(() => setIsOpen(false), []);
|
|
31
|
+
|
|
32
|
+
const hasIcon = value?.source;
|
|
33
|
+
// The stored source is block data, so sanitize it like every other sink.
|
|
34
|
+
const svgSource = (hasIcon && safeSvgSource(value.source)) || PLACEHOLDER_SVG;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
// eslint-disable-next-line @wordpress/no-base-control-with-label-without-id -- the button has its own aria-label.
|
|
38
|
+
<BaseControl
|
|
39
|
+
label={label || __("Icon", "wp-component-library")}
|
|
40
|
+
className={className || undefined}
|
|
41
|
+
__nextHasNoMarginBottom
|
|
42
|
+
>
|
|
43
|
+
<Button
|
|
44
|
+
variant="secondary"
|
|
45
|
+
onClick={openModal}
|
|
46
|
+
style={{ width: "100%", justifyContent: "flex-start", gap: "8px" }}
|
|
47
|
+
aria-label={
|
|
48
|
+
hasIcon
|
|
49
|
+
? __("Change icon", "wp-component-library")
|
|
50
|
+
: __("Choose icon", "wp-component-library")
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
<PickerPreview dangerouslySetInnerHTML={{ __html: svgSource }} />
|
|
54
|
+
<PickerName>
|
|
55
|
+
{hasIcon
|
|
56
|
+
? value.label || value.name
|
|
57
|
+
: __("Choose icon\u2026", "wp-component-library")}
|
|
58
|
+
</PickerName>
|
|
59
|
+
</Button>
|
|
60
|
+
|
|
61
|
+
{isOpen && (
|
|
62
|
+
<IconPickerModal
|
|
63
|
+
value={value}
|
|
64
|
+
onChange={onChange}
|
|
65
|
+
onClose={closeModal}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
</BaseControl>
|
|
69
|
+
);
|
|
70
|
+
}
|