@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,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SelectControl,
|
|
3
|
+
__experimentalToolsPanelItem as ToolsPanelItem,
|
|
4
|
+
} from "@wordpress/components";
|
|
5
|
+
import { __ } from "@wordpress/i18n";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default order options - can be imported and used as a base
|
|
9
|
+
*/
|
|
10
|
+
export const defaultOrderOptions = [
|
|
11
|
+
{ label: "Custom Order", value: "menu_order_asc" },
|
|
12
|
+
{ label: "Date (Newest → Oldest)", value: "date_desc" },
|
|
13
|
+
{ label: "Date (Oldest → Newest)", value: "date_asc" },
|
|
14
|
+
{ label: "Title (A → Z)", value: "title_asc" },
|
|
15
|
+
{ label: "Title (Z → A)", value: "title_desc" },
|
|
16
|
+
{ label: "Random", value: "rand_asc" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Standalone order by selection control
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} props Component props
|
|
23
|
+
* @param {Object} props.attributes Block attributes
|
|
24
|
+
* @param {Function} props.setAttributes Function to set attributes
|
|
25
|
+
* @param {Array} props.orderOptions Custom order options (replaces defaults if provided)
|
|
26
|
+
* @param {Array} props.additionalOrderOptions Additional options to add to defaults (deprecated, use orderOptions instead)
|
|
27
|
+
* @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
|
|
28
|
+
* @returns {WPElement} Element to render
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Split an order option value ("menu_order_asc") into orderby and order at
|
|
32
|
+
* its last underscore: orderby values can hold underscores themselves.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} value Option value, `${orderby}_${order}`.
|
|
35
|
+
* @return {{ orderPostsBy: string, orderPostsDirection: string }} Attributes.
|
|
36
|
+
*/
|
|
37
|
+
export function parseOrderValue(value) {
|
|
38
|
+
const at = value.lastIndexOf("_");
|
|
39
|
+
return {
|
|
40
|
+
orderPostsBy: value.slice(0, at),
|
|
41
|
+
orderPostsDirection: value.slice(at + 1),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function OrderBySelect({
|
|
46
|
+
attributes: { orderPostsBy, orderPostsDirection },
|
|
47
|
+
setAttributes,
|
|
48
|
+
orderOptions,
|
|
49
|
+
additionalOrderOptions = [], // Keep for backward compatibility
|
|
50
|
+
isShownByDefault = true,
|
|
51
|
+
}) {
|
|
52
|
+
const defaultOrderPostsBy = "date";
|
|
53
|
+
const defaultOrderPostsDirection = "desc";
|
|
54
|
+
|
|
55
|
+
// Determine which options to use
|
|
56
|
+
let finalOptions;
|
|
57
|
+
if (orderOptions) {
|
|
58
|
+
// Use custom options if provided
|
|
59
|
+
finalOptions = orderOptions;
|
|
60
|
+
} else if (additionalOrderOptions.length > 0) {
|
|
61
|
+
// Use defaults + additional for backward compatibility
|
|
62
|
+
finalOptions = [...defaultOrderOptions, ...additionalOrderOptions];
|
|
63
|
+
} else {
|
|
64
|
+
// Use defaults
|
|
65
|
+
finalOptions = defaultOrderOptions;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ToolsPanelItem
|
|
70
|
+
hasValue={() =>
|
|
71
|
+
orderPostsBy !== defaultOrderPostsBy ||
|
|
72
|
+
orderPostsDirection !== defaultOrderPostsDirection
|
|
73
|
+
}
|
|
74
|
+
label={__("Order By", "built_starter")}
|
|
75
|
+
onDeselect={() =>
|
|
76
|
+
setAttributes({
|
|
77
|
+
orderPostsBy: defaultOrderPostsBy,
|
|
78
|
+
orderPostsDirection: defaultOrderPostsDirection,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
isShownByDefault={isShownByDefault}
|
|
82
|
+
>
|
|
83
|
+
<SelectControl
|
|
84
|
+
__nextHasNoMarginBottom={true}
|
|
85
|
+
__next40pxDefaultSize
|
|
86
|
+
label="Order By"
|
|
87
|
+
value={`${orderPostsBy}_${orderPostsDirection}`}
|
|
88
|
+
options={finalOptions}
|
|
89
|
+
onChange={(value) => setAttributes(parseOrderValue(value))}
|
|
90
|
+
/>
|
|
91
|
+
</ToolsPanelItem>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { OrderBySelect };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SelectControl,
|
|
3
|
+
__experimentalToolsPanelItem as ToolsPanelItem,
|
|
4
|
+
} from "@wordpress/components";
|
|
5
|
+
import { useSelect } from "@wordpress/data";
|
|
6
|
+
import { __ } from "@wordpress/i18n";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Standalone post type selection control
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} props Component props
|
|
12
|
+
* @param {Object} props.attributes Block attributes
|
|
13
|
+
* @param {Function} props.setAttributes Function to set attributes
|
|
14
|
+
* @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
|
|
15
|
+
* @returns {WPElement} Element to render
|
|
16
|
+
*/
|
|
17
|
+
function PostTypeSelect({
|
|
18
|
+
attributes: { postType },
|
|
19
|
+
setAttributes,
|
|
20
|
+
isShownByDefault = true,
|
|
21
|
+
}) {
|
|
22
|
+
const defaultPostType = "post";
|
|
23
|
+
|
|
24
|
+
const getPostTypeList = useSelect((select) =>
|
|
25
|
+
select("core").getPostTypes({ per_page: 20 }),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const getPostTypesOptions = () => {
|
|
29
|
+
let filteredPostTypes = [];
|
|
30
|
+
let postTypesOptions = [];
|
|
31
|
+
|
|
32
|
+
if (getPostTypeList) {
|
|
33
|
+
filteredPostTypes = getPostTypeList.filter(
|
|
34
|
+
(type) =>
|
|
35
|
+
type.viewable === true &&
|
|
36
|
+
type.rest_base !== "media" &&
|
|
37
|
+
type.rest_base !== "pages",
|
|
38
|
+
);
|
|
39
|
+
postTypesOptions = filteredPostTypes.map((type) => ({
|
|
40
|
+
label: type.name,
|
|
41
|
+
value: type.slug,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return postTypesOptions;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<ToolsPanelItem
|
|
50
|
+
hasValue={() => postType !== defaultPostType}
|
|
51
|
+
label={__("Select Post Type", "built_starter")}
|
|
52
|
+
onDeselect={() => setAttributes({ postType: defaultPostType })}
|
|
53
|
+
isShownByDefault={isShownByDefault}
|
|
54
|
+
>
|
|
55
|
+
<SelectControl
|
|
56
|
+
__nextHasNoMarginBottom={true}
|
|
57
|
+
__next40pxDefaultSize
|
|
58
|
+
label={__("Select Post Type", "built_starter")}
|
|
59
|
+
onChange={(value) => setAttributes({ postType: value })}
|
|
60
|
+
options={getPostTypesOptions()}
|
|
61
|
+
value={postType}
|
|
62
|
+
/>
|
|
63
|
+
</ToolsPanelItem>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { PostTypeSelect };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RangeControl,
|
|
3
|
+
__experimentalToolsPanelItem as ToolsPanelItem,
|
|
4
|
+
} from "@wordpress/components";
|
|
5
|
+
import { __ } from "@wordpress/i18n";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Standalone posts per page control
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} props Component props
|
|
11
|
+
* @param {Object} props.attributes Block attributes
|
|
12
|
+
* @param {Function} props.setAttributes Function to set attributes
|
|
13
|
+
* @param {string} props.label Label for the control
|
|
14
|
+
* @param {number} props.min Minimum value
|
|
15
|
+
* @param {number} props.max Maximum value
|
|
16
|
+
* @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: false)
|
|
17
|
+
* @param {number} props.defaultValue The block's default count (default: 6)
|
|
18
|
+
* @returns {WPElement} Element to render
|
|
19
|
+
*/
|
|
20
|
+
function PostsPerPageControl({
|
|
21
|
+
attributes: { postsPerPage },
|
|
22
|
+
setAttributes,
|
|
23
|
+
label = __("Amount to display", "wp-component-library"),
|
|
24
|
+
min = 1,
|
|
25
|
+
max = 50,
|
|
26
|
+
isShownByDefault = false,
|
|
27
|
+
defaultValue = 6,
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<ToolsPanelItem
|
|
31
|
+
hasValue={() => postsPerPage !== defaultValue}
|
|
32
|
+
label={label}
|
|
33
|
+
onDeselect={() => setAttributes({ postsPerPage: defaultValue })}
|
|
34
|
+
isShownByDefault={isShownByDefault}
|
|
35
|
+
>
|
|
36
|
+
<RangeControl
|
|
37
|
+
__nextHasNoMarginBottom={true}
|
|
38
|
+
__next40pxDefaultSize
|
|
39
|
+
label={label}
|
|
40
|
+
value={postsPerPage}
|
|
41
|
+
onChange={(postsPerPageNew) =>
|
|
42
|
+
setAttributes({ postsPerPage: postsPerPageNew })
|
|
43
|
+
}
|
|
44
|
+
min={min}
|
|
45
|
+
max={max}
|
|
46
|
+
/>
|
|
47
|
+
</ToolsPanelItem>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { PostsPerPageControl };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__experimentalToggleGroupControl as ToggleGroupControl,
|
|
3
|
+
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
|
4
|
+
__experimentalToolsPanelItem as ToolsPanelItem,
|
|
5
|
+
} from "@wordpress/components";
|
|
6
|
+
import { __ } from "@wordpress/i18n";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Standalone selection mode control (auto vs manual)
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} props Component props
|
|
12
|
+
* @param {Object} props.attributes Block attributes
|
|
13
|
+
* @param {Function} props.setAttributes Function to set attributes
|
|
14
|
+
* @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
|
|
15
|
+
* @returns {WPElement} Element to render
|
|
16
|
+
*/
|
|
17
|
+
function SelectionModeControl({
|
|
18
|
+
attributes: { selectionMode = "auto" },
|
|
19
|
+
setAttributes,
|
|
20
|
+
isShownByDefault = true,
|
|
21
|
+
}) {
|
|
22
|
+
const defaultSelectionMode = "auto";
|
|
23
|
+
|
|
24
|
+
const handleSelectionModeChange = (value) => {
|
|
25
|
+
setAttributes({
|
|
26
|
+
selectionMode: value,
|
|
27
|
+
// Preserve existing selections when switching modes
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<ToolsPanelItem
|
|
33
|
+
hasValue={() => selectionMode !== defaultSelectionMode}
|
|
34
|
+
label={__("Selection Mode", "wp-component-library")}
|
|
35
|
+
onDeselect={() =>
|
|
36
|
+
setAttributes({ selectionMode: defaultSelectionMode })
|
|
37
|
+
}
|
|
38
|
+
isShownByDefault={isShownByDefault}
|
|
39
|
+
>
|
|
40
|
+
<ToggleGroupControl
|
|
41
|
+
__next40pxDefaultSize={true}
|
|
42
|
+
__nextHasNoMarginBottom
|
|
43
|
+
label={__("Selection Mode", "wp-component-library")}
|
|
44
|
+
value={selectionMode}
|
|
45
|
+
onChange={handleSelectionModeChange}
|
|
46
|
+
isBlock
|
|
47
|
+
>
|
|
48
|
+
<ToggleGroupControlOption
|
|
49
|
+
label={__("Auto", "wp-component-library")}
|
|
50
|
+
value="auto"
|
|
51
|
+
/>
|
|
52
|
+
<ToggleGroupControlOption
|
|
53
|
+
label={__("Manual", "wp-component-library")}
|
|
54
|
+
value="manual"
|
|
55
|
+
/>
|
|
56
|
+
</ToggleGroupControl>
|
|
57
|
+
</ToolsPanelItem>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { SelectionModeControl };
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Standalone taxonomy selection control
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} props Component props
|
|
13
|
+
* @param {Object} props.attributes Block attributes
|
|
14
|
+
* @param {Function} props.setAttributes Function to set attributes
|
|
15
|
+
* @param {string} props.postType The post type to get taxonomies for
|
|
16
|
+
* @param {boolean} props.showNoneOption Whether to show a "none" option (default: false)
|
|
17
|
+
* @param {string} props.noneOptionLabel Label for the "none" option (default: "No Taxonomy Filter")
|
|
18
|
+
* @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: false)
|
|
19
|
+
* @returns {WPElement} Element to render
|
|
20
|
+
*/
|
|
21
|
+
function TaxonomySelect({
|
|
22
|
+
attributes: { selectedTaxonomy },
|
|
23
|
+
setAttributes,
|
|
24
|
+
postType,
|
|
25
|
+
showNoneOption = false,
|
|
26
|
+
noneOptionLabel = "No Taxonomy Filter",
|
|
27
|
+
isShownByDefault = false,
|
|
28
|
+
}) {
|
|
29
|
+
const defaultSelectedTaxonomy = "";
|
|
30
|
+
|
|
31
|
+
// Get all taxonomies (fetch once, cache the result)
|
|
32
|
+
const allTaxonomies = useSelect((select) => {
|
|
33
|
+
return select("core").getTaxonomies({
|
|
34
|
+
per_page: -1,
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
// Memoize the filtered taxonomies to prevent unnecessary re-renders
|
|
39
|
+
const filteredTaxonomies = useMemo(() => {
|
|
40
|
+
if (!postType || !allTaxonomies) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return allTaxonomies.filter((tax) => tax.types.includes(postType));
|
|
44
|
+
}, [allTaxonomies, postType]);
|
|
45
|
+
|
|
46
|
+
// Auto-select first taxonomy when post type changes (only if showNoneOption is false)
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (filteredTaxonomies.length > 0) {
|
|
49
|
+
// Only set if current selection is empty or not in the filtered list
|
|
50
|
+
const isCurrentSelectionValid =
|
|
51
|
+
selectedTaxonomy === "" || // Empty is always valid when showNoneOption is true
|
|
52
|
+
filteredTaxonomies.some((tax) => tax.slug === selectedTaxonomy);
|
|
53
|
+
|
|
54
|
+
if (!isCurrentSelectionValid) {
|
|
55
|
+
// If showNoneOption is true, default to empty, otherwise select first taxonomy
|
|
56
|
+
const defaultValue = showNoneOption
|
|
57
|
+
? ""
|
|
58
|
+
: filteredTaxonomies[0].slug;
|
|
59
|
+
setAttributes({
|
|
60
|
+
selectedTaxonomy: defaultValue,
|
|
61
|
+
selectedTerms: [], // Clear terms when taxonomy changes
|
|
62
|
+
});
|
|
63
|
+
} else if (
|
|
64
|
+
!showNoneOption &&
|
|
65
|
+
!selectedTaxonomy &&
|
|
66
|
+
filteredTaxonomies.length > 0
|
|
67
|
+
) {
|
|
68
|
+
// If showNoneOption is false and no selection, select first taxonomy
|
|
69
|
+
setAttributes({
|
|
70
|
+
selectedTaxonomy: filteredTaxonomies[0].slug,
|
|
71
|
+
selectedTerms: [],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
} else if (filteredTaxonomies.length === 0 && selectedTaxonomy) {
|
|
75
|
+
// Clear selection if no taxonomies available
|
|
76
|
+
setAttributes({
|
|
77
|
+
selectedTaxonomy: defaultSelectedTaxonomy,
|
|
78
|
+
selectedTerms: [],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}, [filteredTaxonomies, selectedTaxonomy, setAttributes, showNoneOption]);
|
|
82
|
+
|
|
83
|
+
// Memoize taxonomy options to prevent recreation
|
|
84
|
+
const taxonomyOptions = useMemo(() => {
|
|
85
|
+
return filteredTaxonomies.map((tax) => ({
|
|
86
|
+
label: tax.labels.singular_name || tax.name,
|
|
87
|
+
value: tax.slug,
|
|
88
|
+
}));
|
|
89
|
+
}, [filteredTaxonomies]);
|
|
90
|
+
|
|
91
|
+
// Memoize all options to prevent recreation
|
|
92
|
+
const allOptions = useMemo(() => {
|
|
93
|
+
return showNoneOption
|
|
94
|
+
? [{ label: noneOptionLabel, value: "" }, ...taxonomyOptions]
|
|
95
|
+
: taxonomyOptions;
|
|
96
|
+
}, [showNoneOption, noneOptionLabel, taxonomyOptions]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ToolsPanelItem
|
|
100
|
+
hasValue={() => selectedTaxonomy !== defaultSelectedTaxonomy}
|
|
101
|
+
label={__("Select Taxonomy", "built")}
|
|
102
|
+
onDeselect={() =>
|
|
103
|
+
setAttributes({
|
|
104
|
+
selectedTaxonomy: defaultSelectedTaxonomy,
|
|
105
|
+
selectedTerms: [],
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
isShownByDefault={isShownByDefault}
|
|
109
|
+
>
|
|
110
|
+
<SelectControl
|
|
111
|
+
__nextHasNoMarginBottom={true}
|
|
112
|
+
__next40pxDefaultSize
|
|
113
|
+
label={__("Select Taxonomy", "built")}
|
|
114
|
+
value={selectedTaxonomy}
|
|
115
|
+
options={allOptions}
|
|
116
|
+
onChange={(value) =>
|
|
117
|
+
setAttributes({
|
|
118
|
+
selectedTaxonomy: value,
|
|
119
|
+
selectedTerms: [], // Clear terms when taxonomy changes
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
disabled={!postType || filteredTaxonomies.length === 0}
|
|
123
|
+
help={
|
|
124
|
+
// eslint-disable-next-line no-nested-ternary -- render branches.
|
|
125
|
+
!postType
|
|
126
|
+
? __("Please select a post type first", "built")
|
|
127
|
+
: filteredTaxonomies.length === 0
|
|
128
|
+
? __(
|
|
129
|
+
"No taxonomies available for this post type",
|
|
130
|
+
"built",
|
|
131
|
+
)
|
|
132
|
+
: undefined
|
|
133
|
+
}
|
|
134
|
+
/>
|
|
135
|
+
</ToolsPanelItem>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { TaxonomySelect };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useSelect } from '@wordpress/data';
|
|
5
|
+
import { useMemo } from '@wordpress/element';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Utility functions for query components
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reorder an array of items based on a given order of IDs
|
|
13
|
+
*
|
|
14
|
+
* @param {Array} items - Array of objects to reorder
|
|
15
|
+
* @param {Array} orderedIds - Array of IDs in the desired order
|
|
16
|
+
* @param {Function} getItemId - Function to extract ID from an item (default: item => item.id)
|
|
17
|
+
* @returns {Array} Reordered array
|
|
18
|
+
*/
|
|
19
|
+
export function reorderByIds(items, orderedIds, getItemId = (item) => item.id) {
|
|
20
|
+
if (!items || !orderedIds || orderedIds.length === 0) {
|
|
21
|
+
return items || [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create a map for quick lookup
|
|
25
|
+
const itemMap = new Map();
|
|
26
|
+
items.forEach((item) => {
|
|
27
|
+
itemMap.set(getItemId(item), item);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Build the ordered array
|
|
31
|
+
const ordered = [];
|
|
32
|
+
orderedIds.forEach((id) => {
|
|
33
|
+
const item = itemMap.get(id);
|
|
34
|
+
if (item) {
|
|
35
|
+
ordered.push(item);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return ordered;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Shared empty result: useSelect compares results, so a new [] per call would
|
|
43
|
+
// re-render and log "useSelect returns different values".
|
|
44
|
+
const NO_TERMS = [];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hook to fetch and reorder terms based on selectedTerms order
|
|
48
|
+
*
|
|
49
|
+
* Example usage:
|
|
50
|
+
* ```js
|
|
51
|
+
* const orderedTerms = useOrderedTerms(selectedTerms, taxonomy);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function useOrderedTerms(selectedTerms, taxonomy) {
|
|
55
|
+
const { terms } = useSelect(
|
|
56
|
+
(select) => {
|
|
57
|
+
if (!taxonomy || !selectedTerms || selectedTerms.length === 0) {
|
|
58
|
+
return { terms: NO_TERMS };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fetchedTerms = select('core').getEntityRecords('taxonomy', taxonomy, {
|
|
62
|
+
include: selectedTerms,
|
|
63
|
+
per_page: selectedTerms.length,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return { terms: fetchedTerms || NO_TERMS };
|
|
67
|
+
},
|
|
68
|
+
[selectedTerms, taxonomy]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Reorder terms to match selectedTerms order
|
|
72
|
+
return useMemo(() => {
|
|
73
|
+
return reorderByIds(terms, selectedTerms);
|
|
74
|
+
}, [terms, selectedTerms]);
|
|
75
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Repeater Component
|
|
2
|
+
|
|
3
|
+
A flexible repeater component with drag-and-drop functionality powered by @dnd-kit. This component allows users to add, remove, and reorder items in a list with a smooth dragging experience.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Drag and Drop**: Smooth drag-and-drop reordering using @dnd-kit
|
|
8
|
+
- **Keyboard Navigation**: Full keyboard support for accessibility
|
|
9
|
+
- **Flexible Content**: Render any content inside repeater items via renderItem prop
|
|
10
|
+
- **Collapsible Rows**: Optional header summary with expand/collapse (opt-in)
|
|
11
|
+
- **Min/Max Limits**: Configure minimum and maximum number of items
|
|
12
|
+
- **Customizable**: Custom labels, empty states, and styling
|
|
13
|
+
- **Accessible**: ARIA labels and keyboard navigation support
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
import { Repeater } from '@siratik/wp-component-library/components/repeater';
|
|
19
|
+
import { useState } from '@wordpress/element';
|
|
20
|
+
|
|
21
|
+
function MyComponent() {
|
|
22
|
+
const [items, setItems] = useState([
|
|
23
|
+
{ id: '1', title: 'First Item', content: 'Some content' },
|
|
24
|
+
{ id: '2', title: 'Second Item', content: 'More content' }
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const handleAdd = () => {
|
|
28
|
+
const newItem = {
|
|
29
|
+
id: Date.now().toString(),
|
|
30
|
+
title: '',
|
|
31
|
+
content: ''
|
|
32
|
+
};
|
|
33
|
+
setItems([...items, newItem]);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleRemove = (id) => {
|
|
37
|
+
setItems(items.filter(item => item.id !== id));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleReorder = (newItems) => {
|
|
41
|
+
setItems(newItems);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const renderItem = (item) => (
|
|
45
|
+
<div>
|
|
46
|
+
<TextControl
|
|
47
|
+
label="Title"
|
|
48
|
+
value={item.title}
|
|
49
|
+
onChange={(title) => updateItem(item.id, { title })}
|
|
50
|
+
/>
|
|
51
|
+
<TextareaControl
|
|
52
|
+
label="Content"
|
|
53
|
+
value={item.content}
|
|
54
|
+
onChange={(content) => updateItem(item.id, { content })}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Repeater
|
|
61
|
+
items={items}
|
|
62
|
+
renderItem={renderItem}
|
|
63
|
+
onAdd={handleAdd}
|
|
64
|
+
onRemove={handleRemove}
|
|
65
|
+
onReorder={handleReorder}
|
|
66
|
+
addButtonText="Add New Item"
|
|
67
|
+
emptyStateText="No items yet. Click below to add one."
|
|
68
|
+
maxItems={10}
|
|
69
|
+
minItems={1}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Collapsible items
|
|
76
|
+
|
|
77
|
+
Pass `collapsible` to show a header row with an expand/collapse control. Use `renderSummary` for whatever should stay visible when the item is closed (name, enable toggle, badge, etc.). `renderItem` is only mounted while the item is expanded.
|
|
78
|
+
|
|
79
|
+
Existing items start collapsed (`defaultExpanded={false}`). A single newly added item starts open (`expandNewItems`). Drag overlay shows the collapsed header only.
|
|
80
|
+
|
|
81
|
+
When `collapsible` is on, Repeater owns the header chrome (drag handle, chevron, remove). Put interactive header content in `renderSummary` — do not place `DragHandle` or `RemoveButton` there.
|
|
82
|
+
|
|
83
|
+
```jsx
|
|
84
|
+
<Repeater
|
|
85
|
+
items={items}
|
|
86
|
+
collapsible
|
|
87
|
+
enableReorder={false}
|
|
88
|
+
renderSummary={(item) => (
|
|
89
|
+
<>
|
|
90
|
+
<ToggleControl
|
|
91
|
+
__nextHasNoMarginBottom
|
|
92
|
+
label=""
|
|
93
|
+
checked={item.enabled}
|
|
94
|
+
onChange={(enabled) => updateItem(item.id, { enabled })}
|
|
95
|
+
/>
|
|
96
|
+
<span className="built-repeater__summary-name">
|
|
97
|
+
{item.name || "Untitled"}
|
|
98
|
+
</span>
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
renderItem={(item) => (
|
|
102
|
+
<TextControl
|
|
103
|
+
label="Name"
|
|
104
|
+
value={item.name}
|
|
105
|
+
onChange={(name) => updateItem(item.id, { name })}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
onAdd={handleAdd}
|
|
109
|
+
onRemove={handleRemove}
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Props
|
|
114
|
+
|
|
115
|
+
| Prop | Type | Default | Description |
|
|
116
|
+
|------|------|---------|-------------|
|
|
117
|
+
| `items` | `Array` | `[]` | Array of items with unique `id` property |
|
|
118
|
+
| `renderItem` | `Function` | Required | Function to render each item's content. Receives `(item, index)`. When `collapsible` is true, this is the expanded body only. |
|
|
119
|
+
| `onAdd` | `Function` | Required | Callback when add button is clicked |
|
|
120
|
+
| `onRemove` | `Function` | Required | Callback when remove button is clicked |
|
|
121
|
+
| `onReorder` | `Function` | Required | Callback when items are reordered |
|
|
122
|
+
| `className` | `String` | `""` | Additional CSS classes |
|
|
123
|
+
| `addButtonText` | `String` | `"Add Item"` | Text for the add button |
|
|
124
|
+
| `emptyStateText` | `String` | `"No items added yet..."` | Text shown when list is empty |
|
|
125
|
+
| `maxItems` | `Number` | `null` | Maximum number of items allowed |
|
|
126
|
+
| `minItems` | `Number` | `0` | Minimum number of items required |
|
|
127
|
+
| `renderMode` | `String` | `"default"` | `"default"` uses built-in drag/remove chrome. `"integrated"` lets `renderItem` place `DragHandle` and `RemoveButton`. Ignored when `collapsible` is true. |
|
|
128
|
+
| `enableReorder` | `Boolean` | `true` | Whether items can be dragged to reorder. Set to `false` for lists where order doesn't matter (e.g. a set of find/replace pairs) — no drag handle renders, and `onReorder` is not required. |
|
|
129
|
+
| `collapsible` | `Boolean` | `false` | Collapse each item behind a header. Existing callers omit this and keep always-open rows. |
|
|
130
|
+
| `renderSummary` | `Function` | `null` | Header content shown while collapsed (and still shown while expanded). Receives `(item, index)`. |
|
|
131
|
+
| `defaultExpanded` | `Boolean` | `false` | When `collapsible`, existing items start expanded. |
|
|
132
|
+
| `expandNewItems` | `Boolean` | `true` | When `collapsible`, a single newly added item starts expanded. |
|
|
133
|
+
|
|
134
|
+
## Keyboard Shortcuts
|
|
135
|
+
|
|
136
|
+
- **Tab**: Navigate between items and controls
|
|
137
|
+
- **Space/Enter**: Activate drag handle
|
|
138
|
+
- **Arrow Keys**: Move item up/down when dragging
|
|
139
|
+
- **Escape**: Cancel drag operation
|
|
140
|
+
|
|
141
|
+
## Styling
|
|
142
|
+
|
|
143
|
+
The component provides these CSS classes for customization:
|
|
144
|
+
|
|
145
|
+
- `.wpcl-repeater--collapsible` - Root class when `collapsible` is true
|
|
146
|
+
- `.wpcl-repeater__item--dragging` - Applied to the placeholder while an item is being dragged
|
|
147
|
+
- `.wpcl-repeater__item--integrated` - Applied in integrated render mode
|
|
148
|
+
- `.wpcl-repeater__item--collapsible` - Collapsible item (header + optional body)
|
|
149
|
+
- `.wpcl-repeater__item--expanded` - Collapsible item that is currently open
|
|
150
|
+
- `.built-repeater__items` - Items container
|
|
151
|
+
- `.built-repeater__item-handle` - Drag handle
|
|
152
|
+
- `.built-repeater__item-header` - Collapsible header row
|
|
153
|
+
- `.built-repeater__item-toggle` - Expand/collapse button
|
|
154
|
+
- `.built-repeater__summary` - Always-visible header content
|
|
155
|
+
- `.built-repeater__summary-name` - Optional truncated label in the header
|
|
156
|
+
- `.built-repeater__item-content` - Item content area
|
|
157
|
+
- `.built-repeater__item-actions` - Action buttons area
|
|
158
|
+
- `.built-repeater__empty-state` - Empty state message
|
|
159
|
+
- `.built-repeater__add-button` - Add button container
|