@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/spacing.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SpacingField — preset spacing (None / S / M / L) with a custom-value escape hatch.
|
|
3
|
-
*
|
|
4
|
-
* Stored value is either one of the preset keys (`'small'`, etc.) or a CSS
|
|
5
|
-
* length string (e.g. `'2rem'`). When set to a custom value, the toggle group
|
|
6
|
-
* is disabled and a "custom value applied" hint shows.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { __ } from '@wordpress/i18n';
|
|
10
|
-
import { Button, TextControl, Notice } from '@wordpress/components';
|
|
11
|
-
import { useState } from '@wordpress/element';
|
|
12
|
-
import {
|
|
13
|
-
__experimentalToggleGroupControl as ToggleGroupControl,
|
|
14
|
-
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
|
15
|
-
__experimentalHStack as HStack,
|
|
16
|
-
} from '@wordpress/components';
|
|
17
|
-
|
|
18
|
-
const DEFAULT_PRESETS = [
|
|
19
|
-
{ label: 'None', value: 'none' },
|
|
20
|
-
{ label: 'S', value: 'small' },
|
|
21
|
-
{ label: 'M', value: 'medium' },
|
|
22
|
-
{ label: 'L', value: 'large' },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const PRESET_KEYS = new Set(['none', 'small', 'medium', 'large']);
|
|
26
|
-
|
|
27
|
-
function isValidCSSValue(input) {
|
|
28
|
-
if (!input) return true;
|
|
29
|
-
return /^(\d*\.?\d+)(px|rem|em|%|vw|vh|vmin|vmax)?$/.test(String(input).trim());
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export default function SpacingField({ control, value, onChange }) {
|
|
33
|
-
// Decide if value is a preset or a custom string.
|
|
34
|
-
const isCustom = typeof value === 'string' && value !== '' && !PRESET_KEYS.has(value);
|
|
35
|
-
const presetValue = isCustom ? 'medium' : (value || 'medium');
|
|
36
|
-
|
|
37
|
-
const [showCustom, setShowCustom] = useState(isCustom);
|
|
38
|
-
const [customInput, setCustomInput] = useState(isCustom ? value : '');
|
|
39
|
-
const [error, setError] = useState(null);
|
|
40
|
-
|
|
41
|
-
const presets = control.presets || DEFAULT_PRESETS;
|
|
42
|
-
|
|
43
|
-
const handlePreset = (next) => {
|
|
44
|
-
setShowCustom(false);
|
|
45
|
-
setCustomInput('');
|
|
46
|
-
setError(null);
|
|
47
|
-
onChange(next);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const handleCustom = (next) => {
|
|
51
|
-
setCustomInput(next);
|
|
52
|
-
if (next && !isValidCSSValue(next)) {
|
|
53
|
-
setError(__('Invalid spacing value. Use a number with a unit (e.g. 2rem, 20px).', 'gcblite'));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
setError(null);
|
|
57
|
-
onChange(next || 'medium');
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const handleReset = () => {
|
|
61
|
-
setShowCustom(false);
|
|
62
|
-
setCustomInput('');
|
|
63
|
-
setError(null);
|
|
64
|
-
onChange('medium');
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const displayHint = showCustom
|
|
68
|
-
? 'Custom'
|
|
69
|
-
: (presetValue.charAt(0).toUpperCase() + presetValue.slice(1));
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div className={`gcb-spacing-field components-base-control ${control.className || ''}`.trim()}>
|
|
73
|
-
<div className="components-base-control__field">
|
|
74
|
-
<HStack>
|
|
75
|
-
<span className="components-base-control__label">
|
|
76
|
-
{control.label}
|
|
77
|
-
<span className="components-font-size-picker__header__hint">{displayHint}</span>
|
|
78
|
-
</span>
|
|
79
|
-
<Button
|
|
80
|
-
size="small"
|
|
81
|
-
label={__('Set custom spacing', 'gcblite')}
|
|
82
|
-
onClick={() => setShowCustom(true)}
|
|
83
|
-
isPressed={showCustom}
|
|
84
|
-
icon={(
|
|
85
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden focusable="false">
|
|
86
|
-
<path d="m19 7.5h-7.628c-.3089-.87389-1.1423-1.5-2.122-1.5-.97966 0-1.81309.62611-2.12197 1.5h-2.12803v1.5h2.12803c.30888.87389 1.14231 1.5 2.12197 1.5.9797 0 1.8131-.62611 2.122-1.5h7.628z" />
|
|
87
|
-
<path d="m19 15h-2.128c-.3089-.8739-1.1423-1.5-2.122-1.5s-1.8131.6261-2.122 1.5h-7.628v1.5h7.628c.3089.8739 1.1423 1.5 2.122 1.5s1.8131-.6261 2.122-1.5h2.128z" />
|
|
88
|
-
</svg>
|
|
89
|
-
)}
|
|
90
|
-
/>
|
|
91
|
-
</HStack>
|
|
92
|
-
|
|
93
|
-
{error && (
|
|
94
|
-
<Notice status="warning" isDismissible onRemove={() => setError(null)}>
|
|
95
|
-
{error}
|
|
96
|
-
</Notice>
|
|
97
|
-
)}
|
|
98
|
-
|
|
99
|
-
{showCustom && (
|
|
100
|
-
<div style={{ marginBottom: 16, fontSize: 13 }}>
|
|
101
|
-
<span>{__('Custom value applied', 'gcblite')}</span>
|
|
102
|
-
<Button variant="link" onClick={handleReset} style={{ marginLeft: 8, fontSize: 13 }}>
|
|
103
|
-
{__('Reset', 'gcblite')}
|
|
104
|
-
</Button>
|
|
105
|
-
</div>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
<ToggleGroupControl
|
|
109
|
-
label={control.label}
|
|
110
|
-
value={presetValue}
|
|
111
|
-
onChange={handlePreset}
|
|
112
|
-
isBlock
|
|
113
|
-
hideLabelFromVision
|
|
114
|
-
disabled={showCustom}
|
|
115
|
-
className={showCustom ? 'is-disabled' : ''}
|
|
116
|
-
__nextHasNoMarginBottom
|
|
117
|
-
__next40pxDefaultSize
|
|
118
|
-
>
|
|
119
|
-
{presets.map((preset) => (
|
|
120
|
-
<ToggleGroupControlOption
|
|
121
|
-
key={preset.value}
|
|
122
|
-
value={preset.value}
|
|
123
|
-
label={preset.label}
|
|
124
|
-
/>
|
|
125
|
-
))}
|
|
126
|
-
</ToggleGroupControl>
|
|
127
|
-
|
|
128
|
-
{showCustom && (
|
|
129
|
-
<TextControl
|
|
130
|
-
label={__('Custom Spacing', 'gcblite')}
|
|
131
|
-
value={customInput}
|
|
132
|
-
onChange={handleCustom}
|
|
133
|
-
placeholder="e.g. 2rem or 20px"
|
|
134
|
-
help={__('Enter a value with unit (e.g. 2rem, 20px, 5%) or leave empty for 0.', 'gcblite')}
|
|
135
|
-
__nextHasNoMarginBottom
|
|
136
|
-
/>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
}
|
package/src/controls/taxonomy.js
DELETED
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TaxonomyField — ported verbatim from the original GCB.
|
|
3
|
-
*
|
|
4
|
-
* Single OR multiple term selection (control.multiple, default true).
|
|
5
|
-
* Stored as ID(s) or full term object(s) (control.returnFormat).
|
|
6
|
-
* Optional "create new term" UI (control.allowCreateTerms).
|
|
7
|
-
* Drag-and-drop reordering of selected terms (multi-select only).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { __ } from '@wordpress/i18n';
|
|
11
|
-
import {
|
|
12
|
-
Button,
|
|
13
|
-
TextControl,
|
|
14
|
-
} from '@wordpress/components';
|
|
15
|
-
import PopoverOrModal from './PopoverOrModal';
|
|
16
|
-
import { useState, useEffect, useMemo, useCallback } from '@wordpress/element';
|
|
17
|
-
import apiFetch from '@wordpress/api-fetch';
|
|
18
|
-
import {
|
|
19
|
-
DndContext,
|
|
20
|
-
closestCenter,
|
|
21
|
-
PointerSensor,
|
|
22
|
-
useSensor,
|
|
23
|
-
useSensors,
|
|
24
|
-
DragOverlay,
|
|
25
|
-
} from '@dnd-kit/core';
|
|
26
|
-
import {
|
|
27
|
-
SortableContext,
|
|
28
|
-
verticalListSortingStrategy,
|
|
29
|
-
useSortable,
|
|
30
|
-
arrayMove,
|
|
31
|
-
} from '@dnd-kit/sortable';
|
|
32
|
-
import { CSS } from '@dnd-kit/utilities';
|
|
33
|
-
|
|
34
|
-
const TOGGLE_BUTTON_STYLE = {
|
|
35
|
-
width: '100%',
|
|
36
|
-
height: 'auto',
|
|
37
|
-
padding: '12px',
|
|
38
|
-
justifyContent: 'flex-start',
|
|
39
|
-
border: '1px solid #ddd',
|
|
40
|
-
borderRadius: '2px',
|
|
41
|
-
backgroundColor: '#fff',
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function TermIcon() {
|
|
45
|
-
return (
|
|
46
|
-
<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 }}>
|
|
47
|
-
<path d="M8 12c0 1.1.9 2 2 2s2-.9 2-2-.9-2-2-2-2 .9-2 2zm8-2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
|
|
48
|
-
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
|
49
|
-
</svg>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function SortableTermItem({ term, onRemove }) {
|
|
54
|
-
const {
|
|
55
|
-
attributes: dndAttributes,
|
|
56
|
-
listeners,
|
|
57
|
-
setNodeRef,
|
|
58
|
-
transform,
|
|
59
|
-
transition,
|
|
60
|
-
isDragging,
|
|
61
|
-
} = useSortable({ id: term.id });
|
|
62
|
-
|
|
63
|
-
const style = {
|
|
64
|
-
transform: CSS.Transform.toString(transform),
|
|
65
|
-
transition,
|
|
66
|
-
opacity: isDragging ? 0.5 : 1,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div
|
|
71
|
-
ref={setNodeRef}
|
|
72
|
-
style={style}
|
|
73
|
-
className="gcb-post-object-selected-item"
|
|
74
|
-
{...dndAttributes}
|
|
75
|
-
{...listeners}
|
|
76
|
-
>
|
|
77
|
-
<div className="gcb-post-object-drag-handle" aria-hidden>
|
|
78
|
-
<svg viewBox="0 0 20 20" width="12">
|
|
79
|
-
<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" />
|
|
80
|
-
</svg>
|
|
81
|
-
</div>
|
|
82
|
-
<TermIcon />
|
|
83
|
-
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', userSelect: 'none' }}>
|
|
84
|
-
{term.name || __('(no name)', 'gcblite')}
|
|
85
|
-
</span>
|
|
86
|
-
<button
|
|
87
|
-
type="button"
|
|
88
|
-
onClick={(e) => {
|
|
89
|
-
e.stopPropagation();
|
|
90
|
-
onRemove(term.id);
|
|
91
|
-
}}
|
|
92
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
93
|
-
className="gcb-sortable-remove"
|
|
94
|
-
aria-label={__('Remove', 'gcblite')}
|
|
95
|
-
>
|
|
96
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
|
|
97
|
-
<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" />
|
|
98
|
-
</svg>
|
|
99
|
-
</button>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Normalise whatever the value used to look like into the canonical
|
|
105
|
-
// { taxonomy, ids[] } shape. Handles:
|
|
106
|
-
// - bare ID (legacy single) → { taxonomy: schemaDefault, ids: [3] }
|
|
107
|
-
// - array of IDs (legacy multi) → { taxonomy: schemaDefault, ids: [3, 5] }
|
|
108
|
-
// - { id, name, taxonomy } (returnFormat=object, single)
|
|
109
|
-
// - array of those objects (returnFormat=object, multi)
|
|
110
|
-
// - new canonical { taxonomy, ids[] } → passes through
|
|
111
|
-
function normaliseTaxonomyValue(value, schemaDefault) {
|
|
112
|
-
if (!value) return { taxonomy: schemaDefault, ids: [] };
|
|
113
|
-
// Already canonical.
|
|
114
|
-
if (typeof value === 'object' && !Array.isArray(value) && Array.isArray(value.ids)) {
|
|
115
|
-
return { taxonomy: value.taxonomy || schemaDefault, ids: value.ids };
|
|
116
|
-
}
|
|
117
|
-
// Bare scalar (legacy single).
|
|
118
|
-
if (typeof value === 'number' || typeof value === 'string') {
|
|
119
|
-
return { taxonomy: schemaDefault, ids: [Number(value)] };
|
|
120
|
-
}
|
|
121
|
-
// Single object (returnFormat=object).
|
|
122
|
-
if (typeof value === 'object' && !Array.isArray(value) && value.id != null) {
|
|
123
|
-
return { taxonomy: value.taxonomy || schemaDefault, ids: [Number(value.id)] };
|
|
124
|
-
}
|
|
125
|
-
// Array shapes.
|
|
126
|
-
if (Array.isArray(value)) {
|
|
127
|
-
const ids = value
|
|
128
|
-
.map((entry) => typeof entry === 'object' ? Number(entry.id) : Number(entry))
|
|
129
|
-
.filter(Boolean);
|
|
130
|
-
const tx = value.find((entry) => typeof entry === 'object' && entry?.taxonomy)?.taxonomy;
|
|
131
|
-
return { taxonomy: tx || schemaDefault, ids };
|
|
132
|
-
}
|
|
133
|
-
return { taxonomy: schemaDefault, ids: [] };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const REST_BASE_OVERRIDES = { category: 'categories', post_tag: 'tags' };
|
|
137
|
-
function resolveRestBase(taxonomy, override) {
|
|
138
|
-
if (override) return override;
|
|
139
|
-
return REST_BASE_OVERRIDES[taxonomy] || taxonomy;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export default function TaxonomyField({ control, value, onChange }) {
|
|
143
|
-
const [allTerms, setAllTerms] = useState([]);
|
|
144
|
-
const [searchResults, setSearchResults] = useState([]);
|
|
145
|
-
const [loading, setLoading] = useState(false);
|
|
146
|
-
const [search, setSearch] = useState('');
|
|
147
|
-
const [creatingTerm, setCreatingTerm] = useState(false);
|
|
148
|
-
const [newTermName, setNewTermName] = useState('');
|
|
149
|
-
const [activeId, setActiveId] = useState(null);
|
|
150
|
-
const [availableTaxonomies, setAvailableTaxonomies] = useState([]);
|
|
151
|
-
|
|
152
|
-
const isMultiple = control.multiple !== false;
|
|
153
|
-
// Schema-locked vs author-picks-at-edit-time.
|
|
154
|
-
// When the schema omits `taxonomy`, the editor user picks via a
|
|
155
|
-
// dropdown. The picked value is stored alongside the IDs so the
|
|
156
|
-
// renderer can still resolve them at read-time.
|
|
157
|
-
const dynamic = !control.taxonomy;
|
|
158
|
-
const schemaDefault = control.taxonomy || 'category';
|
|
159
|
-
|
|
160
|
-
// Canonical { taxonomy, ids[] } shape.
|
|
161
|
-
const normalised = useMemo(
|
|
162
|
-
() => normaliseTaxonomyValue(value, schemaDefault),
|
|
163
|
-
[value, schemaDefault]
|
|
164
|
-
);
|
|
165
|
-
const taxonomy = normalised.taxonomy || schemaDefault;
|
|
166
|
-
const selectedIds = normalised.ids;
|
|
167
|
-
|
|
168
|
-
const restBase = resolveRestBase(taxonomy, control.restBase);
|
|
169
|
-
|
|
170
|
-
const selectedTerms = useMemo(
|
|
171
|
-
() => selectedIds.map((id) => allTerms.find((t) => t.id === id)).filter(Boolean),
|
|
172
|
-
[selectedIds, allTerms]
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Fetch the list of registered taxonomies once, only when the schema
|
|
176
|
-
// didn't lock to one. Used to populate the taxonomy dropdown.
|
|
177
|
-
useEffect(() => {
|
|
178
|
-
if (!dynamic) return;
|
|
179
|
-
let cancelled = false;
|
|
180
|
-
apiFetch({ path: '/wp/v2/taxonomies?context=view' })
|
|
181
|
-
.then((res) => {
|
|
182
|
-
if (cancelled) return;
|
|
183
|
-
// REST returns an object keyed by taxonomy slug.
|
|
184
|
-
const list = Object.entries(res || {}).map(([slug, info]) => ({
|
|
185
|
-
slug,
|
|
186
|
-
name: info?.name || slug,
|
|
187
|
-
restBase: info?.rest_base || slug,
|
|
188
|
-
}));
|
|
189
|
-
setAvailableTaxonomies(list);
|
|
190
|
-
})
|
|
191
|
-
.catch(() => {});
|
|
192
|
-
return () => { cancelled = true; };
|
|
193
|
-
}, [dynamic]);
|
|
194
|
-
|
|
195
|
-
// Emit the canonical shape — always { taxonomy, ids[] }, regardless
|
|
196
|
-
// of single/multi, so the renderer never has to guess.
|
|
197
|
-
const emitChange = useCallback((ids) => {
|
|
198
|
-
onChange({ taxonomy, ids });
|
|
199
|
-
}, [onChange, taxonomy]);
|
|
200
|
-
|
|
201
|
-
const handleTaxonomyChange = (newTax) => {
|
|
202
|
-
// Switching taxonomy clears the selected terms — IDs from one
|
|
203
|
-
// taxonomy don't translate to another.
|
|
204
|
-
onChange({ taxonomy: newTax, ids: [] });
|
|
205
|
-
setAllTerms([]);
|
|
206
|
-
setSearchResults([]);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const mergeTermsIntoCache = useCallback((newTerms) => {
|
|
210
|
-
setAllTerms((prev) => {
|
|
211
|
-
const merged = [...prev];
|
|
212
|
-
newTerms.forEach((nt) => {
|
|
213
|
-
if (!merged.find((t) => t.id === nt.id)) merged.push(nt);
|
|
214
|
-
});
|
|
215
|
-
return merged;
|
|
216
|
-
});
|
|
217
|
-
}, []);
|
|
218
|
-
|
|
219
|
-
const loadTerms = useCallback(async (term = '') => {
|
|
220
|
-
setLoading(true);
|
|
221
|
-
try {
|
|
222
|
-
const response = await apiFetch({
|
|
223
|
-
path: `/wp/v2/${restBase}?search=${encodeURIComponent(term)}&per_page=100&_fields=id,name`,
|
|
224
|
-
});
|
|
225
|
-
setSearchResults(response);
|
|
226
|
-
mergeTermsIntoCache(response);
|
|
227
|
-
} catch {
|
|
228
|
-
// ignore
|
|
229
|
-
}
|
|
230
|
-
setLoading(false);
|
|
231
|
-
}, [restBase, mergeTermsIntoCache]);
|
|
232
|
-
|
|
233
|
-
useEffect(() => {
|
|
234
|
-
loadTerms();
|
|
235
|
-
}, [loadTerms]);
|
|
236
|
-
|
|
237
|
-
const handleSelect = (termId) => {
|
|
238
|
-
const newIds = isMultiple
|
|
239
|
-
? (selectedIds.includes(termId)
|
|
240
|
-
? selectedIds.filter((id) => id !== termId)
|
|
241
|
-
: [...selectedIds, termId])
|
|
242
|
-
: [termId];
|
|
243
|
-
emitChange(newIds);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const handleRemove = (termId) => {
|
|
247
|
-
emitChange(selectedIds.filter((id) => id !== termId));
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const handleReorder = (newOrder) => {
|
|
251
|
-
emitChange(newOrder);
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const handleClear = () => emitChange([]);
|
|
255
|
-
|
|
256
|
-
const handleCreateTerm = async () => {
|
|
257
|
-
if (!newTermName.trim() || !control.allowCreateTerms) return;
|
|
258
|
-
setCreatingTerm(true);
|
|
259
|
-
try {
|
|
260
|
-
const newTerm = await apiFetch({
|
|
261
|
-
path: `/wp/v2/${restBase}`,
|
|
262
|
-
method: 'POST',
|
|
263
|
-
data: { name: newTermName.trim() },
|
|
264
|
-
});
|
|
265
|
-
mergeTermsIntoCache([newTerm]);
|
|
266
|
-
setSearchResults((prev) => [...prev, newTerm]);
|
|
267
|
-
handleSelect(newTerm.id);
|
|
268
|
-
setNewTermName('');
|
|
269
|
-
} catch {
|
|
270
|
-
// ignore
|
|
271
|
-
}
|
|
272
|
-
setCreatingTerm(false);
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }));
|
|
276
|
-
|
|
277
|
-
const handleDragStart = (e) => setActiveId(e.active.id);
|
|
278
|
-
const handleDragCancel = () => setActiveId(null);
|
|
279
|
-
const handleDragEnd = (e) => {
|
|
280
|
-
const { active, over } = e;
|
|
281
|
-
if (over && active.id !== over.id) {
|
|
282
|
-
const oldIndex = selectedIds.indexOf(active.id);
|
|
283
|
-
const newIndex = selectedIds.indexOf(over.id);
|
|
284
|
-
handleReorder(arrayMove(selectedIds, oldIndex, newIndex));
|
|
285
|
-
}
|
|
286
|
-
setActiveId(null);
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const activeTerm = activeId ? selectedTerms.find((t) => t.id === activeId) : null;
|
|
290
|
-
const availableTerms = searchResults.filter((t) => !selectedIds.includes(t.id));
|
|
291
|
-
|
|
292
|
-
return (
|
|
293
|
-
<div className="components-base-control gcb-taxonomy-control">
|
|
294
|
-
<div className="components-base-control__field">
|
|
295
|
-
<label className="components-base-control__label">{control.label}</label>
|
|
296
|
-
</div>
|
|
297
|
-
{control.helpText && (
|
|
298
|
-
<p className="components-base-control__help">{control.helpText}</p>
|
|
299
|
-
)}
|
|
300
|
-
|
|
301
|
-
<div className="gcb-post-object-stacked">
|
|
302
|
-
{dynamic && (
|
|
303
|
-
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 }}>
|
|
304
|
-
<label style={{ fontSize: 12, fontWeight: 600, color: '#1e1e1e', minWidth: 70 }}>
|
|
305
|
-
{__('Taxonomy', 'gcblite')}
|
|
306
|
-
</label>
|
|
307
|
-
<select
|
|
308
|
-
value={taxonomy}
|
|
309
|
-
onChange={(e) => handleTaxonomyChange(e.target.value)}
|
|
310
|
-
style={{
|
|
311
|
-
flex: 1,
|
|
312
|
-
padding: '6px 8px',
|
|
313
|
-
border: '1px solid #8c8f94',
|
|
314
|
-
borderRadius: 4,
|
|
315
|
-
fontSize: 13,
|
|
316
|
-
background: '#fff',
|
|
317
|
-
}}
|
|
318
|
-
>
|
|
319
|
-
{availableTaxonomies.length === 0 && (
|
|
320
|
-
<option value={taxonomy}>{taxonomy}</option>
|
|
321
|
-
)}
|
|
322
|
-
{availableTaxonomies.map((tx) => (
|
|
323
|
-
<option key={tx.slug} value={tx.slug}>
|
|
324
|
-
{tx.name} ({tx.slug})
|
|
325
|
-
</option>
|
|
326
|
-
))}
|
|
327
|
-
</select>
|
|
328
|
-
</div>
|
|
329
|
-
)}
|
|
330
|
-
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
331
|
-
<PopoverOrModal
|
|
332
|
-
modalTitle={control.label || __('Select terms', 'gcblite')}
|
|
333
|
-
dropdownProps={{ popoverProps: { placement: 'left-start' } }}
|
|
334
|
-
renderToggle={({ isOpen, onToggle }) => (
|
|
335
|
-
<Button
|
|
336
|
-
onClick={onToggle}
|
|
337
|
-
aria-expanded={isOpen}
|
|
338
|
-
className="gcb-modal-toggle-button"
|
|
339
|
-
style={{ ...TOGGLE_BUTTON_STYLE, flex: 1 }}
|
|
340
|
-
>
|
|
341
|
-
{selectedTerms.length > 0
|
|
342
|
-
? `${selectedTerms.length} ${selectedTerms.length === 1 ? __('term', 'gcblite') : __('terms', 'gcblite')} ${__('selected', 'gcblite')}`
|
|
343
|
-
: __('Select Terms', 'gcblite')}
|
|
344
|
-
</Button>
|
|
345
|
-
)}
|
|
346
|
-
renderContent={({ close: onClose, variant }) => (
|
|
347
|
-
<div style={variant === 'modal' ? { width: '100%' } : { minWidth: 320, maxWidth: 400 }}>
|
|
348
|
-
<div style={{ padding: '0 16px 8px 16px' }}>
|
|
349
|
-
<TextControl
|
|
350
|
-
value={search}
|
|
351
|
-
onChange={(val) => {
|
|
352
|
-
setSearch(val);
|
|
353
|
-
loadTerms(val);
|
|
354
|
-
}}
|
|
355
|
-
placeholder={__('Search…', 'gcblite')}
|
|
356
|
-
__nextHasNoMarginBottom
|
|
357
|
-
/>
|
|
358
|
-
|
|
359
|
-
{control.allowCreateTerms && (
|
|
360
|
-
<div style={{ marginTop: 12, padding: 12, background: '#f0f6fc', borderRadius: 4 }}>
|
|
361
|
-
<TextControl
|
|
362
|
-
label={__('Create New Term', 'gcblite')}
|
|
363
|
-
value={newTermName}
|
|
364
|
-
onChange={setNewTermName}
|
|
365
|
-
placeholder={__('Enter term name…', 'gcblite')}
|
|
366
|
-
disabled={creatingTerm}
|
|
367
|
-
__nextHasNoMarginBottom
|
|
368
|
-
/>
|
|
369
|
-
<Button
|
|
370
|
-
variant="primary"
|
|
371
|
-
size="small"
|
|
372
|
-
onClick={handleCreateTerm}
|
|
373
|
-
disabled={!newTermName.trim() || creatingTerm}
|
|
374
|
-
style={{ marginTop: 8 }}
|
|
375
|
-
>
|
|
376
|
-
{creatingTerm ? __('Creating…', 'gcblite') : __('Create', 'gcblite')}
|
|
377
|
-
</Button>
|
|
378
|
-
</div>
|
|
379
|
-
)}
|
|
380
|
-
</div>
|
|
381
|
-
|
|
382
|
-
<div className="block-editor-link-control__search-results-wrapper" style={{ maxHeight: 300, overflowY: 'auto' }}>
|
|
383
|
-
{loading && (
|
|
384
|
-
<p style={{ textAlign: 'center', color: '#757575', padding: 16 }}>
|
|
385
|
-
{__('Loading…', 'gcblite')}
|
|
386
|
-
</p>
|
|
387
|
-
)}
|
|
388
|
-
{!loading && availableTerms.length === 0 && (
|
|
389
|
-
<p style={{ textAlign: 'center', color: '#757575', padding: 16 }}>
|
|
390
|
-
{__('No terms found', 'gcblite')}
|
|
391
|
-
</p>
|
|
392
|
-
)}
|
|
393
|
-
{!loading && availableTerms.length > 0 && (
|
|
394
|
-
<div className="block-editor-link-control__search-results" role="listbox">
|
|
395
|
-
<div className="components-menu-group">
|
|
396
|
-
<div role="group">
|
|
397
|
-
{availableTerms.map((term) => (
|
|
398
|
-
<button
|
|
399
|
-
key={term.id}
|
|
400
|
-
type="button"
|
|
401
|
-
role="option"
|
|
402
|
-
className="components-button components-menu-item__button block-editor-link-control__search-item"
|
|
403
|
-
onClick={() => handleSelect(term.id)}
|
|
404
|
-
style={{
|
|
405
|
-
display: 'flex',
|
|
406
|
-
alignItems: 'center',
|
|
407
|
-
width: '100%',
|
|
408
|
-
padding: '8px 16px',
|
|
409
|
-
textAlign: 'left',
|
|
410
|
-
border: 'none',
|
|
411
|
-
background: 'transparent',
|
|
412
|
-
justifyContent: 'space-between',
|
|
413
|
-
}}
|
|
414
|
-
>
|
|
415
|
-
<span style={{ display: 'flex', alignItems: 'center', flex: 1, overflow: 'hidden' }}>
|
|
416
|
-
<TermIcon />
|
|
417
|
-
<span className="components-menu-item__item" style={{ fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
418
|
-
{term.name}
|
|
419
|
-
</span>
|
|
420
|
-
</span>
|
|
421
|
-
</button>
|
|
422
|
-
))}
|
|
423
|
-
</div>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
)}
|
|
427
|
-
</div>
|
|
428
|
-
</div>
|
|
429
|
-
)}
|
|
430
|
-
/>
|
|
431
|
-
|
|
432
|
-
{selectedTerms.length > 0 && (
|
|
433
|
-
<Button
|
|
434
|
-
onClick={handleClear}
|
|
435
|
-
variant="secondary"
|
|
436
|
-
isSmall
|
|
437
|
-
className="components-range-control__reset"
|
|
438
|
-
>
|
|
439
|
-
{__('Reset', 'gcblite')}
|
|
440
|
-
</Button>
|
|
441
|
-
)}
|
|
442
|
-
</div>
|
|
443
|
-
|
|
444
|
-
{selectedTerms.length > 0 && isMultiple && (
|
|
445
|
-
<div className="gcb-post-object-selected-list" style={{ marginTop: 8 }}>
|
|
446
|
-
<DndContext
|
|
447
|
-
sensors={sensors}
|
|
448
|
-
collisionDetection={closestCenter}
|
|
449
|
-
onDragStart={handleDragStart}
|
|
450
|
-
onDragEnd={handleDragEnd}
|
|
451
|
-
onDragCancel={handleDragCancel}
|
|
452
|
-
>
|
|
453
|
-
<SortableContext items={selectedIds} strategy={verticalListSortingStrategy}>
|
|
454
|
-
{selectedTerms.map((term) => (
|
|
455
|
-
<SortableTermItem key={term.id} term={term} onRemove={handleRemove} />
|
|
456
|
-
))}
|
|
457
|
-
</SortableContext>
|
|
458
|
-
<DragOverlay>
|
|
459
|
-
{activeTerm ? (
|
|
460
|
-
<div className="gcb-post-object-selected-item" style={{ opacity: 0.8, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
|
|
461
|
-
<div className="gcb-post-object-drag-handle" aria-hidden>
|
|
462
|
-
<svg viewBox="0 0 20 20" width="12">
|
|
463
|
-
<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" />
|
|
464
|
-
</svg>
|
|
465
|
-
</div>
|
|
466
|
-
<TermIcon />
|
|
467
|
-
<span style={{ flex: 1 }}>{activeTerm.name}</span>
|
|
468
|
-
</div>
|
|
469
|
-
) : null}
|
|
470
|
-
</DragOverlay>
|
|
471
|
-
</DndContext>
|
|
472
|
-
</div>
|
|
473
|
-
)}
|
|
474
|
-
|
|
475
|
-
{selectedTerms.length > 0 && !isMultiple && (
|
|
476
|
-
<div className="gcb-post-object-selected-list" style={{ marginTop: 8 }}>
|
|
477
|
-
<div className="gcb-post-object-selected-item">
|
|
478
|
-
<TermIcon />
|
|
479
|
-
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
480
|
-
{selectedTerms[0].name}
|
|
481
|
-
</span>
|
|
482
|
-
</div>
|
|
483
|
-
</div>
|
|
484
|
-
)}
|
|
485
|
-
</div>
|
|
486
|
-
</div>
|
|
487
|
-
);
|
|
488
|
-
}
|
package/src/controls/text.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { TextControl } from '@wordpress/components';
|
|
2
|
-
|
|
3
|
-
export default function TextField({ control, value, onChange }) {
|
|
4
|
-
return (
|
|
5
|
-
<TextControl
|
|
6
|
-
label={control.label}
|
|
7
|
-
help={control.helpText}
|
|
8
|
-
placeholder={control.placeholder}
|
|
9
|
-
value={value ?? ''}
|
|
10
|
-
onChange={onChange}
|
|
11
|
-
__nextHasNoMarginBottom
|
|
12
|
-
/>
|
|
13
|
-
);
|
|
14
|
-
}
|
package/src/controls/textarea.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { TextareaControl } from '@wordpress/components';
|
|
2
|
-
|
|
3
|
-
export default function TextareaField({ control, value, onChange }) {
|
|
4
|
-
return (
|
|
5
|
-
<TextareaControl
|
|
6
|
-
label={control.label}
|
|
7
|
-
help={control.helpText}
|
|
8
|
-
placeholder={control.placeholder}
|
|
9
|
-
value={value ?? ''}
|
|
10
|
-
onChange={onChange}
|
|
11
|
-
rows={4}
|
|
12
|
-
__nextHasNoMarginBottom
|
|
13
|
-
/>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ToggleGroup — radio-style segmented control. Stores a single value.
|
|
3
|
-
*/
|
|
4
|
-
import {
|
|
5
|
-
__experimentalToggleGroupControl as ToggleGroupControl,
|
|
6
|
-
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
|
7
|
-
} from '@wordpress/components';
|
|
8
|
-
|
|
9
|
-
export default function ToggleGroupField({ control, value, onChange }) {
|
|
10
|
-
// ToggleGroupControl needs undefined (not '') when nothing is selected,
|
|
11
|
-
// otherwise it shows every option as checked.
|
|
12
|
-
const controlValue = value || undefined;
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<ToggleGroupControl
|
|
16
|
-
label={control.label}
|
|
17
|
-
value={controlValue}
|
|
18
|
-
onChange={onChange}
|
|
19
|
-
help={control.helpText}
|
|
20
|
-
isBlock={control.isBlock !== false}
|
|
21
|
-
className="gcb-toggle-group-control"
|
|
22
|
-
__nextHasNoMarginBottom
|
|
23
|
-
__next40pxDefaultSize
|
|
24
|
-
>
|
|
25
|
-
{(control.options || []).map((option, idx) => (
|
|
26
|
-
<ToggleGroupControlOption
|
|
27
|
-
key={option.value || idx}
|
|
28
|
-
value={option.value}
|
|
29
|
-
label={option.label}
|
|
30
|
-
/>
|
|
31
|
-
))}
|
|
32
|
-
</ToggleGroupControl>
|
|
33
|
-
);
|
|
34
|
-
}
|