@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.
- 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 +241 -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 +141 -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
package/src/controls/gallery.js
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GalleryField — ported verbatim from the original GCB GalleryControlComponent.
|
|
3
|
-
*
|
|
4
|
-
* Stored shape: array of image objects (same shape as ImageField), with
|
|
5
|
-
* focalPoint / size / customWidth / repeat / isFixed preserved per item across
|
|
6
|
-
* media-library reselects.
|
|
7
|
-
*
|
|
8
|
-
* Each row is independently draggable (dnd-kit) and edits open the same
|
|
9
|
-
* focal-point/size panel as the single-image control.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { __ } from '@wordpress/i18n';
|
|
13
|
-
import { useState } from '@wordpress/element';
|
|
14
|
-
import {
|
|
15
|
-
Button,
|
|
16
|
-
__experimentalHStack as HStack,
|
|
17
|
-
__experimentalTruncate as Truncate,
|
|
18
|
-
} from '@wordpress/components';
|
|
19
|
-
import MediaPicker from './MediaPicker';
|
|
20
|
-
import MediaCapabilityGate from './MediaCapabilityGate';
|
|
21
|
-
import PopoverOrModal from './PopoverOrModal';
|
|
22
|
-
import {
|
|
23
|
-
DndContext,
|
|
24
|
-
closestCenter,
|
|
25
|
-
PointerSensor,
|
|
26
|
-
useSensor,
|
|
27
|
-
useSensors,
|
|
28
|
-
DragOverlay,
|
|
29
|
-
} from '@dnd-kit/core';
|
|
30
|
-
import {
|
|
31
|
-
SortableContext,
|
|
32
|
-
verticalListSortingStrategy,
|
|
33
|
-
useSortable,
|
|
34
|
-
arrayMove,
|
|
35
|
-
} from '@dnd-kit/sortable';
|
|
36
|
-
import { CSS } from '@dnd-kit/utilities';
|
|
37
|
-
import { ImageControlContent } from './image';
|
|
38
|
-
|
|
39
|
-
const TOGGLE_BUTTON_STYLE = {
|
|
40
|
-
width: '100%',
|
|
41
|
-
height: 'auto',
|
|
42
|
-
padding: '12px',
|
|
43
|
-
justifyContent: 'flex-start',
|
|
44
|
-
border: '1px solid #ddd',
|
|
45
|
-
borderRadius: '2px',
|
|
46
|
-
backgroundColor: '#fff',
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Single gallery row — drag handle + thumbnail dropdown trigger + remove button.
|
|
51
|
-
*/
|
|
52
|
-
function SortableGalleryImage({ image, onUpdate, onRemove, control }) {
|
|
53
|
-
const {
|
|
54
|
-
attributes,
|
|
55
|
-
listeners,
|
|
56
|
-
setNodeRef,
|
|
57
|
-
transform,
|
|
58
|
-
transition,
|
|
59
|
-
isDragging,
|
|
60
|
-
} = useSortable({ id: image.id });
|
|
61
|
-
|
|
62
|
-
const style = {
|
|
63
|
-
transform: CSS.Transform.toString(transform),
|
|
64
|
-
transition,
|
|
65
|
-
opacity: isDragging ? 0.5 : 1,
|
|
66
|
-
marginBottom: 8,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const displayTitle = image.title || image.filename || image.alt || __('(no description)', 'gcblite');
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div ref={setNodeRef} style={style} className="gcb-gallery-image-item">
|
|
73
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
74
|
-
<div
|
|
75
|
-
{...attributes}
|
|
76
|
-
{...listeners}
|
|
77
|
-
style={{
|
|
78
|
-
cursor: 'grab',
|
|
79
|
-
display: 'flex',
|
|
80
|
-
alignItems: 'center',
|
|
81
|
-
justifyContent: 'center',
|
|
82
|
-
padding: 4,
|
|
83
|
-
flexShrink: 0,
|
|
84
|
-
}}
|
|
85
|
-
aria-label={__('Drag to reorder', 'gcblite')}
|
|
86
|
-
>
|
|
87
|
-
<svg viewBox="0 0 20 20" width="16" height="16" style={{ fill: '#666' }}>
|
|
88
|
-
<path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z" />
|
|
89
|
-
</svg>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
<MediaPicker
|
|
93
|
-
onSelect={(media) => onUpdate(image.id, {
|
|
94
|
-
...image,
|
|
95
|
-
id: media.id,
|
|
96
|
-
url: media.url,
|
|
97
|
-
alt: media.alt || '',
|
|
98
|
-
title: media.title || media.filename || '',
|
|
99
|
-
filename: media.filename || '',
|
|
100
|
-
width: media.width,
|
|
101
|
-
height: media.height,
|
|
102
|
-
filesize: media.filesizeInBytes,
|
|
103
|
-
})}
|
|
104
|
-
allowedTypes={['image']}
|
|
105
|
-
value={image.id}
|
|
106
|
-
render={({ open }) => (
|
|
107
|
-
<PopoverOrModal
|
|
108
|
-
modalTitle={displayTitle || __('Gallery image', 'gcblite')}
|
|
109
|
-
dropdownProps={{ popoverProps: { placement: 'left-start' } }}
|
|
110
|
-
renderToggle={({ isOpen, onToggle }) => (
|
|
111
|
-
<Button
|
|
112
|
-
onClick={onToggle}
|
|
113
|
-
aria-expanded={isOpen}
|
|
114
|
-
aria-label={__('Image settings', 'gcblite')}
|
|
115
|
-
className="gcb-modal-toggle-button gcb-image-control-toggle"
|
|
116
|
-
style={{ ...TOGGLE_BUTTON_STYLE, flex: 1 }}
|
|
117
|
-
>
|
|
118
|
-
<HStack spacing={3} justify="flex-start">
|
|
119
|
-
<span
|
|
120
|
-
aria-hidden
|
|
121
|
-
style={{
|
|
122
|
-
width: 32,
|
|
123
|
-
height: 32,
|
|
124
|
-
borderRadius: '100%',
|
|
125
|
-
backgroundImage: `url(${image.url})`,
|
|
126
|
-
backgroundSize: 'cover',
|
|
127
|
-
backgroundPosition: 'center',
|
|
128
|
-
flexShrink: 0,
|
|
129
|
-
border: '1px solid #ddd',
|
|
130
|
-
display: 'block',
|
|
131
|
-
}}
|
|
132
|
-
/>
|
|
133
|
-
<Truncate numberOfLines={1}>{displayTitle}</Truncate>
|
|
134
|
-
</HStack>
|
|
135
|
-
</Button>
|
|
136
|
-
)}
|
|
137
|
-
renderContent={({ close }) => (
|
|
138
|
-
<div style={{ padding: 16, minWidth: 280 }}>
|
|
139
|
-
<ImageControlContent
|
|
140
|
-
control={control}
|
|
141
|
-
value={image}
|
|
142
|
-
onChange={(newValue) => onUpdate(image.id, newValue)}
|
|
143
|
-
onReplace={() => { close(); open(); }}
|
|
144
|
-
/>
|
|
145
|
-
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid #ddd' }}>
|
|
146
|
-
<Button
|
|
147
|
-
onClick={() => { onRemove(image.id); close(); }}
|
|
148
|
-
variant="link"
|
|
149
|
-
isDestructive
|
|
150
|
-
style={{ width: '100%' }}
|
|
151
|
-
>
|
|
152
|
-
{__('Remove from gallery', 'gcblite')}
|
|
153
|
-
</Button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
)}
|
|
157
|
-
/>
|
|
158
|
-
)}
|
|
159
|
-
/>
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export default function GalleryField({ control, value, onChange }) {
|
|
166
|
-
const [activeId, setActiveId] = useState(null);
|
|
167
|
-
const images = Array.isArray(value) ? value : [];
|
|
168
|
-
|
|
169
|
-
const sensors = useSensors(
|
|
170
|
-
useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const handleDragStart = (event) => setActiveId(event.active.id);
|
|
174
|
-
|
|
175
|
-
const handleDragEnd = (event) => {
|
|
176
|
-
const { active, over } = event;
|
|
177
|
-
setActiveId(null);
|
|
178
|
-
if (over && active.id !== over.id) {
|
|
179
|
-
const oldIndex = images.findIndex((img) => img.id === active.id);
|
|
180
|
-
const newIndex = images.findIndex((img) => img.id === over.id);
|
|
181
|
-
onChange(arrayMove(images, oldIndex, newIndex));
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const handleSelect = (media) => {
|
|
186
|
-
const newImages = Array.isArray(media) ? media : [media];
|
|
187
|
-
const existingMap = new Map(images.map((img) => [img.id, img]));
|
|
188
|
-
|
|
189
|
-
const formatted = newImages.map((img) => {
|
|
190
|
-
const existing = existingMap.get(img.id);
|
|
191
|
-
if (existing) {
|
|
192
|
-
return {
|
|
193
|
-
...existing,
|
|
194
|
-
url: img.url,
|
|
195
|
-
alt: img.alt || existing.alt,
|
|
196
|
-
title: img.title || img.filename || existing.title,
|
|
197
|
-
filename: img.filename || existing.filename,
|
|
198
|
-
width: img.width,
|
|
199
|
-
height: img.height,
|
|
200
|
-
filesize: img.filesizeInBytes,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
id: img.id,
|
|
205
|
-
url: img.url,
|
|
206
|
-
alt: img.alt || '',
|
|
207
|
-
title: img.title || img.filename || '',
|
|
208
|
-
filename: img.filename || '',
|
|
209
|
-
width: img.width,
|
|
210
|
-
height: img.height,
|
|
211
|
-
filesize: img.filesizeInBytes,
|
|
212
|
-
focalPoint: { x: 0.5, y: 0.5 },
|
|
213
|
-
size: 'cover',
|
|
214
|
-
customWidth: '',
|
|
215
|
-
repeat: true,
|
|
216
|
-
isFixed: false,
|
|
217
|
-
};
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
onChange(formatted);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const removeImage = (imageId) => onChange(images.filter((img) => img.id !== imageId));
|
|
224
|
-
const updateImage = (imageId, updates) =>
|
|
225
|
-
onChange(images.map((img) => (img.id === imageId ? { ...img, ...updates } : img)));
|
|
226
|
-
|
|
227
|
-
return (
|
|
228
|
-
<div className="components-base-control gcb-gallery-control">
|
|
229
|
-
<div className="components-base-control__field">
|
|
230
|
-
<label className="components-base-control__label">{control.label}</label>
|
|
231
|
-
</div>
|
|
232
|
-
{control.helpText && (
|
|
233
|
-
<p className="components-base-control__help">{control.helpText}</p>
|
|
234
|
-
)}
|
|
235
|
-
|
|
236
|
-
<MediaCapabilityGate>
|
|
237
|
-
<MediaPicker
|
|
238
|
-
onSelect={handleSelect}
|
|
239
|
-
allowedTypes={['image']}
|
|
240
|
-
multiple
|
|
241
|
-
gallery
|
|
242
|
-
value={images.map((img) => img.id)}
|
|
243
|
-
render={({ open }) => (
|
|
244
|
-
<div className="gcb-gallery-control-content">
|
|
245
|
-
{images.length === 0 ? (
|
|
246
|
-
<Button
|
|
247
|
-
onClick={open}
|
|
248
|
-
variant="secondary"
|
|
249
|
-
style={{ marginBottom: 8 }}
|
|
250
|
-
>
|
|
251
|
-
{__('Add images', 'gcblite')}
|
|
252
|
-
</Button>
|
|
253
|
-
) : (
|
|
254
|
-
<>
|
|
255
|
-
<DndContext
|
|
256
|
-
sensors={sensors}
|
|
257
|
-
collisionDetection={closestCenter}
|
|
258
|
-
onDragStart={handleDragStart}
|
|
259
|
-
onDragEnd={handleDragEnd}
|
|
260
|
-
>
|
|
261
|
-
<SortableContext
|
|
262
|
-
items={images.map((img) => img.id)}
|
|
263
|
-
strategy={verticalListSortingStrategy}
|
|
264
|
-
>
|
|
265
|
-
<div className="gcb-gallery-items">
|
|
266
|
-
{images.map((image) => (
|
|
267
|
-
<SortableGalleryImage
|
|
268
|
-
key={image.id}
|
|
269
|
-
image={image}
|
|
270
|
-
onUpdate={updateImage}
|
|
271
|
-
onRemove={removeImage}
|
|
272
|
-
control={control}
|
|
273
|
-
/>
|
|
274
|
-
))}
|
|
275
|
-
</div>
|
|
276
|
-
</SortableContext>
|
|
277
|
-
|
|
278
|
-
<DragOverlay>
|
|
279
|
-
{activeId
|
|
280
|
-
? (() => {
|
|
281
|
-
const active = images.find((img) => img.id === activeId);
|
|
282
|
-
const title = active?.title || active?.filename || active?.alt || __('(no description)', 'gcblite');
|
|
283
|
-
return (
|
|
284
|
-
<div style={{
|
|
285
|
-
display: 'flex',
|
|
286
|
-
alignItems: 'center',
|
|
287
|
-
gap: 8,
|
|
288
|
-
padding: '8px 12px',
|
|
289
|
-
background: 'white',
|
|
290
|
-
borderRadius: 2,
|
|
291
|
-
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
292
|
-
opacity: 0.9,
|
|
293
|
-
maxWidth: 400,
|
|
294
|
-
border: '1px solid #ddd',
|
|
295
|
-
}}>
|
|
296
|
-
<svg viewBox="0 0 20 20" width="16" height="16" style={{ fill: '#666' }}>
|
|
297
|
-
<path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z" />
|
|
298
|
-
</svg>
|
|
299
|
-
<span
|
|
300
|
-
aria-hidden
|
|
301
|
-
style={{
|
|
302
|
-
width: 32,
|
|
303
|
-
height: 32,
|
|
304
|
-
borderRadius: '100%',
|
|
305
|
-
backgroundImage: `url(${active?.url})`,
|
|
306
|
-
backgroundSize: 'cover',
|
|
307
|
-
backgroundPosition: 'center',
|
|
308
|
-
flexShrink: 0,
|
|
309
|
-
border: '1px solid #ddd',
|
|
310
|
-
display: 'block',
|
|
311
|
-
}}
|
|
312
|
-
/>
|
|
313
|
-
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 13 }}>
|
|
314
|
-
{title}
|
|
315
|
-
</span>
|
|
316
|
-
</div>
|
|
317
|
-
);
|
|
318
|
-
})()
|
|
319
|
-
: null}
|
|
320
|
-
</DragOverlay>
|
|
321
|
-
</DndContext>
|
|
322
|
-
|
|
323
|
-
<Button
|
|
324
|
-
onClick={open}
|
|
325
|
-
variant="secondary"
|
|
326
|
-
style={{ width: '100%', marginTop: 12 }}
|
|
327
|
-
>
|
|
328
|
-
{__('Add more images', 'gcblite')}
|
|
329
|
-
</Button>
|
|
330
|
-
</>
|
|
331
|
-
)}
|
|
332
|
-
</div>
|
|
333
|
-
)}
|
|
334
|
-
/>
|
|
335
|
-
</MediaCapabilityGate>
|
|
336
|
-
</div>
|
|
337
|
-
);
|
|
338
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { BaseControl, Notice, TextControl } from '@wordpress/components';
|
|
2
|
-
import { useState, useEffect, useRef, useCallback } from '@wordpress/element';
|
|
3
|
-
import { __ } from '@wordpress/i18n';
|
|
4
|
-
import { useGoogleMapsEnabled } from '../provider';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Google Map control — address search with autocomplete + interactive map.
|
|
8
|
-
*
|
|
9
|
-
* Stored shape: { address, lat, lng, zoom }
|
|
10
|
-
*
|
|
11
|
-
* Maps features are gated on the host signalling a configured API key —
|
|
12
|
-
* `googleMapsEnabled` from GcbFieldsProvider, falling back to
|
|
13
|
-
* `window.gcbLite.googleMaps.hasApiKey`. With no key, falls back to a plain
|
|
14
|
-
* address input that sets `address` only. (The Maps JS SDK itself is enqueued
|
|
15
|
-
* by the host when a key exists; this control just reads window.google.)
|
|
16
|
-
*/
|
|
17
|
-
export default function GoogleMapField({ control, value, onChange }) {
|
|
18
|
-
const location = value && typeof value === 'object' ? value : { address: '', lat: null, lng: null, zoom: 14 };
|
|
19
|
-
const hasApiKey = useGoogleMapsEnabled();
|
|
20
|
-
|
|
21
|
-
const [address, setAddress] = useState(location.address || '');
|
|
22
|
-
useEffect(() => { setAddress(location.address || ''); }, [location.address]);
|
|
23
|
-
|
|
24
|
-
const inputRef = useRef(null);
|
|
25
|
-
const mapRef = useRef(null);
|
|
26
|
-
const mapInstance = useRef(null);
|
|
27
|
-
const markerRef = useRef(null);
|
|
28
|
-
|
|
29
|
-
const update = useCallback((next) => {
|
|
30
|
-
onChange({ ...location, ...next });
|
|
31
|
-
}, [onChange, location]);
|
|
32
|
-
|
|
33
|
-
// Wire up Places autocomplete on the input.
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (!hasApiKey || !inputRef.current || !window.google?.maps?.places) return;
|
|
36
|
-
|
|
37
|
-
const ac = new window.google.maps.places.Autocomplete(inputRef.current, {
|
|
38
|
-
fields: ['formatted_address', 'geometry'],
|
|
39
|
-
});
|
|
40
|
-
const listener = ac.addListener('place_changed', () => {
|
|
41
|
-
const place = ac.getPlace();
|
|
42
|
-
if (!place.geometry) return;
|
|
43
|
-
const lat = place.geometry.location.lat();
|
|
44
|
-
const lng = place.geometry.location.lng();
|
|
45
|
-
update({ address: place.formatted_address || '', lat, lng });
|
|
46
|
-
});
|
|
47
|
-
return () => window.google.maps.event.removeListener(listener);
|
|
48
|
-
}, [hasApiKey, update]);
|
|
49
|
-
|
|
50
|
-
// Wire up the map preview when we have coords.
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
if (!hasApiKey || !mapRef.current || !window.google?.maps) return;
|
|
53
|
-
if (location.lat == null || location.lng == null) return;
|
|
54
|
-
|
|
55
|
-
const center = { lat: Number(location.lat), lng: Number(location.lng) };
|
|
56
|
-
if (!mapInstance.current) {
|
|
57
|
-
mapInstance.current = new window.google.maps.Map(mapRef.current, {
|
|
58
|
-
center,
|
|
59
|
-
zoom: location.zoom || 14,
|
|
60
|
-
disableDefaultUI: true,
|
|
61
|
-
clickableIcons: false,
|
|
62
|
-
});
|
|
63
|
-
markerRef.current = new window.google.maps.Marker({
|
|
64
|
-
position: center,
|
|
65
|
-
map: mapInstance.current,
|
|
66
|
-
draggable: true,
|
|
67
|
-
});
|
|
68
|
-
markerRef.current.addListener('dragend', (e) => {
|
|
69
|
-
update({ lat: e.latLng.lat(), lng: e.latLng.lng() });
|
|
70
|
-
});
|
|
71
|
-
} else {
|
|
72
|
-
mapInstance.current.setCenter(center);
|
|
73
|
-
markerRef.current.setPosition(center);
|
|
74
|
-
}
|
|
75
|
-
}, [hasApiKey, location.lat, location.lng, location.zoom, update]);
|
|
76
|
-
|
|
77
|
-
if (!hasApiKey) {
|
|
78
|
-
return (
|
|
79
|
-
<BaseControl label={control.label} help={control.helpText} __nextHasNoMarginBottom>
|
|
80
|
-
<Notice status="warning" isDismissible={false}>
|
|
81
|
-
{__('No Google Maps API key configured. Set one with the `gcb_google_maps_api_key` filter to enable autocomplete and the map preview.', 'gcblite')}
|
|
82
|
-
</Notice>
|
|
83
|
-
<TextControl
|
|
84
|
-
label=""
|
|
85
|
-
hideLabelFromVision
|
|
86
|
-
value={address}
|
|
87
|
-
onChange={(next) => {
|
|
88
|
-
setAddress(next);
|
|
89
|
-
update({ address: next });
|
|
90
|
-
}}
|
|
91
|
-
placeholder={__('Enter an address…', 'gcblite')}
|
|
92
|
-
__nextHasNoMarginBottom
|
|
93
|
-
/>
|
|
94
|
-
</BaseControl>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<BaseControl label={control.label} help={control.helpText} __nextHasNoMarginBottom>
|
|
100
|
-
<TextControl
|
|
101
|
-
label=""
|
|
102
|
-
hideLabelFromVision
|
|
103
|
-
ref={inputRef}
|
|
104
|
-
value={address}
|
|
105
|
-
onChange={setAddress}
|
|
106
|
-
placeholder={__('Start typing an address…', 'gcblite')}
|
|
107
|
-
__nextHasNoMarginBottom
|
|
108
|
-
/>
|
|
109
|
-
{location.lat != null && location.lng != null && (
|
|
110
|
-
<div
|
|
111
|
-
ref={mapRef}
|
|
112
|
-
style={{ width: '100%', height: 200, marginTop: 8, borderRadius: 4, overflow: 'hidden' }}
|
|
113
|
-
/>
|
|
114
|
-
)}
|
|
115
|
-
</BaseControl>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HeadingLevel — compound input: a text field for the heading content
|
|
3
|
-
* and an inline dropdown for the semantic level. Visually mirrors WP's
|
|
4
|
-
* UnitControl (the "10 px" combo): input on the left, suffix selector
|
|
5
|
-
* on the right, single 40px-tall row.
|
|
6
|
-
*
|
|
7
|
-
* Stored shape:
|
|
8
|
-
* { text: 'Section title', level: 'h2' }
|
|
9
|
-
*
|
|
10
|
-
* React frontend usage:
|
|
11
|
-
* const { text, level } = heading || {};
|
|
12
|
-
* if (!text) return null;
|
|
13
|
-
* const Tag = level || 'h2';
|
|
14
|
-
* return <Tag className="...">{text}</Tag>;
|
|
15
|
-
*
|
|
16
|
-
* Config:
|
|
17
|
-
* levels ['h1','h2','h3','h4','h5','h6','p','div','span'] (default)
|
|
18
|
-
* — set to your own subset to restrict choice.
|
|
19
|
-
* default { text, level } — initial value when the attribute is empty
|
|
20
|
-
* placeholder string — placeholder for the text input
|
|
21
|
-
*
|
|
22
|
-
* Accessibility: `div` and `span` are non-semantic; the helper text turns
|
|
23
|
-
* red when one is selected so authors see the trade-off before shipping.
|
|
24
|
-
*
|
|
25
|
-
* Implementation note: uses WP's @experimental `InputControl` primitive
|
|
26
|
-
* directly — same component UnitControl is built on top of. The `suffix`
|
|
27
|
-
* slot is exactly the "input + dropdown" pattern; the component handles
|
|
28
|
-
* the row height, focus ring, and emotion class plumbing so we don't.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { __ } from '@wordpress/i18n';
|
|
32
|
-
import {
|
|
33
|
-
__experimentalInputControl as InputControl,
|
|
34
|
-
Notice,
|
|
35
|
-
} from '@wordpress/components';
|
|
36
|
-
|
|
37
|
-
const ALL_LEVELS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'span'];
|
|
38
|
-
const HEADING_LEVELS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
|
|
39
|
-
|
|
40
|
-
function resolveLevels(control) {
|
|
41
|
-
if (Array.isArray(control.levels) && control.levels.length > 0) {
|
|
42
|
-
return control.levels.filter((l) => ALL_LEVELS.includes(l));
|
|
43
|
-
}
|
|
44
|
-
return ALL_LEVELS;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export default function HeadingLevelField({ control, value, onChange }) {
|
|
48
|
-
const levels = resolveLevels(control);
|
|
49
|
-
const heading = value && typeof value === 'object' ? value : {};
|
|
50
|
-
const text = heading.text ?? control.default?.text ?? '';
|
|
51
|
-
const level = heading.level ?? control.default?.level ?? levels[0] ?? 'h2';
|
|
52
|
-
const isNonSemantic = !HEADING_LEVELS.has(level);
|
|
53
|
-
|
|
54
|
-
const update = (patch) => onChange({ text, level, ...patch });
|
|
55
|
-
|
|
56
|
-
const levelSelect = (
|
|
57
|
-
<select
|
|
58
|
-
className="components-unit-control__select"
|
|
59
|
-
aria-label={__('Heading level', 'gcblite')}
|
|
60
|
-
value={level}
|
|
61
|
-
onChange={(e) => update({ level: e.target.value })}
|
|
62
|
-
>
|
|
63
|
-
{levels.map((lvl) => (
|
|
64
|
-
<option key={lvl} value={lvl}>{lvl.toUpperCase()}</option>
|
|
65
|
-
))}
|
|
66
|
-
</select>
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div className="components-base-control gcb-heading-level-control">
|
|
71
|
-
<div className="components-base-control__field">
|
|
72
|
-
<InputControl
|
|
73
|
-
label={control.label}
|
|
74
|
-
value={text}
|
|
75
|
-
placeholder={control.placeholder || __('Heading text', 'gcblite')}
|
|
76
|
-
onChange={(next) => update({ text: next ?? '' })}
|
|
77
|
-
suffix={levelSelect}
|
|
78
|
-
__next40pxDefaultSize
|
|
79
|
-
/>
|
|
80
|
-
|
|
81
|
-
{isNonSemantic && (
|
|
82
|
-
<Notice
|
|
83
|
-
status="warning"
|
|
84
|
-
isDismissible={false}
|
|
85
|
-
className="gcb-heading-level-control__warning"
|
|
86
|
-
>
|
|
87
|
-
{__(
|
|
88
|
-
'Non-heading elements are skipped by screen-reader heading navigation. Prefer H1–H6 for content titles.',
|
|
89
|
-
'gcblite',
|
|
90
|
-
)}
|
|
91
|
-
</Notice>
|
|
92
|
-
)}
|
|
93
|
-
</div>
|
|
94
|
-
{control.helpText && (
|
|
95
|
-
<p className="components-base-control__help">{control.helpText}</p>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
}
|