@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/toggle.js
DELETED
|
@@ -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
|
-
}
|
package/src/controls/url.js
DELETED
|
@@ -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
|
-
}
|
package/src/controls/user.js
DELETED
|
@@ -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
|
-
|