@siratik/wp-component-library 0.1.0 → 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 +51 -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 +16 -6
- package/editor.js +16 -0
- package/index.js +127 -0
- package/package.json +27 -16
- 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 -1233
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// WordPress dependencies.
|
|
2
|
+
import { store as blockEditorStore, Inserter } from "@wordpress/block-editor";
|
|
3
|
+
import { Button } from "@wordpress/components";
|
|
4
|
+
import { useSelect } from "@wordpress/data";
|
|
5
|
+
import { Icon, plus } from "@wordpress/icons";
|
|
6
|
+
|
|
7
|
+
import { __ } from "@wordpress/i18n";
|
|
8
|
+
|
|
9
|
+
function CustomBlockAppender({
|
|
10
|
+
rootClientId,
|
|
11
|
+
appenderTitle,
|
|
12
|
+
appenderLabel,
|
|
13
|
+
appenderClassModifier,
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<Inserter
|
|
17
|
+
__experimentalIsQuick
|
|
18
|
+
rootClientId={rootClientId}
|
|
19
|
+
renderToggle={({ onToggle, disabled }) => (
|
|
20
|
+
<Button
|
|
21
|
+
showTooltip={true}
|
|
22
|
+
className={`block-list-appender__toggle built-block-appender--${appenderClassModifier} `}
|
|
23
|
+
onClick={onToggle}
|
|
24
|
+
disabled={disabled}
|
|
25
|
+
label={appenderLabel}
|
|
26
|
+
icon={plus}
|
|
27
|
+
>
|
|
28
|
+
{appenderTitle}
|
|
29
|
+
</Button>
|
|
30
|
+
)}
|
|
31
|
+
isAppender
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CustomInspectorAppender({
|
|
37
|
+
rootClientId,
|
|
38
|
+
appenderTitle,
|
|
39
|
+
appenderLabel,
|
|
40
|
+
appenderClassModifier,
|
|
41
|
+
}) {
|
|
42
|
+
return (
|
|
43
|
+
<Inserter
|
|
44
|
+
__experimentalIsQuick
|
|
45
|
+
rootClientId={rootClientId}
|
|
46
|
+
renderToggle={({ onToggle, disabled }) => (
|
|
47
|
+
<Button
|
|
48
|
+
className={`built-block-appender built-block-appender--${appenderClassModifier} `}
|
|
49
|
+
onClick={onToggle}
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
label={appenderLabel}
|
|
52
|
+
variant="secondary"
|
|
53
|
+
>
|
|
54
|
+
<Icon icon={plus} />
|
|
55
|
+
{appenderTitle}
|
|
56
|
+
</Button>
|
|
57
|
+
)}
|
|
58
|
+
isAppender
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function CustomColumnAppender({
|
|
64
|
+
rootClientId,
|
|
65
|
+
appenderTitle,
|
|
66
|
+
appenderLabel,
|
|
67
|
+
appenderClassModifier,
|
|
68
|
+
}) {
|
|
69
|
+
return (
|
|
70
|
+
<Inserter
|
|
71
|
+
__experimentalIsQuick
|
|
72
|
+
rootClientId={rootClientId}
|
|
73
|
+
renderToggle={({ onToggle, disabled }) => (
|
|
74
|
+
<div className=" built-block-appender-column__wrapper">
|
|
75
|
+
<Button
|
|
76
|
+
showTooltip={true}
|
|
77
|
+
className={`block-list-appender__toggle built-block-appender-column__appender built-block-appender--${appenderClassModifier} `}
|
|
78
|
+
onClick={onToggle}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
label={appenderLabel}
|
|
81
|
+
icon={plus}
|
|
82
|
+
>
|
|
83
|
+
{appenderTitle}
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
isAppender
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const CustomInlineAppender = ({
|
|
93
|
+
clientId,
|
|
94
|
+
label = __("Add Block", "wp-component-library"),
|
|
95
|
+
}) => {
|
|
96
|
+
const isActive = useSelect(
|
|
97
|
+
(select) => {
|
|
98
|
+
const { isBlockSelected, hasSelectedInnerBlock } =
|
|
99
|
+
select(blockEditorStore);
|
|
100
|
+
return (
|
|
101
|
+
isBlockSelected(clientId) ||
|
|
102
|
+
hasSelectedInnerBlock(clientId, true)
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
[clientId],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!isActive) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Inserter
|
|
114
|
+
__experimentalIsQuick
|
|
115
|
+
rootClientId={clientId}
|
|
116
|
+
renderToggle={({ onToggle, disabled }) => (
|
|
117
|
+
<Button
|
|
118
|
+
className="block-list-appender__toggle"
|
|
119
|
+
icon={plus}
|
|
120
|
+
label={label}
|
|
121
|
+
onClick={onToggle}
|
|
122
|
+
disabled={disabled}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
isAppender
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
CustomBlockAppender,
|
|
132
|
+
CustomColumnAppender,
|
|
133
|
+
CustomInlineAppender,
|
|
134
|
+
CustomInspectorAppender,
|
|
135
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registerBlockExtension / unregisterBlockExtension
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for @10up/block-components registerBlockExtension.
|
|
5
|
+
* Fully backwards-compatible API — same arguments, same behaviour.
|
|
6
|
+
*
|
|
7
|
+
* Registers four WordPress hooks per extension:
|
|
8
|
+
* 1. blocks.registerBlockType — merges extra `attributes`
|
|
9
|
+
* 2. editor.BlockEdit — injects `Edit` controls (before | after block)
|
|
10
|
+
* 3. editor.BlockListBlock — applies `classNameGenerator` / `inlineStyleGenerator` in editor
|
|
11
|
+
* 4. blocks.getSaveContent.extraProps — applies class/style to saved HTML
|
|
12
|
+
*
|
|
13
|
+
* @param {string|string[]} blockName
|
|
14
|
+
* Block name(s) to extend. Pass `"*"` or `"all"` to match every block.
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {Object} [options.attributes={}] Extra attribute definitions.
|
|
17
|
+
* @param {Function} [options.classNameGenerator] (attributes) => string
|
|
18
|
+
* @param {Function} [options.inlineStyleGenerator] (attributes) => Object
|
|
19
|
+
* @param {Function} options.Edit React component for inspector controls.
|
|
20
|
+
* @param {string} options.extensionName Unique name used in filter namespaces.
|
|
21
|
+
* @param {'before'|'after'} [options.order='after'] Whether Edit renders before or after BlockEdit.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { addFilter, removeFilter } from "@wordpress/hooks";
|
|
25
|
+
import { createHigherOrderComponent } from "@wordpress/compose";
|
|
26
|
+
import { createElement, Fragment } from "@wordpress/element";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Minimal className utility — avoids adding clsx as a dependency.
|
|
30
|
+
*
|
|
31
|
+
* @param {...(string|null|undefined|false)} classes
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function cx(...classes) {
|
|
35
|
+
return classes.filter(Boolean).join(" ").trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function registerBlockExtension(
|
|
39
|
+
blockName,
|
|
40
|
+
{
|
|
41
|
+
attributes = {},
|
|
42
|
+
classNameGenerator = () => "",
|
|
43
|
+
inlineStyleGenerator,
|
|
44
|
+
Edit,
|
|
45
|
+
extensionName,
|
|
46
|
+
order = "after",
|
|
47
|
+
},
|
|
48
|
+
) {
|
|
49
|
+
const isArray = Array.isArray(blockName);
|
|
50
|
+
|
|
51
|
+
// Normalise wildcard
|
|
52
|
+
if (blockName === "*") { blockName = "all"; }
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true when `name` should be extended.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} name Block name.
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
const matches = (name) => {
|
|
61
|
+
if (blockName === "all") { return true; }
|
|
62
|
+
if (isArray) { return blockName.includes(name); }
|
|
63
|
+
return name === blockName;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Build a stable namespace segment for hook IDs.
|
|
67
|
+
const ns = isArray ? blockName.join("-") : blockName;
|
|
68
|
+
const prefix = `wpcl/${ns}/${extensionName}`;
|
|
69
|
+
|
|
70
|
+
// ── 1. Attributes ────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
addFilter(
|
|
73
|
+
"blocks.registerBlockType",
|
|
74
|
+
`${prefix}/addAttributesToBlock`,
|
|
75
|
+
(settings, name) => {
|
|
76
|
+
if (!matches(name)) { return settings; }
|
|
77
|
+
return {
|
|
78
|
+
...settings,
|
|
79
|
+
attributes: { ...settings.attributes, ...attributes },
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// ── 2. Editor controls (BlockEdit HOC) ───────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const withEdit = createHigherOrderComponent(
|
|
87
|
+
(BlockEdit) => (props) => {
|
|
88
|
+
const { name, isSelected } = props;
|
|
89
|
+
|
|
90
|
+
if (!matches(name)) { return createElement(BlockEdit, props); }
|
|
91
|
+
|
|
92
|
+
const before = order === "before" && isSelected;
|
|
93
|
+
const after = order === "after" && isSelected;
|
|
94
|
+
// Fallback: any unrecognised order value renders after (same as 10up)
|
|
95
|
+
const fallback = !before && !after && isSelected;
|
|
96
|
+
|
|
97
|
+
return createElement(
|
|
98
|
+
Fragment,
|
|
99
|
+
null,
|
|
100
|
+
before && createElement(Edit, props),
|
|
101
|
+
createElement(BlockEdit, props),
|
|
102
|
+
after && createElement(Edit, props),
|
|
103
|
+
fallback && createElement(Edit, props),
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
"addSettingsToBlock",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
addFilter("editor.BlockEdit", `${prefix}/addSettingsToBlock`, withEdit);
|
|
110
|
+
|
|
111
|
+
// ── 3. Class + style in editor (BlockListBlock HOC) ──────────────────────
|
|
112
|
+
|
|
113
|
+
const withBlockListBlock = createHigherOrderComponent(
|
|
114
|
+
(BlockListBlock) => (props) => {
|
|
115
|
+
const {
|
|
116
|
+
name,
|
|
117
|
+
attributes: blockAttrs,
|
|
118
|
+
className = "",
|
|
119
|
+
style = {},
|
|
120
|
+
wrapperProps,
|
|
121
|
+
} = props;
|
|
122
|
+
|
|
123
|
+
if (!matches(name)) { return createElement(BlockListBlock, props); }
|
|
124
|
+
|
|
125
|
+
const generatedClass = classNameGenerator(blockAttrs);
|
|
126
|
+
|
|
127
|
+
let mergedStyle = { ...style };
|
|
128
|
+
let generatedStyle = null;
|
|
129
|
+
|
|
130
|
+
if (typeof inlineStyleGenerator === "function") {
|
|
131
|
+
generatedStyle = inlineStyleGenerator(blockAttrs);
|
|
132
|
+
mergedStyle = {
|
|
133
|
+
...style,
|
|
134
|
+
...(wrapperProps?.style ?? {}),
|
|
135
|
+
...generatedStyle,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!generatedClass && !generatedStyle) {
|
|
140
|
+
return createElement(BlockListBlock, props);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const mergedClass = cx(className, generatedClass);
|
|
144
|
+
|
|
145
|
+
return createElement(BlockListBlock, {
|
|
146
|
+
...props,
|
|
147
|
+
className: mergedClass,
|
|
148
|
+
wrapperProps: { ...wrapperProps, style: mergedStyle },
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
"addAdditionalPropertiesInEditor",
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
addFilter(
|
|
155
|
+
"editor.BlockListBlock",
|
|
156
|
+
`${prefix}/addAdditionalPropertiesInEditor`,
|
|
157
|
+
withBlockListBlock,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// ── 4. Class + style in saved HTML ───────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
addFilter(
|
|
163
|
+
"blocks.getSaveContent.extraProps",
|
|
164
|
+
`${prefix}/addAdditionalPropertiesToSavedMarkup`,
|
|
165
|
+
(extraProps, blockType, blockAttrs) => {
|
|
166
|
+
const { className = "", style = {} } = extraProps;
|
|
167
|
+
|
|
168
|
+
if (!matches(blockType.name)) { return extraProps; }
|
|
169
|
+
|
|
170
|
+
const generatedClass = classNameGenerator(blockAttrs);
|
|
171
|
+
|
|
172
|
+
let mergedStyle = { ...style };
|
|
173
|
+
let generatedStyle = null;
|
|
174
|
+
|
|
175
|
+
if (typeof inlineStyleGenerator === "function") {
|
|
176
|
+
generatedStyle = inlineStyleGenerator(blockAttrs);
|
|
177
|
+
mergedStyle = { ...style, ...generatedStyle };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!generatedClass && !generatedStyle) { return extraProps; }
|
|
181
|
+
|
|
182
|
+
const mergedClass = cx(className, generatedClass);
|
|
183
|
+
|
|
184
|
+
return { ...extraProps, className: mergedClass, style: mergedStyle };
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Remove a previously registered block extension.
|
|
191
|
+
*
|
|
192
|
+
* @param {string|string[]} blockName Same value passed to registerBlockExtension.
|
|
193
|
+
* @param {string} extensionName Same extensionName passed to registerBlockExtension.
|
|
194
|
+
*/
|
|
195
|
+
export function unregisterBlockExtension(blockName, extensionName) {
|
|
196
|
+
if (!blockName || !extensionName) { return; }
|
|
197
|
+
|
|
198
|
+
if (blockName === "*") { blockName = "all"; }
|
|
199
|
+
|
|
200
|
+
const isArray = Array.isArray(blockName);
|
|
201
|
+
const ns = isArray ? blockName.join("-") : blockName;
|
|
202
|
+
const prefix = `wpcl/${ns}/${extensionName}`;
|
|
203
|
+
|
|
204
|
+
removeFilter("blocks.registerBlockType", `${prefix}/addAttributesToBlock`);
|
|
205
|
+
removeFilter("editor.BlockEdit", `${prefix}/addSettingsToBlock`);
|
|
206
|
+
removeFilter(
|
|
207
|
+
"editor.BlockListBlock",
|
|
208
|
+
`${prefix}/addAdditionalPropertiesInEditor`,
|
|
209
|
+
);
|
|
210
|
+
removeFilter(
|
|
211
|
+
"blocks.getSaveContent.extraProps",
|
|
212
|
+
`${prefix}/addAdditionalPropertiesToSavedMarkup`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holiday / special hours list: date ranges (MM-DD) that are closed or have
|
|
3
|
+
* their own hours. Controlled: `entries` is the special_opening_hours array,
|
|
4
|
+
* `onChange` receives the whole next array.
|
|
5
|
+
*
|
|
6
|
+
* @package Siratik/WP-Component-Library
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Flex,
|
|
11
|
+
FlexBlock,
|
|
12
|
+
FlexItem,
|
|
13
|
+
TextControl,
|
|
14
|
+
TimePicker,
|
|
15
|
+
ToggleControl,
|
|
16
|
+
} from "@wordpress/components";
|
|
17
|
+
import { useState } from "@wordpress/element";
|
|
18
|
+
import { __ } from "@wordpress/i18n";
|
|
19
|
+
import { DragHandle, RemoveButton, Repeater } from "../repeater";
|
|
20
|
+
|
|
21
|
+
function generateId() {
|
|
22
|
+
return Math.random().toString(36).slice(2, 10);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function timeStringToObject(str) {
|
|
26
|
+
if (!str) {
|
|
27
|
+
return { hours: 9, minutes: 0 };
|
|
28
|
+
}
|
|
29
|
+
const [h, m] = str.split(":");
|
|
30
|
+
return { hours: parseInt(h, 10), minutes: parseInt(m, 10) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function timeObjectToString(obj) {
|
|
34
|
+
if (!obj || typeof obj.hours !== "number") {
|
|
35
|
+
return "09:00";
|
|
36
|
+
}
|
|
37
|
+
return `${String(obj.hours).padStart(2, "0")}:${String(obj.minutes ?? 0).padStart(2, "0")}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A single holiday row rendered inside the Repeater.
|
|
42
|
+
*/
|
|
43
|
+
function HolidayRow({ item, onUpdate, is24HourFormat = false }) {
|
|
44
|
+
const [openTime, setOpenTime] = useState(() =>
|
|
45
|
+
timeStringToObject(item.opens || "09:00"),
|
|
46
|
+
);
|
|
47
|
+
const [closeTime, setCloseTime] = useState(() =>
|
|
48
|
+
timeStringToObject(item.closes || "17:00"),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const update = (field, value) => onUpdate({ ...item, [field]: value });
|
|
52
|
+
|
|
53
|
+
const handleOpenChange = (obj) => {
|
|
54
|
+
setOpenTime(obj);
|
|
55
|
+
update("opens", timeObjectToString(obj));
|
|
56
|
+
};
|
|
57
|
+
const handleCloseChange = (obj) => {
|
|
58
|
+
setCloseTime(obj);
|
|
59
|
+
update("closes", timeObjectToString(obj));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Flex gap={3} align="start" wrap>
|
|
64
|
+
<DragHandle />
|
|
65
|
+
|
|
66
|
+
<FlexBlock style={{ minWidth: "130px" }}>
|
|
67
|
+
<TextControl
|
|
68
|
+
__next40pxDefaultSize
|
|
69
|
+
__nextHasNoMarginBottom
|
|
70
|
+
label={__("Label", "wp-component-library")}
|
|
71
|
+
help={__("e.g. Christmas Day", "wp-component-library")}
|
|
72
|
+
value={item.label ?? ""}
|
|
73
|
+
onChange={(v) => update("label", v)}
|
|
74
|
+
/>
|
|
75
|
+
</FlexBlock>
|
|
76
|
+
|
|
77
|
+
<FlexBlock style={{ minWidth: "110px" }}>
|
|
78
|
+
<TextControl
|
|
79
|
+
__next40pxDefaultSize
|
|
80
|
+
__nextHasNoMarginBottom
|
|
81
|
+
label={__("From (MM-DD)", "wp-component-library")}
|
|
82
|
+
// eslint-disable-next-line @wordpress/i18n-hyphenated-range -- MM-DD date, not a range.
|
|
83
|
+
help={__("e.g. 12-25", "wp-component-library")}
|
|
84
|
+
value={item.date_from ?? ""}
|
|
85
|
+
onChange={(v) => update("date_from", v)}
|
|
86
|
+
placeholder="12-25"
|
|
87
|
+
maxLength={5}
|
|
88
|
+
/>
|
|
89
|
+
</FlexBlock>
|
|
90
|
+
|
|
91
|
+
<FlexBlock style={{ minWidth: "110px" }}>
|
|
92
|
+
<TextControl
|
|
93
|
+
__next40pxDefaultSize
|
|
94
|
+
__nextHasNoMarginBottom
|
|
95
|
+
label={__("To (MM-DD)", "wp-component-library")}
|
|
96
|
+
// eslint-disable-next-line @wordpress/i18n-hyphenated-range -- MM-DD date, not a range.
|
|
97
|
+
help={__("e.g. 12-25", "wp-component-library")}
|
|
98
|
+
value={item.date_to ?? ""}
|
|
99
|
+
onChange={(v) => update("date_to", v)}
|
|
100
|
+
placeholder="12-25"
|
|
101
|
+
maxLength={5}
|
|
102
|
+
/>
|
|
103
|
+
</FlexBlock>
|
|
104
|
+
|
|
105
|
+
<FlexItem style={{ minWidth: "90px", paddingTop: "28px" }}>
|
|
106
|
+
<ToggleControl
|
|
107
|
+
__nextHasNoMarginBottom
|
|
108
|
+
label={__("Closed", "wp-component-library")}
|
|
109
|
+
checked={!!item.closed}
|
|
110
|
+
onChange={(v) => update("closed", v)}
|
|
111
|
+
/>
|
|
112
|
+
</FlexItem>
|
|
113
|
+
|
|
114
|
+
<FlexItem style={{ paddingTop: "26px" }}>
|
|
115
|
+
{item.closed ? (
|
|
116
|
+
<span style={{ color: "#757575", fontStyle: "italic" }}>
|
|
117
|
+
{__("Closed all day", "wp-component-library")}
|
|
118
|
+
</span>
|
|
119
|
+
) : (
|
|
120
|
+
<Flex align="center" gap={1}>
|
|
121
|
+
<TimePicker.TimeInput
|
|
122
|
+
value={openTime}
|
|
123
|
+
onChange={handleOpenChange}
|
|
124
|
+
is12Hour={!is24HourFormat}
|
|
125
|
+
/>
|
|
126
|
+
<span style={{ color: "#757575" }}>–</span>
|
|
127
|
+
<TimePicker.TimeInput
|
|
128
|
+
value={closeTime}
|
|
129
|
+
onChange={handleCloseChange}
|
|
130
|
+
is12Hour={!is24HourFormat}
|
|
131
|
+
/>
|
|
132
|
+
</Flex>
|
|
133
|
+
)}
|
|
134
|
+
</FlexItem>
|
|
135
|
+
|
|
136
|
+
<RemoveButton />
|
|
137
|
+
</Flex>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {Object} props
|
|
143
|
+
* @param {Array} props.entries Special hours entries.
|
|
144
|
+
* @param {Function} props.onChange Receives the next entries array.
|
|
145
|
+
* @param {boolean} [props.is24HourFormat]
|
|
146
|
+
*/
|
|
147
|
+
export function SpecialHoursControl({
|
|
148
|
+
entries,
|
|
149
|
+
onChange,
|
|
150
|
+
is24HourFormat = false,
|
|
151
|
+
}) {
|
|
152
|
+
const safeEntries = Array.isArray(entries) ? entries : [];
|
|
153
|
+
|
|
154
|
+
const handleAdd = () => {
|
|
155
|
+
onChange([
|
|
156
|
+
...safeEntries,
|
|
157
|
+
{
|
|
158
|
+
id: generateId(),
|
|
159
|
+
label: "",
|
|
160
|
+
date_from: "",
|
|
161
|
+
date_to: "",
|
|
162
|
+
closed: true,
|
|
163
|
+
opens: "09:00",
|
|
164
|
+
closes: "17:00",
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleRemove = (id) => {
|
|
170
|
+
onChange(safeEntries.filter((e) => e.id !== id));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleReorder = (reordered) => {
|
|
174
|
+
onChange(reordered);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const handleUpdate = (id, updated) => {
|
|
178
|
+
onChange(safeEntries.map((e) => (e.id === id ? updated : e)));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="special-hours-section">
|
|
183
|
+
<h4 style={{ marginBottom: "0.75rem", marginTop: 0 }}>
|
|
184
|
+
{__("Holiday / Special Hours", "wp-component-library")}
|
|
185
|
+
</h4>
|
|
186
|
+
<Repeater
|
|
187
|
+
items={safeEntries}
|
|
188
|
+
renderItem={(item) => (
|
|
189
|
+
<HolidayRow
|
|
190
|
+
item={item}
|
|
191
|
+
is24HourFormat={is24HourFormat}
|
|
192
|
+
onUpdate={(updated) =>
|
|
193
|
+
handleUpdate(item.id, updated)
|
|
194
|
+
}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
onAdd={handleAdd}
|
|
198
|
+
onRemove={handleRemove}
|
|
199
|
+
onReorder={handleReorder}
|
|
200
|
+
renderMode="integrated"
|
|
201
|
+
addButtonText={__("Add exception", "wp-component-library")}
|
|
202
|
+
emptyStateText={__(
|
|
203
|
+
"No special hours set. Add exceptions for holidays, early closings, or special events.",
|
|
204
|
+
"wp-component-library",
|
|
205
|
+
)}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default SpecialHoursControl;
|