@siratik/wp-component-library 0.1.1 → 0.1.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/LICENSE +5 -0
- package/README.md +43 -5
- package/components/attachment-image/README.md +118 -0
- package/components/attachment-image/index.js +105 -0
- package/components/attributes-panel/README.md +92 -0
- package/components/attributes-panel/index.js +324 -0
- package/components/badge/README.md +61 -0
- package/components/badge/index.js +105 -0
- package/components/block-appender/README.md +158 -0
- package/components/block-appender/index.js +135 -0
- package/components/block-extension/index.js +214 -0
- package/components/business-hours/SpecialHoursControl.js +211 -0
- package/components/business-hours/WeeklyHoursControl.js +419 -0
- package/components/business-hours/index.js +8 -0
- package/components/button/README.md +172 -0
- package/components/button/index.js +84 -0
- package/components/captcha-placeholder/CloudflareTurnstile.js +111 -0
- package/components/captcha-placeholder/GoogleRecaptcha.js +132 -0
- package/components/captcha-placeholder/HCaptcha.js +99 -0
- package/components/captcha-placeholder/README.md +228 -0
- package/components/captcha-placeholder/index.js +39 -0
- package/components/captcha-placeholder/styles.js +33 -0
- package/components/geocoder/AddressEntryControl.js +390 -0
- package/components/geocoder/GeocoderSearchControl.js +353 -0
- package/components/geocoder/countryOptions.js +66 -0
- package/components/geocoder/geocoder.js +400 -0
- package/components/geocoder/index.js +16 -0
- package/components/icon-picker/Icon.js +53 -0
- package/components/icon-picker/IconPicker.js +70 -0
- package/components/icon-picker/IconPickerModal.js +474 -0
- package/components/icon-picker/IconPickerToolbarButton.js +60 -0
- package/components/icon-picker/InlineIconPicker.js +92 -0
- package/components/icon-picker/api.js +181 -0
- package/components/icon-picker/config-bootstrap.js +108 -0
- package/components/icon-picker/hooks.js +157 -0
- package/components/icon-picker/index.js +39 -0
- package/components/icon-picker/load-script.js +33 -0
- package/components/icon-picker/loaders.js +79 -0
- package/components/icon-picker/store.js +223 -0
- package/components/icon-picker/styles.js +232 -0
- package/components/icon-picker/utils.js +138 -0
- package/components/layout/README.md +268 -0
- package/components/link-tools-panel/README.md +49 -0
- package/components/link-tools-panel/index.js +198 -0
- package/components/link-tools-panel/styles.js +90 -0
- package/components/media/README.md +288 -0
- package/components/media/SettingsMediaUpload.js +187 -0
- package/components/media/editor-media-upload.js +65 -0
- package/components/media/image-controls.js +196 -0
- package/components/media/index.js +13 -0
- package/components/media/inspector-media-upload.js +290 -0
- package/components/media/toolbar-media-upload.js +53 -0
- package/components/media/utils/aspect-ratios.js +118 -0
- package/components/meta/MetaPanel.js +75 -0
- package/components/meta/README.md +46 -0
- package/components/meta/index.js +6 -0
- package/components/meta/meta-gated-editor.js +282 -0
- package/components/query/README.md +340 -0
- package/components/query/cardTemplatePartPanel.js +69 -0
- package/components/query/cardTemplatePartSelect.js +165 -0
- package/components/query/cardTemplatePartUtils.js +119 -0
- package/components/query/columnCountControl.js +63 -0
- package/components/query/displayTypeSelect.js +52 -0
- package/components/query/examples.js +166 -0
- package/components/query/hideCurrentPostControl.js +47 -0
- package/components/query/index.js +20 -0
- package/components/query/manualPostSelector.js +125 -0
- package/components/query/manualTermSelector.js +141 -0
- package/components/query/orderBySelect.js +95 -0
- package/components/query/postTypeSelect.js +67 -0
- package/components/query/postsPerPageControl.js +51 -0
- package/components/query/selectionModeControl.js +61 -0
- package/components/query/taxonomySelect.js +139 -0
- package/components/query/utils.js +75 -0
- package/components/repeater/README.md +159 -0
- package/components/repeater/index.js +829 -0
- package/components/segmented-bar/README.md +30 -0
- package/components/segmented-bar/index.js +68 -0
- package/components/sortable-select/README.md +153 -0
- package/components/sortable-select/custom-sortable-token.js +127 -0
- package/components/sortable-select/index.js +693 -0
- package/components/style-picker/README.md +36 -0
- package/components/style-picker/index.js +92 -0
- package/components/variable-field/README.md +299 -0
- package/components/variable-field/index.js +592 -0
- package/components/variable-field/value-to-chip-html.js +38 -0
- package/components/variable-inserter/README.md +68 -0
- package/components/variable-inserter/index.js +168 -0
- package/constants/statusColors.js +1 -1
- package/editor.js +16 -0
- package/index.js +127 -0
- package/package.json +21 -13
- package/utils/kitaris-localize.js +128 -0
- package/utils/slug-aware-color-setter.js +27 -0
- package/build/index.asset.php +0 -20
- package/build/index.js +0 -1242
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Query Controls
|
|
2
|
+
|
|
3
|
+
This directory contains standalone, modular query controls that can be composed together to build custom query interfaces. Each control is focused on a single responsibility and is always rendered as a ToolsPanelItem.
|
|
4
|
+
|
|
5
|
+
## Individual Controls
|
|
6
|
+
|
|
7
|
+
These controls are always ToolsPanelItems that can be composed together in ToolsPanel containers:
|
|
8
|
+
|
|
9
|
+
### Core Controls
|
|
10
|
+
|
|
11
|
+
- **`PostTypeSelect`** - Select post type
|
|
12
|
+
- **`OrderBySelect`** - Order posts by date, title, etc.
|
|
13
|
+
- **`SelectionModeControl`** - Toggle between auto and manual selection
|
|
14
|
+
- **`HideCurrentPostControl`** - Exclude the post being viewed from feed results on singles/templates (default on)
|
|
15
|
+
|
|
16
|
+
### Manual Selection Controls
|
|
17
|
+
|
|
18
|
+
- **`ManualPostSelector`** - Search and select specific posts
|
|
19
|
+
- **`TermSelector`** - Search and select taxonomy terms
|
|
20
|
+
- **`TaxonomySelect`** - Select taxonomy for term filtering
|
|
21
|
+
|
|
22
|
+
### Display Controls
|
|
23
|
+
|
|
24
|
+
- **`DisplayTypeSelect`** - Choose grid, list, slider, or pills display
|
|
25
|
+
- **`PostsPerPageControl`** - Control number of items to show
|
|
26
|
+
- **`ColumnCountControl`** - Set columns for grid or slides for slider
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Basic Usage
|
|
31
|
+
|
|
32
|
+
```jsx
|
|
33
|
+
import { __experimentalToolsPanel as ToolsPanel } from "@wordpress/components";
|
|
34
|
+
import { PostTypeSelect, OrderBySelect } from "../query";
|
|
35
|
+
|
|
36
|
+
function MyQueryInterface({ attributes, setAttributes }) {
|
|
37
|
+
const resetAll = () => {
|
|
38
|
+
setAttributes({
|
|
39
|
+
postType: "post",
|
|
40
|
+
orderPostsBy: "date",
|
|
41
|
+
orderPostsDirection: "desc",
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<ToolsPanel label="Query Settings" resetAll={resetAll}>
|
|
47
|
+
<PostTypeSelect
|
|
48
|
+
attributes={attributes}
|
|
49
|
+
setAttributes={setAttributes}
|
|
50
|
+
/>
|
|
51
|
+
<OrderBySelect
|
|
52
|
+
attributes={attributes}
|
|
53
|
+
setAttributes={setAttributes}
|
|
54
|
+
/>
|
|
55
|
+
</ToolsPanel>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Custom Order Options
|
|
61
|
+
|
|
62
|
+
```jsx
|
|
63
|
+
import { OrderBySelect, defaultOrderOptions } from "../query";
|
|
64
|
+
|
|
65
|
+
function CustomOrderExample({ attributes, setAttributes }) {
|
|
66
|
+
// Option 1: Completely replace with custom options
|
|
67
|
+
const customOptions = [
|
|
68
|
+
{ label: "Popularity", value: "meta_value_num_desc" },
|
|
69
|
+
{ label: "Price (Low to High)", value: "meta_value_num_asc" },
|
|
70
|
+
{ label: "Rating", value: "rating_desc" },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// Option 2: Use defaults as a base and modify
|
|
74
|
+
const extendedOptions = [
|
|
75
|
+
...defaultOrderOptions,
|
|
76
|
+
{ label: "Custom Field", value: "meta_value_desc" },
|
|
77
|
+
{ label: "View Count", value: "views_desc" },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Option 3: Mix defaults with custom (reorder, filter, etc.)
|
|
81
|
+
const mixedOptions = [
|
|
82
|
+
defaultOrderOptions[1], // Date (Newest → Oldest)
|
|
83
|
+
defaultOrderOptions[2], // Date (Oldest → Newest)
|
|
84
|
+
{ label: "Featured First", value: "featured_desc" },
|
|
85
|
+
defaultOrderOptions[5], // Random
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ToolsPanel label="Query Settings" resetAll={resetAll}>
|
|
90
|
+
{/* Use completely custom options */}
|
|
91
|
+
<OrderBySelect
|
|
92
|
+
attributes={attributes}
|
|
93
|
+
setAttributes={setAttributes}
|
|
94
|
+
orderOptions={customOptions}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
{/* Or use extended options */}
|
|
98
|
+
<OrderBySelect
|
|
99
|
+
attributes={attributes}
|
|
100
|
+
setAttributes={setAttributes}
|
|
101
|
+
orderOptions={extendedOptions}
|
|
102
|
+
/>
|
|
103
|
+
</ToolsPanel>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Complete Query Interface
|
|
109
|
+
|
|
110
|
+
```jsx
|
|
111
|
+
import { __experimentalToolsPanel as ToolsPanel } from "@wordpress/components";
|
|
112
|
+
import {
|
|
113
|
+
PostTypeSelect,
|
|
114
|
+
OrderBySelect,
|
|
115
|
+
SelectionModeControl,
|
|
116
|
+
ManualPostSelector,
|
|
117
|
+
DisplayTypeSelect,
|
|
118
|
+
} from "../query";
|
|
119
|
+
|
|
120
|
+
function CompleteQueryInterface({ attributes, setAttributes }) {
|
|
121
|
+
const resetAll = () => {
|
|
122
|
+
setAttributes({
|
|
123
|
+
postType: "post",
|
|
124
|
+
orderPostsBy: "date",
|
|
125
|
+
orderPostsDirection: "desc",
|
|
126
|
+
selectionMode: "auto",
|
|
127
|
+
selectedPosts: [],
|
|
128
|
+
displayAs: "grid",
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<ToolsPanel label="Query Settings" resetAll={resetAll}>
|
|
134
|
+
<PostTypeSelect
|
|
135
|
+
attributes={attributes}
|
|
136
|
+
setAttributes={setAttributes}
|
|
137
|
+
/>
|
|
138
|
+
<OrderBySelect
|
|
139
|
+
attributes={attributes}
|
|
140
|
+
setAttributes={setAttributes}
|
|
141
|
+
/>
|
|
142
|
+
<SelectionModeControl
|
|
143
|
+
attributes={attributes}
|
|
144
|
+
setAttributes={setAttributes}
|
|
145
|
+
/>
|
|
146
|
+
{attributes.selectionMode === "manual" && (
|
|
147
|
+
<ManualPostSelector
|
|
148
|
+
attributes={attributes}
|
|
149
|
+
setAttributes={setAttributes}
|
|
150
|
+
postType={attributes.postType}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
<DisplayTypeSelect
|
|
154
|
+
attributes={attributes}
|
|
155
|
+
setAttributes={setAttributes}
|
|
156
|
+
/>
|
|
157
|
+
</ToolsPanel>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Multiple Panels for Organization
|
|
163
|
+
|
|
164
|
+
```jsx
|
|
165
|
+
function OrganizedInterface({ attributes, setAttributes }) {
|
|
166
|
+
return (
|
|
167
|
+
<>
|
|
168
|
+
<ToolsPanel label="Content Selection" resetAll={resetContent}>
|
|
169
|
+
<PostTypeSelect
|
|
170
|
+
attributes={attributes}
|
|
171
|
+
setAttributes={setAttributes}
|
|
172
|
+
/>
|
|
173
|
+
<OrderBySelect
|
|
174
|
+
attributes={attributes}
|
|
175
|
+
setAttributes={setAttributes}
|
|
176
|
+
/>
|
|
177
|
+
</ToolsPanel>
|
|
178
|
+
|
|
179
|
+
<ToolsPanel label="Display Settings" resetAll={resetDisplay}>
|
|
180
|
+
<DisplayTypeSelect
|
|
181
|
+
attributes={attributes}
|
|
182
|
+
setAttributes={setAttributes}
|
|
183
|
+
/>
|
|
184
|
+
<PostsPerPageControl
|
|
185
|
+
attributes={attributes}
|
|
186
|
+
setAttributes={setAttributes}
|
|
187
|
+
/>
|
|
188
|
+
<ColumnCountControl
|
|
189
|
+
attributes={attributes}
|
|
190
|
+
setAttributes={setAttributes}
|
|
191
|
+
/>
|
|
192
|
+
</ToolsPanel>
|
|
193
|
+
</>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Required Attributes
|
|
199
|
+
|
|
200
|
+
Make sure your block attributes include these for the controls to work properly:
|
|
201
|
+
|
|
202
|
+
```jsx
|
|
203
|
+
// Required attributes for query controls
|
|
204
|
+
const attributes = {
|
|
205
|
+
// Core query attributes
|
|
206
|
+
postType: {
|
|
207
|
+
type: "string",
|
|
208
|
+
default: "post",
|
|
209
|
+
},
|
|
210
|
+
orderPostsBy: {
|
|
211
|
+
type: "string",
|
|
212
|
+
default: "date",
|
|
213
|
+
},
|
|
214
|
+
orderPostsDirection: {
|
|
215
|
+
type: "string",
|
|
216
|
+
default: "desc",
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// Selection mode attributes
|
|
220
|
+
selectionMode: {
|
|
221
|
+
type: "string",
|
|
222
|
+
default: "auto",
|
|
223
|
+
},
|
|
224
|
+
selectedPosts: {
|
|
225
|
+
type: "array",
|
|
226
|
+
default: [],
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Taxonomy attributes
|
|
230
|
+
selectedTaxonomy: {
|
|
231
|
+
type: "string",
|
|
232
|
+
default: "",
|
|
233
|
+
},
|
|
234
|
+
selectedTerms: {
|
|
235
|
+
type: "array",
|
|
236
|
+
default: [],
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// Display attributes
|
|
240
|
+
displayAs: {
|
|
241
|
+
type: "string",
|
|
242
|
+
default: "grid",
|
|
243
|
+
},
|
|
244
|
+
postsPerPage: {
|
|
245
|
+
type: "number",
|
|
246
|
+
default: 6,
|
|
247
|
+
},
|
|
248
|
+
columnCount: {
|
|
249
|
+
type: "number",
|
|
250
|
+
default: 3,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Exports
|
|
256
|
+
|
|
257
|
+
### Components
|
|
258
|
+
|
|
259
|
+
All individual controls are exported as named exports.
|
|
260
|
+
|
|
261
|
+
### Utilities
|
|
262
|
+
|
|
263
|
+
- **`defaultOrderOptions`** - Array of default order options for OrderBySelect
|
|
264
|
+
|
|
265
|
+
```jsx
|
|
266
|
+
import { OrderBySelect, defaultOrderOptions } from "../query";
|
|
267
|
+
|
|
268
|
+
// defaultOrderOptions contains:
|
|
269
|
+
// [
|
|
270
|
+
// { label: "Custom Order", value: "menu_order_asc" },
|
|
271
|
+
// { label: "Date (Newest → Oldest)", value: "date_desc" },
|
|
272
|
+
// { label: "Date (Oldest → Newest)", value: "date_asc" },
|
|
273
|
+
// { label: "Title (A → Z)", value: "title_asc" },
|
|
274
|
+
// { label: "Title (Z → A)", value: "title_desc" },
|
|
275
|
+
// { label: "Random", value: "rand_asc" },
|
|
276
|
+
// ]
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Control Props
|
|
280
|
+
|
|
281
|
+
### Common Props
|
|
282
|
+
|
|
283
|
+
All controls accept these common props:
|
|
284
|
+
|
|
285
|
+
- **`attributes`** (Object) - Block attributes object
|
|
286
|
+
- **`setAttributes`** (Function) - Function to update block attributes
|
|
287
|
+
|
|
288
|
+
### Specific Props
|
|
289
|
+
|
|
290
|
+
#### PostTypeSelect
|
|
291
|
+
|
|
292
|
+
- No additional props
|
|
293
|
+
|
|
294
|
+
#### OrderBySelect
|
|
295
|
+
|
|
296
|
+
- **`orderOptions`** (Array) - Custom order options (replaces defaults)
|
|
297
|
+
- **`additionalOrderOptions`** (Array) - Additional options to add to defaults (deprecated)
|
|
298
|
+
|
|
299
|
+
#### TaxonomySelect
|
|
300
|
+
|
|
301
|
+
- **`postType`** (String) - Post type to get taxonomies for
|
|
302
|
+
|
|
303
|
+
#### TermSelector
|
|
304
|
+
|
|
305
|
+
- **`selectedTaxonomy`** (String) - Taxonomy slug to get terms from
|
|
306
|
+
|
|
307
|
+
#### ManualPostSelector
|
|
308
|
+
|
|
309
|
+
- **`postType`** (String) - Post type to search (default: 'post')
|
|
310
|
+
|
|
311
|
+
#### DisplayTypeSelect
|
|
312
|
+
|
|
313
|
+
- **`allowedDisplayOptions`** (Array) - Allowed display types (default: ['grid', 'slider', 'list', 'pills'])
|
|
314
|
+
|
|
315
|
+
#### PostsPerPageControl
|
|
316
|
+
|
|
317
|
+
- **`label`** (String) - Label for control (default: 'Amount to Display')
|
|
318
|
+
- **`min`** (Number) - Minimum value (default: 1)
|
|
319
|
+
- **`max`** (Number) - Maximum value (default: 50)
|
|
320
|
+
|
|
321
|
+
#### ColumnCountControl
|
|
322
|
+
|
|
323
|
+
- **`min`** (Number) - Minimum value (default: 1)
|
|
324
|
+
- **`max`** (Number) - Maximum value (default: 4)
|
|
325
|
+
|
|
326
|
+
## Design Principles
|
|
327
|
+
|
|
328
|
+
1. **Single Responsibility** - Each control does one thing well
|
|
329
|
+
2. **Always ToolsPanelItems** - Controls are consistent ToolsPanelItem components
|
|
330
|
+
3. **Composable** - Controls can be mixed and matched as needed
|
|
331
|
+
4. **Consistent API** - All controls follow the same prop patterns
|
|
332
|
+
5. **No Forced Panels** - Controls don't create their own ToolsPanel containers
|
|
333
|
+
|
|
334
|
+
## Architecture Benefits
|
|
335
|
+
|
|
336
|
+
- **Modular**: Use only the controls you need
|
|
337
|
+
- **Flexible**: Arrange controls in any panel structure
|
|
338
|
+
- **Consistent**: All controls work the same way
|
|
339
|
+
- **Maintainable**: Each control is focused and isolated
|
|
340
|
+
- **Composable**: Build complex interfaces from simple parts
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { __experimentalToolsPanel as ToolsPanel } from "@wordpress/components";
|
|
2
|
+
import { __ } from "@wordpress/i18n";
|
|
3
|
+
import { CardTemplatePartSelect } from "./cardTemplatePartSelect";
|
|
4
|
+
import { getDefaultCardSlugForPostType } from "./cardTemplatePartUtils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Card layout ToolsPanel — matches core/query block extension UI.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} props Component props.
|
|
10
|
+
* @param {Object} props.attributes Block attributes (postType, templatePartSlug).
|
|
11
|
+
* @param {Function} props.setAttributes Set block attributes.
|
|
12
|
+
* @param {string} [props.postType] Post type override.
|
|
13
|
+
* @param {string} [props.area] Template part area (default kitaris-cards).
|
|
14
|
+
* @param {string} [props.defaultSlug] Default slug for reset / deselect.
|
|
15
|
+
* @param {Function} [props.slugFilter] Optional (slug, postType) => boolean.
|
|
16
|
+
* @param {Function} [props.onSlugChange] Optional side effect when slug changes.
|
|
17
|
+
* @param {string} [props.textDomain] i18n text domain.
|
|
18
|
+
* @param {boolean} [props.disableAutoFill] Passed to CardTemplatePartSelect.
|
|
19
|
+
* @param {boolean} [props.usePersistedSlugOnly] Passed to CardTemplatePartSelect.
|
|
20
|
+
* @returns {import('react').WPElement|null} Element to render.
|
|
21
|
+
*/
|
|
22
|
+
function CardTemplatePartPanel({
|
|
23
|
+
attributes,
|
|
24
|
+
setAttributes,
|
|
25
|
+
postType: postTypeProp,
|
|
26
|
+
area,
|
|
27
|
+
defaultSlug: defaultSlugProp,
|
|
28
|
+
slugFilter,
|
|
29
|
+
onSlugChange,
|
|
30
|
+
textDomain = "built_starter",
|
|
31
|
+
disableAutoFill = false,
|
|
32
|
+
usePersistedSlugOnly = false,
|
|
33
|
+
}) {
|
|
34
|
+
const postType = postTypeProp || attributes?.postType || "post";
|
|
35
|
+
const resolvedDefault =
|
|
36
|
+
defaultSlugProp || getDefaultCardSlugForPostType(postType);
|
|
37
|
+
|
|
38
|
+
const resetCardLayout = () => {
|
|
39
|
+
setAttributes({ templatePartSlug: resolvedDefault });
|
|
40
|
+
|
|
41
|
+
// onSlugChange is optional; query blocks handle inner sync via setAttributes.
|
|
42
|
+
if (onSlugChange) {
|
|
43
|
+
onSlugChange(resolvedDefault);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<ToolsPanel
|
|
49
|
+
// eslint-disable-next-line @wordpress/i18n-text-domain -- text domain is a prop for the consumer.
|
|
50
|
+
label={__("Card layout", textDomain)}
|
|
51
|
+
resetAll={resetCardLayout}
|
|
52
|
+
>
|
|
53
|
+
<CardTemplatePartSelect
|
|
54
|
+
attributes={attributes}
|
|
55
|
+
setAttributes={setAttributes}
|
|
56
|
+
postType={postType}
|
|
57
|
+
area={area}
|
|
58
|
+
defaultSlug={resolvedDefault}
|
|
59
|
+
slugFilter={slugFilter}
|
|
60
|
+
onSlugChange={onSlugChange}
|
|
61
|
+
textDomain={textDomain}
|
|
62
|
+
disableAutoFill={disableAutoFill}
|
|
63
|
+
usePersistedSlugOnly={usePersistedSlugOnly}
|
|
64
|
+
/>
|
|
65
|
+
</ToolsPanel>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { CardTemplatePartPanel };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SelectControl,
|
|
3
|
+
__experimentalToolsPanelItem as ToolsPanelItem,
|
|
4
|
+
} from "@wordpress/components";
|
|
5
|
+
import { useSelect } from "@wordpress/data";
|
|
6
|
+
import { useEffect, useMemo } from "@wordpress/element";
|
|
7
|
+
import { __ } from "@wordpress/i18n";
|
|
8
|
+
import {
|
|
9
|
+
buildCardTemplatePartOptions,
|
|
10
|
+
getDefaultCardSlugForPostType,
|
|
11
|
+
} from "./cardTemplatePartUtils";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Card template part selector for query/post-feed blocks (kitaris-cards area).
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} props Component props.
|
|
17
|
+
* @param {Object} props.attributes Block attributes (postType, templatePartSlug).
|
|
18
|
+
* @param {Function} props.setAttributes Set block attributes.
|
|
19
|
+
* @param {string} [props.postType] Post type override.
|
|
20
|
+
* @param {string} [props.area] Template part area (default kitaris-cards).
|
|
21
|
+
* @param {string} [props.defaultSlug] Default slug for deselect / empty state.
|
|
22
|
+
* @param {Function} [props.slugFilter] Optional (slug, postType) => boolean.
|
|
23
|
+
* @param {Function} [props.onSlugChange] Optional side effect when slug changes (e.g. sync inner blocks).
|
|
24
|
+
* @param {boolean} [props.isShownByDefault] ToolsPanelItem default visibility.
|
|
25
|
+
* @param {string} [props.textDomain] i18n text domain.
|
|
26
|
+
* @param {boolean} [props.disableAutoFill] Skip auto-setting default slug on mount.
|
|
27
|
+
* @param {boolean} [props.usePersistedSlugOnly] Select value is the saved slug only (no default fallback writes).
|
|
28
|
+
* @returns {import('react').WPElement|null} Element to render.
|
|
29
|
+
*/
|
|
30
|
+
function CardTemplatePartSelect({
|
|
31
|
+
attributes,
|
|
32
|
+
setAttributes,
|
|
33
|
+
postType: postTypeProp,
|
|
34
|
+
area = "kitaris-cards",
|
|
35
|
+
defaultSlug: defaultSlugProp,
|
|
36
|
+
slugFilter,
|
|
37
|
+
onSlugChange,
|
|
38
|
+
isShownByDefault = true,
|
|
39
|
+
textDomain = "built_starter",
|
|
40
|
+
disableAutoFill = false,
|
|
41
|
+
usePersistedSlugOnly = false,
|
|
42
|
+
}) {
|
|
43
|
+
const postType = postTypeProp || attributes?.postType || "post";
|
|
44
|
+
const templatePartSlug = attributes?.templatePartSlug || "";
|
|
45
|
+
const resolvedDefault =
|
|
46
|
+
defaultSlugProp || getDefaultCardSlugForPostType(postType);
|
|
47
|
+
|
|
48
|
+
const templateParts = useSelect(
|
|
49
|
+
(select) =>
|
|
50
|
+
select("core").getEntityRecords("postType", "wp_template_part", {
|
|
51
|
+
per_page: 200,
|
|
52
|
+
area,
|
|
53
|
+
}),
|
|
54
|
+
[area],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const options = useMemo(() => {
|
|
58
|
+
const filtered = buildCardTemplatePartOptions(
|
|
59
|
+
templateParts,
|
|
60
|
+
postType,
|
|
61
|
+
slugFilter,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
!templatePartSlug ||
|
|
66
|
+
filtered.some((option) => option.value === templatePartSlug)
|
|
67
|
+
) {
|
|
68
|
+
return filtered;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const savedPart = templateParts?.find(
|
|
72
|
+
(part) => part.slug === templatePartSlug,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (!savedPart) {
|
|
76
|
+
return filtered;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
label:
|
|
82
|
+
savedPart.title?.rendered ||
|
|
83
|
+
savedPart.title ||
|
|
84
|
+
savedPart.slug,
|
|
85
|
+
value: savedPart.slug,
|
|
86
|
+
},
|
|
87
|
+
...filtered,
|
|
88
|
+
];
|
|
89
|
+
}, [templateParts, postType, slugFilter, templatePartSlug]);
|
|
90
|
+
|
|
91
|
+
const selectValue = usePersistedSlugOnly
|
|
92
|
+
? templatePartSlug
|
|
93
|
+
: templatePartSlug || resolvedDefault;
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (disableAutoFill) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!templatePartSlug && resolvedDefault && options.length > 0) {
|
|
101
|
+
const hasDefault = options.some((o) => o.value === resolvedDefault);
|
|
102
|
+
if (hasDefault) {
|
|
103
|
+
setAttributes({ templatePartSlug: resolvedDefault });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [
|
|
107
|
+
templatePartSlug,
|
|
108
|
+
resolvedDefault,
|
|
109
|
+
options,
|
|
110
|
+
setAttributes,
|
|
111
|
+
disableAutoFill,
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
if (!options.length) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handleChange = (value) => {
|
|
119
|
+
if (!value || value === templatePartSlug) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setAttributes({ templatePartSlug: value });
|
|
124
|
+
|
|
125
|
+
// Caller may already sync in setAttributes (e.g. core/query); skip duplicate updates.
|
|
126
|
+
if (onSlugChange) {
|
|
127
|
+
onSlugChange(value);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<ToolsPanelItem
|
|
133
|
+
hasValue={() =>
|
|
134
|
+
!!templatePartSlug && templatePartSlug !== resolvedDefault
|
|
135
|
+
}
|
|
136
|
+
// eslint-disable-next-line @wordpress/i18n-text-domain -- text domain is a prop for the consumer.
|
|
137
|
+
label={__("Card layout", textDomain)}
|
|
138
|
+
onDeselect={() => {
|
|
139
|
+
setAttributes({ templatePartSlug: resolvedDefault });
|
|
140
|
+
|
|
141
|
+
if (onSlugChange) {
|
|
142
|
+
onSlugChange(resolvedDefault);
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
isShownByDefault={isShownByDefault}
|
|
146
|
+
>
|
|
147
|
+
<SelectControl
|
|
148
|
+
__nextHasNoMarginBottom={true}
|
|
149
|
+
__next40pxDefaultSize
|
|
150
|
+
// eslint-disable-next-line @wordpress/i18n-text-domain -- text domain is a prop for the consumer.
|
|
151
|
+
label={__("Card layout", textDomain)}
|
|
152
|
+
// eslint-disable-next-line @wordpress/i18n-text-domain -- text domain is a prop for the consumer.
|
|
153
|
+
help={__(
|
|
154
|
+
"Choose which card template part to use for each post in the loop.",
|
|
155
|
+
textDomain,
|
|
156
|
+
)}
|
|
157
|
+
value={selectValue}
|
|
158
|
+
options={options}
|
|
159
|
+
onChange={handleChange}
|
|
160
|
+
/>
|
|
161
|
+
</ToolsPanelItem>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { CardTemplatePartSelect };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve card template part options for a post type.
|
|
3
|
+
*
|
|
4
|
+
* Override via editor_experience.card_templates[postType] in the host plugin's
|
|
5
|
+
* localize config (window.kitaris_localize.blocks.editor_experience).
|
|
6
|
+
*
|
|
7
|
+
* @param {string} postType Post type slug.
|
|
8
|
+
* @returns {string[]} Slug prefixes — a part matches if slug equals prefix or starts with prefix + '-'.
|
|
9
|
+
*/
|
|
10
|
+
export function getCardSlugPrefixesForPostType(postType) {
|
|
11
|
+
if (!postType) {
|
|
12
|
+
return ["card-"];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const config =
|
|
16
|
+
typeof window !== "undefined" &&
|
|
17
|
+
window.kitaris_localize?.blocks?.editor_experience?.card_templates?.[
|
|
18
|
+
postType
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(config) && config.length > 0) {
|
|
22
|
+
return config.map((entry) =>
|
|
23
|
+
typeof entry === "string" ? entry : entry?.slug,
|
|
24
|
+
).filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const knownPrefixes = {
|
|
28
|
+
kitaris_service: ["card-service"],
|
|
29
|
+
kitaris_team: ["card-team"],
|
|
30
|
+
kitaris_promotion: ["card-promotion"],
|
|
31
|
+
kitaris_coupon: ["card-promotion"],
|
|
32
|
+
kitaris_listing: ["kitaris_listing-card", "card-listing"],
|
|
33
|
+
kitaris_review: ["kitaris_review-card", "card-review"],
|
|
34
|
+
post: ["post-card", "card-post"],
|
|
35
|
+
page: ["page-card", "card-page"],
|
|
36
|
+
search: ["search-result-card", "card-search"],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (knownPrefixes[postType]) {
|
|
40
|
+
return knownPrefixes[postType];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const base = postType.replace(/^kitaris_/, "");
|
|
44
|
+
|
|
45
|
+
return [`card-${base}`, `${postType}-card`];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} slug Template part slug.
|
|
50
|
+
* @param {string} postType Post type slug.
|
|
51
|
+
* @param {Function} [customFilter] Optional (slug, postType) => boolean.
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function cardTemplatePartMatchesPostType(slug, postType, customFilter) {
|
|
55
|
+
if (typeof customFilter === "function") {
|
|
56
|
+
return customFilter(slug, postType);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!slug) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const prefixes = getCardSlugPrefixesForPostType(postType);
|
|
64
|
+
|
|
65
|
+
return prefixes.some(
|
|
66
|
+
(prefix) => slug === prefix || slug.startsWith(`${prefix}-`),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Default card slug for a post type (first configured prefix).
|
|
72
|
+
*
|
|
73
|
+
* @param {string} postType Post type slug.
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function getDefaultCardSlugForPostType(postType) {
|
|
77
|
+
const defaults = {
|
|
78
|
+
kitaris_service: "card-service-001",
|
|
79
|
+
kitaris_team: "card-team-001",
|
|
80
|
+
kitaris_promotion: "card-promotion-001",
|
|
81
|
+
kitaris_coupon: "card-promotion-001",
|
|
82
|
+
kitaris_location: "card-location-001",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (defaults[postType]) {
|
|
86
|
+
return defaults[postType];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const prefixes = getCardSlugPrefixesForPostType(postType);
|
|
90
|
+
|
|
91
|
+
return prefixes[0] || `${postType}-card`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build select options from template part entities.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array} templateParts wp_template_part records.
|
|
98
|
+
* @param {string} postType Post type slug.
|
|
99
|
+
* @param {Function} [slugFilter] Optional filter.
|
|
100
|
+
* @returns {Array<{ label: string, value: string }>}
|
|
101
|
+
*/
|
|
102
|
+
export function buildCardTemplatePartOptions(
|
|
103
|
+
templateParts,
|
|
104
|
+
postType,
|
|
105
|
+
slugFilter,
|
|
106
|
+
) {
|
|
107
|
+
if (!templateParts?.length) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const filtered = templateParts.filter((part) =>
|
|
112
|
+
cardTemplatePartMatchesPostType(part.slug, postType, slugFilter),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return filtered.map((part) => ({
|
|
116
|
+
label: part.title?.rendered || part.title || part.slug,
|
|
117
|
+
value: part.slug,
|
|
118
|
+
}));
|
|
119
|
+
}
|