@siratik/wp-component-library 0.1.1 → 0.1.3

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.
Files changed (96) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +43 -5
  3. package/components/attachment-image/README.md +118 -0
  4. package/components/attachment-image/index.js +105 -0
  5. package/components/attributes-panel/README.md +92 -0
  6. package/components/attributes-panel/index.js +324 -0
  7. package/components/badge/README.md +61 -0
  8. package/components/badge/index.js +105 -0
  9. package/components/block-appender/README.md +158 -0
  10. package/components/block-appender/index.js +135 -0
  11. package/components/block-extension/index.js +214 -0
  12. package/components/business-hours/SpecialHoursControl.js +211 -0
  13. package/components/business-hours/WeeklyHoursControl.js +419 -0
  14. package/components/business-hours/index.js +8 -0
  15. package/components/button/README.md +172 -0
  16. package/components/button/index.js +84 -0
  17. package/components/captcha-placeholder/CloudflareTurnstile.js +111 -0
  18. package/components/captcha-placeholder/GoogleRecaptcha.js +132 -0
  19. package/components/captcha-placeholder/HCaptcha.js +99 -0
  20. package/components/captcha-placeholder/README.md +228 -0
  21. package/components/captcha-placeholder/index.js +39 -0
  22. package/components/captcha-placeholder/styles.js +33 -0
  23. package/components/geocoder/AddressEntryControl.js +390 -0
  24. package/components/geocoder/GeocoderSearchControl.js +353 -0
  25. package/components/geocoder/countryOptions.js +66 -0
  26. package/components/geocoder/geocoder.js +400 -0
  27. package/components/geocoder/index.js +16 -0
  28. package/components/icon-picker/Icon.js +53 -0
  29. package/components/icon-picker/IconPicker.js +70 -0
  30. package/components/icon-picker/IconPickerModal.js +474 -0
  31. package/components/icon-picker/IconPickerToolbarButton.js +60 -0
  32. package/components/icon-picker/InlineIconPicker.js +92 -0
  33. package/components/icon-picker/api.js +181 -0
  34. package/components/icon-picker/config-bootstrap.js +108 -0
  35. package/components/icon-picker/hooks.js +157 -0
  36. package/components/icon-picker/index.js +39 -0
  37. package/components/icon-picker/load-script.js +33 -0
  38. package/components/icon-picker/loaders.js +79 -0
  39. package/components/icon-picker/store.js +223 -0
  40. package/components/icon-picker/styles.js +232 -0
  41. package/components/icon-picker/utils.js +138 -0
  42. package/components/layout/README.md +268 -0
  43. package/components/link-tools-panel/README.md +49 -0
  44. package/components/link-tools-panel/index.js +198 -0
  45. package/components/link-tools-panel/styles.js +90 -0
  46. package/components/media/README.md +288 -0
  47. package/components/media/SettingsMediaUpload.js +186 -0
  48. package/components/media/editor-media-upload.js +65 -0
  49. package/components/media/image-controls.js +196 -0
  50. package/components/media/index.js +13 -0
  51. package/components/media/inspector-media-upload.js +290 -0
  52. package/components/media/toolbar-media-upload.js +53 -0
  53. package/components/media/utils/aspect-ratios.js +118 -0
  54. package/components/meta/MetaPanel.js +75 -0
  55. package/components/meta/README.md +46 -0
  56. package/components/meta/index.js +6 -0
  57. package/components/meta/meta-gated-editor.js +282 -0
  58. package/components/query/README.md +340 -0
  59. package/components/query/cardTemplatePartPanel.js +69 -0
  60. package/components/query/cardTemplatePartSelect.js +165 -0
  61. package/components/query/cardTemplatePartUtils.js +119 -0
  62. package/components/query/columnCountControl.js +63 -0
  63. package/components/query/displayTypeSelect.js +52 -0
  64. package/components/query/examples.js +166 -0
  65. package/components/query/hideCurrentPostControl.js +47 -0
  66. package/components/query/index.js +20 -0
  67. package/components/query/manualPostSelector.js +125 -0
  68. package/components/query/manualTermSelector.js +141 -0
  69. package/components/query/orderBySelect.js +95 -0
  70. package/components/query/postTypeSelect.js +67 -0
  71. package/components/query/postsPerPageControl.js +51 -0
  72. package/components/query/selectionModeControl.js +61 -0
  73. package/components/query/taxonomySelect.js +139 -0
  74. package/components/query/utils.js +75 -0
  75. package/components/repeater/README.md +159 -0
  76. package/components/repeater/index.js +829 -0
  77. package/components/segmented-bar/README.md +30 -0
  78. package/components/segmented-bar/index.js +68 -0
  79. package/components/sortable-select/README.md +153 -0
  80. package/components/sortable-select/custom-sortable-token.js +127 -0
  81. package/components/sortable-select/index.js +693 -0
  82. package/components/style-picker/README.md +36 -0
  83. package/components/style-picker/index.js +92 -0
  84. package/components/variable-field/README.md +299 -0
  85. package/components/variable-field/index.js +592 -0
  86. package/components/variable-field/value-to-chip-html.js +38 -0
  87. package/components/variable-inserter/README.md +68 -0
  88. package/components/variable-inserter/index.js +168 -0
  89. package/constants/statusColors.js +1 -1
  90. package/editor.js +16 -0
  91. package/index.js +127 -0
  92. package/package.json +21 -13
  93. package/utils/kitaris-localize.js +128 -0
  94. package/utils/slug-aware-color-setter.js +27 -0
  95. package/build/index.asset.php +0 -20
  96. package/build/index.js +0 -1242
