@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.
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 +249 -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 +172 -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,13 +0,0 @@
1
- import { ToggleControl } from '@wordpress/components';
2
-
3
- export default function ToggleField({ control, value, onChange }) {
4
- return (
5
- <ToggleControl
6
- label={control.label}
7
- help={control.helpText}
8
- checked={!!value}
9
- onChange={onChange}
10
- __nextHasNoMarginBottom
11
- />
12
- );
13
- }
@@ -1,164 +0,0 @@
1
- import { BaseControl, Button, Popover, Modal, TextControl, CheckboxControl, __experimentalHStack as HStack, __experimentalSpacer as Spacer } from '@wordpress/components';
2
- import { useState, useContext } from '@wordpress/element';
3
- import { __ } from '@wordpress/i18n';
4
- import { __experimentalLinkControl as LinkControl } from '@wordpress/block-editor';
5
- import { ControlContext } from '../control-context';
6
-
7
- /**
8
- * URL control — stores `{ url, text, opensInNewTab }`.
9
- *
10
- * Two renderings of the same stored shape:
11
- * - **Sidebar context** (block Inspector): compact summary + Edit popover
12
- * that mounts the rich @wordpress/block-editor LinkControl.
13
- * - **Meta-box context** (post-fields panel): three stacked inputs
14
- * (URL, link text, open-in-new-tab checkbox). The popover-based UI
15
- * feels out of place in a wide meta-box and the LinkControl's
16
- * post/page suggestions push the popover off-screen.
17
- *
18
- * Both render the same data; the chooser is the `variant` field on the
19
- * ControlContext provider supplied by the host (block edit.js vs the
20
- * post-fields meta-box App). Default is sidebar.
21
- *
22
- * When wiring a React component on the frontend:
23
- * const { url, text, opensInNewTab } = link || {};
24
- * if (!url) return null;
25
- * return <a
26
- * href={url}
27
- * target={opensInNewTab ? '_blank' : undefined}
28
- * rel={opensInNewTab ? 'noopener noreferrer' : undefined}
29
- * >{text || url}</a>;
30
- */
31
- export default function UrlField({ control, value, onChange }) {
32
- const ctx = useContext(ControlContext);
33
- const link = value && typeof value === 'object'
34
- ? value
35
- : { url: '', text: '', opensInNewTab: false };
36
-
37
- if (ctx.variant === 'metabox') {
38
- return <MetaboxUrl control={control} link={link} onChange={onChange} />;
39
- }
40
- return <SidebarUrl control={control} link={link} onChange={onChange} />;
41
- }
42
-
43
- function MetaboxUrl({ control, link, onChange }) {
44
- const [editing, setEditing] = useState(false);
45
- // Draft state so Cancel can discard pending edits without touching the
46
- // real meta value. Seed from `link` whenever the modal opens.
47
- const [draft, setDraft] = useState(link);
48
- const hasUrl = !!link.url;
49
-
50
- const openModal = () => {
51
- setDraft(link);
52
- setEditing(true);
53
- };
54
-
55
- const save = () => {
56
- onChange({
57
- url: draft.url || '',
58
- text: draft.text || '',
59
- opensInNewTab: !!draft.opensInNewTab,
60
- });
61
- setEditing(false);
62
- };
63
-
64
- return (
65
- <BaseControl label={control.label} help={control.helpText} __nextHasNoMarginBottom>
66
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
67
- <div style={{ flex: 1, fontSize: 13, color: hasUrl ? '#1e1e1e' : '#757575', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
68
- {hasUrl ? (link.text || link.url) : __('No link set', 'gcblite')}
69
- </div>
70
- <Button variant="secondary" onClick={openModal}>
71
- {hasUrl ? __('Edit', 'gcblite') : __('Set link', 'gcblite')}
72
- </Button>
73
- {hasUrl && (
74
- <Button variant="tertiary" isDestructive onClick={() => onChange({ url: '', text: '', opensInNewTab: false })}>
75
- {__('Clear', 'gcblite')}
76
- </Button>
77
- )}
78
- </div>
79
-
80
- {editing && (
81
- <Modal
82
- title={control.label || __('Edit link', 'gcblite')}
83
- onRequestClose={() => setEditing(false)}
84
- size="medium"
85
- >
86
- <div style={{ display: 'grid', gap: 16 }}>
87
- <TextControl
88
- label={__('URL', 'gcblite')}
89
- value={draft.url || ''}
90
- onChange={(url) => setDraft({ ...draft, url })}
91
- placeholder="https://example.com"
92
- type="url"
93
- __nextHasNoMarginBottom
94
- __next40pxDefaultSize
95
- />
96
- <TextControl
97
- label={__('Link text', 'gcblite')}
98
- value={draft.text || ''}
99
- onChange={(text) => setDraft({ ...draft, text })}
100
- placeholder={draft.url || __('Optional display label', 'gcblite')}
101
- __nextHasNoMarginBottom
102
- __next40pxDefaultSize
103
- />
104
- <CheckboxControl
105
- label={__('Open in new tab', 'gcblite')}
106
- checked={!!draft.opensInNewTab}
107
- onChange={(opensInNewTab) => setDraft({ ...draft, opensInNewTab })}
108
- __nextHasNoMarginBottom
109
- />
110
- </div>
111
- <Spacer marginTop={6} />
112
- <HStack justify="flex-end" spacing={3}>
113
- <Button variant="tertiary" onClick={() => setEditing(false)}>
114
- {__('Cancel', 'gcblite')}
115
- </Button>
116
- <Button variant="primary" onClick={save}>
117
- {__('Save link', 'gcblite')}
118
- </Button>
119
- </HStack>
120
- </Modal>
121
- )}
122
- </BaseControl>
123
- );
124
- }
125
-
126
- function SidebarUrl({ control, link, onChange }) {
127
- const [editing, setEditing] = useState(false);
128
- const hasUrl = !!link.url;
129
-
130
- return (
131
- <BaseControl label={control.label} help={control.helpText} __nextHasNoMarginBottom>
132
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
133
- <div style={{ flex: 1, fontSize: 12, color: hasUrl ? '#1e1e1e' : '#757575', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
134
- {hasUrl ? (link.text || link.url) : __('No link set', 'gcblite')}
135
- </div>
136
- <Button variant="secondary" size="small" onClick={() => setEditing((e) => !e)}>
137
- {hasUrl ? __('Edit', 'gcblite') : __('Set link', 'gcblite')}
138
- </Button>
139
- {hasUrl && (
140
- <Button variant="tertiary" size="small" isDestructive onClick={() => onChange({ url: '', text: '', opensInNewTab: false })}>
141
- {__('Clear', 'gcblite')}
142
- </Button>
143
- )}
144
- </div>
145
- {editing && (
146
- <Popover onClose={() => setEditing(false)} placement="bottom-start">
147
- <div style={{ width: 320, padding: 8 }}>
148
- <LinkControl
149
- value={link}
150
- onChange={(next) => onChange({
151
- url: next.url || '',
152
- text: next.title || link.text || '',
153
- opensInNewTab: !!next.opensInNewTab,
154
- })}
155
- settings={[
156
- { id: 'opensInNewTab', title: __('Open in new tab', 'gcblite') },
157
- ]}
158
- />
159
- </div>
160
- </Popover>
161
- )}
162
- </BaseControl>
163
- );
164
- }
@@ -1,343 +0,0 @@
1
- /**
2
- * UserField — ported from the original GCB.
3
- *
4
- * Single OR multiple user selection (control.multiple). Stored as ID(s) or
5
- * full user object(s) (control.returnFormat). Drag-and-drop reordering for
6
- * multi-select.
7
- */
8
-
9
- import { __ } from '@wordpress/i18n';
10
- import {
11
- Button,
12
- TextControl,
13
- } from '@wordpress/components';
14
- import PopoverOrModal from './PopoverOrModal';
15
- import { useState, useEffect, useMemo, useCallback } from '@wordpress/element';
16
- import apiFetch from '@wordpress/api-fetch';
17
- import {
18
- DndContext,
19
- closestCenter,
20
- PointerSensor,
21
- useSensor,
22
- useSensors,
23
- DragOverlay,
24
- } from '@dnd-kit/core';
25
- import {
26
- SortableContext,
27
- verticalListSortingStrategy,
28
- useSortable,
29
- arrayMove,
30
- } from '@dnd-kit/sortable';
31
- import { CSS } from '@dnd-kit/utilities';
32
-
33
- const TOGGLE_BUTTON_STYLE = {
34
- width: '100%',
35
- height: 'auto',
36
- padding: '12px',
37
- justifyContent: 'flex-start',
38
- border: '1px solid #ddd',
39
- borderRadius: '2px',
40
- backgroundColor: '#fff',
41
- };
42
-
43
- function UserIcon() {
44
- return (
45
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style={{ marginRight: 8, flexShrink: 0, opacity: 0.6 }}>
46
- <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
47
- </svg>
48
- );
49
- }
50
-
51
- function SortableUserItem({ user, onRemove }) {
52
- const {
53
- attributes: dndAttributes,
54
- listeners,
55
- setNodeRef,
56
- transform,
57
- transition,
58
- isDragging,
59
- } = useSortable({ id: user.id });
60
-
61
- const style = {
62
- transform: CSS.Transform.toString(transform),
63
- transition,
64
- opacity: isDragging ? 0.5 : 1,
65
- };
66
-
67
- return (
68
- <div
69
- ref={setNodeRef}
70
- style={style}
71
- className="gcb-post-object-selected-item"
72
- {...dndAttributes}
73
- {...listeners}
74
- >
75
- <div className="gcb-post-object-drag-handle" aria-hidden>
76
- <svg viewBox="0 0 20 20" width="12">
77
- <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" />
78
- </svg>
79
- </div>
80
- <UserIcon />
81
- <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', userSelect: 'none' }}>
82
- {user.name || __('(no name)', 'gcblite')}
83
- </span>
84
- <button
85
- type="button"
86
- onClick={(e) => {
87
- e.stopPropagation();
88
- onRemove(user.id);
89
- }}
90
- onPointerDown={(e) => e.stopPropagation()}
91
- className="gcb-sortable-remove"
92
- aria-label={__('Remove', 'gcblite')}
93
- >
94
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
95
- <path d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z" />
96
- </svg>
97
- </button>
98
- </div>
99
- );
100
- }
101
-
102
- export default function UserField({ control, value, onChange }) {
103
- const [allUsers, setAllUsers] = useState([]);
104
- const [searchResults, setSearchResults] = useState([]);
105
- const [loading, setLoading] = useState(false);
106
- const [search, setSearch] = useState('');
107
- const [activeId, setActiveId] = useState(null);
108
-
109
- const isMultiple = !!control.multiple;
110
-
111
- const selectedIds = isMultiple
112
- ? (Array.isArray(value) ? value : (value ? [value] : []))
113
- : (value ? [value] : []);
114
-
115
- const selectedUsers = useMemo(
116
- () => selectedIds.map((id) => allUsers.find((u) => u.id === id)).filter(Boolean),
117
- [selectedIds, allUsers]
118
- );
119
-
120
- const mergeUsersIntoCache = useCallback((newUsers) => {
121
- setAllUsers((prev) => {
122
- const merged = [...prev];
123
- newUsers.forEach((nu) => {
124
- if (!merged.find((u) => u.id === nu.id)) merged.push(nu);
125
- });
126
- return merged;
127
- });
128
- }, []);
129
-
130
- const loadUsers = useCallback(async (term = '') => {
131
- setLoading(true);
132
- try {
133
- const response = await apiFetch({
134
- path: `/wp/v2/users?search=${encodeURIComponent(term)}&per_page=50&_fields=id,name`,
135
- });
136
- setSearchResults(response);
137
- mergeUsersIntoCache(response);
138
- } catch {
139
- // ignore
140
- }
141
- setLoading(false);
142
- }, [mergeUsersIntoCache]);
143
-
144
- useEffect(() => {
145
- loadUsers();
146
- }, [loadUsers]);
147
-
148
- const handleSelect = (userId) => {
149
- const newIds = isMultiple
150
- ? (selectedIds.includes(userId)
151
- ? selectedIds.filter((id) => id !== userId)
152
- : [...selectedIds, userId])
153
- : [userId];
154
-
155
- if (control.returnFormat === 'object') {
156
- const objs = newIds.map((id) => allUsers.find((u) => u.id === id)).filter(Boolean);
157
- onChange(isMultiple ? objs : (objs[0] || null));
158
- } else {
159
- onChange(isMultiple ? newIds : (newIds[0] || null));
160
- }
161
- };
162
-
163
- const handleRemove = (userId) => {
164
- const newIds = selectedIds.filter((id) => id !== userId);
165
- if (control.returnFormat === 'object') {
166
- const objs = newIds.map((id) => allUsers.find((u) => u.id === id)).filter(Boolean);
167
- onChange(isMultiple ? objs : (objs[0] || null));
168
- } else {
169
- onChange(isMultiple ? newIds : (newIds[0] || null));
170
- }
171
- };
172
-
173
- const handleReorder = (newOrder) => {
174
- if (control.returnFormat === 'object') {
175
- onChange(newOrder.map((id) => allUsers.find((u) => u.id === id)).filter(Boolean));
176
- } else {
177
- onChange(newOrder);
178
- }
179
- };
180
-
181
- const handleClear = () => onChange(isMultiple ? [] : null);
182
-
183
- const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }));
184
-
185
- const handleDragStart = (e) => setActiveId(e.active.id);
186
- const handleDragCancel = () => setActiveId(null);
187
- const handleDragEnd = (e) => {
188
- const { active, over } = e;
189
- if (over && active.id !== over.id) {
190
- const oldIndex = selectedIds.indexOf(active.id);
191
- const newIndex = selectedIds.indexOf(over.id);
192
- handleReorder(arrayMove(selectedIds, oldIndex, newIndex));
193
- }
194
- setActiveId(null);
195
- };
196
-
197
- const activeUser = activeId ? selectedUsers.find((u) => u.id === activeId) : null;
198
- const availableUsers = searchResults.filter((u) => !selectedIds.includes(u.id));
199
-
200
- return (
201
- <div className="components-base-control gcb-user-control">
202
- <div className="components-base-control__field">
203
- <label className="components-base-control__label">{control.label}</label>
204
- </div>
205
- {control.helpText && (
206
- <p className="components-base-control__help">{control.helpText}</p>
207
- )}
208
-
209
- <div className="gcb-post-object-stacked">
210
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
211
- <PopoverOrModal
212
- modalTitle={control.label || __('Select users', 'gcblite')}
213
- dropdownProps={{ popoverProps: { placement: 'left-start' } }}
214
- renderToggle={({ isOpen, onToggle }) => (
215
- <Button
216
- onClick={onToggle}
217
- aria-expanded={isOpen}
218
- className="gcb-modal-toggle-button"
219
- style={{ ...TOGGLE_BUTTON_STYLE, flex: 1 }}
220
- >
221
- {selectedUsers.length > 0
222
- ? `${selectedUsers.length} ${selectedUsers.length === 1 ? __('user', 'gcblite') : __('users', 'gcblite')} ${__('selected', 'gcblite')}`
223
- : __('Select Users', 'gcblite')}
224
- </Button>
225
- )}
226
- renderContent={({ close: onClose, variant }) => (
227
- <div style={variant === 'modal' ? { width: '100%' } : { minWidth: 320, maxWidth: 400 }}>
228
- <div style={{ padding: '0 16px 8px 16px' }}>
229
- <TextControl
230
- value={search}
231
- onChange={(val) => {
232
- setSearch(val);
233
- loadUsers(val);
234
- }}
235
- placeholder={__('Search users…', 'gcblite')}
236
- __nextHasNoMarginBottom
237
- />
238
- </div>
239
-
240
- <div className="block-editor-link-control__search-results-wrapper" style={{ maxHeight: 300, overflowY: 'auto' }}>
241
- {loading && (
242
- <p style={{ textAlign: 'center', color: '#757575', padding: 16 }}>
243
- {__('Loading…', 'gcblite')}
244
- </p>
245
- )}
246
- {!loading && availableUsers.length === 0 && (
247
- <p style={{ textAlign: 'center', color: '#757575', padding: 16 }}>
248
- {__('No users found', 'gcblite')}
249
- </p>
250
- )}
251
- {!loading && availableUsers.length > 0 && (
252
- <div className="block-editor-link-control__search-results" role="listbox">
253
- <div className="components-menu-group">
254
- <div role="group">
255
- {availableUsers.map((user) => (
256
- <button
257
- key={user.id}
258
- type="button"
259
- role="option"
260
- className="components-button components-menu-item__button block-editor-link-control__search-item"
261
- onClick={() => handleSelect(user.id)}
262
- style={{
263
- display: 'flex',
264
- alignItems: 'center',
265
- width: '100%',
266
- padding: '8px 16px',
267
- textAlign: 'left',
268
- border: 'none',
269
- background: 'transparent',
270
- }}
271
- >
272
- <UserIcon />
273
- <span className="components-menu-item__item" style={{ fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
274
- {user.name}
275
- </span>
276
- </button>
277
- ))}
278
- </div>
279
- </div>
280
- </div>
281
- )}
282
- </div>
283
- </div>
284
- )}
285
- />
286
-
287
- {selectedUsers.length > 0 && (
288
- <Button
289
- onClick={handleClear}
290
- variant="secondary"
291
- isSmall
292
- className="components-range-control__reset"
293
- >
294
- {__('Reset', 'gcblite')}
295
- </Button>
296
- )}
297
- </div>
298
-
299
- {selectedUsers.length > 0 && isMultiple && (
300
- <div className="gcb-post-object-selected-list" style={{ marginTop: 8 }}>
301
- <DndContext
302
- sensors={sensors}
303
- collisionDetection={closestCenter}
304
- onDragStart={handleDragStart}
305
- onDragEnd={handleDragEnd}
306
- onDragCancel={handleDragCancel}
307
- >
308
- <SortableContext items={selectedIds} strategy={verticalListSortingStrategy}>
309
- {selectedUsers.map((user) => (
310
- <SortableUserItem key={user.id} user={user} onRemove={handleRemove} />
311
- ))}
312
- </SortableContext>
313
- <DragOverlay>
314
- {activeUser ? (
315
- <div className="gcb-post-object-selected-item" style={{ opacity: 0.8, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
316
- <div className="gcb-post-object-drag-handle" aria-hidden>
317
- <svg viewBox="0 0 20 20" width="12">
318
- <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" />
319
- </svg>
320
- </div>
321
- <UserIcon />
322
- <span style={{ flex: 1 }}>{activeUser.name}</span>
323
- </div>
324
- ) : null}
325
- </DragOverlay>
326
- </DndContext>
327
- </div>
328
- )}
329
-
330
- {selectedUsers.length > 0 && !isMultiple && (
331
- <div className="gcb-post-object-selected-list" style={{ marginTop: 8 }}>
332
- <div className="gcb-post-object-selected-item">
333
- <UserIcon />
334
- <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
335
- {selectedUsers[0].name}
336
- </span>
337
- </div>
338
- </div>
339
- )}
340
- </div>
341
- </div>
342
- );
343
- }
package/src/inspector.js DELETED
@@ -1,174 +0,0 @@
1
- /**
2
- * Render an Inspector panel tree from a block's `gcb.controls` array.
3
- *
4
- * Controls are flat in the JSON; structure comes from `parentPanelId` references
5
- * to `type: "group"` controls. We bucket controls under their group and emit one
6
- * <PanelBody> per group, plus a default panel for anything ungrouped.
7
- */
8
-
9
- import { PanelBody } from '@wordpress/components';
10
- import { Fragment, useContext } from '@wordpress/element';
11
- import { __ } from '@wordpress/i18n';
12
- import { controlComponents } from './controls';
13
- import { ValidationContext } from './validation-context';
14
- import {
15
- STRUCTURAL_TYPES,
16
- shouldRender,
17
- panelsContainingErrors,
18
- } from './conditional-logic';
19
-
20
- // Re-export so existing callers keep working (post-fields.js imports
21
- // shouldRender + panelsContainingErrors from './inspector').
22
- export { shouldRender, panelsContainingErrors };
23
-
24
- /**
25
- * @param {Array} controls
26
- * @param {Object} attributes
27
- * @param {Function} setAttributes
28
- * @param {Object} [options]
29
- * @param {boolean} [options.flatten] When true and no groups exist, render
30
- * ungrouped controls flat (no outer "Settings" PanelBody). Used by the
31
- * post-fields meta-box where the meta-box itself IS the panel — nesting
32
- * another PanelBody would look like a redundant dropdown.
33
- * @param {Set<string>} [options.forceOpenPanelIds] Panel ids to render
34
- * with initialOpen=true. Used by the meta-box to auto-open any panel
35
- * containing a field that just failed validation, so the user can see
36
- * the offending field.
37
- */
38
- export function renderInspector(controls, attributes, setAttributes, options = {}) {
39
- const { groups, ungrouped } = bucketControls(controls);
40
- const flatten = options.flatten === true && groups.length === 0;
41
- const forceOpen = options.forceOpenPanelIds || new Set();
42
-
43
- return (
44
- <Fragment>
45
- {ungrouped.length > 0 && (
46
- flatten ? (
47
- ungrouped.map((control) =>
48
- renderControl(control, attributes, setAttributes)
49
- )
50
- ) : (
51
- <PanelBody title={__('Settings', 'gcblite')} initialOpen={true}>
52
- {ungrouped.map((control) =>
53
- renderControl(control, attributes, setAttributes)
54
- )}
55
- </PanelBody>
56
- )
57
- )}
58
- {groups.map(({ group, children }) => (
59
- <PanelBody
60
- // Remount the panel when its forced-open status changes so
61
- // the new initialOpen value is honoured. (PanelBody only
62
- // reads initialOpen on mount.)
63
- key={`${group.id}:${forceOpen.has(group.id) ? 'open' : 'closed'}`}
64
- title={group.label}
65
- initialOpen={forceOpen.has(group.id)}
66
- >
67
- {children.map((control) =>
68
- renderControl(control, attributes, setAttributes)
69
- )}
70
- </PanelBody>
71
- ))}
72
- </Fragment>
73
- );
74
- }
75
-
76
- function bucketControls(controls) {
77
- const groupsById = new Map();
78
- const groupOrder = [];
79
- const ungrouped = [];
80
-
81
- // First pass: register structural controls (group / panel / tools-panel)
82
- // in declaration order.
83
- controls.forEach((control) => {
84
- if (STRUCTURAL_TYPES.has(control.type) && control.id) {
85
- groupsById.set(control.id, { group: control, children: [] });
86
- groupOrder.push(control.id);
87
- }
88
- });
89
-
90
- // Second pass: assign each non-structural control to a panel or to ungrouped.
91
- controls.forEach((control) => {
92
- if (STRUCTURAL_TYPES.has(control.type)) return;
93
-
94
- const parentId = control.parentPanelId;
95
- if (parentId && groupsById.has(parentId)) {
96
- groupsById.get(parentId).children.push(control);
97
- } else {
98
- ungrouped.push(control);
99
- }
100
- });
101
-
102
- return {
103
- groups: groupOrder.map((id) => groupsById.get(id)),
104
- ungrouped,
105
- };
106
- }
107
-
108
- function renderControl(control, attributes, setAttributes) {
109
- if (!shouldRender(control, attributes)) {
110
- return null;
111
- }
112
-
113
- const Component = controlComponents[control.type];
114
- if (!Component) {
115
- return (
116
- <div key={control.id} style={{ padding: 8, background: '#fff3cd', border: '1px solid #ffeeba', marginBottom: 8 }}>
117
- <strong>{control.label}</strong>: unknown control type <code>{control.type}</code>
118
- </div>
119
- );
120
- }
121
-
122
- const value = attributes[control.attributeKey];
123
- const onChange = (next) => setAttributes({ [control.attributeKey]: next });
124
-
125
- // Wrap each rendered control in a ValidationWrapper that overlays the
126
- // required `*`, the inline error message, and the data-attribute used
127
- // by the meta-box submit interceptor to scroll-into-view. The wrapper
128
- // is invisible in surfaces without validation (sidebar default
129
- // ValidationContext is { errors:{}, showErrors:false }) so blocks
130
- // don't see any change.
131
- return (
132
- <ValidationWrapper key={control.id} control={control}>
133
- <Component
134
- control={control}
135
- value={value}
136
- onChange={onChange}
137
- attributes={attributes}
138
- />
139
- </ValidationWrapper>
140
- );
141
- }
142
-
143
- /**
144
- * Per-field decorator: stamps data-gcblite-field for scroll-to-error,
145
- * shows the required asterisk, and surfaces the inline error message
146
- * once the host has flipped showErrors on.
147
- */
148
- function ValidationWrapper({ control, children }) {
149
- const { errors, showErrors } = useContext(ValidationContext);
150
- const key = control.attributeKey;
151
- const required = !!control.validation?.required;
152
- const errorMessage = showErrors && key ? errors[key] : null;
153
-
154
- if (!required && !errorMessage && !key) {
155
- return children;
156
- }
157
-
158
- return (
159
- <div
160
- data-gcblite-field={key || undefined}
161
- className={[
162
- 'gcblite-field',
163
- required ? 'gcblite-field--required' : '',
164
- errorMessage ? 'gcblite-field--has-error' : '',
165
- ].filter(Boolean).join(' ')}
166
- >
167
- {children}
168
- {errorMessage && (
169
- <p className="gcblite-field__error" role="alert">{errorMessage}</p>
170
- )}
171
- </div>
172
- );
173
- }
174
-