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