@@ -0,0 +1,63 @@
1
+ import {
2
+ RangeControl,
3
+ __experimentalToolsPanelItem as ToolsPanelItem,
4
+ } from "@wordpress/components";
5
+ import { __ } from "@wordpress/i18n";
6
+
7
+ /**
8
+ * Standalone column count 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.displayAs Current display type to determine label
14
+ * @param {string} props.label Label override (default: from displayAs)
15
+ * @param {number} props.defaultValue The block's default column count (default: 3)
16
+ * @param {number} props.min Minimum value
17
+ * @param {number} props.max Maximum value
18
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: false)
19
+ * @returns {WPElement} Element to render
20
+ */
21
+ function ColumnCountControl({
22
+ attributes: { columnCount, displayAs },
23
+ setAttributes,
24
+ min = 1,
25
+ max = 6,
26
+ isShownByDefault = false,
27
+ label,
28
+ defaultValue = 3,
29
+ }) {
30
+ // Don't render for list or pills display types
31
+ if (displayAs === "list" || displayAs === "pills") {
32
+ return null;
33
+ }
34
+
35
+ const itemLabel =
36
+ label ||
37
+ (displayAs === "grid"
38
+ ? __("Columns", "wp-component-library")
39
+ : __("Slides to show", "wp-component-library"));
40
+
41
+ return (
42
+ <ToolsPanelItem
43
+ hasValue={() => columnCount !== defaultValue}
44
+ label={itemLabel}
45
+ onDeselect={() => setAttributes({ columnCount: defaultValue })}
46
+ isShownByDefault={isShownByDefault}
47
+ >
48
+ <RangeControl
49
+ __nextHasNoMarginBottom={true}
50
+ __next40pxDefaultSize
51
+ label={itemLabel}
52
+ value={columnCount}
53
+ onChange={(columnCountNew) =>
54
+ setAttributes({ columnCount: columnCountNew })
55
+ }
56
+ min={min}
57
+ max={max}
58
+ />
59
+ </ToolsPanelItem>
60
+ );
61
+ }
62
+
63
+ export { ColumnCountControl };
@@ -0,0 +1,52 @@
1
+ import {
2
+ SelectControl,
3
+ __experimentalToolsPanelItem as ToolsPanelItem,
4
+ } from "@wordpress/components";
5
+ import { __ } from "@wordpress/i18n";
6
+
7
+ /**
8
+ * Standalone display type selection 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 {Array} props.allowedDisplayOptions Allowed display options
14
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
15
+ * @returns {WPElement} Element to render
16
+ */
17
+ function DisplayTypeSelect({
18
+ attributes: { displayAs },
19
+ setAttributes,
20
+ allowedDisplayOptions = ["grid", "slider", "list", "pills"],
21
+ isShownByDefault = true,
22
+ }) {
23
+ const defaultDisplayAs = "grid";
24
+
25
+ const displayOptions = [
26
+ { label: "Grid", value: "grid" },
27
+ { label: "Slider", value: "slider" },
28
+ { label: "List", value: "list" },
29
+ { label: "Pills", value: "pills" },
30
+ { label: "Marquee", value: "marquee" },
31
+ ].filter((option) => allowedDisplayOptions.includes(option.value));
32
+
33
+ return (
34
+ <ToolsPanelItem
35
+ hasValue={() => displayAs !== defaultDisplayAs}
36
+ label={__("Display Type", "built_starter")}
37
+ onDeselect={() => setAttributes({ displayAs: defaultDisplayAs })}
38
+ isShownByDefault={isShownByDefault}
39
+ >
40
+ <SelectControl
41
+ __nextHasNoMarginBottom={true}
42
+ __next40pxDefaultSize
43
+ label="Display As"
44
+ value={displayAs}
45
+ options={displayOptions}
46
+ onChange={(value) => setAttributes({ displayAs: value })}
47
+ />
48
+ </ToolsPanelItem>
49
+ );
50
+ }
51
+
52
+ export { DisplayTypeSelect };
@@ -0,0 +1,166 @@
1
+ import { __experimentalToolsPanel as ToolsPanel } from "@wordpress/components";
2
+ import { __ } from "@wordpress/i18n";
3
+ import {
4
+ ColumnCountControl,
5
+ DisplayTypeSelect,
6
+ ManualPostSelector,
7
+ OrderBySelect,
8
+ PostsPerPageControl,
9
+ PostTypeSelect,
10
+ SelectionModeControl,
11
+ TaxonomySelect,
12
+ TermSelector,
13
+ } from "./";
14
+
15
+ /**
16
+ * Example: Basic Post Query Interface
17
+ * Simple interface with just post selection and ordering
18
+ */
19
+ export function BasicPostQueryExample({ attributes, setAttributes }) {
20
+ const resetAll = () => {
21
+ setAttributes({
22
+ postType: "post",
23
+ orderPostsBy: "date",
24
+ orderPostsDirection: "desc",
25
+ });
26
+ };
27
+
28
+ return (
29
+ <ToolsPanel
30
+ label={__("Post Query", "built_starter")}
31
+ resetAll={resetAll}
32
+ >
33
+ <PostTypeSelect
34
+ attributes={attributes}
35
+ setAttributes={setAttributes}
36
+ />
37
+ <OrderBySelect
38
+ attributes={attributes}
39
+ setAttributes={setAttributes}
40
+ />
41
+ </ToolsPanel>
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Example: Advanced Query Interface with Manual Selection
47
+ * Complete interface with manual post selection and taxonomy filtering
48
+ */
49
+ export function AdvancedQueryExample({ attributes, setAttributes }) {
50
+ const resetAll = () => {
51
+ setAttributes({
52
+ postType: "post",
53
+ orderPostsBy: "date",
54
+ orderPostsDirection: "desc",
55
+ selectionMode: "auto",
56
+ selectedPosts: [],
57
+ selectedTaxonomy: "",
58
+ selectedTerms: [],
59
+ });
60
+ };
61
+
62
+ return (
63
+ <ToolsPanel
64
+ label={__("Advanced Query", "built_starter")}
65
+ resetAll={resetAll}
66
+ >
67
+ <PostTypeSelect
68
+ attributes={attributes}
69
+ setAttributes={setAttributes}
70
+ />
71
+
72
+ <OrderBySelect
73
+ attributes={attributes}
74
+ setAttributes={setAttributes}
75
+ />
76
+
77
+ <SelectionModeControl
78
+ attributes={attributes}
79
+ setAttributes={setAttributes}
80
+ />
81
+
82
+ {attributes.selectionMode === "manual" && (
83
+ <ManualPostSelector
84
+ attributes={attributes}
85
+ setAttributes={setAttributes}
86
+ postType={attributes.postType}
87
+ />
88
+ )}
89
+
90
+ <TaxonomySelect
91
+ attributes={attributes}
92
+ setAttributes={setAttributes}
93
+ postType={attributes.postType}
94
+ />
95
+
96
+ {attributes.selectedTaxonomy && (
97
+ <TermSelector
98
+ attributes={attributes}
99
+ setAttributes={setAttributes}
100
+ selectedTaxonomy={attributes.selectedTaxonomy}
101
+ />
102
+ )}
103
+ </ToolsPanel>
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Example: Display Settings Interface
109
+ * Just the display-related controls
110
+ */
111
+ export function DisplaySettingsExample({ attributes, setAttributes }) {
112
+ const resetAll = () => {
113
+ setAttributes({
114
+ displayAs: "grid",
115
+ postsPerPage: 6,
116
+ columnCount: 3,
117
+ });
118
+ };
119
+
120
+ return (
121
+ <ToolsPanel
122
+ label={__("Display Settings", "built_starter")}
123
+ resetAll={resetAll}
124
+ >
125
+ <DisplayTypeSelect
126
+ attributes={attributes}
127
+ setAttributes={setAttributes}
128
+ />
129
+
130
+ <PostsPerPageControl
131
+ attributes={attributes}
132
+ setAttributes={setAttributes}
133
+ />
134
+
135
+ <ColumnCountControl
136
+ attributes={attributes}
137
+ setAttributes={setAttributes}
138
+ />
139
+ </ToolsPanel>
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Example: Raw Controls (No Panels)
145
+ * Using controls without ToolsPanel wrappers
146
+ */
147
+ export function RawControlsExample({ attributes, setAttributes }) {
148
+ return (
149
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
150
+ <PostTypeSelect
151
+ attributes={attributes}
152
+ setAttributes={setAttributes}
153
+ />
154
+
155
+ <OrderBySelect
156
+ attributes={attributes}
157
+ setAttributes={setAttributes}
158
+ />
159
+
160
+ <DisplayTypeSelect
161
+ attributes={attributes}
162
+ setAttributes={setAttributes}
163
+ />
164
+ </div>
165
+ );
166
+ }
@@ -0,0 +1,47 @@
1
+ import {
2
+ ToggleControl,
3
+ __experimentalToolsPanelItem as ToolsPanelItem,
4
+ } from "@wordpress/components";
5
+ import { __ } from "@wordpress/i18n";
6
+
7
+ /**
8
+ * Toggle to exclude the post currently being viewed from feed query results.
9
+ * Intended for single templates where the same layout renders every post.
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 HideCurrentPostControl({
18
+ attributes: { hideCurrentPost = true },
19
+ setAttributes,
20
+ isShownByDefault = true,
21
+ }) {
22
+ const defaultHideCurrentPost = true;
23
+
24
+ return (
25
+ <ToolsPanelItem
26
+ hasValue={() => hideCurrentPost !== defaultHideCurrentPost}
27
+ label={__("Hide Current Post", "built_starter")}
28
+ onDeselect={() =>
29
+ setAttributes({ hideCurrentPost: defaultHideCurrentPost })
30
+ }
31
+ isShownByDefault={isShownByDefault}
32
+ >
33
+ <ToggleControl
34
+ __nextHasNoMarginBottom={true}
35
+ label={__("Hide Current Post", "built_starter")}
36
+ checked={hideCurrentPost}
37
+ onChange={(value) => setAttributes({ hideCurrentPost: value })}
38
+ help={__(
39
+ "On single posts and templates, exclude the post being viewed from this feed.",
40
+ "built_starter",
41
+ )}
42
+ />
43
+ </ToolsPanelItem>
44
+ );
45
+ }
46
+
47
+ export { HideCurrentPostControl };
@@ -0,0 +1,20 @@
1
+ // Individual Query Controls - Always render as ToolsPanelItems
2
+ export { ColumnCountControl } from "./columnCountControl";
3
+ export { DisplayTypeSelect } from "./displayTypeSelect";
4
+ export { HideCurrentPostControl } from "./hideCurrentPostControl";
5
+ export { ManualPostSelector } from "./manualPostSelector";
6
+ export { ManualTermSelector } from "./manualTermSelector";
7
+ export { defaultOrderOptions, OrderBySelect } from "./orderBySelect";
8
+ export { PostsPerPageControl } from "./postsPerPageControl";
9
+ export { PostTypeSelect } from "./postTypeSelect";
10
+ export { CardTemplatePartSelect } from "./cardTemplatePartSelect";
11
+ export { CardTemplatePartPanel } from "./cardTemplatePartPanel";
12
+ export {
13
+ buildCardTemplatePartOptions,
14
+ cardTemplatePartMatchesPostType,
15
+ getCardSlugPrefixesForPostType,
16
+ getDefaultCardSlugForPostType,
17
+ } from "./cardTemplatePartUtils";
18
+ export { SelectionModeControl } from "./selectionModeControl";
19
+ export { TaxonomySelect } from "./taxonomySelect";
20
+ export { reorderByIds, useOrderedTerms } from "./utils";
@@ -0,0 +1,125 @@
1
+ import { __experimentalToolsPanelItem as ToolsPanelItem } from "@wordpress/components";
2
+ import { useSelect } from "@wordpress/data";
3
+ import { useCallback, useMemo } from "@wordpress/element";
4
+ import { __ } from "@wordpress/i18n";
5
+ import SortableSelect from "../sortable-select";
6
+
7
+ /**
8
+ * Standalone manual post selection 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.postType The post type to select from
14
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
15
+ * @returns {WPElement} Element to render
16
+ */
17
+ function ManualPostSelector({
18
+ attributes: { selectedPosts = [] },
19
+ setAttributes,
20
+ postType = "post",
21
+ isShownByDefault = true,
22
+ }) {
23
+ const defaultSelectedPosts = [];
24
+
25
+ // Fetch selected posts data
26
+ const selectedPostsData = useSelect(
27
+ (select) => {
28
+ if (!postType || selectedPosts.length === 0) {
29
+ return [];
30
+ }
31
+
32
+ return (
33
+ select("core").getEntityRecords("postType", postType, {
34
+ include: selectedPosts,
35
+ per_page: selectedPosts.length,
36
+ }) || []
37
+ );
38
+ },
39
+ [postType, selectedPosts],
40
+ );
41
+
42
+ // Load posts with search
43
+ const loadPosts = useCallback(
44
+ async (inputValue) => {
45
+ if (!postType) {
46
+ return [];
47
+ }
48
+
49
+ try {
50
+ const query = {
51
+ per_page: 20,
52
+ status: "publish",
53
+ search: inputValue || '',
54
+ };
55
+
56
+ // Use resolveSelect for async data fetching
57
+ const posts = await wp.data.resolveSelect("core").getEntityRecords(
58
+ "postType",
59
+ postType,
60
+ query,
61
+ );
62
+
63
+ return posts || [];
64
+ } catch (error) {
65
+ // eslint-disable-next-line no-console -- surface REST failures to developers.
66
+ console.error('Error loading posts:', error);
67
+ return [];
68
+ }
69
+ },
70
+ [postType],
71
+ );
72
+
73
+ // Handle selection change
74
+ const handleChange = useCallback(
75
+ (newSelection) => {
76
+ const postIds = newSelection.map((post) => post.id);
77
+ setAttributes({ selectedPosts: postIds });
78
+ },
79
+ [setAttributes],
80
+ );
81
+
82
+ // Format selected value for SortableSelect - maintain order
83
+ const selectedValue = useMemo(() => {
84
+ return selectedPosts
85
+ .map((postId) =>
86
+ selectedPostsData.find((post) => post.id === postId),
87
+ )
88
+ .filter(Boolean);
89
+ }, [selectedPostsData, selectedPosts]);
90
+
91
+ const control = (
92
+ <SortableSelect
93
+ value={selectedValue}
94
+ onChange={handleChange}
95
+ loadOptions={loadPosts}
96
+ getOptionLabel={(post) => post.title?.rendered || `Post #${post.id}`}
97
+ getOptionValue={(post) => post.id}
98
+ placeholder={__("Type to search posts…", "built")}
99
+ label={__("Select Posts", "built")}
100
+ help={__(
101
+ "Start typing to search for posts, then select them from the dropdown. Drag to reorder.",
102
+ "built",
103
+ )}
104
+ isMulti={true}
105
+ isClearable={false}
106
+ isSearchable={true}
107
+ hideDropdownIndicator={true}
108
+ />
109
+ );
110
+
111
+ return (
112
+ <ToolsPanelItem
113
+ hasValue={() => selectedPosts && selectedPosts.length > 0}
114
+ label={__("Select Posts", "built")}
115
+ onDeselect={() =>
116
+ setAttributes({ selectedPosts: defaultSelectedPosts })
117
+ }
118
+ isShownByDefault={isShownByDefault}
119
+ >
120
+ {control}
121
+ </ToolsPanelItem>
122
+ );
123
+ }
124
+
125
+ export { ManualPostSelector };
@@ -0,0 +1,141 @@
1
+ import { __experimentalToolsPanelItem as ToolsPanelItem } from "@wordpress/components";
2
+ import { useSelect } from "@wordpress/data";
3
+ import { useCallback, useMemo } from "@wordpress/element";
4
+ import { __ } from "@wordpress/i18n";
5
+ import SortableSelect from "../sortable-select";
6
+
7
+ /**
8
+ * Standalone term selection 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.selectedTaxonomy The taxonomy to select terms from
14
+ * @param {boolean} props.renderAsToolsPanelItem Whether to render as ToolsPanelItem
15
+ * @param {boolean} props.isShownByDefault Whether to show by default in ToolsPanel (default: true)
16
+ * @returns {WPElement} Element to render
17
+ */
18
+ function ManualTermSelector({
19
+ attributes: { selectedTerms = [] },
20
+ setAttributes,
21
+ selectedTaxonomy,
22
+ renderAsToolsPanelItem = true,
23
+ isShownByDefault = true,
24
+ }) {
25
+ const defaultSelectedTerms = [];
26
+
27
+ // Fetch selected terms data
28
+ const selectedTermsData = useSelect(
29
+ (select) => {
30
+ if (!selectedTaxonomy || selectedTerms.length === 0) {
31
+ return [];
32
+ }
33
+
34
+ return (
35
+ select("core").getEntityRecords("taxonomy", selectedTaxonomy, {
36
+ include: selectedTerms,
37
+ per_page: selectedTerms.length,
38
+ }) || []
39
+ );
40
+ },
41
+ [selectedTaxonomy, selectedTerms],
42
+ );
43
+
44
+ // Load terms with search
45
+ const loadTerms = useCallback(
46
+ async (inputValue) => {
47
+ if (!selectedTaxonomy) {
48
+ return [];
49
+ }
50
+
51
+ try {
52
+ const query = {
53
+ per_page: 20,
54
+ hide_empty: false,
55
+ search: inputValue || '',
56
+ };
57
+
58
+ // Use resolveSelect for async data fetching
59
+ const terms = await wp.data.resolveSelect("core").getEntityRecords(
60
+ "taxonomy",
61
+ selectedTaxonomy,
62
+ query,
63
+ );
64
+
65
+ return terms || [];
66
+ } catch (error) {
67
+ // eslint-disable-next-line no-console -- surface REST failures to developers.
68
+ console.error('Error loading terms:', error);
69
+ return [];
70
+ }
71
+ },
72
+ [selectedTaxonomy],
73
+ );
74
+
75
+ // Handle selection change
76
+ const handleChange = useCallback(
77
+ (newSelection) => {
78
+ const termIds = newSelection.map((term) => term.id);
79
+ setAttributes({ selectedTerms: termIds });
80
+ },
81
+ [setAttributes],
82
+ );
83
+
84
+ // Format selected value for SortableSelect - maintain order
85
+ const selectedValue = useMemo(() => {
86
+ return selectedTerms
87
+ .map((termId) =>
88
+ selectedTermsData.find((term) => term.id === termId),
89
+ )
90
+ .filter(Boolean);
91
+ }, [selectedTermsData, selectedTerms]);
92
+
93
+ const control = (
94
+ <SortableSelect
95
+ value={selectedValue}
96
+ onChange={handleChange}
97
+ loadOptions={loadTerms}
98
+ getOptionLabel={(term) => term.name || `Term #${term.id}`}
99
+ getOptionValue={(term) => term.id}
100
+ placeholder={__("Type to search terms…", "built")}
101
+ label={__("Select Terms", "built")}
102
+ help={__(
103
+ "Start typing to search for taxonomy terms, then select them from the dropdown. Drag to reorder.",
104
+ "built",
105
+ )}
106
+ isMulti={true}
107
+ isClearable={false}
108
+ isSearchable={true}
109
+ hideDropdownIndicator={true}
110
+ renderSelectedItem={(term) => (
111
+ <span>
112
+ {term.name}
113
+ {term.count !== undefined && (
114
+ <span style={{ color: "#666", marginLeft: "4px" }}>
115
+ ({term.count})
116
+ </span>
117
+ )}
118
+ </span>
119
+ )}
120
+ />
121
+ );
122
+
123
+ if (!renderAsToolsPanelItem) {
124
+ return control;
125
+ }
126
+
127
+ return (
128
+ <ToolsPanelItem
129
+ hasValue={() => selectedTerms && selectedTerms.length > 0}
130
+ label={__("Select Terms", "built")}
131
+ onDeselect={() =>
132
+ setAttributes({ selectedTerms: defaultSelectedTerms })
133
+ }
134
+ isShownByDefault={isShownByDefault}
135
+ >
136
+ {control}
137
+ </ToolsPanelItem>
138
+ );
139
+ }
140
+
141
+ export { ManualTermSelector };