@wordpress-gcb/fields 0.2.1 → 0.2.3
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/dist/conditional-logic.js +83 -0
- package/{src → dist}/control-context.js +3 -2
- package/{src → dist}/controls/MediaCapabilityGate.js +12 -8
- package/dist/controls/MediaPicker.js +149 -0
- package/dist/controls/MediaTriggerBadges.js +35 -0
- package/{src → dist}/controls/PopoverOrModal.js +49 -43
- package/dist/controls/SortableItem.js +126 -0
- package/dist/controls/button-group.js +46 -0
- package/dist/controls/checkbox-group.js +65 -0
- package/dist/controls/checkbox.js +15 -0
- package/dist/controls/code.js +24 -0
- package/dist/controls/color.js +249 -0
- package/dist/controls/date.js +55 -0
- package/dist/controls/datetime.js +61 -0
- package/dist/controls/email.js +17 -0
- package/dist/controls/file.js +163 -0
- package/dist/controls/gallery.js +371 -0
- package/dist/controls/google-map.js +143 -0
- package/dist/controls/heading-level.js +93 -0
- package/dist/controls/icon.js +292 -0
- package/dist/controls/image.js +360 -0
- package/dist/controls/index.js +88 -0
- package/dist/controls/message.js +86 -0
- package/dist/controls/number.js +19 -0
- package/dist/controls/oembed.js +42 -0
- package/{src → dist}/controls/page-link.js +1 -2
- package/dist/controls/post-object.js +913 -0
- package/dist/controls/radio.js +19 -0
- package/dist/controls/range.js +108 -0
- package/{src → dist}/controls/relationship.js +12 -7
- package/dist/controls/repeater.js +277 -0
- package/dist/controls/richtext.js +494 -0
- package/dist/controls/select.js +144 -0
- package/dist/controls/size.js +59 -0
- package/dist/controls/spacing.js +172 -0
- package/dist/controls/taxonomy.js +569 -0
- package/dist/controls/text.js +16 -0
- package/dist/controls/textarea.js +17 -0
- package/dist/controls/toggle-group.js +28 -0
- package/dist/controls/toggle.js +15 -0
- package/dist/controls/url.js +235 -0
- package/dist/controls/user.js +383 -0
- package/{src → dist}/controls/wysiwyg.js +1 -1
- package/{src → dist}/hooks/useTokens.js +25 -21
- package/{src → dist}/index.js +2 -8
- package/dist/inspector.js +163 -0
- package/{src → dist}/provider.js +18 -17
- package/dist/utils/map-utils.js +54 -0
- package/dist/utils/token-helper.js +396 -0
- package/{src → dist}/validation-context.js +4 -4
- package/package.json +20 -13
- package/src/conditional-logic.js +0 -77
- package/src/controls/MediaPicker.js +0 -139
- package/src/controls/MediaTriggerBadges.js +0 -31
- package/src/controls/SortableItem.js +0 -110
- package/src/controls/button-group.js +0 -49
- package/src/controls/checkbox-group.js +0 -55
- package/src/controls/checkbox.js +0 -13
- package/src/controls/code.js +0 -21
- package/src/controls/color.js +0 -235
- package/src/controls/date.js +0 -37
- package/src/controls/datetime.js +0 -54
- package/src/controls/email.js +0 -15
- package/src/controls/file.js +0 -134
- package/src/controls/gallery.js +0 -338
- package/src/controls/google-map.js +0 -117
- package/src/controls/heading-level.js +0 -99
- package/src/controls/icon.js +0 -301
- package/src/controls/image.js +0 -334
- package/src/controls/index.js +0 -95
- package/src/controls/message.js +0 -56
- package/src/controls/number.js +0 -17
- package/src/controls/oembed.js +0 -32
- package/src/controls/post-object.js +0 -788
- package/src/controls/radio.js +0 -18
- package/src/controls/range.js +0 -110
- package/src/controls/repeater.js +0 -290
- package/src/controls/richtext.js +0 -505
- package/src/controls/select.js +0 -141
- package/src/controls/size.js +0 -49
- package/src/controls/spacing.js +0 -141
- package/src/controls/taxonomy.js +0 -488
- package/src/controls/text.js +0 -14
- package/src/controls/textarea.js +0 -15
- package/src/controls/toggle-group.js +0 -34
- package/src/controls/toggle.js +0 -13
- package/src/controls/url.js +0 -164
- package/src/controls/user.js +0 -343
- package/src/inspector.js +0 -174
- package/src/utils/map-utils.js +0 -51
- package/src/utils/token-helper.js +0 -243
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconField — visual picker over the WP 7.0+ icon registry.
|
|
3
|
+
*
|
|
4
|
+
* Stored shape:
|
|
5
|
+
* { source: 'wp', name: 'core/arrow-down-left' }
|
|
6
|
+
*
|
|
7
|
+
* Authors don't type the name — they pick from a searchable grid in
|
|
8
|
+
* a Popover. Icons come from /wp/v2/icons (the new WP 7.0 endpoint,
|
|
9
|
+
* server-side registry exposed over REST). Storage is just the name;
|
|
10
|
+
* render.php on the server hits WP_Icons_Registry to resolve to SVG
|
|
11
|
+
* at render time so post_content stays small.
|
|
12
|
+
*
|
|
13
|
+
* Config options:
|
|
14
|
+
* namespace string — restrict the picker to icons whose name
|
|
15
|
+
* starts with `${namespace}/`. Useful once
|
|
16
|
+
* themes/plugins register their own sets via
|
|
17
|
+
* WP 7.1's register_block_icon API. Example:
|
|
18
|
+
* `"namespace": "icomoon"` shows only icons a
|
|
19
|
+
* theme registered with names like
|
|
20
|
+
* `icomoon/twitter`, `icomoon/youtube`.
|
|
21
|
+
* filter string[] — explicit allow-list of full icon names.
|
|
22
|
+
* Takes precedence over namespace when set.
|
|
23
|
+
* Use this when you want a hand-curated
|
|
24
|
+
* subset for a specific block ("only show
|
|
25
|
+
* these 5 brand icons here").
|
|
26
|
+
*
|
|
27
|
+
* Both options are no-ops on plain WP 7.0 because the registry only
|
|
28
|
+
* contains `core/*` icons (filtering to `namespace: "icomoon"`
|
|
29
|
+
* yields zero icons until something registers them). When WP 7.1
|
|
30
|
+
* ships register_block_icon, the same config Just Works.
|
|
31
|
+
*
|
|
32
|
+
* Requires WP 7.0+. On older WP the endpoint 404s and the picker
|
|
33
|
+
* surfaces a clear "needs WordPress 7.0" message rather than trying
|
|
34
|
+
* to be clever with a legacy dashicon fallback.
|
|
35
|
+
*
|
|
36
|
+
* Older saved values that used the v1 shape ({ source: 'dashicon', icon })
|
|
37
|
+
* still render — render.php dispatches by source. The picker just can't
|
|
38
|
+
* MAKE a dashicon value anymore (one-way migration).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
42
|
+
import { useState, useEffect, useRef, useMemo } from '@wordpress/element';
|
|
43
|
+
import { Button, Popover, Spinner, TextControl, BaseControl } from '@wordpress/components';
|
|
44
|
+
import apiFetch from '@wordpress/api-fetch';
|
|
45
|
+
|
|
46
|
+
// Module-level cache for the icon catalogue. /wp/v2/icons returns all
|
|
47
|
+
// 88 icons in one request; we only need to fetch it once per page load
|
|
48
|
+
// regardless of how many icon fields are rendered.
|
|
49
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
50
|
+
let iconCache = null;
|
|
51
|
+
let iconFetchPromise = null;
|
|
52
|
+
let iconFetchError = null;
|
|
53
|
+
async function fetchAllIconPages() {
|
|
54
|
+
// WP 7.0's icons endpoint caps per_page at 100 and there are 88
|
|
55
|
+
// core icons. WP 7.1+ will let plugins register more, so paginate
|
|
56
|
+
// at 100/page until a short page comes back.
|
|
57
|
+
const all = [];
|
|
58
|
+
for (let page = 1; page < 50; page++) {
|
|
59
|
+
// eslint-disable-next-line no-await-in-loop
|
|
60
|
+
const chunk = await apiFetch({
|
|
61
|
+
path: `/wp/v2/icons?per_page=100&page=${page}`
|
|
62
|
+
});
|
|
63
|
+
if (!Array.isArray(chunk) || chunk.length === 0) break;
|
|
64
|
+
all.push(...chunk);
|
|
65
|
+
if (chunk.length < 100) break;
|
|
66
|
+
}
|
|
67
|
+
return all;
|
|
68
|
+
}
|
|
69
|
+
function fetchIcons() {
|
|
70
|
+
if (iconCache) return Promise.resolve(iconCache);
|
|
71
|
+
if (iconFetchError) return Promise.reject(iconFetchError);
|
|
72
|
+
if (iconFetchPromise) return iconFetchPromise;
|
|
73
|
+
iconFetchPromise = fetchAllIconPages().then(items => {
|
|
74
|
+
iconCache = items;
|
|
75
|
+
return iconCache;
|
|
76
|
+
}).catch(err => {
|
|
77
|
+
iconFetchError = err;
|
|
78
|
+
iconFetchPromise = null;
|
|
79
|
+
throw err;
|
|
80
|
+
});
|
|
81
|
+
return iconFetchPromise;
|
|
82
|
+
}
|
|
83
|
+
function useIcons() {
|
|
84
|
+
const [icons, setIcons] = useState(iconCache);
|
|
85
|
+
const [error, setError] = useState(null);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (icons) return;
|
|
88
|
+
let cancelled = false;
|
|
89
|
+
fetchIcons().then(list => {
|
|
90
|
+
if (!cancelled) setIcons(list);
|
|
91
|
+
}).catch(err => {
|
|
92
|
+
if (cancelled) return;
|
|
93
|
+
const status = err?.data?.status;
|
|
94
|
+
if (status === 404 || err?.code === 'rest_no_route') {
|
|
95
|
+
setError('unsupported');
|
|
96
|
+
} else {
|
|
97
|
+
setError('fetch-failed');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return () => {
|
|
101
|
+
cancelled = true;
|
|
102
|
+
};
|
|
103
|
+
}, [icons]);
|
|
104
|
+
return {
|
|
105
|
+
icons,
|
|
106
|
+
error
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Inline-render an SVG string. We have to use dangerouslySetInnerHTML
|
|
112
|
+
* because the icon `content` is an `<svg>...</svg>` string from the
|
|
113
|
+
* registry, not a React element. The strings come from a trusted
|
|
114
|
+
* server-side registry — no user content.
|
|
115
|
+
*/
|
|
116
|
+
function IconSvg({
|
|
117
|
+
content,
|
|
118
|
+
className
|
|
119
|
+
}) {
|
|
120
|
+
if (!content) return null;
|
|
121
|
+
return /*#__PURE__*/_jsx("span", {
|
|
122
|
+
className: className,
|
|
123
|
+
"aria-hidden": "true",
|
|
124
|
+
dangerouslySetInnerHTML: {
|
|
125
|
+
__html: content
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function normalize(v) {
|
|
130
|
+
if (!v || typeof v !== 'object') return {
|
|
131
|
+
source: 'wp',
|
|
132
|
+
name: ''
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
source: v.source || 'wp',
|
|
136
|
+
name: v.name || v.icon || ''
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export default function IconField({
|
|
140
|
+
control,
|
|
141
|
+
value,
|
|
142
|
+
onChange
|
|
143
|
+
}) {
|
|
144
|
+
const current = normalize(value);
|
|
145
|
+
const {
|
|
146
|
+
icons,
|
|
147
|
+
error
|
|
148
|
+
} = useIcons();
|
|
149
|
+
const [open, setOpen] = useState(false);
|
|
150
|
+
const [search, setSearch] = useState('');
|
|
151
|
+
const triggerRef = useRef(null);
|
|
152
|
+
const currentIcon = useMemo(() => {
|
|
153
|
+
if (!icons || !current.name) return null;
|
|
154
|
+
return icons.find(i => i.name === current.name) || null;
|
|
155
|
+
}, [icons, current.name]);
|
|
156
|
+
|
|
157
|
+
// Restrict the candidate set BEFORE the search filter — control
|
|
158
|
+
// config can narrow to a specific namespace or hand-curated list.
|
|
159
|
+
const candidates = useMemo(() => {
|
|
160
|
+
if (!icons) return [];
|
|
161
|
+
let list = icons;
|
|
162
|
+
if (Array.isArray(control.filter) && control.filter.length > 0) {
|
|
163
|
+
const allow = new Set(control.filter);
|
|
164
|
+
list = list.filter(i => allow.has(i.name));
|
|
165
|
+
} else if (typeof control.namespace === 'string' && control.namespace !== '') {
|
|
166
|
+
const prefix = control.namespace.replace(/\/+$/, '') + '/';
|
|
167
|
+
list = list.filter(i => i.name.startsWith(prefix));
|
|
168
|
+
}
|
|
169
|
+
return list;
|
|
170
|
+
}, [icons, control.filter, control.namespace]);
|
|
171
|
+
const filtered = useMemo(() => {
|
|
172
|
+
if (!candidates.length) return [];
|
|
173
|
+
if (!search) return candidates;
|
|
174
|
+
const q = search.toLowerCase();
|
|
175
|
+
return candidates.filter(i => i.name.toLowerCase().includes(q) || (i.label || '').toLowerCase().includes(q));
|
|
176
|
+
}, [candidates, search]);
|
|
177
|
+
const pick = name => {
|
|
178
|
+
onChange({
|
|
179
|
+
source: 'wp',
|
|
180
|
+
name
|
|
181
|
+
});
|
|
182
|
+
setOpen(false);
|
|
183
|
+
setSearch('');
|
|
184
|
+
};
|
|
185
|
+
const clear = () => {
|
|
186
|
+
onChange({
|
|
187
|
+
source: 'wp',
|
|
188
|
+
name: ''
|
|
189
|
+
});
|
|
190
|
+
setOpen(false);
|
|
191
|
+
};
|
|
192
|
+
return /*#__PURE__*/_jsxs(BaseControl, {
|
|
193
|
+
label: control.label,
|
|
194
|
+
help: control.helpText,
|
|
195
|
+
className: "gcb-icon-control",
|
|
196
|
+
__nextHasNoMarginBottom: true,
|
|
197
|
+
children: [/*#__PURE__*/_jsxs("div", {
|
|
198
|
+
style: {
|
|
199
|
+
display: 'inline-flex',
|
|
200
|
+
alignItems: 'center',
|
|
201
|
+
gap: 6
|
|
202
|
+
},
|
|
203
|
+
children: [/*#__PURE__*/_jsxs(Button, {
|
|
204
|
+
ref: triggerRef,
|
|
205
|
+
variant: "secondary",
|
|
206
|
+
onClick: () => setOpen(v => !v),
|
|
207
|
+
className: "gcb-icon-control__trigger",
|
|
208
|
+
"aria-expanded": open,
|
|
209
|
+
children: [currentIcon ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
210
|
+
children: [/*#__PURE__*/_jsx(IconSvg, {
|
|
211
|
+
content: currentIcon.content,
|
|
212
|
+
className: "gcb-icon-control__trigger-svg"
|
|
213
|
+
}), /*#__PURE__*/_jsx("span", {
|
|
214
|
+
className: "gcb-icon-control__trigger-label",
|
|
215
|
+
children: currentIcon.label
|
|
216
|
+
})]
|
|
217
|
+
}) : /*#__PURE__*/_jsx("span", {
|
|
218
|
+
className: "gcb-icon-control__trigger-empty",
|
|
219
|
+
children: current.name ? sprintf(__('Unknown icon (%s)', 'gcblite'), current.name) : __('Choose an icon…', 'gcblite')
|
|
220
|
+
}), /*#__PURE__*/_jsx("span", {
|
|
221
|
+
"aria-hidden": true,
|
|
222
|
+
className: "gcb-icon-control__trigger-caret",
|
|
223
|
+
children: "\u25BE"
|
|
224
|
+
})]
|
|
225
|
+
}), currentIcon && /*#__PURE__*/_jsx(Button, {
|
|
226
|
+
variant: "tertiary",
|
|
227
|
+
isDestructive: true,
|
|
228
|
+
onClick: clear,
|
|
229
|
+
"aria-label": __('Clear icon', 'gcblite'),
|
|
230
|
+
title: __('Clear icon', 'gcblite'),
|
|
231
|
+
children: "\u2715"
|
|
232
|
+
})]
|
|
233
|
+
}), open && /*#__PURE__*/_jsx(Popover, {
|
|
234
|
+
anchor: triggerRef.current,
|
|
235
|
+
onClose: () => {
|
|
236
|
+
setOpen(false);
|
|
237
|
+
setSearch('');
|
|
238
|
+
},
|
|
239
|
+
placement: "bottom-start",
|
|
240
|
+
className: "gcb-icon-control__popover",
|
|
241
|
+
children: /*#__PURE__*/_jsxs("div", {
|
|
242
|
+
className: "gcb-icon-control__panel",
|
|
243
|
+
children: [error === 'unsupported' && /*#__PURE__*/_jsx("p", {
|
|
244
|
+
className: "gcb-icon-control__error",
|
|
245
|
+
children: __('The icon picker needs WordPress 7.0 or newer.', 'gcblite')
|
|
246
|
+
}), error === 'fetch-failed' && /*#__PURE__*/_jsx("p", {
|
|
247
|
+
className: "gcb-icon-control__error",
|
|
248
|
+
children: __('Could not load icons from the REST API.', 'gcblite')
|
|
249
|
+
}), !error && !icons && /*#__PURE__*/_jsx("div", {
|
|
250
|
+
className: "gcb-icon-control__loading",
|
|
251
|
+
children: /*#__PURE__*/_jsx(Spinner, {})
|
|
252
|
+
}), !error && icons && /*#__PURE__*/_jsxs(_Fragment, {
|
|
253
|
+
children: [/*#__PURE__*/_jsx(TextControl, {
|
|
254
|
+
label: __('Search icons', 'gcblite'),
|
|
255
|
+
hideLabelFromVision: true,
|
|
256
|
+
placeholder: __('Search…', 'gcblite'),
|
|
257
|
+
value: search,
|
|
258
|
+
onChange: setSearch,
|
|
259
|
+
__nextHasNoMarginBottom: true,
|
|
260
|
+
__next40pxDefaultSize: true
|
|
261
|
+
}), /*#__PURE__*/_jsxs("div", {
|
|
262
|
+
className: "gcb-icon-control__grid",
|
|
263
|
+
role: "listbox",
|
|
264
|
+
children: [filtered.length === 0 && /*#__PURE__*/_jsx("p", {
|
|
265
|
+
className: "gcb-icon-control__empty",
|
|
266
|
+
children: candidates.length === 0 && (control.namespace || control.filter) ? sprintf(__('No icons registered for %s yet. Use register_block_icon() to add some.', 'gcblite'), control.namespace ? `"${control.namespace}/*"` : __('this picker', 'gcblite')) : __('No icons match.', 'gcblite')
|
|
267
|
+
}), filtered.map(icon => /*#__PURE__*/_jsx("button", {
|
|
268
|
+
type: "button",
|
|
269
|
+
role: "option",
|
|
270
|
+
"aria-selected": icon.name === current.name,
|
|
271
|
+
className: `gcb-icon-control__option${icon.name === current.name ? ' is-selected' : ''}`,
|
|
272
|
+
title: icon.label,
|
|
273
|
+
onClick: () => pick(icon.name),
|
|
274
|
+
children: /*#__PURE__*/_jsx(IconSvg, {
|
|
275
|
+
content: icon.content,
|
|
276
|
+
className: "gcb-icon-control__option-svg"
|
|
277
|
+
})
|
|
278
|
+
}, icon.name))]
|
|
279
|
+
}), current.name && /*#__PURE__*/_jsx("div", {
|
|
280
|
+
className: "gcb-icon-control__footer",
|
|
281
|
+
children: /*#__PURE__*/_jsx(Button, {
|
|
282
|
+
variant: "tertiary",
|
|
283
|
+
onClick: clear,
|
|
284
|
+
isDestructive: true,
|
|
285
|
+
children: __('Clear icon', 'gcblite')
|
|
286
|
+
})
|
|
287
|
+
})]
|
|
288
|
+
})]
|
|
289
|
+
})
|
|
290
|
+
})]
|
|
291
|
+
});
|
|
292
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageField — ported verbatim from the original GCB ImageControlComponent.
|
|
3
|
+
*
|
|
4
|
+
* Stored shape:
|
|
5
|
+
* {
|
|
6
|
+
* id, url, alt, title, filename, width, height, filesize,
|
|
7
|
+
* focalPoint: { x, y },
|
|
8
|
+
* size: 'cover' | 'contain' | 'auto',
|
|
9
|
+
* repeat: boolean,
|
|
10
|
+
* isFixed: boolean,
|
|
11
|
+
* customWidth: string // e.g. "320px", "50%"
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* Features (toggleable via control config):
|
|
15
|
+
* - enableFocalPoint (default true) → FocalPointPicker on the selected image
|
|
16
|
+
* - enableSizeOptions (default true) → cover / contain / tile + custom width
|
|
17
|
+
* - enableRepeatOptions (default true) → repeat toggle (when size != cover)
|
|
18
|
+
* - enableFixedBackground (default true) → background-attachment: fixed
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { __ } from '@wordpress/i18n';
|
|
22
|
+
import { Button, FocalPointPicker, Icon, ToggleControl, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, __experimentalUnitControl as UnitControl, __experimentalHStack as HStack, __experimentalTruncate as Truncate, FlexItem, VisuallyHidden } from '@wordpress/components';
|
|
23
|
+
import { pencil as editIcon } from '@wordpress/icons';
|
|
24
|
+
import MediaPicker from './MediaPicker';
|
|
25
|
+
import PopoverOrModal from './PopoverOrModal';
|
|
26
|
+
import MediaCapabilityGate from './MediaCapabilityGate';
|
|
27
|
+
import MediaTriggerBadges from './MediaTriggerBadges';
|
|
28
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
29
|
+
const TOGGLE_BUTTON_STYLE = {
|
|
30
|
+
width: '100%',
|
|
31
|
+
height: 'auto',
|
|
32
|
+
padding: '12px',
|
|
33
|
+
justifyContent: 'flex-start',
|
|
34
|
+
border: '1px solid #ddd',
|
|
35
|
+
borderRadius: '2px',
|
|
36
|
+
backgroundColor: '#fff'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve which MIME family the media library modal should allow. The
|
|
41
|
+
* control defaults to images only; opt in to broader picks via the
|
|
42
|
+
* schema:
|
|
43
|
+
*
|
|
44
|
+
* { "type": "image", "allowVideo": true } // image OR video
|
|
45
|
+
* { "type": "image", "allowedTypes": ["video"] } // explicit override
|
|
46
|
+
*
|
|
47
|
+
* Authors uploading a video get a working URL back through the same
|
|
48
|
+
* value shape — render templates pick <img> vs <video> by extension
|
|
49
|
+
* (see the saas-banner / saas-feature-scroll item render.php examples).
|
|
50
|
+
*/
|
|
51
|
+
function resolveAllowedTypes(control) {
|
|
52
|
+
if (Array.isArray(control?.allowedTypes) && control.allowedTypes.length > 0) {
|
|
53
|
+
return control.allowedTypes;
|
|
54
|
+
}
|
|
55
|
+
if (control?.allowVideo) {
|
|
56
|
+
return ['image', 'video'];
|
|
57
|
+
}
|
|
58
|
+
return ['image'];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The settings panel rendered inside the Dropdown popover. Shows focal point,
|
|
63
|
+
* size, custom width, repeat, fixed-background. Also offers a Replace button.
|
|
64
|
+
*
|
|
65
|
+
* Exported so the gallery control can reuse it for each gallery row.
|
|
66
|
+
*/
|
|
67
|
+
export function ImageControlContent({
|
|
68
|
+
control,
|
|
69
|
+
value,
|
|
70
|
+
onChange,
|
|
71
|
+
onReplace
|
|
72
|
+
}) {
|
|
73
|
+
const enableFocalPoint = control.enableFocalPoint !== false;
|
|
74
|
+
const enableSizeOptions = control.enableSizeOptions !== false;
|
|
75
|
+
const enableRepeatOptions = control.enableRepeatOptions !== false;
|
|
76
|
+
const enableFixedBackground = control.enableFixedBackground !== false;
|
|
77
|
+
const imageValue = value || {};
|
|
78
|
+
const focalPoint = imageValue.focalPoint || {
|
|
79
|
+
x: 0.5,
|
|
80
|
+
y: 0.5
|
|
81
|
+
};
|
|
82
|
+
const size = imageValue.size || 'cover';
|
|
83
|
+
const customWidth = imageValue.customWidth || '';
|
|
84
|
+
const repeat = imageValue.repeat !== false;
|
|
85
|
+
const isFixed = imageValue.isFixed || false;
|
|
86
|
+
const updateValue = updates => onChange({
|
|
87
|
+
...imageValue,
|
|
88
|
+
...updates
|
|
89
|
+
});
|
|
90
|
+
if (!imageValue.url) return null;
|
|
91
|
+
const displayTitle = imageValue.title || imageValue.filename || imageValue.alt || __('(no description)', 'gcblite');
|
|
92
|
+
return /*#__PURE__*/_jsxs("div", {
|
|
93
|
+
className: "gcb-image-control-content__sections",
|
|
94
|
+
children: [onReplace && /*#__PURE__*/_jsxs(Button, {
|
|
95
|
+
onClick: onReplace,
|
|
96
|
+
className: "gcb-modal-toggle-button",
|
|
97
|
+
"aria-label": __('Replace image', 'gcblite'),
|
|
98
|
+
style: {
|
|
99
|
+
width: '100%',
|
|
100
|
+
display: 'flex',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
justifyContent: 'flex-start',
|
|
103
|
+
padding: '8px 12px',
|
|
104
|
+
border: '1px solid #ddd',
|
|
105
|
+
borderRadius: 2,
|
|
106
|
+
background: 'white',
|
|
107
|
+
cursor: 'pointer',
|
|
108
|
+
gap: 12
|
|
109
|
+
},
|
|
110
|
+
children: [/*#__PURE__*/_jsx("span", {
|
|
111
|
+
"aria-hidden": true,
|
|
112
|
+
style: {
|
|
113
|
+
width: 32,
|
|
114
|
+
height: 32,
|
|
115
|
+
borderRadius: '100%',
|
|
116
|
+
backgroundImage: `url(${imageValue.url})`,
|
|
117
|
+
backgroundSize: 'cover',
|
|
118
|
+
backgroundPosition: 'center',
|
|
119
|
+
flexShrink: 0,
|
|
120
|
+
border: '1px solid #ddd',
|
|
121
|
+
display: 'block'
|
|
122
|
+
}
|
|
123
|
+
}), /*#__PURE__*/_jsx("span", {
|
|
124
|
+
style: {
|
|
125
|
+
flex: 1,
|
|
126
|
+
overflow: 'hidden',
|
|
127
|
+
textOverflow: 'ellipsis',
|
|
128
|
+
whiteSpace: 'nowrap',
|
|
129
|
+
textAlign: 'left'
|
|
130
|
+
},
|
|
131
|
+
children: displayTitle
|
|
132
|
+
}), /*#__PURE__*/_jsx(Icon, {
|
|
133
|
+
icon: editIcon,
|
|
134
|
+
size: 20,
|
|
135
|
+
style: {
|
|
136
|
+
fill: '#1e1e1e',
|
|
137
|
+
flexShrink: 0
|
|
138
|
+
}
|
|
139
|
+
})]
|
|
140
|
+
}), enableFocalPoint && /*#__PURE__*/_jsx(FocalPointPicker, {
|
|
141
|
+
url: imageValue.url,
|
|
142
|
+
value: focalPoint,
|
|
143
|
+
onChange: p => updateValue({
|
|
144
|
+
focalPoint: p
|
|
145
|
+
})
|
|
146
|
+
}), enableFixedBackground && /*#__PURE__*/_jsx(ToggleControl, {
|
|
147
|
+
label: __('Fixed background', 'gcblite'),
|
|
148
|
+
checked: isFixed,
|
|
149
|
+
onChange: v => updateValue({
|
|
150
|
+
isFixed: v
|
|
151
|
+
}),
|
|
152
|
+
__nextHasNoMarginBottom: true
|
|
153
|
+
}), enableSizeOptions && /*#__PURE__*/_jsxs("div", {
|
|
154
|
+
className: "gcb-image-control-content__sections",
|
|
155
|
+
children: [/*#__PURE__*/_jsxs(ToggleGroupControl, {
|
|
156
|
+
label: __('Size', 'gcblite'),
|
|
157
|
+
value: size,
|
|
158
|
+
onChange: v => {
|
|
159
|
+
if (v === 'cover' || v === 'contain') {
|
|
160
|
+
updateValue({
|
|
161
|
+
size: v,
|
|
162
|
+
repeat: false
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
updateValue({
|
|
166
|
+
size: v
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
isBlock: true,
|
|
171
|
+
__nextHasNoMarginBottom: true,
|
|
172
|
+
__next40pxDefaultSize: true,
|
|
173
|
+
help: size === 'cover' ? __('Image covers the space evenly.', 'gcblite') : size === 'contain' ? __('Image is contained without distortion.', 'gcblite') : undefined,
|
|
174
|
+
children: [/*#__PURE__*/_jsx(ToggleGroupControlOption, {
|
|
175
|
+
value: "cover",
|
|
176
|
+
label: __('Cover', 'gcblite')
|
|
177
|
+
}), /*#__PURE__*/_jsx(ToggleGroupControlOption, {
|
|
178
|
+
value: "contain",
|
|
179
|
+
label: __('Contain', 'gcblite')
|
|
180
|
+
}), /*#__PURE__*/_jsx(ToggleGroupControlOption, {
|
|
181
|
+
value: "auto",
|
|
182
|
+
label: __('Tile', 'gcblite')
|
|
183
|
+
})]
|
|
184
|
+
}), /*#__PURE__*/_jsxs(HStack, {
|
|
185
|
+
spacing: 3,
|
|
186
|
+
children: [/*#__PURE__*/_jsx(UnitControl, {
|
|
187
|
+
value: customWidth,
|
|
188
|
+
onChange: v => updateValue({
|
|
189
|
+
customWidth: v
|
|
190
|
+
}),
|
|
191
|
+
units: [{
|
|
192
|
+
value: 'px',
|
|
193
|
+
label: 'px'
|
|
194
|
+
}, {
|
|
195
|
+
value: '%',
|
|
196
|
+
label: '%'
|
|
197
|
+
}, {
|
|
198
|
+
value: 'em',
|
|
199
|
+
label: 'em'
|
|
200
|
+
}, {
|
|
201
|
+
value: 'rem',
|
|
202
|
+
label: 'rem'
|
|
203
|
+
}, {
|
|
204
|
+
value: 'vw',
|
|
205
|
+
label: 'vw'
|
|
206
|
+
}, {
|
|
207
|
+
value: 'vh',
|
|
208
|
+
label: 'vh'
|
|
209
|
+
}],
|
|
210
|
+
placeholder: __('Auto', 'gcblite'),
|
|
211
|
+
min: 0,
|
|
212
|
+
step: 1,
|
|
213
|
+
disabled: size === 'cover',
|
|
214
|
+
"aria-label": __('Background image width', 'gcblite'),
|
|
215
|
+
__nextHasNoMarginBottom: true
|
|
216
|
+
}), enableRepeatOptions && /*#__PURE__*/_jsx(ToggleControl, {
|
|
217
|
+
label: __('Repeat', 'gcblite'),
|
|
218
|
+
checked: repeat,
|
|
219
|
+
onChange: v => updateValue({
|
|
220
|
+
repeat: v
|
|
221
|
+
}),
|
|
222
|
+
disabled: size === 'cover',
|
|
223
|
+
__nextHasNoMarginBottom: true
|
|
224
|
+
})]
|
|
225
|
+
})]
|
|
226
|
+
})]
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
export default function ImageField({
|
|
230
|
+
control,
|
|
231
|
+
value,
|
|
232
|
+
onChange
|
|
233
|
+
}) {
|
|
234
|
+
const imageValue = value || {};
|
|
235
|
+
return /*#__PURE__*/_jsxs("div", {
|
|
236
|
+
className: "components-base-control gcb-image-control",
|
|
237
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
238
|
+
className: "components-base-control__field",
|
|
239
|
+
children: /*#__PURE__*/_jsx("label", {
|
|
240
|
+
className: "components-base-control__label",
|
|
241
|
+
children: control.label
|
|
242
|
+
})
|
|
243
|
+
}), control.helpText && /*#__PURE__*/_jsx("p", {
|
|
244
|
+
className: "components-base-control__help",
|
|
245
|
+
children: control.helpText
|
|
246
|
+
}), /*#__PURE__*/_jsx(MediaCapabilityGate, {
|
|
247
|
+
children: /*#__PURE__*/_jsx(MediaPicker, {
|
|
248
|
+
onSelect: media => {
|
|
249
|
+
onChange({
|
|
250
|
+
id: media.id,
|
|
251
|
+
url: media.url,
|
|
252
|
+
alt: media.alt || '',
|
|
253
|
+
title: media.title || media.filename || '',
|
|
254
|
+
filename: media.filename || '',
|
|
255
|
+
width: media.width,
|
|
256
|
+
height: media.height,
|
|
257
|
+
filesize: media.filesizeInBytes,
|
|
258
|
+
focalPoint: imageValue.focalPoint || {
|
|
259
|
+
x: 0.5,
|
|
260
|
+
y: 0.5
|
|
261
|
+
},
|
|
262
|
+
size: imageValue.size || 'cover',
|
|
263
|
+
repeat: imageValue.repeat !== false,
|
|
264
|
+
isFixed: imageValue.isFixed || false,
|
|
265
|
+
customWidth: imageValue.customWidth || ''
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
allowedTypes: resolveAllowedTypes(control),
|
|
269
|
+
value: imageValue?.id,
|
|
270
|
+
render: ({
|
|
271
|
+
open
|
|
272
|
+
}) => /*#__PURE__*/_jsxs("div", {
|
|
273
|
+
className: "gcb-image-control-content",
|
|
274
|
+
children: [!imageValue?.url && /*#__PURE__*/_jsxs(_Fragment, {
|
|
275
|
+
children: [/*#__PURE__*/_jsx(Button, {
|
|
276
|
+
onClick: open,
|
|
277
|
+
variant: "secondary",
|
|
278
|
+
style: {
|
|
279
|
+
marginBottom: 8
|
|
280
|
+
},
|
|
281
|
+
children: __('Add image', 'gcblite')
|
|
282
|
+
}), /*#__PURE__*/_jsx("p", {
|
|
283
|
+
style: {
|
|
284
|
+
fontSize: 13,
|
|
285
|
+
color: '#757575',
|
|
286
|
+
margin: 0
|
|
287
|
+
},
|
|
288
|
+
children: __('No image selected', 'gcblite')
|
|
289
|
+
})]
|
|
290
|
+
}), imageValue?.url && /*#__PURE__*/_jsx(PopoverOrModal, {
|
|
291
|
+
modalTitle: control.label || __('Image settings', 'gcblite'),
|
|
292
|
+
dropdownProps: {
|
|
293
|
+
popoverProps: {
|
|
294
|
+
placement: 'left-start'
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
renderToggle: ({
|
|
298
|
+
isOpen,
|
|
299
|
+
onToggle
|
|
300
|
+
}) => {
|
|
301
|
+
const displayTitle = imageValue.title || imageValue.filename || imageValue.alt || __('(no description)', 'gcblite');
|
|
302
|
+
return /*#__PURE__*/_jsx(MediaTriggerBadges, {
|
|
303
|
+
onClear: () => onChange(null),
|
|
304
|
+
children: /*#__PURE__*/_jsx(Button, {
|
|
305
|
+
onClick: onToggle,
|
|
306
|
+
"aria-expanded": isOpen,
|
|
307
|
+
"aria-label": __('Image size, position and focal point options.', 'gcblite'),
|
|
308
|
+
className: "gcb-modal-toggle-button gcb-image-control-toggle",
|
|
309
|
+
style: TOGGLE_BUTTON_STYLE,
|
|
310
|
+
children: /*#__PURE__*/_jsxs(HStack, {
|
|
311
|
+
spacing: 3,
|
|
312
|
+
justify: "flex-start",
|
|
313
|
+
children: [/*#__PURE__*/_jsx("span", {
|
|
314
|
+
"aria-hidden": true,
|
|
315
|
+
style: {
|
|
316
|
+
width: 32,
|
|
317
|
+
height: 32,
|
|
318
|
+
borderRadius: '100%',
|
|
319
|
+
backgroundImage: `url(${imageValue.url})`,
|
|
320
|
+
backgroundSize: 'cover',
|
|
321
|
+
backgroundPosition: 'center',
|
|
322
|
+
flexShrink: 0,
|
|
323
|
+
border: '1px solid #ddd',
|
|
324
|
+
display: 'block'
|
|
325
|
+
}
|
|
326
|
+
}), /*#__PURE__*/_jsxs(FlexItem, {
|
|
327
|
+
children: [/*#__PURE__*/_jsx(Truncate, {
|
|
328
|
+
numberOfLines: 1,
|
|
329
|
+
children: displayTitle
|
|
330
|
+
}), /*#__PURE__*/_jsxs(VisuallyHidden, {
|
|
331
|
+
children: [__('Image:', 'gcblite'), " ", displayTitle]
|
|
332
|
+
})]
|
|
333
|
+
})]
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
renderContent: ({
|
|
339
|
+
close
|
|
340
|
+
}) => /*#__PURE__*/_jsx("div", {
|
|
341
|
+
style: {
|
|
342
|
+
padding: 16,
|
|
343
|
+
minWidth: 280
|
|
344
|
+
},
|
|
345
|
+
children: /*#__PURE__*/_jsx(ImageControlContent, {
|
|
346
|
+
control: control,
|
|
347
|
+
value: imageValue,
|
|
348
|
+
onChange: onChange,
|
|
349
|
+
onReplace: () => {
|
|
350
|
+
close();
|
|
351
|
+
open();
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
})]
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
})]
|
|
359
|
+
});
|
|
360
|
+
}
|