@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
@@ -0,0 +1,913 @@
1
+ /**
2
+ * PostObjectField — ported verbatim from the original GCB PostObjectControlComponent.
3
+ *
4
+ * Features preserved:
5
+ * - Single OR multiple selection (control.multiple)
6
+ * - Stored as ID(s) or full post object(s) (control.returnFormat)
7
+ * - Multiple post types (CSV string or array)
8
+ * - Multiple post statuses
9
+ * - Optional taxonomy filter dropdowns (control.enableTaxonomyFilter / filterTaxonomies)
10
+ * - Optional post-type filter dropdown (control.enablePostTypeFilter)
11
+ * - Drag-and-drop reordering of selected posts (multi-select only)
12
+ * - REST endpoint discovery via /wp/v2/types
13
+ */
14
+
15
+ import { __ } from '@wordpress/i18n';
16
+ import { Button, SelectControl, TextControl, __experimentalTruncate as Truncate } from '@wordpress/components';
17
+ import PopoverOrModal from './PopoverOrModal';
18
+ import { useState, useEffect, useMemo, useCallback } from '@wordpress/element';
19
+ import apiFetch from '@wordpress/api-fetch';
20
+ import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core';
21
+ import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
22
+ import { CSS } from '@dnd-kit/utilities';
23
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
+ const TOGGLE_BUTTON_STYLE = {
25
+ width: '100%',
26
+ height: 'auto',
27
+ padding: '12px',
28
+ justifyContent: 'flex-start',
29
+ border: '1px solid #ddd',
30
+ borderRadius: '2px',
31
+ backgroundColor: '#fff'
32
+ };
33
+
34
+ /**
35
+ * Sortable selected-post row.
36
+ */
37
+ function SortablePostItem({
38
+ post,
39
+ onRemove
40
+ }) {
41
+ const {
42
+ attributes,
43
+ listeners,
44
+ setNodeRef,
45
+ transform,
46
+ transition,
47
+ isDragging
48
+ } = useSortable({
49
+ id: post.id
50
+ });
51
+ const style = {
52
+ transform: CSS.Transform.toString(transform),
53
+ transition,
54
+ opacity: isDragging ? 0.5 : 1
55
+ };
56
+ return /*#__PURE__*/_jsxs("div", {
57
+ ref: setNodeRef,
58
+ style: style,
59
+ className: "gcb-post-object-selected-item",
60
+ ...attributes,
61
+ ...listeners,
62
+ children: [/*#__PURE__*/_jsx("div", {
63
+ className: "gcb-post-object-drag-handle",
64
+ "aria-hidden": true,
65
+ children: /*#__PURE__*/_jsx("svg", {
66
+ viewBox: "0 0 20 20",
67
+ width: "12",
68
+ children: /*#__PURE__*/_jsx("path", {
69
+ 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"
70
+ })
71
+ })
72
+ }), /*#__PURE__*/_jsxs("svg", {
73
+ xmlns: "http://www.w3.org/2000/svg",
74
+ viewBox: "0 0 24 24",
75
+ width: "20",
76
+ height: "20",
77
+ style: {
78
+ marginRight: 8,
79
+ flexShrink: 0,
80
+ opacity: 0.6
81
+ },
82
+ children: [/*#__PURE__*/_jsx("path", {
83
+ d: "M15.5 7.5h-7V9h7V7.5Zm-7 3.5h7v1.5h-7V11Zm7 3.5h-7V16h7v-1.5Z"
84
+ }), /*#__PURE__*/_jsx("path", {
85
+ d: "M17 4H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM7 5.5h10a.5.5 0 0 1 .5.5v12a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5Z"
86
+ })]
87
+ }), /*#__PURE__*/_jsx("span", {
88
+ style: {
89
+ flex: 1,
90
+ overflow: 'hidden',
91
+ textOverflow: 'ellipsis',
92
+ whiteSpace: 'nowrap',
93
+ userSelect: 'none'
94
+ },
95
+ children: post.title?.rendered || __('(no title)', 'gcblite')
96
+ }), /*#__PURE__*/_jsx("button", {
97
+ type: "button",
98
+ onClick: e => {
99
+ e.stopPropagation();
100
+ onRemove(post.id);
101
+ },
102
+ onPointerDown: e => e.stopPropagation(),
103
+ className: "gcb-sortable-remove",
104
+ "aria-label": __('Remove', 'gcblite'),
105
+ children: /*#__PURE__*/_jsx("svg", {
106
+ xmlns: "http://www.w3.org/2000/svg",
107
+ viewBox: "0 0 24 24",
108
+ width: "20",
109
+ height: "20",
110
+ children: /*#__PURE__*/_jsx("path", {
111
+ 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"
112
+ })
113
+ })
114
+ })]
115
+ });
116
+ }
117
+
118
+ /**
119
+ * The multi-select panel — search dropdown plus a sortable list of selected posts.
120
+ */
121
+ function PostObjectMultiSelect({
122
+ posts,
123
+ selectedIds,
124
+ selectedPosts,
125
+ loading,
126
+ search,
127
+ setSearch,
128
+ loadPosts,
129
+ handleSelect,
130
+ handleRemove,
131
+ handleReorder,
132
+ handleClear,
133
+ control,
134
+ postTypeFilter,
135
+ setPostTypeFilter,
136
+ availablePostTypes
137
+ }) {
138
+ const [activeId, setActiveId] = useState(null);
139
+ const [taxonomyTermsBySlug, setTaxonomyTermsBySlug] = useState({});
140
+ const [loadingTaxonomies, setLoadingTaxonomies] = useState(false);
141
+ const [taxonomyFilters, setTaxonomyFilters] = useState({});
142
+ useEffect(() => {
143
+ if (!control.enableTaxonomyFilter || !Array.isArray(control.filterTaxonomies) || control.filterTaxonomies.length === 0) {
144
+ return;
145
+ }
146
+ setLoadingTaxonomies(true);
147
+ const promises = control.filterTaxonomies.map(tax => apiFetch({
148
+ path: `/wp/v2/${tax.slug}?per_page=100`
149
+ }).then(terms => ({
150
+ slug: tax.slug,
151
+ label: tax.label,
152
+ terms: terms.map(t => ({
153
+ value: t.id,
154
+ label: t.name
155
+ }))
156
+ })).catch(() => ({
157
+ slug: tax.slug,
158
+ label: tax.label,
159
+ terms: []
160
+ })));
161
+ Promise.all(promises).then(results => {
162
+ const map = {};
163
+ const initialFilters = {};
164
+ results.forEach(r => {
165
+ map[r.slug] = {
166
+ label: r.label,
167
+ terms: r.terms
168
+ };
169
+ initialFilters[r.slug] = 'all';
170
+ });
171
+ setTaxonomyTermsBySlug(map);
172
+ setTaxonomyFilters(initialFilters);
173
+ }).finally(() => setLoadingTaxonomies(false));
174
+ }, [control.enableTaxonomyFilter, control.filterTaxonomies]);
175
+ const sensors = useSensors(useSensor(PointerSensor, {
176
+ activationConstraint: {
177
+ distance: 8
178
+ }
179
+ }));
180
+ const handleDragStart = event => setActiveId(event.active.id);
181
+ const handleDragCancel = () => setActiveId(null);
182
+ const handleDragEnd = event => {
183
+ const {
184
+ active,
185
+ over
186
+ } = event;
187
+ if (over && active.id !== over.id) {
188
+ const oldIndex = selectedIds.indexOf(active.id);
189
+ const newIndex = selectedIds.indexOf(over.id);
190
+ handleReorder(arrayMove(selectedIds, oldIndex, newIndex));
191
+ }
192
+ setActiveId(null);
193
+ };
194
+ const activePost = activeId ? selectedPosts.find(p => p.id === activeId) : null;
195
+ const availablePosts = posts.filter(p => !selectedIds.includes(p.id));
196
+ return /*#__PURE__*/_jsxs("div", {
197
+ className: "gcb-post-object-stacked",
198
+ children: [/*#__PURE__*/_jsxs("div", {
199
+ style: {
200
+ display: 'flex',
201
+ gap: 8,
202
+ alignItems: 'center'
203
+ },
204
+ children: [/*#__PURE__*/_jsx(PopoverOrModal, {
205
+ modalTitle: control.label || __('Select posts', 'gcblite'),
206
+ dropdownProps: {
207
+ popoverProps: {
208
+ placement: 'left-start'
209
+ }
210
+ },
211
+ renderToggle: ({
212
+ isOpen,
213
+ onToggle
214
+ }) => /*#__PURE__*/_jsx(Button, {
215
+ onClick: onToggle,
216
+ "aria-expanded": isOpen,
217
+ className: "gcb-modal-toggle-button",
218
+ style: {
219
+ ...TOGGLE_BUTTON_STYLE,
220
+ flex: 1
221
+ },
222
+ children: selectedPosts.length > 0 ? `${selectedPosts.length} ${selectedPosts.length === 1 ? __('post', 'gcblite') : __('posts', 'gcblite')} ${__('selected', 'gcblite')}` : __('Select Posts', 'gcblite')
223
+ }),
224
+ renderContent: ({
225
+ close: onClose,
226
+ variant
227
+ }) => /*#__PURE__*/_jsxs("div", {
228
+ style: variant === 'modal' ? {
229
+ width: '100%'
230
+ } : {
231
+ minWidth: 320,
232
+ maxWidth: 400
233
+ },
234
+ children: [/*#__PURE__*/_jsxs("div", {
235
+ style: {
236
+ padding: '0 16px 8px 16px'
237
+ },
238
+ children: [/*#__PURE__*/_jsx(TextControl, {
239
+ value: search,
240
+ onChange: val => {
241
+ setSearch(val);
242
+ loadPosts(val, postTypeFilter, taxonomyFilters);
243
+ },
244
+ placeholder: __('Search…', 'gcblite'),
245
+ __nextHasNoMarginBottom: true
246
+ }), control.enablePostTypeFilter && availablePostTypes.length > 1 && /*#__PURE__*/_jsx("div", {
247
+ style: {
248
+ marginTop: 8
249
+ },
250
+ children: /*#__PURE__*/_jsx(SelectControl, {
251
+ label: __('Filter by Post Type', 'gcblite'),
252
+ value: postTypeFilter,
253
+ options: [{
254
+ value: 'all',
255
+ label: __('All Types', 'gcblite')
256
+ }, ...availablePostTypes],
257
+ onChange: val => {
258
+ setPostTypeFilter(val);
259
+ loadPosts(search, val, taxonomyFilters);
260
+ },
261
+ __nextHasNoMarginBottom: true
262
+ })
263
+ }), control.enableTaxonomyFilter && Object.keys(taxonomyTermsBySlug).length > 0 && Object.entries(taxonomyTermsBySlug).map(([slug, data]) => /*#__PURE__*/_jsx("div", {
264
+ style: {
265
+ marginTop: 8
266
+ },
267
+ children: /*#__PURE__*/_jsx(SelectControl, {
268
+ label: __('Filter by ', 'gcblite') + data.label,
269
+ value: taxonomyFilters[slug] || 'all',
270
+ options: [{
271
+ value: 'all',
272
+ label: __('All', 'gcblite')
273
+ }, ...data.terms],
274
+ onChange: val => {
275
+ const newFilters = {
276
+ ...taxonomyFilters,
277
+ [slug]: val
278
+ };
279
+ setTaxonomyFilters(newFilters);
280
+ loadPosts(search, postTypeFilter, newFilters);
281
+ },
282
+ disabled: loadingTaxonomies,
283
+ __nextHasNoMarginBottom: true
284
+ })
285
+ }, slug))]
286
+ }), /*#__PURE__*/_jsxs("div", {
287
+ className: "block-editor-link-control__search-results-wrapper",
288
+ style: {
289
+ maxHeight: 300,
290
+ overflowY: 'auto'
291
+ },
292
+ children: [loading && /*#__PURE__*/_jsx("p", {
293
+ style: {
294
+ textAlign: 'center',
295
+ color: '#757575',
296
+ padding: 16
297
+ },
298
+ children: __('Loading…', 'gcblite')
299
+ }), !loading && availablePosts.length === 0 && /*#__PURE__*/_jsx("p", {
300
+ style: {
301
+ textAlign: 'center',
302
+ color: '#757575',
303
+ padding: 16
304
+ },
305
+ children: __('No posts found', 'gcblite')
306
+ }), !loading && availablePosts.length > 0 && /*#__PURE__*/_jsx("div", {
307
+ className: "block-editor-link-control__search-results",
308
+ role: "listbox",
309
+ children: /*#__PURE__*/_jsx("div", {
310
+ className: "components-menu-group",
311
+ children: /*#__PURE__*/_jsx("div", {
312
+ role: "group",
313
+ children: availablePosts.map(post => /*#__PURE__*/_jsx("button", {
314
+ type: "button",
315
+ role: "option",
316
+ className: "components-button components-menu-item__button block-editor-link-control__search-item",
317
+ onClick: () => handleSelect(post.id),
318
+ style: {
319
+ display: 'flex',
320
+ alignItems: 'center',
321
+ width: '100%',
322
+ padding: '8px 16px',
323
+ textAlign: 'left',
324
+ border: 'none',
325
+ background: 'transparent',
326
+ justifyContent: 'space-between'
327
+ },
328
+ children: /*#__PURE__*/_jsxs("span", {
329
+ style: {
330
+ display: 'flex',
331
+ alignItems: 'center',
332
+ flex: 1,
333
+ overflow: 'hidden'
334
+ },
335
+ children: [/*#__PURE__*/_jsxs("svg", {
336
+ xmlns: "http://www.w3.org/2000/svg",
337
+ viewBox: "0 0 24 24",
338
+ width: "24",
339
+ height: "24",
340
+ className: "block-editor-link-control__search-item-icon",
341
+ style: {
342
+ marginRight: 12,
343
+ flexShrink: 0
344
+ },
345
+ children: [/*#__PURE__*/_jsx("path", {
346
+ d: "M15.5 7.5h-7V9h7V7.5Zm-7 3.5h7v1.5h-7V11Zm7 3.5h-7V16h7v-1.5Z"
347
+ }), /*#__PURE__*/_jsx("path", {
348
+ d: "M17 4H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM7 5.5h10a.5.5 0 0 1 .5.5v12a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5Z"
349
+ })]
350
+ }), /*#__PURE__*/_jsx("span", {
351
+ className: "components-menu-item__item",
352
+ style: {
353
+ fontWeight: 500,
354
+ overflow: 'hidden',
355
+ textOverflow: 'ellipsis',
356
+ whiteSpace: 'nowrap'
357
+ },
358
+ children: post.title?.rendered || __('(no title)', 'gcblite')
359
+ })]
360
+ })
361
+ }, post.id))
362
+ })
363
+ })
364
+ })]
365
+ })]
366
+ })
367
+ }), selectedPosts.length > 0 && /*#__PURE__*/_jsx(Button, {
368
+ onClick: handleClear,
369
+ variant: "secondary",
370
+ isSmall: true,
371
+ className: "components-range-control__reset",
372
+ children: __('Reset', 'gcblite')
373
+ })]
374
+ }), selectedPosts.length > 0 && /*#__PURE__*/_jsx("div", {
375
+ className: "gcb-post-object-selected-list",
376
+ style: {
377
+ marginTop: 8
378
+ },
379
+ children: /*#__PURE__*/_jsxs(DndContext, {
380
+ sensors: sensors,
381
+ collisionDetection: closestCenter,
382
+ onDragStart: handleDragStart,
383
+ onDragEnd: handleDragEnd,
384
+ onDragCancel: handleDragCancel,
385
+ children: [/*#__PURE__*/_jsx(SortableContext, {
386
+ items: selectedIds,
387
+ strategy: verticalListSortingStrategy,
388
+ children: selectedPosts.map(post => /*#__PURE__*/_jsx(SortablePostItem, {
389
+ post: post,
390
+ onRemove: handleRemove
391
+ }, post.id))
392
+ }), /*#__PURE__*/_jsx(DragOverlay, {
393
+ children: activePost ? /*#__PURE__*/_jsxs("div", {
394
+ className: "gcb-post-object-selected-item",
395
+ style: {
396
+ opacity: 0.8,
397
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
398
+ },
399
+ children: [/*#__PURE__*/_jsx("div", {
400
+ className: "gcb-post-object-drag-handle",
401
+ "aria-hidden": true,
402
+ children: /*#__PURE__*/_jsx("svg", {
403
+ viewBox: "0 0 20 20",
404
+ width: "12",
405
+ children: /*#__PURE__*/_jsx("path", {
406
+ 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"
407
+ })
408
+ })
409
+ }), /*#__PURE__*/_jsxs("svg", {
410
+ xmlns: "http://www.w3.org/2000/svg",
411
+ viewBox: "0 0 24 24",
412
+ width: "20",
413
+ height: "20",
414
+ style: {
415
+ marginRight: 8,
416
+ flexShrink: 0,
417
+ opacity: 0.6
418
+ },
419
+ children: [/*#__PURE__*/_jsx("path", {
420
+ d: "M15.5 7.5h-7V9h7V7.5Zm-7 3.5h7v1.5h-7V11Zm7 3.5h-7V16h7v-1.5Z"
421
+ }), /*#__PURE__*/_jsx("path", {
422
+ d: "M17 4H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM7 5.5h10a.5.5 0 0 1 .5.5v12a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5Z"
423
+ })]
424
+ }), /*#__PURE__*/_jsx("span", {
425
+ style: {
426
+ flex: 1
427
+ },
428
+ children: activePost.title?.rendered || __('(no title)', 'gcblite')
429
+ })]
430
+ }) : null
431
+ })]
432
+ })
433
+ })]
434
+ });
435
+ }
436
+
437
+ // Normalise whatever shape the post-object value used to take into the
438
+ // canonical { post_type, ids[] } shape. Handles:
439
+ // - canonical { post_type, ids[] } (preferred since v0.2)
440
+ // - bare scalar id (legacy single)
441
+ // - array of ids (legacy multi)
442
+ // - { id, ... } object (returnFormat=object, single)
443
+ // - array of those objects (returnFormat=object, multi)
444
+ function normalisePostObjectValue(value, schemaDefault) {
445
+ if (!value) return {
446
+ post_type: schemaDefault,
447
+ ids: []
448
+ };
449
+ if (typeof value === 'object' && !Array.isArray(value) && Array.isArray(value.ids)) {
450
+ return {
451
+ post_type: value.post_type || schemaDefault,
452
+ ids: value.ids
453
+ };
454
+ }
455
+ if (typeof value === 'number' || typeof value === 'string') {
456
+ return {
457
+ post_type: schemaDefault,
458
+ ids: [Number(value)]
459
+ };
460
+ }
461
+ if (typeof value === 'object' && !Array.isArray(value) && value.id != null) {
462
+ return {
463
+ post_type: value.type || schemaDefault,
464
+ ids: [Number(value.id)]
465
+ };
466
+ }
467
+ if (Array.isArray(value)) {
468
+ const ids = value.map(entry => typeof entry === 'object' ? Number(entry.id) : Number(entry)).filter(Boolean);
469
+ const pt = value.find(entry => typeof entry === 'object' && entry?.type)?.type;
470
+ return {
471
+ post_type: pt || schemaDefault,
472
+ ids
473
+ };
474
+ }
475
+ return {
476
+ post_type: schemaDefault,
477
+ ids: []
478
+ };
479
+ }
480
+ export default function PostObjectField({
481
+ control,
482
+ value,
483
+ onChange
484
+ }) {
485
+ const [allPosts, setAllPosts] = useState([]);
486
+ const [searchResults, setSearchResults] = useState([]);
487
+ const [loading, setLoading] = useState(false);
488
+ const [search, setSearch] = useState('');
489
+ const [postTypeFilter, setPostTypeFilter] = useState('all');
490
+ const [postTypeEndpoints, setPostTypeEndpoints] = useState({});
491
+ const [allAvailableTypes, setAllAvailableTypes] = useState([]);
492
+
493
+ // Schema-locked vs editor-picks-at-edit-time. When control.postType
494
+ // is omitted entirely, the field is dynamic — user picks at edit
495
+ // time and we store { post_type, ids } alongside the selection.
496
+ const dynamic = !control.postType;
497
+ const schemaDefault = (() => {
498
+ if (!control.postType) return 'post';
499
+ if (typeof control.postType === 'string') {
500
+ return control.postType.split(',')[0].trim() || 'post';
501
+ }
502
+ if (Array.isArray(control.postType)) return control.postType[0] || 'post';
503
+ return 'post';
504
+ })();
505
+ const normalised = useMemo(() => normalisePostObjectValue(value, schemaDefault), [value, schemaDefault]);
506
+ const currentPostType = normalised.post_type;
507
+ const availablePostTypes = useMemo(() => {
508
+ if (!control.postType) {
509
+ // Dynamic mode: offer everything the live registry knows about.
510
+ return allAvailableTypes.length > 0 ? allAvailableTypes : [{
511
+ value: 'post',
512
+ label: 'Posts'
513
+ }];
514
+ }
515
+ const types = typeof control.postType === 'string' ? control.postType.split(',').map(pt => pt.trim()).filter(Boolean) : Array.isArray(control.postType) ? control.postType : ['post'];
516
+ return types.map(type => ({
517
+ value: type,
518
+ label: type.charAt(0).toUpperCase() + type.slice(1) + 's'
519
+ }));
520
+ }, [control.postType, allAvailableTypes]);
521
+ useEffect(() => {
522
+ (async () => {
523
+ try {
524
+ const types = await apiFetch({
525
+ path: '/wp/v2/types'
526
+ });
527
+ const endpoints = {};
528
+ const list = [];
529
+ Object.entries(types).forEach(([key, info]) => {
530
+ endpoints[key] = info.rest_base || key;
531
+ // Skip non-author-facing types in the dynamic dropdown.
532
+ if (info.viewable && key !== 'attachment') {
533
+ list.push({
534
+ value: key,
535
+ label: info.name || key
536
+ });
537
+ }
538
+ });
539
+ setPostTypeEndpoints(endpoints);
540
+ setAllAvailableTypes(list);
541
+ } catch {
542
+ setPostTypeEndpoints({
543
+ post: 'posts',
544
+ page: 'pages',
545
+ media: 'media',
546
+ attachment: 'media'
547
+ });
548
+ setAllAvailableTypes([{
549
+ value: 'post',
550
+ label: 'Posts'
551
+ }, {
552
+ value: 'page',
553
+ label: 'Pages'
554
+ }]);
555
+ }
556
+ })();
557
+ }, []);
558
+ const isMultiple = !!control.multiple;
559
+ const selectedIds = isMultiple ? normalised.ids : null;
560
+ const singleSelectedId = !isMultiple ? normalised.ids[0] || null : null;
561
+
562
+ // Emit the canonical shape — always { post_type, ids[] } regardless
563
+ // of single/multi. Renderer reads both old and canonical so this is
564
+ // safe across migration.
565
+ const emitChange = useCallback(ids => {
566
+ onChange({
567
+ post_type: currentPostType,
568
+ ids
569
+ });
570
+ }, [onChange, currentPostType]);
571
+ const handlePostTypeChange = newType => {
572
+ // Switching post type clears the selected IDs — IDs from one
573
+ // type don't translate to another.
574
+ onChange({
575
+ post_type: newType,
576
+ ids: []
577
+ });
578
+ setAllPosts([]);
579
+ setSearchResults([]);
580
+ };
581
+ const mergePostsIntoCache = useCallback(newPosts => {
582
+ setAllPosts(prev => {
583
+ const merged = [...prev];
584
+ newPosts.forEach(np => {
585
+ if (!merged.find(p => p.id === np.id)) merged.push(np);
586
+ });
587
+ return merged;
588
+ });
589
+ }, []);
590
+ const selectedPosts = useMemo(() => {
591
+ if (isMultiple) {
592
+ return selectedIds.map(id => allPosts.find(p => p.id === id)).filter(Boolean);
593
+ }
594
+ return allPosts.find(p => p.id === singleSelectedId) || null;
595
+ }, [allPosts, selectedIds, singleSelectedId, isMultiple]);
596
+ const loadPosts = useCallback(async (searchTerm = '', filterPostType = 'all', taxFilters = {}) => {
597
+ setLoading(true);
598
+ try {
599
+ let postTypes;
600
+ if (dynamic) {
601
+ // In dynamic mode the editor has chosen the type via the
602
+ // picker dropdown; search only within that one type.
603
+ postTypes = [currentPostType];
604
+ } else {
605
+ postTypes = control.postType;
606
+ if (!postTypes || postTypes === '') {
607
+ postTypes = ['post'];
608
+ } else if (typeof postTypes === 'string') {
609
+ postTypes = postTypes.split(',').map(pt => pt.trim()).filter(Boolean);
610
+ if (postTypes.length === 0) postTypes = ['post'];
611
+ } else if (!Array.isArray(postTypes)) {
612
+ postTypes = ['post'];
613
+ }
614
+ }
615
+ if (filterPostType !== 'all') postTypes = [filterPostType];
616
+ let postStatuses = control.postStatus;
617
+ if (!postStatuses || postStatuses === '') {
618
+ postStatuses = ['publish'];
619
+ } else if (typeof postStatuses === 'string') {
620
+ postStatuses = postStatuses.split(',').map(s => s.trim()).filter(Boolean);
621
+ if (postStatuses.length === 0) postStatuses = ['publish'];
622
+ } else if (!Array.isArray(postStatuses)) {
623
+ postStatuses = ['publish'];
624
+ }
625
+ if (Object.keys(postTypeEndpoints).length === 0) {
626
+ setLoading(false);
627
+ return;
628
+ }
629
+ const collected = [];
630
+ for (const postType of postTypes) {
631
+ try {
632
+ const endpoint = postTypeEndpoints[postType] || postType;
633
+ let statusParam = 'publish';
634
+ if (postStatuses.length > 1 || postStatuses.length === 1 && postStatuses[0] !== 'publish') {
635
+ statusParam = 'any';
636
+ } else if (postStatuses.length === 1) {
637
+ statusParam = postStatuses[0];
638
+ }
639
+ const fields = postStatuses.length > 1 ? 'id,title,type,status' : 'id,title,type';
640
+ let query = `search=${searchTerm}&per_page=50&_fields=${fields}&status=${statusParam}`;
641
+ if (taxFilters && typeof taxFilters === 'object') {
642
+ Object.entries(taxFilters).forEach(([slug, termId]) => {
643
+ if (termId !== 'all') query += `&${slug}=${termId}`;
644
+ });
645
+ }
646
+ if (control.taxonomy && control.taxonomyTerms) {
647
+ const taxonomies = typeof control.taxonomy === 'string' ? control.taxonomy.split(',').map(t => t.trim()).filter(Boolean) : [control.taxonomy];
648
+ taxonomies.forEach(tax => {
649
+ query += `&${tax}=${control.taxonomyTerms}`;
650
+ });
651
+ }
652
+ const response = await apiFetch({
653
+ path: `/wp/v2/${endpoint}?${query}`
654
+ });
655
+ if (Array.isArray(response)) {
656
+ if (postStatuses.length > 1 && statusParam === 'any') {
657
+ collected.push(...response.filter(post => postStatuses.includes(post.status)));
658
+ } else {
659
+ collected.push(...response);
660
+ }
661
+ }
662
+ } catch {
663
+ // ignore per-type errors; continue
664
+ }
665
+ }
666
+ const unique = collected.filter((post, i, self) => i === self.findIndex(p => p.id === post.id));
667
+ setSearchResults(unique);
668
+ mergePostsIntoCache(unique);
669
+ } catch {
670
+ // ignore
671
+ }
672
+ setLoading(false);
673
+ }, [control.postType, control.postStatus, control.taxonomy, control.taxonomyTerms, mergePostsIntoCache, postTypeEndpoints, dynamic, currentPostType]);
674
+ useEffect(() => {
675
+ loadPosts();
676
+ }, [loadPosts]);
677
+ const handleSelect = postId => {
678
+ if (isMultiple) {
679
+ const newIds = selectedIds.includes(postId) ? selectedIds.filter(id => id !== postId) : [...selectedIds, postId];
680
+ emitChange(newIds);
681
+ } else {
682
+ emitChange([postId]);
683
+ }
684
+ };
685
+ const handleRemove = postId => {
686
+ if (!isMultiple) return;
687
+ emitChange(selectedIds.filter(id => id !== postId));
688
+ };
689
+ const handleReorder = newOrder => {
690
+ emitChange(newOrder);
691
+ };
692
+ const handleClear = () => emitChange([]);
693
+ const getDisplayText = () => {
694
+ if (isMultiple) {
695
+ return selectedPosts.length > 0 ? `${selectedPosts.length} ${__('selected', 'gcblite')}` : __('Select Posts', 'gcblite');
696
+ }
697
+ return selectedPosts?.title?.rendered || __('Select Post', 'gcblite');
698
+ };
699
+ return /*#__PURE__*/_jsxs("div", {
700
+ className: "components-base-control gcb-post-object-control",
701
+ children: [/*#__PURE__*/_jsx("div", {
702
+ className: "components-base-control__field",
703
+ children: /*#__PURE__*/_jsx("label", {
704
+ className: "components-base-control__label",
705
+ children: control.label
706
+ })
707
+ }), control.helpText && /*#__PURE__*/_jsx("p", {
708
+ className: "components-base-control__help",
709
+ children: control.helpText
710
+ }), dynamic && /*#__PURE__*/_jsxs("div", {
711
+ style: {
712
+ display: 'flex',
713
+ gap: 8,
714
+ alignItems: 'center',
715
+ marginBottom: 8
716
+ },
717
+ children: [/*#__PURE__*/_jsx("label", {
718
+ style: {
719
+ fontSize: 12,
720
+ fontWeight: 600,
721
+ color: '#1e1e1e',
722
+ minWidth: 70
723
+ },
724
+ children: __('Post type', 'gcblite')
725
+ }), /*#__PURE__*/_jsx("select", {
726
+ value: currentPostType,
727
+ onChange: e => handlePostTypeChange(e.target.value),
728
+ style: {
729
+ flex: 1,
730
+ padding: '6px 8px',
731
+ border: '1px solid #8c8f94',
732
+ borderRadius: 4,
733
+ fontSize: 13,
734
+ background: '#fff'
735
+ },
736
+ children: availablePostTypes.map(pt => /*#__PURE__*/_jsxs("option", {
737
+ value: pt.value,
738
+ children: [pt.label, " (", pt.value, ")"]
739
+ }, pt.value))
740
+ })]
741
+ }), isMultiple ? /*#__PURE__*/_jsx(PostObjectMultiSelect, {
742
+ posts: searchResults,
743
+ selectedIds: selectedIds,
744
+ selectedPosts: selectedPosts,
745
+ loading: loading,
746
+ search: search,
747
+ setSearch: setSearch,
748
+ loadPosts: loadPosts,
749
+ handleSelect: handleSelect,
750
+ handleRemove: handleRemove,
751
+ handleReorder: handleReorder,
752
+ handleClear: handleClear,
753
+ control: control,
754
+ postTypeFilter: postTypeFilter,
755
+ setPostTypeFilter: setPostTypeFilter,
756
+ availablePostTypes: availablePostTypes
757
+ }) : /*#__PURE__*/_jsx(PopoverOrModal, {
758
+ modalTitle: control.label || __('Select a post', 'gcblite'),
759
+ dropdownProps: {
760
+ popoverProps: {
761
+ placement: 'left-start'
762
+ }
763
+ },
764
+ renderToggle: ({
765
+ isOpen,
766
+ onToggle
767
+ }) => /*#__PURE__*/_jsx(Button, {
768
+ onClick: onToggle,
769
+ "aria-expanded": isOpen,
770
+ className: "gcb-modal-toggle-button",
771
+ style: TOGGLE_BUTTON_STYLE,
772
+ children: /*#__PURE__*/_jsx(Truncate, {
773
+ numberOfLines: 1,
774
+ children: getDisplayText()
775
+ })
776
+ }),
777
+ renderContent: ({
778
+ close: onClose,
779
+ variant
780
+ }) => /*#__PURE__*/_jsxs("div", {
781
+ style: variant === 'modal' ? {
782
+ width: '100%'
783
+ } : {
784
+ minWidth: 320,
785
+ maxWidth: 400
786
+ },
787
+ children: [/*#__PURE__*/_jsx("div", {
788
+ style: {
789
+ padding: '0 16px 8px 16px'
790
+ },
791
+ children: /*#__PURE__*/_jsx(TextControl, {
792
+ value: search,
793
+ onChange: val => {
794
+ setSearch(val);
795
+ loadPosts(val);
796
+ },
797
+ placeholder: __('Search or type title', 'gcblite'),
798
+ __nextHasNoMarginBottom: true
799
+ })
800
+ }), /*#__PURE__*/_jsxs("div", {
801
+ className: "block-editor-link-control__search-results-wrapper",
802
+ style: {
803
+ maxHeight: 300,
804
+ overflowY: 'auto'
805
+ },
806
+ children: [loading && /*#__PURE__*/_jsx("p", {
807
+ style: {
808
+ textAlign: 'center',
809
+ color: '#757575',
810
+ padding: 16
811
+ },
812
+ children: __('Loading…', 'gcblite')
813
+ }), !loading && searchResults.length === 0 && /*#__PURE__*/_jsx("p", {
814
+ style: {
815
+ textAlign: 'center',
816
+ color: '#757575',
817
+ padding: 16
818
+ },
819
+ children: __('No posts found', 'gcblite')
820
+ }), !loading && searchResults.length > 0 && /*#__PURE__*/_jsx("div", {
821
+ className: "block-editor-link-control__search-results",
822
+ role: "listbox",
823
+ children: /*#__PURE__*/_jsx("div", {
824
+ className: "components-menu-group",
825
+ children: /*#__PURE__*/_jsx("div", {
826
+ role: "group",
827
+ children: searchResults.map(post => {
828
+ const isSelected = singleSelectedId === post.id;
829
+ return /*#__PURE__*/_jsxs("button", {
830
+ type: "button",
831
+ role: "option",
832
+ className: "components-button components-menu-item__button block-editor-link-control__search-item",
833
+ onClick: () => handleSelect(post.id),
834
+ style: {
835
+ display: 'flex',
836
+ alignItems: 'center',
837
+ width: '100%',
838
+ padding: '8px 16px',
839
+ textAlign: 'left',
840
+ border: 'none',
841
+ background: 'transparent',
842
+ justifyContent: 'space-between'
843
+ },
844
+ children: [/*#__PURE__*/_jsxs("span", {
845
+ style: {
846
+ display: 'flex',
847
+ alignItems: 'center',
848
+ flex: 1,
849
+ overflow: 'hidden'
850
+ },
851
+ children: [/*#__PURE__*/_jsxs("svg", {
852
+ xmlns: "http://www.w3.org/2000/svg",
853
+ viewBox: "0 0 24 24",
854
+ width: "24",
855
+ height: "24",
856
+ className: "block-editor-link-control__search-item-icon",
857
+ style: {
858
+ marginRight: 12,
859
+ flexShrink: 0
860
+ },
861
+ children: [/*#__PURE__*/_jsx("path", {
862
+ d: "M15.5 7.5h-7V9h7V7.5Zm-7 3.5h7v1.5h-7V11Zm7 3.5h-7V16h7v-1.5Z"
863
+ }), /*#__PURE__*/_jsx("path", {
864
+ d: "M17 4H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM7 5.5h10a.5.5 0 0 1 .5.5v12a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5Z"
865
+ })]
866
+ }), /*#__PURE__*/_jsx("span", {
867
+ className: "components-menu-item__item",
868
+ style: {
869
+ fontWeight: 500,
870
+ overflow: 'hidden',
871
+ textOverflow: 'ellipsis',
872
+ whiteSpace: 'nowrap'
873
+ },
874
+ children: post.title?.rendered || __('(no title)', 'gcblite')
875
+ })]
876
+ }), isSelected && /*#__PURE__*/_jsx("svg", {
877
+ xmlns: "http://www.w3.org/2000/svg",
878
+ viewBox: "0 0 24 24",
879
+ width: "24",
880
+ height: "24",
881
+ className: "components-menu-items__item-icon has-icon-right",
882
+ style: {
883
+ marginLeft: 8,
884
+ flexShrink: 0,
885
+ fill: '#2271b1'
886
+ },
887
+ children: /*#__PURE__*/_jsx("path", {
888
+ d: "M16.5 7.5 10 13.9l-2.5-2.4-1 1 3.5 3.6 7.5-7.6z"
889
+ })
890
+ })]
891
+ }, post.id);
892
+ })
893
+ })
894
+ })
895
+ })]
896
+ }), selectedPosts && /*#__PURE__*/_jsx("div", {
897
+ style: {
898
+ padding: '8px 16px 16px',
899
+ borderTop: '1px solid #ddd'
900
+ },
901
+ children: /*#__PURE__*/_jsx(Button, {
902
+ onClick: handleClear,
903
+ variant: "tertiary",
904
+ style: {
905
+ width: '100%'
906
+ },
907
+ children: __('Clear Selection', 'gcblite')
908
+ })
909
+ })]
910
+ })
911
+ })]
912
+ });
913
+ }