@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,829 @@
|
|
|
1
|
+
import { ADMIN_COLOR } from "../../constants/adminColors";
|
|
2
|
+
import {
|
|
3
|
+
closestCenter,
|
|
4
|
+
DndContext,
|
|
5
|
+
DragOverlay,
|
|
6
|
+
KeyboardSensor,
|
|
7
|
+
PointerSensor,
|
|
8
|
+
useSensor,
|
|
9
|
+
useSensors,
|
|
10
|
+
} from "@dnd-kit/core";
|
|
11
|
+
import {
|
|
12
|
+
restrictToParentElement,
|
|
13
|
+
restrictToVerticalAxis,
|
|
14
|
+
} from "@dnd-kit/modifiers";
|
|
15
|
+
import {
|
|
16
|
+
arrayMove,
|
|
17
|
+
SortableContext,
|
|
18
|
+
sortableKeyboardCoordinates,
|
|
19
|
+
useSortable,
|
|
20
|
+
verticalListSortingStrategy,
|
|
21
|
+
} from "@dnd-kit/sortable";
|
|
22
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
23
|
+
import styled from "@emotion/styled";
|
|
24
|
+
import { Button } from "@wordpress/components";
|
|
25
|
+
import { __ } from "@wordpress/i18n";
|
|
26
|
+
import {
|
|
27
|
+
chevronDown,
|
|
28
|
+
chevronUp,
|
|
29
|
+
dragHandle,
|
|
30
|
+
plusCircle,
|
|
31
|
+
trash,
|
|
32
|
+
} from "@wordpress/icons";
|
|
33
|
+
import PropTypes from "prop-types";
|
|
34
|
+
import {
|
|
35
|
+
createContext,
|
|
36
|
+
useContext,
|
|
37
|
+
useEffect,
|
|
38
|
+
useRef,
|
|
39
|
+
useState,
|
|
40
|
+
} from "@wordpress/element";
|
|
41
|
+
|
|
42
|
+
// Styled components
|
|
43
|
+
const StyledRepeater = styled.div`
|
|
44
|
+
/* A core Modal's open animation keeps its end transform (fill-mode
|
|
45
|
+
forwards); a transformed frame throws off dnd-kit's keyboard
|
|
46
|
+
measuring, so arrow keys skip an item. The end keyframe is the
|
|
47
|
+
resting state, so dropping the fill changes nothing visually.
|
|
48
|
+
The held transform was also what positioned the absolute modal
|
|
49
|
+
header; position: relative keeps the header in the frame without
|
|
50
|
+
affecting the fixed-position drag overlay. */
|
|
51
|
+
.components-modal__screen-overlay:not(.is-animating-out)
|
|
52
|
+
.components-modal__frame:has(&) {
|
|
53
|
+
animation-fill-mode: none;
|
|
54
|
+
position: relative;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.built-repeater__items {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 1.5rem;
|
|
61
|
+
margin-bottom: 3rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&.wpcl-repeater--collapsible .built-repeater__items {
|
|
65
|
+
gap: 0.75rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.built-repeater__empty-state {
|
|
69
|
+
padding: 1rem;
|
|
70
|
+
background: #f0f0f0;
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
margin-bottom: 1rem;
|
|
73
|
+
opacity: 0.75;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.built-repeater__drag-overlay {
|
|
77
|
+
opacity: 0.95;
|
|
78
|
+
box-shadow:
|
|
79
|
+
0 15px 30px rgba(0, 0, 0, 0.15),
|
|
80
|
+
0 5px 15px rgba(0, 0, 0, 0.08);
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
background: ${ADMIN_COLOR.surface};
|
|
83
|
+
cursor: grabbing;
|
|
84
|
+
transition: transform 0.2s ease;
|
|
85
|
+
|
|
86
|
+
> div {
|
|
87
|
+
background: ${ADMIN_COLOR.surface};
|
|
88
|
+
border-radius: 8px;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const StyledSortableItem = styled.div`
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: flex-start;
|
|
96
|
+
gap: 1rem;
|
|
97
|
+
background-color: ${ADMIN_COLOR.surface};
|
|
98
|
+
border: 1px solid ${ADMIN_COLOR.border};
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
padding: 1rem;
|
|
101
|
+
transition: all 0.2s ease;
|
|
102
|
+
position: relative;
|
|
103
|
+
|
|
104
|
+
&.wpcl-repeater__item--dragging {
|
|
105
|
+
opacity: 0.3;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&.wpcl-repeater__item--integrated {
|
|
109
|
+
/* In integrated mode, ensure proper flex layout */
|
|
110
|
+
> * {
|
|
111
|
+
flex-grow: 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Drag handle, row actions, and remove button should not grow */
|
|
115
|
+
.built-repeater__drag-handle,
|
|
116
|
+
.built-repeater__remove-item,
|
|
117
|
+
.built-repeater__item-action {
|
|
118
|
+
flex-grow: 0;
|
|
119
|
+
flex-shrink: 0;
|
|
120
|
+
margin-top: 26px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.built-repeater__item-action.components-button {
|
|
124
|
+
width: auto;
|
|
125
|
+
min-width: 36px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Inline toggles beside labeled TextControl / SelectControl rows. */
|
|
129
|
+
.built-repeater__inline-toggle {
|
|
130
|
+
flex-grow: 0;
|
|
131
|
+
flex-shrink: 0;
|
|
132
|
+
align-self: flex-end;
|
|
133
|
+
|
|
134
|
+
.components-toggle-control {
|
|
135
|
+
margin-bottom: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
&.built-repeater__inline-toggle--category-selected
|
|
139
|
+
.components-base-control__help {
|
|
140
|
+
visibility: hidden;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Read-only summary rows — meta left, actions right. */
|
|
145
|
+
&:has(.built-repeater__summary) {
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: space-between;
|
|
148
|
+
gap: 1rem;
|
|
149
|
+
|
|
150
|
+
> * {
|
|
151
|
+
flex-grow: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.built-repeater__summary {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
flex-wrap: wrap;
|
|
158
|
+
gap: 0.75rem;
|
|
159
|
+
flex: 1 1 auto;
|
|
160
|
+
min-width: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.built-repeater__summary-actions {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
gap: 0.75rem;
|
|
167
|
+
flex-shrink: 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.built-repeater__summary-name {
|
|
171
|
+
flex-shrink: 1;
|
|
172
|
+
min-width: 0;
|
|
173
|
+
font-weight: 500;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
text-overflow: ellipsis;
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.built-repeater__drag-handle,
|
|
180
|
+
.built-repeater__remove-item,
|
|
181
|
+
.built-repeater__item-action {
|
|
182
|
+
margin-top: 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.built-repeater__item-handle {
|
|
188
|
+
cursor: grab;
|
|
189
|
+
flex-shrink: 0;
|
|
190
|
+
transition:
|
|
191
|
+
transform 0.15s ease,
|
|
192
|
+
opacity 0.15s ease;
|
|
193
|
+
|
|
194
|
+
&:active {
|
|
195
|
+
cursor: grabbing;
|
|
196
|
+
opacity: 0.7;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
button {
|
|
200
|
+
transition:
|
|
201
|
+
background-color 0.15s ease,
|
|
202
|
+
color 0.15s ease;
|
|
203
|
+
|
|
204
|
+
&:hover {
|
|
205
|
+
background-color: ${ADMIN_COLOR.brandFill};
|
|
206
|
+
color: ${ADMIN_COLOR.onBrand};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.built-repeater__item-content {
|
|
212
|
+
flex-grow: 1;
|
|
213
|
+
width: 100%;
|
|
214
|
+
box-sizing: border-box;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.built-repeater__item-actions {
|
|
218
|
+
flex-shrink: 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
&.wpcl-repeater__item--collapsible {
|
|
222
|
+
flex-direction: column;
|
|
223
|
+
align-items: stretch;
|
|
224
|
+
gap: 0;
|
|
225
|
+
padding: 0;
|
|
226
|
+
|
|
227
|
+
.built-repeater__item-header {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
gap: 0.75rem;
|
|
231
|
+
padding: 0.75rem 1rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
&.wpcl-repeater__item--expanded .built-repeater__item-header {
|
|
235
|
+
border-bottom: 1px solid ${ADMIN_COLOR.border};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.built-repeater__summary {
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
flex-wrap: wrap;
|
|
242
|
+
gap: 0.75rem;
|
|
243
|
+
flex: 1 1 auto;
|
|
244
|
+
min-width: 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.built-repeater__summary-name {
|
|
248
|
+
flex-shrink: 1;
|
|
249
|
+
min-width: 0;
|
|
250
|
+
font-weight: 500;
|
|
251
|
+
overflow: hidden;
|
|
252
|
+
text-overflow: ellipsis;
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.built-repeater__item-header .components-toggle-control {
|
|
257
|
+
margin-bottom: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.built-repeater__item-toggle,
|
|
261
|
+
.built-repeater__item-handle,
|
|
262
|
+
.built-repeater__item-actions,
|
|
263
|
+
.built-repeater__drag-handle,
|
|
264
|
+
.built-repeater__remove-item {
|
|
265
|
+
flex-shrink: 0;
|
|
266
|
+
margin-top: 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.built-repeater__item-content {
|
|
270
|
+
padding: 1rem;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
`;
|
|
274
|
+
|
|
275
|
+
const StyledDragHandle = styled.div`
|
|
276
|
+
margin-top: 26px;
|
|
277
|
+
flex-shrink: 0;
|
|
278
|
+
`;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* restrictToParentElement, with the dragged rect's edges rebuilt from its size.
|
|
282
|
+
*
|
|
283
|
+
* When the drag overlay resizes after mounting (e.g. responsive columns
|
|
284
|
+
* settling), dnd-kit updates only its width/height and keeps the first
|
|
285
|
+
* bottom/right, so the item looks taller than it is and can't reach the
|
|
286
|
+
* last slot.
|
|
287
|
+
*/
|
|
288
|
+
const restrictToParent = (args) => {
|
|
289
|
+
const rect = args.draggingNodeRect;
|
|
290
|
+
|
|
291
|
+
return restrictToParentElement({
|
|
292
|
+
...args,
|
|
293
|
+
draggingNodeRect: rect
|
|
294
|
+
? {
|
|
295
|
+
...rect,
|
|
296
|
+
bottom: rect.top + rect.height,
|
|
297
|
+
right: rect.left + rect.width,
|
|
298
|
+
}
|
|
299
|
+
: rect,
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const itemChromeButtonProps = {
|
|
304
|
+
variant: "secondary",
|
|
305
|
+
iconSize: 20,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Context for repeater item data
|
|
310
|
+
*/
|
|
311
|
+
const RepeaterItemContext = createContext();
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Drag handle component that can be used within renderItem
|
|
315
|
+
*/
|
|
316
|
+
export const DragHandle = ({
|
|
317
|
+
label = __("Drag to reorder", "wp-component-library"),
|
|
318
|
+
...props
|
|
319
|
+
}) => {
|
|
320
|
+
const context = useContext(RepeaterItemContext);
|
|
321
|
+
|
|
322
|
+
if (!context) {
|
|
323
|
+
// eslint-disable-next-line no-console -- developer misuse warning.
|
|
324
|
+
console.warn("DragHandle must be used within a Repeater item");
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const { attributes, listeners, isDragging } = context;
|
|
329
|
+
|
|
330
|
+
// Escape cancels the drag (dnd-kit listens on the document); mark it
|
|
331
|
+
// handled so a surrounding Modal doesn't close as well.
|
|
332
|
+
const onKeyDown = (event) => {
|
|
333
|
+
if (isDragging && event.key === "Escape") {
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
}
|
|
336
|
+
listeners?.onKeyDown?.(event);
|
|
337
|
+
props.onKeyDown?.(event);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<StyledDragHandle className="built-repeater__drag-handle">
|
|
342
|
+
{/* The button is the drag control, so it isn't nested in another one. */}
|
|
343
|
+
<Button
|
|
344
|
+
icon={dragHandle}
|
|
345
|
+
label={label}
|
|
346
|
+
{...attributes}
|
|
347
|
+
{...listeners}
|
|
348
|
+
{...itemChromeButtonProps}
|
|
349
|
+
{...props}
|
|
350
|
+
onKeyDown={onKeyDown}
|
|
351
|
+
/>
|
|
352
|
+
</StyledDragHandle>
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Remove button component that can be used within renderItem
|
|
358
|
+
*/
|
|
359
|
+
export const RemoveButton = ({ label = __("Remove", "wp-component-library"), ...props }) => {
|
|
360
|
+
const context = useContext(RepeaterItemContext);
|
|
361
|
+
|
|
362
|
+
if (!context) {
|
|
363
|
+
// eslint-disable-next-line no-console -- developer misuse warning.
|
|
364
|
+
console.warn("RemoveButton must be used within a Repeater item");
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const { onRemove, id, canRemove } = context;
|
|
369
|
+
|
|
370
|
+
if (!canRemove) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<Button
|
|
376
|
+
className="built-repeater__remove-item"
|
|
377
|
+
label={label}
|
|
378
|
+
icon={trash}
|
|
379
|
+
isDestructive={true}
|
|
380
|
+
onClick={() => onRemove(id)}
|
|
381
|
+
{...itemChromeButtonProps}
|
|
382
|
+
{...props}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const ExpandButton = ({ isExpanded, onToggle }) => (
|
|
388
|
+
<Button
|
|
389
|
+
className="built-repeater__item-toggle"
|
|
390
|
+
icon={isExpanded ? chevronUp : chevronDown}
|
|
391
|
+
label={
|
|
392
|
+
isExpanded
|
|
393
|
+
? __("Collapse", "wp-component-library")
|
|
394
|
+
: __("Expand", "wp-component-library")
|
|
395
|
+
}
|
|
396
|
+
onClick={(event) => {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
event.stopPropagation();
|
|
399
|
+
onToggle?.();
|
|
400
|
+
}}
|
|
401
|
+
aria-expanded={isExpanded}
|
|
402
|
+
{...itemChromeButtonProps}
|
|
403
|
+
/>
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Sortable item component for the repeater
|
|
408
|
+
*/
|
|
409
|
+
const SortableItem = ({
|
|
410
|
+
id,
|
|
411
|
+
children,
|
|
412
|
+
onRemove,
|
|
413
|
+
canRemove,
|
|
414
|
+
isDragOverlay = false,
|
|
415
|
+
renderMode = "default",
|
|
416
|
+
enableReorder = true,
|
|
417
|
+
collapsible = false,
|
|
418
|
+
isExpanded = false,
|
|
419
|
+
onToggle,
|
|
420
|
+
summary = null,
|
|
421
|
+
}) => {
|
|
422
|
+
const {
|
|
423
|
+
attributes,
|
|
424
|
+
listeners,
|
|
425
|
+
setNodeRef,
|
|
426
|
+
transform,
|
|
427
|
+
transition,
|
|
428
|
+
isDragging,
|
|
429
|
+
} = useSortable({ id, disabled: !enableReorder });
|
|
430
|
+
|
|
431
|
+
const style = {
|
|
432
|
+
transform: CSS.Transform.toString(transform),
|
|
433
|
+
transition,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const dragProps = enableReorder ? { ...attributes, ...listeners } : {};
|
|
437
|
+
|
|
438
|
+
const contextValue = {
|
|
439
|
+
attributes: enableReorder ? attributes : {},
|
|
440
|
+
listeners: enableReorder ? listeners : {},
|
|
441
|
+
onRemove,
|
|
442
|
+
id,
|
|
443
|
+
canRemove,
|
|
444
|
+
isDragging,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const itemClassName = [
|
|
448
|
+
renderMode === "integrated" && !collapsible
|
|
449
|
+
? "wpcl-repeater__item--integrated"
|
|
450
|
+
: "",
|
|
451
|
+
collapsible ? "wpcl-repeater__item--collapsible" : "",
|
|
452
|
+
collapsible && isExpanded ? "wpcl-repeater__item--expanded" : "",
|
|
453
|
+
isDragging ? "wpcl-repeater__item--dragging" : "",
|
|
454
|
+
]
|
|
455
|
+
.filter(Boolean)
|
|
456
|
+
.join(" ");
|
|
457
|
+
|
|
458
|
+
if (collapsible) {
|
|
459
|
+
return (
|
|
460
|
+
<RepeaterItemContext.Provider value={contextValue}>
|
|
461
|
+
<StyledSortableItem
|
|
462
|
+
ref={setNodeRef}
|
|
463
|
+
style={style}
|
|
464
|
+
className={itemClassName}
|
|
465
|
+
>
|
|
466
|
+
<div className="built-repeater__item-header">
|
|
467
|
+
{enableReorder && <DragHandle />}
|
|
468
|
+
{summary ? (
|
|
469
|
+
<div className="built-repeater__summary">
|
|
470
|
+
{summary}
|
|
471
|
+
</div>
|
|
472
|
+
) : (
|
|
473
|
+
<div className="built-repeater__summary" />
|
|
474
|
+
)}
|
|
475
|
+
{!isDragOverlay && (
|
|
476
|
+
<ExpandButton
|
|
477
|
+
isExpanded={isExpanded}
|
|
478
|
+
onToggle={onToggle}
|
|
479
|
+
/>
|
|
480
|
+
)}
|
|
481
|
+
{!isDragOverlay && <RemoveButton />}
|
|
482
|
+
</div>
|
|
483
|
+
{isExpanded && children ? (
|
|
484
|
+
<div className="built-repeater__item-content">
|
|
485
|
+
{children}
|
|
486
|
+
</div>
|
|
487
|
+
) : null}
|
|
488
|
+
</StyledSortableItem>
|
|
489
|
+
</RepeaterItemContext.Provider>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (renderMode === "integrated") {
|
|
494
|
+
return (
|
|
495
|
+
<RepeaterItemContext.Provider value={contextValue}>
|
|
496
|
+
<StyledSortableItem
|
|
497
|
+
ref={setNodeRef}
|
|
498
|
+
style={style}
|
|
499
|
+
className={`wpcl-repeater__item--integrated ${isDragging ? "wpcl-repeater__item--dragging" : ""}`}
|
|
500
|
+
>
|
|
501
|
+
{children}
|
|
502
|
+
</StyledSortableItem>
|
|
503
|
+
</RepeaterItemContext.Provider>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Default mode with built-in controls
|
|
508
|
+
return (
|
|
509
|
+
<StyledSortableItem
|
|
510
|
+
ref={setNodeRef}
|
|
511
|
+
style={style}
|
|
512
|
+
className={isDragging ? "wpcl-repeater__item--dragging" : ""}
|
|
513
|
+
>
|
|
514
|
+
{enableReorder && (
|
|
515
|
+
<div className="built-repeater__item-handle" {...dragProps}>
|
|
516
|
+
<Button
|
|
517
|
+
icon={dragHandle}
|
|
518
|
+
label={__("Drag to reorder", "wp-component-library")}
|
|
519
|
+
tabIndex={-1}
|
|
520
|
+
{...itemChromeButtonProps}
|
|
521
|
+
/>
|
|
522
|
+
</div>
|
|
523
|
+
)}
|
|
524
|
+
<div className="built-repeater__item-content">{children}</div>
|
|
525
|
+
{!isDragOverlay && canRemove && (
|
|
526
|
+
<div className="built-repeater__item-actions">
|
|
527
|
+
<Button
|
|
528
|
+
label={__("Remove", "wp-component-library")}
|
|
529
|
+
icon={trash}
|
|
530
|
+
isDestructive={true}
|
|
531
|
+
onClick={() => onRemove(id)}
|
|
532
|
+
{...itemChromeButtonProps}
|
|
533
|
+
/>
|
|
534
|
+
</div>
|
|
535
|
+
)}
|
|
536
|
+
</StyledSortableItem>
|
|
537
|
+
);
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* A flexible repeater component with drag-and-drop functionality
|
|
542
|
+
*
|
|
543
|
+
* @param {Object} props
|
|
544
|
+
* @param {Array} props.items - Array of items to be rendered
|
|
545
|
+
* @param {Function} props.renderItem - Function to render each item's content
|
|
546
|
+
* @param {Function} props.onAdd - Callback when add button is clicked
|
|
547
|
+
* @param {Function} props.onRemove - Callback when remove button is clicked
|
|
548
|
+
* @param {Function} props.onReorder - Callback when items are reordered
|
|
549
|
+
* @param {string} props.className - Additional CSS classes
|
|
550
|
+
* @param {string} props.addButtonText - Text for the add button
|
|
551
|
+
* @param {string} props.emptyStateText - Text to show when list is empty
|
|
552
|
+
* @param {number} props.maxItems - Maximum number of items allowed
|
|
553
|
+
* @param {number} props.minItems - Minimum number of items required
|
|
554
|
+
* @param {string} props.renderMode - Control layout mode: "default" or "integrated"
|
|
555
|
+
* @param {boolean} props.enableReorder - Whether items can be dragged to reorder (default true).
|
|
556
|
+
* When false, no drag handle renders and items keep
|
|
557
|
+
* whatever order `items` is in — for lists where order
|
|
558
|
+
* doesn't matter (e.g. a set of find/replace pairs).
|
|
559
|
+
* @param {boolean} props.collapsible - When true, each item shows a header (optional
|
|
560
|
+
* `renderSummary` + chevron) and `renderItem` only while expanded.
|
|
561
|
+
* @param {Function} props.renderSummary - Header content always visible when `collapsible` is true.
|
|
562
|
+
* @param {boolean} props.defaultExpanded - When `collapsible`, existing items start open (default false).
|
|
563
|
+
* @param {boolean} props.expandNewItems - When `collapsible`, a newly added item starts open (default true).
|
|
564
|
+
*/
|
|
565
|
+
const Repeater = ({
|
|
566
|
+
items = [],
|
|
567
|
+
renderItem,
|
|
568
|
+
onAdd,
|
|
569
|
+
onRemove,
|
|
570
|
+
onReorder,
|
|
571
|
+
className = "",
|
|
572
|
+
addButtonText = __("Add Item", "wp-component-library"),
|
|
573
|
+
emptyStateText = __(
|
|
574
|
+
"No items added yet. Click the button below to add one.",
|
|
575
|
+
"wp-component-library",
|
|
576
|
+
),
|
|
577
|
+
maxItems = null,
|
|
578
|
+
minItems = 0,
|
|
579
|
+
renderMode = "default",
|
|
580
|
+
enableReorder = true,
|
|
581
|
+
collapsible = false,
|
|
582
|
+
renderSummary = null,
|
|
583
|
+
defaultExpanded = false,
|
|
584
|
+
expandNewItems = true,
|
|
585
|
+
}) => {
|
|
586
|
+
const [activeId, setActiveId] = useState(null);
|
|
587
|
+
const [expandedIds, setExpandedIds] = useState(() => {
|
|
588
|
+
if (!collapsible || !defaultExpanded) {
|
|
589
|
+
return new Set();
|
|
590
|
+
}
|
|
591
|
+
return new Set(items.map((item) => item.id));
|
|
592
|
+
});
|
|
593
|
+
const prevIdsRef = useRef(null);
|
|
594
|
+
const itemIdsKey = items.map((item) => item.id).join("\0");
|
|
595
|
+
|
|
596
|
+
useEffect(() => {
|
|
597
|
+
const currentIds = itemIdsKey ? itemIdsKey.split("\0") : [];
|
|
598
|
+
|
|
599
|
+
if (!collapsible) {
|
|
600
|
+
prevIdsRef.current = currentIds;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (prevIdsRef.current === null) {
|
|
605
|
+
prevIdsRef.current = currentIds;
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const prev = new Set(prevIdsRef.current);
|
|
610
|
+
const added = currentIds.filter((id) => !prev.has(id));
|
|
611
|
+
const currentIdSet = new Set(currentIds);
|
|
612
|
+
|
|
613
|
+
setExpandedIds((current) => {
|
|
614
|
+
const next = new Set();
|
|
615
|
+
current.forEach((id) => {
|
|
616
|
+
if (currentIdSet.has(id)) {
|
|
617
|
+
next.add(id);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
if (expandNewItems && added.length === 1) {
|
|
621
|
+
next.add(added[0]);
|
|
622
|
+
}
|
|
623
|
+
if (
|
|
624
|
+
next.size === current.size &&
|
|
625
|
+
[...next].every((id) => current.has(id))
|
|
626
|
+
) {
|
|
627
|
+
return current;
|
|
628
|
+
}
|
|
629
|
+
return next;
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
prevIdsRef.current = currentIds;
|
|
633
|
+
}, [collapsible, expandNewItems, itemIdsKey]);
|
|
634
|
+
|
|
635
|
+
const toggleExpanded = (id) => {
|
|
636
|
+
setExpandedIds((current) => {
|
|
637
|
+
const next = new Set(current);
|
|
638
|
+
if (next.has(id)) {
|
|
639
|
+
next.delete(id);
|
|
640
|
+
} else {
|
|
641
|
+
next.add(id);
|
|
642
|
+
}
|
|
643
|
+
return next;
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const sensors = useSensors(
|
|
648
|
+
useSensor(PointerSensor, {
|
|
649
|
+
activationConstraint: {
|
|
650
|
+
distance: 8,
|
|
651
|
+
},
|
|
652
|
+
}),
|
|
653
|
+
useSensor(KeyboardSensor, {
|
|
654
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
655
|
+
}),
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
const handleDragStart = (event) => {
|
|
659
|
+
if (!enableReorder) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
setActiveId(event.active.id);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const handleDragEnd = (event) => {
|
|
666
|
+
if (!enableReorder) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const { active, over } = event;
|
|
670
|
+
|
|
671
|
+
if (active.id !== over.id) {
|
|
672
|
+
const oldIndex = items.findIndex((item) => item.id === active.id);
|
|
673
|
+
const newIndex = items.findIndex((item) => item.id === over.id);
|
|
674
|
+
|
|
675
|
+
const newItems = arrayMove(items, oldIndex, newIndex);
|
|
676
|
+
onReorder(newItems);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
setActiveId(null);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const handleRemove = (id) => {
|
|
683
|
+
if (items.length > minItems) {
|
|
684
|
+
onRemove(id);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const canAddItem = maxItems === null || items.length < maxItems;
|
|
689
|
+
const canRemoveItem = items.length > minItems;
|
|
690
|
+
|
|
691
|
+
const activeItem = activeId
|
|
692
|
+
? items.find((item) => item.id === activeId)
|
|
693
|
+
: null;
|
|
694
|
+
const activeIndex = activeItem
|
|
695
|
+
? items.findIndex((item) => item.id === activeId)
|
|
696
|
+
: -1;
|
|
697
|
+
|
|
698
|
+
const repeaterClassName = [
|
|
699
|
+
className,
|
|
700
|
+
collapsible ? "wpcl-repeater--collapsible" : "",
|
|
701
|
+
]
|
|
702
|
+
.filter(Boolean)
|
|
703
|
+
.join(" ");
|
|
704
|
+
|
|
705
|
+
return (
|
|
706
|
+
<StyledRepeater className={repeaterClassName}>
|
|
707
|
+
<DndContext
|
|
708
|
+
sensors={sensors}
|
|
709
|
+
collisionDetection={closestCenter}
|
|
710
|
+
onDragStart={handleDragStart}
|
|
711
|
+
onDragEnd={handleDragEnd}
|
|
712
|
+
modifiers={[restrictToVerticalAxis, restrictToParent]}
|
|
713
|
+
>
|
|
714
|
+
{items.length === 0 ? (
|
|
715
|
+
<div className="built-repeater__empty-state">
|
|
716
|
+
{emptyStateText}
|
|
717
|
+
</div>
|
|
718
|
+
) : (
|
|
719
|
+
<SortableContext
|
|
720
|
+
items={items.map((item) => item.id)}
|
|
721
|
+
strategy={verticalListSortingStrategy}
|
|
722
|
+
>
|
|
723
|
+
<div className="built-repeater__items">
|
|
724
|
+
{items.map((item, index) => {
|
|
725
|
+
const isExpanded =
|
|
726
|
+
!collapsible || expandedIds.has(item.id);
|
|
727
|
+
|
|
728
|
+
return (
|
|
729
|
+
<SortableItem
|
|
730
|
+
key={item.id}
|
|
731
|
+
id={item.id}
|
|
732
|
+
onRemove={handleRemove}
|
|
733
|
+
canRemove={canRemoveItem}
|
|
734
|
+
renderMode={renderMode}
|
|
735
|
+
enableReorder={enableReorder}
|
|
736
|
+
collapsible={collapsible}
|
|
737
|
+
isExpanded={isExpanded}
|
|
738
|
+
onToggle={() => toggleExpanded(item.id)}
|
|
739
|
+
summary={
|
|
740
|
+
collapsible && renderSummary
|
|
741
|
+
? renderSummary(item, index)
|
|
742
|
+
: null
|
|
743
|
+
}
|
|
744
|
+
>
|
|
745
|
+
{isExpanded
|
|
746
|
+
? renderItem(item, index)
|
|
747
|
+
: null}
|
|
748
|
+
</SortableItem>
|
|
749
|
+
);
|
|
750
|
+
})}
|
|
751
|
+
</div>
|
|
752
|
+
</SortableContext>
|
|
753
|
+
)}
|
|
754
|
+
<DragOverlay>
|
|
755
|
+
{activeId && activeItem ? (
|
|
756
|
+
<div className="built-repeater__drag-overlay">
|
|
757
|
+
<SortableItem
|
|
758
|
+
id={activeId}
|
|
759
|
+
isDragOverlay={true}
|
|
760
|
+
renderMode={renderMode}
|
|
761
|
+
canRemove={canRemoveItem}
|
|
762
|
+
onRemove={handleRemove}
|
|
763
|
+
enableReorder={enableReorder}
|
|
764
|
+
collapsible={collapsible}
|
|
765
|
+
isExpanded={false}
|
|
766
|
+
summary={
|
|
767
|
+
collapsible && renderSummary
|
|
768
|
+
? renderSummary(activeItem, activeIndex)
|
|
769
|
+
: null
|
|
770
|
+
}
|
|
771
|
+
>
|
|
772
|
+
{collapsible
|
|
773
|
+
? null
|
|
774
|
+
: renderItem(activeItem, activeIndex)}
|
|
775
|
+
</SortableItem>
|
|
776
|
+
</div>
|
|
777
|
+
) : null}
|
|
778
|
+
</DragOverlay>
|
|
779
|
+
</DndContext>
|
|
780
|
+
{canAddItem && (
|
|
781
|
+
<div className="built-repeater__add-button">
|
|
782
|
+
<Button
|
|
783
|
+
variant="secondary"
|
|
784
|
+
icon={plusCircle}
|
|
785
|
+
iconPosition="left"
|
|
786
|
+
onClick={onAdd}
|
|
787
|
+
>
|
|
788
|
+
{addButtonText}
|
|
789
|
+
</Button>
|
|
790
|
+
</div>
|
|
791
|
+
)}
|
|
792
|
+
</StyledRepeater>
|
|
793
|
+
);
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
Repeater.propTypes = {
|
|
797
|
+
items: PropTypes.arrayOf(
|
|
798
|
+
PropTypes.shape({
|
|
799
|
+
id: PropTypes.string.isRequired,
|
|
800
|
+
}),
|
|
801
|
+
).isRequired,
|
|
802
|
+
renderItem: PropTypes.func.isRequired,
|
|
803
|
+
onAdd: PropTypes.func.isRequired,
|
|
804
|
+
onRemove: PropTypes.func.isRequired,
|
|
805
|
+
onReorder: (props, propName, componentName) => {
|
|
806
|
+
if (props.enableReorder === false) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
if (typeof props[propName] !== "function") {
|
|
810
|
+
return new Error(
|
|
811
|
+
`\`${propName}\` is required in \`${componentName}\` unless \`enableReorder\` is false.`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
},
|
|
816
|
+
className: PropTypes.string,
|
|
817
|
+
addButtonText: PropTypes.string,
|
|
818
|
+
emptyStateText: PropTypes.string,
|
|
819
|
+
maxItems: PropTypes.number,
|
|
820
|
+
minItems: PropTypes.number,
|
|
821
|
+
renderMode: PropTypes.oneOf(["default", "integrated"]),
|
|
822
|
+
enableReorder: PropTypes.bool,
|
|
823
|
+
collapsible: PropTypes.bool,
|
|
824
|
+
renderSummary: PropTypes.func,
|
|
825
|
+
defaultExpanded: PropTypes.bool,
|
|
826
|
+
expandNewItems: PropTypes.bool,
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
export { Repeater };
|