@wordpress-gcb/fields 0.2.0 → 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/README.md +51 -35
- 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 +35 -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/README.md
CHANGED
|
@@ -1,49 +1,65 @@
|
|
|
1
1
|
# @wordpress-gcb/fields
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
conditional-logic
|
|
6
|
-
|
|
3
|
+
Typed-field UI for the WordPress block editor: control components, an inspector
|
|
4
|
+
renderer that turns a `block.fields.json` schema into a settings panel, plus
|
|
5
|
+
conditional-logic and validation helpers — **decoupled from any plugin**. Host
|
|
6
|
+
data (design tokens, media picker, etc.) is injected, so the same controls run
|
|
7
|
+
inside the [GCB Lite plugin](https://github.com/wordpress-gcb/gutenberg-control-blocks-lite),
|
|
8
|
+
a meta box, or a headless editor.
|
|
7
9
|
|
|
8
|
-
This is the free, open core of GCB.
|
|
9
|
-
|
|
10
|
-
project, and `gcb-pro/docs/eject-blocks-scope.md` for the architecture rationale.
|
|
10
|
+
This is the free, open core of **[GCB](https://gutenbergcontrolblocks.com)** —
|
|
11
|
+
[Gutenberg Control Blocks](https://gutenbergcontrolblocks.com).
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
defined (`src/index.js`); the control source is extracted from the plugin in a
|
|
14
|
-
later step — see `EXTRACTION.md`.
|
|
13
|
+
## Install
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm install @wordpress-gcb/fields
|
|
17
|
+
```
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
it into `@wordpress-gcb/fields` means:
|
|
19
|
+
`@wordpress/*` are peer dependencies — provided by the WordPress editor runtime
|
|
20
|
+
(or your build's externals). `@dnd-kit/*` and `@tiptap/*` ship bundled.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
- "eject" can scaffold a block that uses the SDK and works plugin-free;
|
|
24
|
-
- graceful degradation: static `block.json` is the floor, the SDK restores live UI.
|
|
22
|
+
## Usage
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
```jsx
|
|
25
|
+
import { GcbFieldsProvider, renderInspector } from '@wordpress-gcb/fields';
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
// controls = the `controls` array from a block.fields.json
|
|
28
|
+
<GcbFieldsProvider value={{ tokens, googleMapsEnabled: true }}>
|
|
29
|
+
{renderInspector(controls, attributes, setAttributes)}
|
|
30
|
+
</GcbFieldsProvider>
|
|
31
|
+
```
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
| `controls` (arg to `renderInspector`) | `window.gcbLite.blocks[name].controls` | the fields to render |
|
|
33
|
-
| `tokens` | `window.gcbLite.tokens` | token-aware controls (select, range, spacing) |
|
|
34
|
-
| `googleMapsEnabled` | `window.gcbLite.googleMaps.hasApiKey` | the google-map control |
|
|
35
|
-
| `media` adapter | `wp.media` / block-editor `MediaUpload` | image / gallery / file / richtext |
|
|
36
|
-
| `apiFetch` (optional) | `@wordpress/api-fetch` | reference controls — **core `wp/v2` only** |
|
|
37
|
-
| `variant` | `ControlContext` | `'sidebar'` (popovers) vs `'metabox'` (modals) |
|
|
33
|
+
Each control receives `{ control, value, onChange, attributes }`. The provider
|
|
34
|
+
supplies what the controls need from the host:
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
| Inject | For |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `tokens` | token-aware controls (select, range, spacing) |
|
|
39
|
+
| `googleMapsEnabled` | the google-map control |
|
|
40
|
+
| `media` | media picker adapter (image / gallery / file / richtext) |
|
|
41
|
+
| `apiFetch` | reference controls — calls core `wp/v2` (optional; defaults to `@wordpress/api-fetch`) |
|
|
42
|
+
| `variant` | `'sidebar'` (popovers) vs `'metabox'` (modals) |
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
PHP-preview pipeline, focus-field handling, the admin builder. The `repeater`
|
|
44
|
-
**control** (form-of-forms field) IS included — don't confuse the two.
|
|
44
|
+
All are optional; with none provided the package falls back to the WordPress
|
|
45
|
+
editor globals.
|
|
45
46
|
|
|
46
|
-
##
|
|
47
|
+
## Exports
|
|
47
48
|
|
|
48
|
-
-
|
|
49
|
-
-
|
|
49
|
+
- `renderInspector(controls, attributes, setAttributes, options?)`
|
|
50
|
+
- `controlComponents` — the control registry (`type` → component)
|
|
51
|
+
- `GcbFieldsProvider`, `useGcbFieldsConfig`
|
|
52
|
+
- `shouldRender`, `panelsContainingErrors`, `STRUCTURAL_TYPES`
|
|
53
|
+
- `ValidationContext`, `ControlContext`
|
|
54
|
+
- token helpers: `useTokens`, `getTokensByGroup`, `getAllTokenGroups`
|
|
55
|
+
|
|
56
|
+
## Links
|
|
57
|
+
|
|
58
|
+
- **Website:** <https://gutenbergcontrolblocks.com>
|
|
59
|
+
- **Source:** <https://github.com/wordpress-gcb/gcb-fields>
|
|
60
|
+
- **Issues:** <https://github.com/wordpress-gcb/gcb-fields/issues>
|
|
61
|
+
- **GCB Lite plugin:** <https://github.com/wordpress-gcb/gutenberg-control-blocks-lite>
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
GPL-2.0-or-later
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional logic — pure functions, no React, no @wordpress/* deps.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from inspector.js so unit tests can import without
|
|
5
|
+
* pulling in the whole control library. Keep in sync with PHP mirror:
|
|
6
|
+
* includes/PostFields/Conditional.php
|
|
7
|
+
*
|
|
8
|
+
* Config shape on a control:
|
|
9
|
+
*
|
|
10
|
+
* {
|
|
11
|
+
* conditionalLogic: {
|
|
12
|
+
* enabled: true,
|
|
13
|
+
* operator: 'and' | 'or', // default: 'and'
|
|
14
|
+
* rules: [
|
|
15
|
+
* { field: 'show_cta', operator: '==', value: true },
|
|
16
|
+
* { field: 'count', operator: '>', value: 0 },
|
|
17
|
+
* ],
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Structural control types (group, panel, tools-panel) render as panel
|
|
23
|
+
// containers, not as fields. Exported so callers that walk the control
|
|
24
|
+
// list can skip them consistently.
|
|
25
|
+
export const STRUCTURAL_TYPES = new Set(['group', 'panel', 'tools-panel']);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Decide whether a control should render against the current attribute
|
|
29
|
+
* values. A control with no `conditionalLogic` (or a disabled one) is
|
|
30
|
+
* always rendered.
|
|
31
|
+
*/
|
|
32
|
+
export function shouldRender(control, attributes) {
|
|
33
|
+
const cl = control.conditionalLogic;
|
|
34
|
+
if (!cl?.enabled || !Array.isArray(cl.rules) || cl.rules.length === 0) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const op = cl.operator === 'or' ? 'or' : 'and';
|
|
38
|
+
const results = cl.rules.map(rule => evalRule(rule, attributes));
|
|
39
|
+
return op === 'or' ? results.some(Boolean) : results.every(Boolean);
|
|
40
|
+
}
|
|
41
|
+
function evalRule(rule, attributes) {
|
|
42
|
+
const actual = attributes[rule.field];
|
|
43
|
+
switch (rule.operator) {
|
|
44
|
+
case '==':
|
|
45
|
+
return actual == rule.value;
|
|
46
|
+
// eslint-disable-line eqeqeq
|
|
47
|
+
case '!=':
|
|
48
|
+
return actual != rule.value;
|
|
49
|
+
// eslint-disable-line eqeqeq
|
|
50
|
+
case '>':
|
|
51
|
+
return Number.isFinite(Number(actual)) && Number.isFinite(Number(rule.value)) && Number(actual) > Number(rule.value);
|
|
52
|
+
case '<':
|
|
53
|
+
return Number.isFinite(Number(actual)) && Number.isFinite(Number(rule.value)) && Number(actual) < Number(rule.value);
|
|
54
|
+
case '>=':
|
|
55
|
+
return Number.isFinite(Number(actual)) && Number.isFinite(Number(rule.value)) && Number(actual) >= Number(rule.value);
|
|
56
|
+
case '<=':
|
|
57
|
+
return Number.isFinite(Number(actual)) && Number.isFinite(Number(rule.value)) && Number(actual) <= Number(rule.value);
|
|
58
|
+
case 'contains':
|
|
59
|
+
return typeof actual === 'string' && actual.includes(rule.value);
|
|
60
|
+
case 'in':
|
|
61
|
+
return Array.isArray(rule.value) && rule.value.includes(actual);
|
|
62
|
+
default:
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Walk controls and return the set of panel ids that contain at least one
|
|
69
|
+
* control whose attributeKey appears in `errors`. Used by the meta-box to
|
|
70
|
+
* auto-open panels that have validation errors.
|
|
71
|
+
*/
|
|
72
|
+
export function panelsContainingErrors(controls, errors) {
|
|
73
|
+
const errorKeys = new Set(Object.keys(errors || {}));
|
|
74
|
+
if (errorKeys.size === 0) return new Set();
|
|
75
|
+
const ids = new Set();
|
|
76
|
+
for (const c of controls) {
|
|
77
|
+
if (STRUCTURAL_TYPES.has(c.type)) continue;
|
|
78
|
+
if (c.attributeKey && errorKeys.has(c.attributeKey) && c.parentPanelId) {
|
|
79
|
+
ids.add(c.parentPanelId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return ids;
|
|
83
|
+
}
|
|
@@ -23,11 +23,15 @@
|
|
|
23
23
|
import { useContext } from '@wordpress/element';
|
|
24
24
|
import { MediaUploadCheck } from '@wordpress/block-editor';
|
|
25
25
|
import { ControlContext } from '../control-context';
|
|
26
|
-
|
|
27
|
-
export default function MediaCapabilityGate({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
26
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
27
|
+
export default function MediaCapabilityGate({
|
|
28
|
+
children
|
|
29
|
+
}) {
|
|
30
|
+
const ctx = useContext(ControlContext);
|
|
31
|
+
if (ctx.variant === 'metabox') {
|
|
32
|
+
return children;
|
|
33
|
+
}
|
|
34
|
+
return /*#__PURE__*/_jsx(MediaUploadCheck, {
|
|
35
|
+
children: children
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaPicker — a `MediaUpload`-shaped component that works on any
|
|
3
|
+
* admin screen.
|
|
4
|
+
*
|
|
5
|
+
* `@wordpress/block-editor`'s `MediaUpload` requires the `core/block-editor`
|
|
6
|
+
* data store, which is only initialised on screens that bootstrap the
|
|
7
|
+
* block editor itself (post.php, site-editor.php, widgets.php). On
|
|
8
|
+
* profile.php, term.php, options.php — anywhere we mount our post-fields
|
|
9
|
+
* bundle on a classic admin page — the store isn't there, so the
|
|
10
|
+
* MediaUpload modal silently fails to open and the image / gallery /
|
|
11
|
+
* file controls feel broken.
|
|
12
|
+
*
|
|
13
|
+
* This wrapper falls back to the lower-level `wp.media()` JS API on
|
|
14
|
+
* non-editor screens. wp.media is what WP's own avatar uploader uses
|
|
15
|
+
* and is loaded by wp_enqueue_media() — available on every admin page
|
|
16
|
+
* we care about. It's not React; we wrap it in the same render-prop
|
|
17
|
+
* shape MediaUpload exposes so callers don't need to know which path
|
|
18
|
+
* they're on.
|
|
19
|
+
*
|
|
20
|
+
* Shape:
|
|
21
|
+
* <MediaPicker
|
|
22
|
+
* onSelect={(media) => …} // single media object (or array)
|
|
23
|
+
* allowedTypes={['image']}
|
|
24
|
+
* value={123 | [123,124] | null}
|
|
25
|
+
* multiple={false}
|
|
26
|
+
* gallery={false}
|
|
27
|
+
* render={({ open }) => <button onClick={open}>Pick</button>}
|
|
28
|
+
* />
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { useContext } from '@wordpress/element';
|
|
32
|
+
import { MediaUpload } from '@wordpress/block-editor';
|
|
33
|
+
import { ControlContext } from '../control-context';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Translate a wp.media attachment model into the same plain object
|
|
37
|
+
* @wordpress/block-editor's MediaUpload onSelect receives.
|
|
38
|
+
*/
|
|
39
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
40
|
+
function toPlainAttachment(attachment) {
|
|
41
|
+
const a = attachment.toJSON ? attachment.toJSON() : attachment;
|
|
42
|
+
return {
|
|
43
|
+
id: a.id,
|
|
44
|
+
url: a.url,
|
|
45
|
+
alt: a.alt || '',
|
|
46
|
+
title: a.title || a.filename || '',
|
|
47
|
+
filename: a.filename || '',
|
|
48
|
+
caption: a.caption || '',
|
|
49
|
+
description: a.description || '',
|
|
50
|
+
mime: a.mime || a.type || '',
|
|
51
|
+
type: a.type || '',
|
|
52
|
+
subtype: a.subtype || '',
|
|
53
|
+
width: a.width,
|
|
54
|
+
height: a.height,
|
|
55
|
+
filesizeInBytes: a.filesizeInBytes,
|
|
56
|
+
sizes: a.sizes
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function openClassicMediaFrame({
|
|
60
|
+
allowedTypes,
|
|
61
|
+
multiple,
|
|
62
|
+
gallery,
|
|
63
|
+
value,
|
|
64
|
+
onSelect
|
|
65
|
+
}) {
|
|
66
|
+
if (typeof window === 'undefined' || !window.wp || !window.wp.media) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.warn('gcb-lite: wp.media not loaded — call wp_enqueue_media() on this screen.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// IMPORTANT: don't open directly into 'gallery-edit' — WP 7.0's
|
|
73
|
+
// media-views.js bootstraps menu/router state lazily, and asking for
|
|
74
|
+
// 'gallery-edit' before the menu state exists throws
|
|
75
|
+
// `Cannot read properties of undefined (reading 'get')` in
|
|
76
|
+
// setMenuTabPanelAriaAttributes. The supported gallery flow is to open
|
|
77
|
+
// the library with `multiple: true`, pre-select the existing images,
|
|
78
|
+
// and treat the result as a gallery on 'select'.
|
|
79
|
+
const isMulti = !!(gallery || multiple);
|
|
80
|
+
const frame = window.wp.media({
|
|
81
|
+
title: gallery ? 'Edit gallery' : isMulti ? 'Select media' : 'Select media',
|
|
82
|
+
button: {
|
|
83
|
+
text: gallery ? 'Update gallery' : 'Use this media'
|
|
84
|
+
},
|
|
85
|
+
library: allowedTypes ? {
|
|
86
|
+
type: allowedTypes
|
|
87
|
+
} : undefined,
|
|
88
|
+
multiple: isMulti
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Pre-select prior value so the user lands on the existing pick.
|
|
92
|
+
frame.on('open', () => {
|
|
93
|
+
const selection = frame.state().get('selection');
|
|
94
|
+
if (!selection) return;
|
|
95
|
+
const ids = Array.isArray(value) ? value : value ? [value] : [];
|
|
96
|
+
ids.forEach(id => {
|
|
97
|
+
if (!id) return;
|
|
98
|
+
const attachment = window.wp.media.attachment(id);
|
|
99
|
+
if (!attachment) return;
|
|
100
|
+
attachment.fetch();
|
|
101
|
+
selection.add([attachment]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
frame.on('select', () => {
|
|
105
|
+
const selection = frame.state().get('selection');
|
|
106
|
+
if (!selection) return;
|
|
107
|
+
const picks = selection.toArray().map(toPlainAttachment);
|
|
108
|
+
if (isMulti) {
|
|
109
|
+
onSelect(picks);
|
|
110
|
+
} else {
|
|
111
|
+
onSelect(picks[0] || null);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
frame.open();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Decide which backend to use based on render surface:
|
|
119
|
+
*
|
|
120
|
+
* - Block Inspector ('sidebar' variant): MediaUpload from
|
|
121
|
+
* @wordpress/block-editor. That's the right tool inside the block
|
|
122
|
+
* editor — it integrates with the editor's upload flow + capability
|
|
123
|
+
* store.
|
|
124
|
+
*
|
|
125
|
+
* - Post-fields meta-box / Options / Taxonomy / User pages
|
|
126
|
+
* ('metabox' variant): wp.media() directly. These screens often DON'T
|
|
127
|
+
* bootstrap the block editor (we strip 'editor' support from
|
|
128
|
+
* field-only CPTs), and MediaUpload silently renders nothing when the
|
|
129
|
+
* editor stores aren't fully initialised. wp.media() is the lower-
|
|
130
|
+
* level JS API loaded by wp_enqueue_media() — works on any admin page.
|
|
131
|
+
*
|
|
132
|
+
* Surface decision is made via ControlContext, which post-fields.js
|
|
133
|
+
* sets to 'metabox' and the block Inspector leaves at 'sidebar'.
|
|
134
|
+
*/
|
|
135
|
+
export default function MediaPicker(props) {
|
|
136
|
+
const ctx = useContext(ControlContext);
|
|
137
|
+
if (ctx.variant !== 'metabox') {
|
|
138
|
+
return /*#__PURE__*/_jsx(MediaUpload, {
|
|
139
|
+
...props
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Metabox surface — synthesize the same render-prop shape with a
|
|
144
|
+
// wp.media() backend.
|
|
145
|
+
const open = () => openClassicMediaFrame(props);
|
|
146
|
+
return props.render ? props.render({
|
|
147
|
+
open
|
|
148
|
+
}) : null;
|
|
149
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaTriggerBadges — places an inline clear (×) button to the right of
|
|
3
|
+
* a media-control toggle. Used by ImageField and FileField on the metabox
|
|
4
|
+
* surface. Edit affordance isn't needed — clicking the trigger itself
|
|
5
|
+
* opens the picker/popover.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { __ } from '@wordpress/i18n';
|
|
9
|
+
import { Button } from '@wordpress/components';
|
|
10
|
+
|
|
11
|
+
// Default sizing is content-width (inline-flex). In sidebar / Inspector
|
|
12
|
+
// contexts an external rule promotes it to full-width — see editor.scss
|
|
13
|
+
// "Sidebar / Inspector surface: media trigger fills width".
|
|
14
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
15
|
+
export default function MediaTriggerBadges({
|
|
16
|
+
children,
|
|
17
|
+
onClear,
|
|
18
|
+
hideClear
|
|
19
|
+
}) {
|
|
20
|
+
return /*#__PURE__*/_jsxs("span", {
|
|
21
|
+
className: "gcb-media-trigger-badges",
|
|
22
|
+
children: [children, !hideClear && /*#__PURE__*/_jsx(Button, {
|
|
23
|
+
variant: "tertiary",
|
|
24
|
+
isDestructive: true,
|
|
25
|
+
onClick: e => {
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
onClear?.();
|
|
29
|
+
},
|
|
30
|
+
"aria-label": __('Clear', 'gcblite'),
|
|
31
|
+
title: __('Clear', 'gcblite'),
|
|
32
|
+
children: "\u2715"
|
|
33
|
+
})]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -35,49 +35,55 @@
|
|
|
35
35
|
import { useContext, useState } from '@wordpress/element';
|
|
36
36
|
import { Dropdown, Modal } from '@wordpress/components';
|
|
37
37
|
import { ControlContext } from '../control-context';
|
|
38
|
-
|
|
38
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
39
39
|
export default function PopoverOrModal({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
renderToggle,
|
|
41
|
+
renderContent,
|
|
42
|
+
modalTitle,
|
|
43
|
+
modalSize = 'medium',
|
|
44
|
+
dropdownProps = {}
|
|
45
45
|
}) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function ModalAffordance({ renderToggle, renderContent, modalTitle, modalSize }) {
|
|
69
|
-
const [open, setOpen] = useState(false);
|
|
70
|
-
const onToggle = () => setOpen((v) => !v);
|
|
71
|
-
const close = () => setOpen(false);
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<>
|
|
75
|
-
{renderToggle({ onToggle, isOpen: open })}
|
|
76
|
-
{open && (
|
|
77
|
-
<Modal title={modalTitle} onRequestClose={close} size={modalSize}>
|
|
78
|
-
{renderContent({ close, variant: 'modal' })}
|
|
79
|
-
</Modal>
|
|
80
|
-
)}
|
|
81
|
-
</>
|
|
82
|
-
);
|
|
46
|
+
const ctx = useContext(ControlContext);
|
|
47
|
+
if (ctx.variant === 'metabox') {
|
|
48
|
+
return /*#__PURE__*/_jsx(ModalAffordance, {
|
|
49
|
+
renderToggle: renderToggle,
|
|
50
|
+
renderContent: renderContent,
|
|
51
|
+
modalTitle: modalTitle,
|
|
52
|
+
modalSize: modalSize
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return /*#__PURE__*/_jsx(Dropdown, {
|
|
56
|
+
...dropdownProps,
|
|
57
|
+
renderToggle: renderToggle,
|
|
58
|
+
renderContent: ({
|
|
59
|
+
onClose
|
|
60
|
+
}) => renderContent({
|
|
61
|
+
close: onClose,
|
|
62
|
+
variant: 'popover'
|
|
63
|
+
})
|
|
64
|
+
});
|
|
83
65
|
}
|
|
66
|
+
function ModalAffordance({
|
|
67
|
+
renderToggle,
|
|
68
|
+
renderContent,
|
|
69
|
+
modalTitle,
|
|
70
|
+
modalSize
|
|
71
|
+
}) {
|
|
72
|
+
const [open, setOpen] = useState(false);
|
|
73
|
+
const onToggle = () => setOpen(v => !v);
|
|
74
|
+
const close = () => setOpen(false);
|
|
75
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
76
|
+
children: [renderToggle({
|
|
77
|
+
onToggle,
|
|
78
|
+
isOpen: open
|
|
79
|
+
}), open && /*#__PURE__*/_jsx(Modal, {
|
|
80
|
+
title: modalTitle,
|
|
81
|
+
onRequestClose: close,
|
|
82
|
+
size: modalSize,
|
|
83
|
+
children: renderContent({
|
|
84
|
+
close,
|
|
85
|
+
variant: 'modal'
|
|
86
|
+
})
|
|
87
|
+
})]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sortable item — ported from the original GCB SortableItemComponent.
|
|
3
|
+
*
|
|
4
|
+
* Used by gallery, post-object (and any other future multi-select reorderable
|
|
5
|
+
* field) to render a draggable row with handle, title, and remove button.
|
|
6
|
+
*/
|
|
7
|
+
import { useSortable } from '@dnd-kit/sortable';
|
|
8
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
9
|
+
import { __ } from '@wordpress/i18n';
|
|
10
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
const ICONS = {
|
|
12
|
+
post: {
|
|
13
|
+
viewBox: '0 0 24 24',
|
|
14
|
+
path: /*#__PURE__*/_jsxs(_Fragment, {
|
|
15
|
+
children: [/*#__PURE__*/_jsx("path", {
|
|
16
|
+
d: "M15.5 7.5h-7V9h7V7.5Zm-7 3.5h7v1.5h-7V11Zm7 3.5h-7V16h7v-1.5Z"
|
|
17
|
+
}), /*#__PURE__*/_jsx("path", {
|
|
18
|
+
d: "M17 4H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM7 5.5h10a.5.5 0 0 1 .5.5v12a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5Z"
|
|
19
|
+
})]
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
tag: {
|
|
23
|
+
viewBox: '0 0 24 24',
|
|
24
|
+
path: /*#__PURE__*/_jsx("path", {
|
|
25
|
+
d: "M12.5 4.5h-2v7h7v-2l-5-5zm-2.5 8.5v5h2v-5h-2zm5 5h2v-5h-2v5z"
|
|
26
|
+
})
|
|
27
|
+
},
|
|
28
|
+
image: {
|
|
29
|
+
viewBox: '0 0 24 24',
|
|
30
|
+
path: /*#__PURE__*/_jsx("path", {
|
|
31
|
+
d: "M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z"
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export default function SortableItem({
|
|
36
|
+
item,
|
|
37
|
+
onRemove,
|
|
38
|
+
icon = 'post',
|
|
39
|
+
getTitle,
|
|
40
|
+
thumb
|
|
41
|
+
}) {
|
|
42
|
+
const {
|
|
43
|
+
attributes,
|
|
44
|
+
listeners,
|
|
45
|
+
setNodeRef,
|
|
46
|
+
transform,
|
|
47
|
+
transition,
|
|
48
|
+
isDragging
|
|
49
|
+
} = useSortable({
|
|
50
|
+
id: item.id
|
|
51
|
+
});
|
|
52
|
+
const style = {
|
|
53
|
+
transform: CSS.Transform.toString(transform),
|
|
54
|
+
transition,
|
|
55
|
+
opacity: isDragging ? 0.5 : 1
|
|
56
|
+
};
|
|
57
|
+
const selectedIcon = ICONS[icon] || ICONS.post;
|
|
58
|
+
return /*#__PURE__*/_jsxs("div", {
|
|
59
|
+
ref: setNodeRef,
|
|
60
|
+
style: style,
|
|
61
|
+
className: "gcb-sortable-item",
|
|
62
|
+
...attributes,
|
|
63
|
+
...listeners,
|
|
64
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
65
|
+
className: "gcb-sortable-drag-handle",
|
|
66
|
+
"aria-hidden": true,
|
|
67
|
+
children: /*#__PURE__*/_jsx("svg", {
|
|
68
|
+
viewBox: "0 0 20 20",
|
|
69
|
+
width: "12",
|
|
70
|
+
children: /*#__PURE__*/_jsx("path", {
|
|
71
|
+
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"
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
}), thumb ? /*#__PURE__*/_jsx("img", {
|
|
75
|
+
src: thumb,
|
|
76
|
+
alt: "",
|
|
77
|
+
style: {
|
|
78
|
+
width: 28,
|
|
79
|
+
height: 28,
|
|
80
|
+
borderRadius: 2,
|
|
81
|
+
objectFit: 'cover',
|
|
82
|
+
marginRight: 8,
|
|
83
|
+
flexShrink: 0,
|
|
84
|
+
border: '1px solid #ddd'
|
|
85
|
+
}
|
|
86
|
+
}) : /*#__PURE__*/_jsx("svg", {
|
|
87
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
88
|
+
viewBox: selectedIcon.viewBox,
|
|
89
|
+
width: "20",
|
|
90
|
+
height: "20",
|
|
91
|
+
style: {
|
|
92
|
+
marginRight: 8,
|
|
93
|
+
flexShrink: 0,
|
|
94
|
+
opacity: 0.6
|
|
95
|
+
},
|
|
96
|
+
children: selectedIcon.path
|
|
97
|
+
}), /*#__PURE__*/_jsx("span", {
|
|
98
|
+
style: {
|
|
99
|
+
flex: 1,
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
textOverflow: 'ellipsis',
|
|
102
|
+
whiteSpace: 'nowrap',
|
|
103
|
+
userSelect: 'none'
|
|
104
|
+
},
|
|
105
|
+
children: getTitle ? getTitle(item) : item.title?.rendered || item.name || __('(no title)', 'gcblite')
|
|
106
|
+
}), /*#__PURE__*/_jsx("button", {
|
|
107
|
+
type: "button",
|
|
108
|
+
onClick: e => {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
onRemove(item.id);
|
|
111
|
+
},
|
|
112
|
+
onPointerDown: e => e.stopPropagation(),
|
|
113
|
+
className: "gcb-sortable-remove",
|
|
114
|
+
"aria-label": __('Remove', 'gcblite'),
|
|
115
|
+
children: /*#__PURE__*/_jsx("svg", {
|
|
116
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
117
|
+
viewBox: "0 0 24 24",
|
|
118
|
+
width: "20",
|
|
119
|
+
height: "20",
|
|
120
|
+
children: /*#__PURE__*/_jsx("path", {
|
|
121
|
+
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"
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
})]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ButtonGroup — multi-select shown as a row of toggle buttons. Each option
|
|
3
|
+
* looks like a pressable button; clicking toggles it on/off.
|
|
4
|
+
*
|
|
5
|
+
* Stored shape: array of selected values, same as checkbox-group.
|
|
6
|
+
* ['apple', 'cherry']
|
|
7
|
+
*
|
|
8
|
+
* Why not alias checkbox-group? Checkbox-group is checkboxes-in-a-column
|
|
9
|
+
* (vertical, ☑ ☐ ☐). ButtonGroup is buttons-in-a-row (horizontal, compact,
|
|
10
|
+
* visually weightier). Same data shape, different affordance.
|
|
11
|
+
*
|
|
12
|
+
* For single-select segmented controls use `toggle-group` instead.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { BaseControl, Button, __experimentalHStack as HStack } from '@wordpress/components';
|
|
16
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
17
|
+
export default function ButtonGroupField({
|
|
18
|
+
control,
|
|
19
|
+
value,
|
|
20
|
+
onChange
|
|
21
|
+
}) {
|
|
22
|
+
const current = Array.isArray(value) ? value : [];
|
|
23
|
+
return /*#__PURE__*/_jsx(BaseControl, {
|
|
24
|
+
label: control.label,
|
|
25
|
+
help: control.helpText,
|
|
26
|
+
className: "gcb-button-group-control components-base-control",
|
|
27
|
+
__nextHasNoMarginBottom: true,
|
|
28
|
+
children: /*#__PURE__*/_jsx(HStack, {
|
|
29
|
+
spacing: 1,
|
|
30
|
+
justify: "flex-start",
|
|
31
|
+
wrap: true,
|
|
32
|
+
children: (control.options || []).map(option => {
|
|
33
|
+
const isOn = current.includes(option.value);
|
|
34
|
+
return /*#__PURE__*/_jsx(Button, {
|
|
35
|
+
variant: isOn ? 'primary' : 'secondary',
|
|
36
|
+
onClick: () => {
|
|
37
|
+
const next = isOn ? current.filter(v => v !== option.value) : [...current, option.value];
|
|
38
|
+
onChange(next);
|
|
39
|
+
},
|
|
40
|
+
"aria-pressed": isOn,
|
|
41
|
+
children: option.label
|
|
42
|
+
}, option.value);
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
}
|