@wordpress-gcb/fields 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +249 -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 +172 -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/richtext.js
DELETED
|
@@ -1,505 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RichtextField — modern alternative to the `wysiwyg` (TinyMCE) control.
|
|
3
|
-
*
|
|
4
|
-
* Built on Tiptap (ProseMirror under the hood). Lighter, no jQuery, no
|
|
5
|
-
* global state — fits the rest of the Inspector model where every
|
|
6
|
-
* control is a self-contained React component.
|
|
7
|
-
*
|
|
8
|
-
* Stored shape: HTML string (e.g. `<p>Hello <strong>world</strong></p>`).
|
|
9
|
-
* Compatible with WP's standard wp_kses() flow and any frontend that
|
|
10
|
-
* renders HTML via dangerouslySetInnerHTML.
|
|
11
|
-
*
|
|
12
|
-
* Why TWO rich-text controls? The existing `wysiwyg` control wraps
|
|
13
|
-
* wp_editor() / TinyMCE — it's what authors expect on every WordPress
|
|
14
|
-
* install and we keep it for backwards compat. `richtext` is the new
|
|
15
|
-
* default for themes that don't need TinyMCE-era plugin compatibility
|
|
16
|
-
* and want a smaller, more modern editor.
|
|
17
|
-
*
|
|
18
|
-
* Toolbar uses @wordpress/components so it visually matches the rest
|
|
19
|
-
* of our Inspector. Image button opens wp.media() and inserts an <img>
|
|
20
|
-
* referencing the existing media library — same lower-level API as
|
|
21
|
-
* MediaPicker, no @wordpress/block-editor dependency required (works
|
|
22
|
-
* on any admin screen).
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { __ } from '@wordpress/i18n';
|
|
26
|
-
import { useState, useEffect, useContext } from '@wordpress/element';
|
|
27
|
-
import {
|
|
28
|
-
Button,
|
|
29
|
-
Icon,
|
|
30
|
-
Modal,
|
|
31
|
-
Popover,
|
|
32
|
-
TextControl,
|
|
33
|
-
} from '@wordpress/components';
|
|
34
|
-
import { ControlContext } from '../control-context';
|
|
35
|
-
import PopoverOrModal from './PopoverOrModal';
|
|
36
|
-
import {
|
|
37
|
-
formatBold,
|
|
38
|
-
formatItalic,
|
|
39
|
-
formatStrikethrough,
|
|
40
|
-
formatListBullets,
|
|
41
|
-
formatListNumbered,
|
|
42
|
-
code as codeIcon,
|
|
43
|
-
quote as quoteIcon,
|
|
44
|
-
link as linkIcon,
|
|
45
|
-
image as imageIcon,
|
|
46
|
-
undo as undoIcon,
|
|
47
|
-
redo as redoIcon,
|
|
48
|
-
pencil as editIcon,
|
|
49
|
-
} from '@wordpress/icons';
|
|
50
|
-
import { useEditor, EditorContent } from '@tiptap/react';
|
|
51
|
-
import StarterKit from '@tiptap/starter-kit';
|
|
52
|
-
import Link from '@tiptap/extension-link';
|
|
53
|
-
import Image from '@tiptap/extension-image';
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Open wp.media() and resolve with the picked image. Same wp.media
|
|
57
|
-
* API our MediaPicker shim uses — no @wordpress/block-editor required.
|
|
58
|
-
*/
|
|
59
|
-
function pickImageFromMedia() {
|
|
60
|
-
return new Promise((resolve) => {
|
|
61
|
-
if (typeof window === 'undefined' || !window.wp?.media) {
|
|
62
|
-
// eslint-disable-next-line no-console
|
|
63
|
-
console.warn('gcb-lite: wp.media not loaded — call wp_enqueue_media() on this screen.');
|
|
64
|
-
resolve(null);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const frame = window.wp.media({
|
|
68
|
-
title: __('Insert image', 'gcblite'),
|
|
69
|
-
button: { text: __('Insert image', 'gcblite') },
|
|
70
|
-
library: { type: ['image'] },
|
|
71
|
-
multiple: false,
|
|
72
|
-
});
|
|
73
|
-
frame.on('select', () => {
|
|
74
|
-
const a = frame.state().get('selection').first()?.toJSON();
|
|
75
|
-
resolve(a || null);
|
|
76
|
-
});
|
|
77
|
-
frame.on('close', () => resolve(null));
|
|
78
|
-
frame.open();
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function ToolbarSeparator() {
|
|
83
|
-
return (
|
|
84
|
-
<span aria-hidden style={{
|
|
85
|
-
display: 'inline-block',
|
|
86
|
-
width: 1,
|
|
87
|
-
height: 24,
|
|
88
|
-
background: '#ddd',
|
|
89
|
-
margin: '0 4px',
|
|
90
|
-
alignSelf: 'center',
|
|
91
|
-
}} />
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Toolbar button. Uses @wordpress/icons so the toolbar visually
|
|
97
|
-
* matches the rest of WP admin (block-editor toolbar, post-meta
|
|
98
|
-
* controls, etc.). The icon prop on @wordpress/components Button
|
|
99
|
-
* renders the SVG at WP's standard 24×24.
|
|
100
|
-
*/
|
|
101
|
-
function TbButton({ icon, label, isPressed, disabled, onClick }) {
|
|
102
|
-
return (
|
|
103
|
-
<Button
|
|
104
|
-
icon={icon}
|
|
105
|
-
label={label}
|
|
106
|
-
showTooltip
|
|
107
|
-
onClick={onClick}
|
|
108
|
-
disabled={disabled}
|
|
109
|
-
aria-pressed={isPressed}
|
|
110
|
-
className="gcb-richtext-toolbar__btn"
|
|
111
|
-
/>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function LinkPopover({ editor, anchor, onClose }) {
|
|
116
|
-
const initial = editor.getAttributes('link').href || '';
|
|
117
|
-
const [url, setUrl] = useState(initial);
|
|
118
|
-
|
|
119
|
-
const apply = () => {
|
|
120
|
-
if (!url) {
|
|
121
|
-
editor.chain().focus().unsetLink().run();
|
|
122
|
-
} else {
|
|
123
|
-
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
|
124
|
-
}
|
|
125
|
-
onClose();
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<Popover anchor={anchor} onClose={onClose} placement="bottom-start">
|
|
130
|
-
<div style={{ padding: 12, minWidth: 280 }}>
|
|
131
|
-
<TextControl
|
|
132
|
-
label={__('Link URL', 'gcblite')}
|
|
133
|
-
value={url}
|
|
134
|
-
onChange={setUrl}
|
|
135
|
-
placeholder="https://example.com"
|
|
136
|
-
__nextHasNoMarginBottom
|
|
137
|
-
__next40pxDefaultSize
|
|
138
|
-
/>
|
|
139
|
-
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
|
140
|
-
<Button variant="primary" onClick={apply}>{__('Apply', 'gcblite')}</Button>
|
|
141
|
-
{initial && (
|
|
142
|
-
<Button
|
|
143
|
-
variant="tertiary"
|
|
144
|
-
onClick={() => {
|
|
145
|
-
editor.chain().focus().unsetLink().run();
|
|
146
|
-
onClose();
|
|
147
|
-
}}
|
|
148
|
-
>
|
|
149
|
-
{__('Remove', 'gcblite')}
|
|
150
|
-
</Button>
|
|
151
|
-
)}
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
</Popover>
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function Toolbar({ editor, control }) {
|
|
159
|
-
const [linkPopoverAnchor, setLinkPopoverAnchor] = useState(null);
|
|
160
|
-
|
|
161
|
-
if (!editor) return null;
|
|
162
|
-
|
|
163
|
-
const headingLevels = control.headingLevels || [2, 3, 4];
|
|
164
|
-
// Build heading select options. 'p' is paragraph (no heading).
|
|
165
|
-
const headingValue = editor.isActive('heading', { level: headingLevels[0] })
|
|
166
|
-
? `h${headingLevels[0]}`
|
|
167
|
-
: editor.isActive('heading', { level: headingLevels[1] })
|
|
168
|
-
? `h${headingLevels[1]}`
|
|
169
|
-
: editor.isActive('heading', { level: headingLevels[2] })
|
|
170
|
-
? `h${headingLevels[2]}`
|
|
171
|
-
: 'p';
|
|
172
|
-
|
|
173
|
-
const setHeading = (val) => {
|
|
174
|
-
if (val === 'p') {
|
|
175
|
-
editor.chain().focus().setParagraph().run();
|
|
176
|
-
} else {
|
|
177
|
-
const level = parseInt(val.slice(1), 10);
|
|
178
|
-
editor.chain().focus().toggleHeading({ level }).run();
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const insertImage = async () => {
|
|
183
|
-
const img = await pickImageFromMedia();
|
|
184
|
-
if (!img?.url) return;
|
|
185
|
-
editor.chain().focus().setImage({ src: img.url, alt: img.alt || '' }).run();
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
<div className="gcb-richtext-toolbar" style={{
|
|
190
|
-
display: 'flex',
|
|
191
|
-
flexWrap: 'wrap',
|
|
192
|
-
alignItems: 'center',
|
|
193
|
-
gap: 2,
|
|
194
|
-
padding: 6,
|
|
195
|
-
// Light border — the surface should feel calm. Matches the
|
|
196
|
-
// editor body below; their shared edge (toolbar-bottom +
|
|
197
|
-
// body-top) collapses to one line.
|
|
198
|
-
border: '1px solid #ddd',
|
|
199
|
-
borderBottom: '1px solid #ddd',
|
|
200
|
-
background: '#fff',
|
|
201
|
-
borderRadius: '8px 8px 0 0',
|
|
202
|
-
}}>
|
|
203
|
-
{/* Block-type select */}
|
|
204
|
-
<select
|
|
205
|
-
value={headingValue}
|
|
206
|
-
onChange={(e) => setHeading(e.target.value)}
|
|
207
|
-
className="gcb-richtext-block-select"
|
|
208
|
-
aria-label={__('Text style', 'gcblite')}
|
|
209
|
-
>
|
|
210
|
-
<option value="p">{__('Paragraph', 'gcblite')}</option>
|
|
211
|
-
{headingLevels.map((l) => (
|
|
212
|
-
<option key={l} value={`h${l}`}>{`Heading ${l}`}</option>
|
|
213
|
-
))}
|
|
214
|
-
</select>
|
|
215
|
-
|
|
216
|
-
<ToolbarSeparator />
|
|
217
|
-
|
|
218
|
-
<TbButton
|
|
219
|
-
icon={formatBold}
|
|
220
|
-
label={__('Bold', 'gcblite')}
|
|
221
|
-
isPressed={editor.isActive('bold')}
|
|
222
|
-
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
223
|
-
/>
|
|
224
|
-
<TbButton
|
|
225
|
-
icon={formatItalic}
|
|
226
|
-
label={__('Italic', 'gcblite')}
|
|
227
|
-
isPressed={editor.isActive('italic')}
|
|
228
|
-
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
229
|
-
/>
|
|
230
|
-
<TbButton
|
|
231
|
-
icon={formatStrikethrough}
|
|
232
|
-
label={__('Strikethrough', 'gcblite')}
|
|
233
|
-
isPressed={editor.isActive('strike')}
|
|
234
|
-
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
235
|
-
/>
|
|
236
|
-
<TbButton
|
|
237
|
-
icon={codeIcon}
|
|
238
|
-
label={__('Inline code', 'gcblite')}
|
|
239
|
-
isPressed={editor.isActive('code')}
|
|
240
|
-
onClick={() => editor.chain().focus().toggleCode().run()}
|
|
241
|
-
/>
|
|
242
|
-
|
|
243
|
-
<ToolbarSeparator />
|
|
244
|
-
|
|
245
|
-
<TbButton
|
|
246
|
-
icon={formatListBullets}
|
|
247
|
-
label={__('Bulleted list', 'gcblite')}
|
|
248
|
-
isPressed={editor.isActive('bulletList')}
|
|
249
|
-
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
250
|
-
/>
|
|
251
|
-
<TbButton
|
|
252
|
-
icon={formatListNumbered}
|
|
253
|
-
label={__('Numbered list', 'gcblite')}
|
|
254
|
-
isPressed={editor.isActive('orderedList')}
|
|
255
|
-
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
256
|
-
/>
|
|
257
|
-
<TbButton
|
|
258
|
-
icon={quoteIcon}
|
|
259
|
-
label={__('Blockquote', 'gcblite')}
|
|
260
|
-
isPressed={editor.isActive('blockquote')}
|
|
261
|
-
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
262
|
-
/>
|
|
263
|
-
|
|
264
|
-
<ToolbarSeparator />
|
|
265
|
-
|
|
266
|
-
<TbButton
|
|
267
|
-
icon={linkIcon}
|
|
268
|
-
label={__('Link', 'gcblite')}
|
|
269
|
-
isPressed={editor.isActive('link')}
|
|
270
|
-
onClick={(e) => setLinkPopoverAnchor(e.currentTarget)}
|
|
271
|
-
/>
|
|
272
|
-
{control.allowImages !== false && (
|
|
273
|
-
<TbButton
|
|
274
|
-
icon={imageIcon}
|
|
275
|
-
label={__('Insert image', 'gcblite')}
|
|
276
|
-
onClick={insertImage}
|
|
277
|
-
/>
|
|
278
|
-
)}
|
|
279
|
-
|
|
280
|
-
<ToolbarSeparator />
|
|
281
|
-
|
|
282
|
-
<TbButton
|
|
283
|
-
icon={undoIcon}
|
|
284
|
-
label={__('Undo', 'gcblite')}
|
|
285
|
-
onClick={() => editor.chain().focus().undo().run()}
|
|
286
|
-
disabled={!editor.can().undo()}
|
|
287
|
-
/>
|
|
288
|
-
<TbButton
|
|
289
|
-
icon={redoIcon}
|
|
290
|
-
label={__('Redo', 'gcblite')}
|
|
291
|
-
onClick={() => editor.chain().focus().redo().run()}
|
|
292
|
-
disabled={!editor.can().redo()}
|
|
293
|
-
/>
|
|
294
|
-
|
|
295
|
-
{linkPopoverAnchor && (
|
|
296
|
-
<LinkPopover
|
|
297
|
-
editor={editor}
|
|
298
|
-
anchor={linkPopoverAnchor}
|
|
299
|
-
onClose={() => setLinkPopoverAnchor(null)}
|
|
300
|
-
/>
|
|
301
|
-
)}
|
|
302
|
-
</div>
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Strip a chunk of HTML down to a plain-text preview for the
|
|
308
|
-
* collapsed button in sidebar variant. No DOMParser — we run inside
|
|
309
|
-
* the block-editor JS bundle which has access to document, but a
|
|
310
|
-
* regex-based strip is plenty for a short summary line.
|
|
311
|
-
*/
|
|
312
|
-
function htmlToPreview(html, limit = 80) {
|
|
313
|
-
if (!html) return '';
|
|
314
|
-
const text = String(html)
|
|
315
|
-
.replace(/<[^>]+>/g, ' ')
|
|
316
|
-
.replace(/ /g, ' ')
|
|
317
|
-
.replace(/\s+/g, ' ')
|
|
318
|
-
.trim();
|
|
319
|
-
if (text.length <= limit) return text;
|
|
320
|
-
return text.slice(0, limit - 1) + '…';
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* The actual Tiptap editor surface — toolbar + EditorContent. Used
|
|
325
|
-
* inline on wide surfaces (meta-box, options page) and inside a Modal
|
|
326
|
-
* on the narrow block Inspector sidebar.
|
|
327
|
-
*/
|
|
328
|
-
function RichtextEditorSurface({ editor, control }) {
|
|
329
|
-
if (!editor) return null;
|
|
330
|
-
return (
|
|
331
|
-
<>
|
|
332
|
-
<Toolbar editor={editor} control={control} />
|
|
333
|
-
<EditorContent editor={editor} className="gcb-richtext-editor" />
|
|
334
|
-
</>
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Resolve the display mode for the field. Authors can pin a specific
|
|
340
|
-
* mode via control.display ('inline' | 'popover' | 'modal'); otherwise
|
|
341
|
-
* we pick the best fit for the surface:
|
|
342
|
-
* - sidebar variant (narrow ~280px block Inspector) → 'popover' so
|
|
343
|
-
* the editor hangs off to the side, leaving the canvas visible
|
|
344
|
-
* for live preview of edits
|
|
345
|
-
* - metabox/wide variants → 'inline' (toolbar + editor fit fine)
|
|
346
|
-
*/
|
|
347
|
-
function resolveDisplay(control, ctx) {
|
|
348
|
-
if (control.display && ['inline', 'popover', 'modal'].includes(control.display)) {
|
|
349
|
-
return control.display;
|
|
350
|
-
}
|
|
351
|
-
if (ctx?.variant === 'sidebar') return 'popover';
|
|
352
|
-
return 'inline';
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export default function RichtextField({ control, value, onChange }) {
|
|
356
|
-
const ctx = useContext(ControlContext);
|
|
357
|
-
const display = resolveDisplay(control, ctx);
|
|
358
|
-
const headingLevels = control.headingLevels || [2, 3, 4];
|
|
359
|
-
|
|
360
|
-
const [modalOpen, setModalOpen] = useState(false);
|
|
361
|
-
|
|
362
|
-
const editor = useEditor({
|
|
363
|
-
extensions: [
|
|
364
|
-
StarterKit.configure({
|
|
365
|
-
// Disable headings here; we'll re-add only the levels the
|
|
366
|
-
// theme has allowed so the toolbar select stays in sync.
|
|
367
|
-
heading: { levels: headingLevels },
|
|
368
|
-
}),
|
|
369
|
-
Link.configure({
|
|
370
|
-
openOnClick: false,
|
|
371
|
-
HTMLAttributes: { rel: 'noopener noreferrer' },
|
|
372
|
-
}),
|
|
373
|
-
Image.configure({
|
|
374
|
-
HTMLAttributes: { class: 'gcb-richtext-image' },
|
|
375
|
-
}),
|
|
376
|
-
],
|
|
377
|
-
content: value || '',
|
|
378
|
-
onUpdate: ({ editor: ed }) => {
|
|
379
|
-
const html = ed.getHTML();
|
|
380
|
-
// Tiptap returns "<p></p>" for an empty document. Normalise
|
|
381
|
-
// to '' so validation's required-check still triggers on
|
|
382
|
-
// truly-empty input.
|
|
383
|
-
onChange(html === '<p></p>' ? '' : html);
|
|
384
|
-
},
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// Sync external value changes (e.g. value reset, conditional logic
|
|
388
|
-
// swap) without re-triggering onUpdate.
|
|
389
|
-
useEffect(() => {
|
|
390
|
-
if (!editor) return;
|
|
391
|
-
const current = editor.getHTML();
|
|
392
|
-
const incoming = value || '';
|
|
393
|
-
if (current === incoming) return;
|
|
394
|
-
if (current === '<p></p>' && incoming === '') return;
|
|
395
|
-
editor.commands.setContent(incoming, false);
|
|
396
|
-
}, [value, editor]);
|
|
397
|
-
|
|
398
|
-
// Popover or Modal: collapsed button + opens the editor in an
|
|
399
|
-
// overlay. Popover (default for sidebar) anchors to the side of
|
|
400
|
-
// the field, leaving the canvas visible for live-update editing.
|
|
401
|
-
// Modal is for surfaces where you want focus-mode authoring and
|
|
402
|
-
// don't mind covering the canvas.
|
|
403
|
-
if (display === 'popover' || display === 'modal') {
|
|
404
|
-
const preview = htmlToPreview(value);
|
|
405
|
-
const triggerButton = ({ onToggle, isOpen }) => (
|
|
406
|
-
<Button
|
|
407
|
-
onClick={onToggle}
|
|
408
|
-
aria-expanded={isOpen}
|
|
409
|
-
className="gcb-richtext-control__open"
|
|
410
|
-
>
|
|
411
|
-
<span className="gcb-richtext-control__open-text">
|
|
412
|
-
{preview || control.placeholder || __('Edit rich text…', 'gcblite')}
|
|
413
|
-
</span>
|
|
414
|
-
<Icon icon={editIcon} size={20} className="gcb-richtext-control__open-icon" />
|
|
415
|
-
</Button>
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
const editorPanel = ({ close, variant }) => (
|
|
419
|
-
<div
|
|
420
|
-
className={`gcb-richtext-control__panel gcb-richtext-control__panel--${variant}`}
|
|
421
|
-
>
|
|
422
|
-
<RichtextEditorSurface editor={editor} control={control} />
|
|
423
|
-
</div>
|
|
424
|
-
);
|
|
425
|
-
|
|
426
|
-
// For 'modal' mode, force PopoverOrModal into modal by mounting
|
|
427
|
-
// inside a metabox-flavoured ControlContext. (PopoverOrModal
|
|
428
|
-
// dispatches purely on ctx.variant.) Pinning it explicitly
|
|
429
|
-
// keeps the per-field opt-in honest regardless of where the
|
|
430
|
-
// control is used.
|
|
431
|
-
if (display === 'modal') {
|
|
432
|
-
return (
|
|
433
|
-
<div className="components-base-control gcb-richtext-control gcb-richtext-control--popout">
|
|
434
|
-
<div className="components-base-control__field">
|
|
435
|
-
{control.label && (
|
|
436
|
-
<label className="components-base-control__label">
|
|
437
|
-
{control.label}
|
|
438
|
-
</label>
|
|
439
|
-
)}
|
|
440
|
-
<ControlContext.Provider value={{ variant: 'metabox' }}>
|
|
441
|
-
<PopoverOrModal
|
|
442
|
-
modalTitle={control.label || __('Edit rich text', 'gcblite')}
|
|
443
|
-
modalSize="large"
|
|
444
|
-
renderToggle={triggerButton}
|
|
445
|
-
renderContent={editorPanel}
|
|
446
|
-
/>
|
|
447
|
-
</ControlContext.Provider>
|
|
448
|
-
</div>
|
|
449
|
-
{control.helpText && (
|
|
450
|
-
<p className="components-base-control__help">{control.helpText}</p>
|
|
451
|
-
)}
|
|
452
|
-
</div>
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Popover variant — used by default in sidebar. PopoverOrModal
|
|
457
|
-
// reads the surrounding variant, which is 'sidebar' here, so
|
|
458
|
-
// it'll render as a Dropdown popover anchored to the trigger.
|
|
459
|
-
return (
|
|
460
|
-
<div className="components-base-control gcb-richtext-control gcb-richtext-control--popout">
|
|
461
|
-
<div className="components-base-control__field">
|
|
462
|
-
{control.label && (
|
|
463
|
-
<label className="components-base-control__label">
|
|
464
|
-
{control.label}
|
|
465
|
-
</label>
|
|
466
|
-
)}
|
|
467
|
-
<PopoverOrModal
|
|
468
|
-
modalTitle={control.label || __('Edit rich text', 'gcblite')}
|
|
469
|
-
modalSize="large"
|
|
470
|
-
dropdownProps={{
|
|
471
|
-
popoverProps: {
|
|
472
|
-
placement: 'left-start',
|
|
473
|
-
className: 'gcb-richtext-control__popover',
|
|
474
|
-
},
|
|
475
|
-
}}
|
|
476
|
-
renderToggle={triggerButton}
|
|
477
|
-
renderContent={editorPanel}
|
|
478
|
-
/>
|
|
479
|
-
</div>
|
|
480
|
-
{control.helpText && (
|
|
481
|
-
<p className="components-base-control__help">{control.helpText}</p>
|
|
482
|
-
)}
|
|
483
|
-
</div>
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Inline variant (default on wide surfaces: meta-box, options,
|
|
488
|
-
// taxonomy, user-profile). Toolbar + editor render directly into
|
|
489
|
-
// the field — no overlay.
|
|
490
|
-
return (
|
|
491
|
-
<div className="components-base-control gcb-richtext-control">
|
|
492
|
-
<div className="components-base-control__field">
|
|
493
|
-
{control.label && (
|
|
494
|
-
<label className="components-base-control__label">
|
|
495
|
-
{control.label}
|
|
496
|
-
</label>
|
|
497
|
-
)}
|
|
498
|
-
<RichtextEditorSurface editor={editor} control={control} />
|
|
499
|
-
</div>
|
|
500
|
-
{control.helpText && (
|
|
501
|
-
<p className="components-base-control__help">{control.helpText}</p>
|
|
502
|
-
)}
|
|
503
|
-
</div>
|
|
504
|
-
);
|
|
505
|
-
}
|
package/src/controls/select.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SelectField — ported from the original GCB SelectField.js verbatim.
|
|
3
|
-
*
|
|
4
|
-
* Supports plain {label, value} options, legacy `map` configs, and full
|
|
5
|
-
* `tokenGroup` binding to theme.json tokens (with optional tokenKeys filter
|
|
6
|
-
* and defaultOptionKey). When tokenGroup is set, the saved value is the
|
|
7
|
-
* full token object { key, slug, value, cssVar } rather than a string.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { __ } from '@wordpress/i18n';
|
|
11
|
-
import { SelectControl, Spinner } from '@wordpress/components';
|
|
12
|
-
import { parseMap, getTokenFromKey, getKeyFromToken, mapToOptions } from '../utils/map-utils';
|
|
13
|
-
import { useTokens, getTokensByGroup, generateMapFromTokens } from '../hooks/useTokens';
|
|
14
|
-
|
|
15
|
-
function SelectFieldImpl({
|
|
16
|
-
label,
|
|
17
|
-
value,
|
|
18
|
-
options = [],
|
|
19
|
-
onChange,
|
|
20
|
-
help,
|
|
21
|
-
className = '',
|
|
22
|
-
map,
|
|
23
|
-
tokenGroup,
|
|
24
|
-
tokenKeys,
|
|
25
|
-
defaultOptionKey,
|
|
26
|
-
placeholder,
|
|
27
|
-
}) {
|
|
28
|
-
const { tokens, loading } = useTokens();
|
|
29
|
-
|
|
30
|
-
let groupTokens = null;
|
|
31
|
-
let normalizedMap = null;
|
|
32
|
-
|
|
33
|
-
if (tokenGroup && tokens) {
|
|
34
|
-
groupTokens = getTokensByGroup(tokens, tokenGroup);
|
|
35
|
-
if (groupTokens && tokenKeys && Array.isArray(tokenKeys) && tokenKeys.length > 0) {
|
|
36
|
-
groupTokens = groupTokens.filter((t) => tokenKeys.includes(t.key));
|
|
37
|
-
}
|
|
38
|
-
if (groupTokens) {
|
|
39
|
-
const generatedMap = generateMapFromTokens(groupTokens);
|
|
40
|
-
normalizedMap = generatedMap ? parseMap(generatedMap) : null;
|
|
41
|
-
}
|
|
42
|
-
} else if (map) {
|
|
43
|
-
normalizedMap = parseMap(map);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (tokenGroup && loading) {
|
|
47
|
-
return (
|
|
48
|
-
<div style={{ padding: '12px 0' }}>
|
|
49
|
-
<div style={{ fontWeight: 500, marginBottom: '8px' }}>{label}</div>
|
|
50
|
-
<Spinner />
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let effectiveOptions = normalizedMap ? mapToOptions(normalizedMap) : options;
|
|
56
|
-
|
|
57
|
-
// Add a clearing/placeholder option if no default is set, so users can pick blank.
|
|
58
|
-
if (!defaultOptionKey && !effectiveOptions.some((o) => o.value === '')) {
|
|
59
|
-
effectiveOptions = [
|
|
60
|
-
{ label: '— ' + (placeholder || __('Select', 'gcblite')) + ' —', value: '' },
|
|
61
|
-
...effectiveOptions,
|
|
62
|
-
];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Apply default if value is empty.
|
|
66
|
-
if ((value === undefined || value === null || value === '') && defaultOptionKey && tokenGroup && groupTokens) {
|
|
67
|
-
const defaultToken = groupTokens.find((t) => t.key === defaultOptionKey);
|
|
68
|
-
if (defaultToken && onChange) {
|
|
69
|
-
const defaultValue = {
|
|
70
|
-
key: defaultToken.key,
|
|
71
|
-
slug: defaultToken.slug,
|
|
72
|
-
value: defaultToken.value,
|
|
73
|
-
cssVar: defaultToken.cssVar,
|
|
74
|
-
};
|
|
75
|
-
onChange(defaultValue);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let displayValue = value;
|
|
80
|
-
if (typeof value === 'object' && value !== null) {
|
|
81
|
-
displayValue = value.key !== undefined ? value.key : '';
|
|
82
|
-
} else if (normalizedMap && value !== undefined && value !== null && value !== '') {
|
|
83
|
-
displayValue = getKeyFromToken(normalizedMap, value) || value;
|
|
84
|
-
}
|
|
85
|
-
if ((displayValue === undefined || displayValue === null || displayValue === '') && defaultOptionKey) {
|
|
86
|
-
displayValue = defaultOptionKey;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const handleChange = (newOptionKey) => {
|
|
90
|
-
let finalValue = newOptionKey;
|
|
91
|
-
|
|
92
|
-
if (tokenGroup && groupTokens) {
|
|
93
|
-
const selected = groupTokens.find((t) => t.key === newOptionKey);
|
|
94
|
-
if (selected) {
|
|
95
|
-
finalValue = {
|
|
96
|
-
key: selected.key,
|
|
97
|
-
slug: selected.slug,
|
|
98
|
-
value: selected.value,
|
|
99
|
-
cssVar: selected.cssVar,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
} else if (normalizedMap) {
|
|
103
|
-
finalValue = getTokenFromKey(normalizedMap, newOptionKey);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (onChange) onChange(finalValue);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<SelectControl
|
|
111
|
-
label={label}
|
|
112
|
-
value={displayValue ?? ''}
|
|
113
|
-
options={effectiveOptions}
|
|
114
|
-
onChange={handleChange}
|
|
115
|
-
help={help}
|
|
116
|
-
className={className}
|
|
117
|
-
__nextHasNoMarginBottom
|
|
118
|
-
/>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Adapter: maps the registry's `{ control, value, onChange }` contract onto
|
|
124
|
-
* SelectFieldImpl's flat-prop contract.
|
|
125
|
-
*/
|
|
126
|
-
export default function SelectField({ control, value, onChange }) {
|
|
127
|
-
return (
|
|
128
|
-
<SelectFieldImpl
|
|
129
|
-
label={control.label}
|
|
130
|
-
value={value}
|
|
131
|
-
options={(control.options || []).map((o) => ({ label: o.label, value: o.value }))}
|
|
132
|
-
onChange={onChange}
|
|
133
|
-
help={control.helpText}
|
|
134
|
-
placeholder={control.placeholder}
|
|
135
|
-
map={control.map}
|
|
136
|
-
tokenGroup={control.tokenGroup}
|
|
137
|
-
tokenKeys={control.tokenKeys}
|
|
138
|
-
defaultOptionKey={control.defaultOptionKey ?? control.default}
|
|
139
|
-
/>
|
|
140
|
-
);
|
|
141
|
-
}
|
package/src/controls/size.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Size — dimension with a unit selector.
|
|
3
|
-
*
|
|
4
|
-
* Stored shape: a CSS length string with unit, e.g. `'16px'`, `'1.5rem'`,
|
|
5
|
-
* `'100%'`. Empty string when not set.
|
|
6
|
-
*
|
|
7
|
-
* Supported units: px, em, rem, %, vh, vw.
|
|
8
|
-
*
|
|
9
|
-
* In a React component you can pass the value directly to `style.width`,
|
|
10
|
-
* `style.height`, `style.padding`, etc. — it's already a valid CSS length.
|
|
11
|
-
*/
|
|
12
|
-
import { __experimentalUnitControl as UnitControl, TextControl } from '@wordpress/components';
|
|
13
|
-
import { __ } from '@wordpress/i18n';
|
|
14
|
-
|
|
15
|
-
const UNITS = [
|
|
16
|
-
{ value: 'px', label: 'px' },
|
|
17
|
-
{ value: 'em', label: 'em' },
|
|
18
|
-
{ value: 'rem', label: 'rem' },
|
|
19
|
-
{ value: '%', label: '%' },
|
|
20
|
-
{ value: 'vh', label: 'vh' },
|
|
21
|
-
{ value: 'vw', label: 'vw' },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export default function SizeField({ control, value, onChange }) {
|
|
25
|
-
if (UnitControl) {
|
|
26
|
-
return (
|
|
27
|
-
<UnitControl
|
|
28
|
-
label={control.label}
|
|
29
|
-
value={value || ''}
|
|
30
|
-
onChange={onChange}
|
|
31
|
-
units={UNITS}
|
|
32
|
-
help={control.helpText}
|
|
33
|
-
__nextHasNoMarginBottom
|
|
34
|
-
/>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Fallback when UnitControl isn't available — manual entry.
|
|
39
|
-
return (
|
|
40
|
-
<TextControl
|
|
41
|
-
label={control.label}
|
|
42
|
-
value={value || ''}
|
|
43
|
-
onChange={onChange}
|
|
44
|
-
placeholder="e.g. 16px, 1em, 100%"
|
|
45
|
-
help={control.helpText || __('Enter a size with unit (px, em, rem, %, vh, vw)', 'gcblite')}
|
|
46
|
-
__nextHasNoMarginBottom
|
|
47
|
-
/>
|
|
48
|
-
);
|
|
49
|
-
}
|