@plaudit/gutenberg-api-extensions 2.94.2 → 2.96.0

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 (103) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/lib/useful-types.d.ts +1 -4
  3. package/package.json +4 -4
  4. package/src/blocks/MoveError.ts +0 -7
  5. package/src/blocks/PathError.ts +0 -18
  6. package/src/blocks/SNPFlexibleItemsListComponent.tsx +0 -30
  7. package/src/blocks/SNPGroupComponent.tsx +0 -38
  8. package/src/blocks/SNPListComponent.tsx +0 -25
  9. package/src/blocks/SNPTreeContext.tsx +0 -13
  10. package/src/blocks/basic-custom-block-bindings-support.tsx +0 -248
  11. package/src/blocks/common-native-property-constructors.tsx +0 -927
  12. package/src/blocks/conditions.ts +0 -261
  13. package/src/blocks/csnp-api.ts +0 -221
  14. package/src/blocks/data-controller/actions.ts +0 -20
  15. package/src/blocks/data-controller/reducer.ts +0 -146
  16. package/src/blocks/data-controller/trigger-handlers.ts +0 -150
  17. package/src/blocks/data-controller/utils.ts +0 -415
  18. package/src/blocks/data-controller-manager.ts +0 -50
  19. package/src/blocks/data-controller.ts +0 -165
  20. package/src/blocks/hooks/built-in-suspendable-option-protocols/select.ts +0 -51
  21. package/src/blocks/hooks/built-in-suspendable-option-protocols/settings.ts +0 -70
  22. package/src/blocks/hooks/useSuspendableOptions.ts +0 -122
  23. package/src/blocks/index.ts +0 -23
  24. package/src/blocks/layered-styles-api.ts +0 -142
  25. package/src/blocks/layered-styles-impl.ts +0 -95
  26. package/src/blocks/layout/LaidOutProperty.tsx +0 -72
  27. package/src/blocks/layout/LaidOutPropertyRow.tsx +0 -28
  28. package/src/blocks/layout/NodeContext.tsx +0 -54
  29. package/src/blocks/layout/PanelRoot.tsx +0 -30
  30. package/src/blocks/layout/TabsRoot.tsx +0 -56
  31. package/src/blocks/layout/ToolsPanelContext.tsx +0 -22
  32. package/src/blocks/problematic-blocks-blocker.ts +0 -24
  33. package/src/blocks/problematic-variations-blocker.ts +0 -32
  34. package/src/blocks/shared-exportable-types.ts +0 -6
  35. package/src/blocks/shared-internal-types.ts +0 -18
  36. package/src/blocks/simple-block.tsx +0 -74
  37. package/src/blocks/simple-native-property-api.ts +0 -173
  38. package/src/blocks/simple-native-property-impl.tsx +0 -335
  39. package/src/blocks/simple-native-property-internal-shared.ts +0 -19
  40. package/src/blocks/snp-api.ts +0 -5
  41. package/src/blocks/snp-data-store.ts +0 -72
  42. package/src/blocks/utilities.ts +0 -66
  43. package/src/controls/AsynchronousFormTokenField.tsx +0 -86
  44. package/src/controls/BaseSortableItemsControl.tsx +0 -84
  45. package/src/controls/ExtendedFormTokenField.tsx +0 -144
  46. package/src/controls/ExtendedPostPicker.ts +0 -57
  47. package/src/controls/ExtendedRadioControl.tsx +0 -107
  48. package/src/controls/ExtendedTaxonomyPicker.tsx +0 -100
  49. package/src/controls/ExtendedTermPicker.tsx +0 -61
  50. package/src/controls/ExtendedTextareaControl.tsx +0 -65
  51. package/src/controls/ExtendedUserPicker.ts +0 -56
  52. package/src/controls/FileControl.tsx +0 -48
  53. package/src/controls/FullSizeToggleControl.tsx +0 -95
  54. package/src/controls/ImageControl.tsx +0 -143
  55. package/src/controls/InspectorPanel.tsx +0 -37
  56. package/src/controls/LazySuggestionsComboboxControl.tsx +0 -64
  57. package/src/controls/MultiSelectControl.tsx +0 -59
  58. package/src/controls/PickOne.tsx +0 -88
  59. package/src/controls/PromisableComponent.tsx +0 -56
  60. package/src/controls/ProperLinkControl.tsx +0 -98
  61. package/src/controls/SimpleToggle.tsx +0 -9
  62. package/src/controls/SortableFlexibleItemsControl.tsx +0 -37
  63. package/src/controls/SortableItemsControl.tsx +0 -22
  64. package/src/controls/basicNumericallyIdedItemPicker.tsx +0 -75
  65. package/src/controls/hooks/useImprovedTokenManager.ts +0 -163
  66. package/src/controls/hooks/useMultiSingleConversionLayer.ts +0 -17
  67. package/src/controls/hooks/useNonRenderingCounter.ts +0 -6
  68. package/src/controls/hooks/useOutputMemoizingFilter.ts +0 -16
  69. package/src/controls/hooks/useSortableItemsModel.ts +0 -196
  70. package/src/controls/hooks/useSuggestions.ts +0 -91
  71. package/src/controls/hooks/useTokenManager.ts +0 -177
  72. package/src/controls/index.ts +0 -24
  73. package/src/controls/shared.ts +0 -50
  74. package/src/controls/types.ts +0 -18
  75. package/src/editor/insert-sibling-or-child-block-shortcut.tsx +0 -60
  76. package/src/editor/install-insert-sole-allowed-block-shortcut-support.tsx +0 -51
  77. package/src/editor/simple-gutenberg-endpoints-api.ts +0 -31
  78. package/src/editor/simple-gutenberg-endpoints-impl.ts +0 -126
  79. package/src/index.ts +0 -30
  80. package/src/lib/compat-types.ts +0 -21
  81. package/src/lib/gutenberg-api-extensions-state/custom-block-bindings-support-logic.ts +0 -35
  82. package/src/lib/gutenberg-api-extensions-state/general-logic.ts +0 -41
  83. package/src/lib/gutenberg-api-extensions-state/layered-block-styles-logic.ts +0 -43
  84. package/src/lib/gutenberg-api-extensions-state/snp-logic.ts +0 -240
  85. package/src/lib/gutenberg-api-extensions-state.ts +0 -69
  86. package/src/lib/helpers.ts +0 -115
  87. package/src/lib/modified-fast-deep-equals.ts +0 -91
  88. package/src/lib/plaudit-icons/column-1.tsx +0 -6
  89. package/src/lib/plaudit-icons/column-2.tsx +0 -6
  90. package/src/lib/plaudit-icons/column-3.tsx +0 -6
  91. package/src/lib/plaudit-icons/placement-center.tsx +0 -3
  92. package/src/lib/plaudit-icons/placement-end.tsx +0 -3
  93. package/src/lib/plaudit-icons/placement-start.tsx +0 -3
  94. package/src/lib/plaudit-icons/placement-stretch.tsx +0 -3
  95. package/src/lib/plaudit-icons/plaudit-icon.tsx +0 -4
  96. package/src/lib/plaudit-icons/reusable-block-marker.tsx +0 -3
  97. package/src/lib/plaudit-icons.ts +0 -13
  98. package/src/lib/sectioned-cache-store.ts +0 -120
  99. package/src/lib/suspense/promise-handlers.ts +0 -72
  100. package/src/lib/suspense.tsx +0 -18
  101. package/src/lib/useful-types.ts +0 -82
  102. package/src/schemas/README.md +0 -1
  103. package/src/schemas/plaudit-block-schema.json +0 -818
@@ -1,37 +0,0 @@
1
- import {Button, SelectControl} from "@wordpress/components";
2
- import {useCallback, useState} from "@wordpress/element";
3
-
4
- import {BaseSortableItemsControl, type BaseSortableItemsControlProps} from "./BaseSortableItemsControl";
5
- import type {NonEmptyArray} from "../lib/useful-types";
6
- import type {FlexibleItem} from "./types";
7
-
8
- import type {ReactNode} from "react";
9
-
10
- type SortableFlexibleItemsControlSpecificProps<T extends string> = {
11
- availableTypes: NonEmptyArray<{label: string, value: T}>,
12
- emptyValueBuilder: (type: T) => FlexibleItem<T>
13
- };
14
- export type SortableFlexibleItemsControlProps<T extends string> = Omit<BaseSortableItemsControlProps<FlexibleItem<T>>, 'buttonsArea'>&SortableFlexibleItemsControlSpecificProps<T>;
15
- export function SortableFlexibleItemsControl<T extends string>({availableTypes, emptyValueBuilder, ...baseProps}: SortableFlexibleItemsControlProps<T>) {
16
- const buttonsArea = useCallback<BaseSortableItemsControlProps<FlexibleItem<T>>['buttonsArea']>(
17
- // Unfortunately, the typedefs for React memo components don't allow us to properly check the emptyValueBuilder function
18
- args => <ButtonsArea {...args} availableTypes={availableTypes} emptyValueBuilder={emptyValueBuilder} />, [availableTypes, emptyValueBuilder]);
19
- return <BaseSortableItemsControl {...baseProps} buttonsArea={buttonsArea} />;
20
- }
21
-
22
- type ButtonsAreaProps<T extends string> = Parameters<BaseSortableItemsControlProps<FlexibleItem<T>>['buttonsArea']>[0]&SortableFlexibleItemsControlSpecificProps<T>;
23
- function ButtonsArea<T extends string>({addHandler, disabled, availableTypes, emptyValueBuilder}: ButtonsAreaProps<T>): ReactNode {
24
- const [selectedType, setSelectedType] = useState(availableTypes[0].value);
25
- return <div className="plaudit-sortable-items-buttons flexible-items">
26
- <Button icon="insert" disabled={disabled} onClick={() => addHandler(emptyValueBuilder(selectedType))} __next40pxDefaultSize accessibleWhenDisabled>Add</Button>
27
- <SelectControl
28
- __next40pxDefaultSize
29
- __nextHasNoMarginBottom
30
- label="Type"
31
- hideLabelFromVision={true}
32
- value={selectedType}
33
- options={availableTypes}
34
- onChange={setSelectedType}
35
- />
36
- </div>;
37
- }
@@ -1,22 +0,0 @@
1
- import {Button} from "@wordpress/components";
2
- import {useCallback} from "@wordpress/element";
3
-
4
- import {BaseSortableItemsControl, type BaseSortableItemsControlProps} from "./BaseSortableItemsControl";
5
-
6
- export type SortableItemsControlProps<D> = Omit<BaseSortableItemsControlProps<D>, 'buttonsArea'|'emptyValue'>&{emptyValue: D, addRowText?: string};
7
- export function SortableItemsControl<D>({emptyValue, addRowText, ...baseProps}: SortableItemsControlProps<D>) {
8
- const buttonsArea = useCallback<BaseSortableItemsControlProps<D>['buttonsArea']>(
9
- args => <ButtonsArea {...args} emptyValue={emptyValue} addRowText={addRowText} />, [emptyValue, addRowText]);
10
- return <BaseSortableItemsControl {...baseProps} buttonsArea={buttonsArea} emptyValue={emptyValue} />;
11
- }
12
-
13
- type ButtonsAreaProps<D = any> = Parameters<BaseSortableItemsControlProps<D>['buttonsArea']>[0]&{emptyValue: D, addRowText?: string};
14
- function ButtonsArea<D>({addHandler, disabled, emptyValue, addRowText}: ButtonsAreaProps<D>) {
15
- return <div className="plaudit-sortable-items-buttons">
16
- <Button
17
- icon="insert" disabled={disabled}
18
- onClick={() => addHandler(typeof emptyValue === 'object' ? {...emptyValue} : emptyValue)}
19
- __next40pxDefaultSize accessibleWhenDisabled
20
- >{addRowText ?? "Add Row"}</Button>
21
- </div>
22
- }
@@ -1,75 +0,0 @@
1
- import {select as dataSelect} from "@wordpress/data";
2
- import {useCallback} from "@wordpress/element";
3
-
4
- import type {SNPControlSlots} from "../blocks";
5
- import {ExtendedFormTokenField, unpackDisplayedTokenText, ValidationState} from "./ExtendedFormTokenField";
6
- import {store as endpointsStore} from "../editor/simple-gutenberg-endpoints-impl";
7
- import {isNumeric} from "./shared";
8
- import type {TokenItem} from "../lib/useful-types";
9
-
10
- import type {ReactNode} from "react";
11
-
12
- type BasicNumericallyIdedItemPickerPropsBase = {
13
- label?: string;
14
- help?: ReactNode;
15
- placeholder?: string;
16
- }&Partial<Pick<SNPControlSlots, 'Messages'>>;
17
- type BasicNumericallyIdedItemPickerPropsSingle = BasicNumericallyIdedItemPickerPropsBase&{
18
- onChange(value: string): void;
19
- value?: string;
20
- multiple: false;
21
- };
22
- type BasicNumericallyIdedItemPickerPropsMultiple = BasicNumericallyIdedItemPickerPropsBase&{
23
- onChange(value: string[]): void;
24
- value?: string[];
25
- multiple?: true|undefined;
26
- };
27
- export type BasicNumericallyIdedItemPickerProps = BasicNumericallyIdedItemPickerPropsSingle|BasicNumericallyIdedItemPickerPropsMultiple;
28
-
29
- export function basicNumericallyIdedItemPicker(props: BasicNumericallyIdedItemPickerProps, endpoint: string, queryProps: {[key: string]: any}) {
30
- const {multiple, value, ...remainder} = props;
31
-
32
- const suggestionQuery = useCallback((input: string, select: typeof dataSelect) => {
33
- return select(endpointsStore).get(endpoint + "-options", {search: input, ...queryProps}) as TokenItem[]|undefined;
34
- }, [queryProps]);
35
- const validationQuery = useCallback((idsBeingValidated: string[], select: typeof dataSelect) => {
36
- return select(endpointsStore).get(endpoint + "-validation", {ids: idsBeingValidated.join(','), ...queryProps}) as TokenItem[]|undefined;
37
- }, [queryProps]);
38
-
39
- if (props.multiple === false) {
40
- return <ExtendedFormTokenField
41
- {...remainder}
42
- value={props.value}
43
- multiple={false}
44
- onChange={props.onChange}
45
- stringToTokenConverter={makeTokenFromSuggestion}
46
- suggestionQuery={suggestionQuery}
47
- validationQuery={validationQuery}
48
- validator={validator}
49
- />;
50
- } else {
51
- return <ExtendedFormTokenField
52
- {...remainder}
53
- value={props.value}
54
- multiple={true}
55
- onChange={props.onChange}
56
- stringToTokenConverter={makeTokenFromSuggestion}
57
- suggestionQuery={suggestionQuery}
58
- validationQuery={validationQuery}
59
- validator={validator}
60
- />;
61
- }
62
- }
63
-
64
- function makeTokenFromSuggestion(token: string): TokenItem {
65
- if (isNumeric(token)) {
66
- return {value: token, status: ValidationState.Validating};
67
- } else {
68
- const {value, title} = unpackDisplayedTokenText(token);
69
- return {value, status: isNumeric(value) ? ValidationState.Validating : ValidationState.Invalid, title};
70
- }
71
- }
72
-
73
- function validator(token: string) {
74
- return /\(#([0-9]+)\)$/.exec(token)?.[1] !== undefined;
75
- }
@@ -1,163 +0,0 @@
1
- import {type MapSelect, useSelect} from "@wordpress/data";
2
- import {useEffect, useMemo, useRef, useState} from "@wordpress/element";
3
-
4
- import {potentialWpErrorToString} from "../../lib/helpers";
5
- import type {TokenItem} from "../../lib/useful-types";
6
-
7
- export function packDisplayTokenText(token: TokenItem) {
8
- return `${token.title || token.value} (#${token.value})`;
9
- }
10
- export function unpackDisplayedTokenText(token: string) {
11
- const result = /(.+) \(#([^)]+)\)$/.exec(token);
12
- return {title: result?.[1], value: result?.[2] ?? token};
13
- }
14
-
15
- // The strange values correspond to the literals that are expected by TokenItem.status, which allows the assignment code to be cleaner
16
- export const enum ValidationState {
17
- Valid = 'success',
18
- Invalid = 'error',
19
- Validating = 'validating'
20
- }
21
-
22
- export type TokenManagementCallbacks = {
23
- validationQuery(tokens: string[], ...args: Parameters<MapSelect>): TokenItem[]|undefined;
24
- suggestionQuery(input: string, ...args: Parameters<MapSelect>): TokenItem[]|undefined;
25
- stringToTokenConverter(suggestion: string): TokenItem;
26
- }
27
-
28
- type ImprovedTokenManagerReturnType = {
29
- currentTokens: TokenItem[];
30
- isValidating: boolean;
31
- updateComponentValue: (value: (((prevState: (string | TokenItem)[]) => (string|TokenItem)[])|(string|TokenItem)[])) => void;
32
- tokenStatusCache: Map<TokenItem["value"], TokenItem["status"]>;
33
- thrownValidationError?: string;
34
- };
35
-
36
- export function useImprovedTokenManager(
37
- initialValue: (string|TokenItem)[]|undefined,
38
- writeBackingValue: (value: TokenItem[]) => void,
39
- validationQuery: TokenManagementCallbacks['validationQuery'],
40
- makeTokenFromString: TokenManagementCallbacks['stringToTokenConverter'],
41
- tokenTitleCache: Map<TokenItem['value'], TokenItem['title']>
42
- ): ImprovedTokenManagerReturnType {
43
- const [componentValue, updateComponentValue] = useState<(string|TokenItem)[]>(
44
- () => initialValue?.map(item => typeof item === 'string' ? makeTokenFromString(item) : item) ?? []);
45
-
46
- const tokenStatusCacheRef = useRef<Map<TokenItem['value'], TokenItem['status']>|undefined>(undefined);
47
- if (tokenStatusCacheRef.current === undefined) {
48
- tokenStatusCacheRef.current = new Map();
49
- }
50
-
51
- const convertedTokens = useTokenConverter(componentValue, makeTokenFromString, tokenStatusCacheRef.current);
52
- const {isValidating, validatedTokens, thrownValidationError} = useTokenValidator(convertedTokens, validationQuery, tokenStatusCacheRef.current)
53
- const titledTokens = useTokenTitleApplier(validatedTokens, tokenTitleCache);
54
-
55
- useEffect(() => {
56
- if (!isValidating) {
57
- writeBackingValue(titledTokens);
58
- }
59
- }, [titledTokens, isValidating]);
60
-
61
- return {currentTokens: titledTokens, isValidating, updateComponentValue, tokenStatusCache: tokenStatusCacheRef.current, thrownValidationError};
62
- }
63
- function syncTokenTitles(token: TokenItem, cache: Map<TokenItem['value'], TokenItem['title']>): TokenItem {
64
- return {...token, title: cache.get(token.value) ?? addTokenTitleToCache(token, cache) ?? packDisplayTokenText(token)};
65
- }
66
- function addTokenTitleToCache(token: TokenItem, cache: Map<TokenItem['value'], TokenItem['title']>) {
67
- let label = token.title?.trim();
68
- if (!label) {
69
- return;
70
- }
71
- const valueSuffix = `(#${token.value})`;
72
- if (label.endsWith(valueSuffix)) {
73
- label = label.substring(0, label.length - valueSuffix.length).trim();
74
- }
75
- const res = `${label.length > 30 ? `${label.substring(0, 30).trim()}…` : label} ${valueSuffix}`;
76
- cache.set(token.value, res);
77
- return res;
78
- }
79
-
80
- function isAllValidTokens(arr: (string|TokenItem)[]): arr is (Omit<TokenItem, 'title'|'status'>&Required<Pick<TokenItem, 'title'|'status'>>)[] {
81
- return arr.every(token => typeof token !== 'string' && token.status !== undefined && token.status !== ValidationState.Validating && token.title !== undefined);
82
- }
83
-
84
- function useTokenConverter(
85
- componentValue: (string|TokenItem)[], makeTokenFromString: (token: string) => TokenItem, tokenStatusCache: Map<TokenItem['value'], TokenItem['status']>
86
- ) {
87
- const priorComponentValue = useRef<(string|TokenItem)[]>([]);
88
- const alreadyConvertedTokens = useRef<TokenItem[]>([]);
89
- if (priorComponentValue.current === componentValue) {
90
- return alreadyConvertedTokens.current;
91
- }
92
-
93
- const newlyConvertedTokens = isAllValidTokens(componentValue) ? componentValue
94
- : componentValue.map(token => {
95
- if (typeof token === 'string') {
96
- const res = makeTokenFromString(token);
97
- if (res.status === undefined) {
98
- res.status = tokenStatusCache.get(res.value);
99
- }
100
- return res;
101
- } else {
102
- if (token.status === undefined || token.status === ValidationState.Validating) {
103
- token.status = tokenStatusCache.get(token.value);
104
- }
105
- return token;
106
- }
107
- });
108
-
109
- const convertedTokens = alreadyConvertedTokens.current;
110
- if (newlyConvertedTokens.length !== convertedTokens.length) {
111
- priorComponentValue.current = componentValue;
112
- alreadyConvertedTokens.current = newlyConvertedTokens;
113
- return newlyConvertedTokens;
114
- }
115
- for (let i = 0; i < newlyConvertedTokens.length; i++) {
116
- if (newlyConvertedTokens[i]?.value !== convertedTokens[i]?.value || newlyConvertedTokens[i]?.status !== convertedTokens[i]?.status) {
117
- priorComponentValue.current = componentValue;
118
- alreadyConvertedTokens.current = newlyConvertedTokens;
119
- return newlyConvertedTokens;
120
- }
121
- }
122
- return convertedTokens;
123
- }
124
-
125
- function useTokenValidator(
126
- convertedTokens: TokenItem[], validationQuery: TokenManagementCallbacks['validationQuery'], tokenStatusCache: Map<TokenItem['value'], TokenItem['status']>
127
- ) {
128
- const [statusUpdatedConvertedTokens, statusUpdatedConvertedTokenValues] = useMemo(() => {
129
- const res = convertedTokens
130
- .map(token => {
131
- return (token.status === undefined || token.status === ValidationState.Validating) && tokenStatusCache.has(token.value)
132
- ? {...token, status: tokenStatusCache.get(token.value)} : token;
133
- });
134
- return [res, res.map(token => token.value)];
135
- }, [convertedTokens, tokenStatusCache]);
136
-
137
- return useSelect((select, registry) => {
138
- if (statusUpdatedConvertedTokens.some(token => token.status === undefined || token.status === ValidationState.Validating)) {
139
- let res;
140
- try {
141
- res = validationQuery(statusUpdatedConvertedTokenValues, select, registry);
142
- } catch (e) {
143
- return {validatedTokens: statusUpdatedConvertedTokens, isValidating: false, thrownValidationError: potentialWpErrorToString(e)};
144
- }
145
- if (res !== undefined) {
146
- const validTokens = new Map(res.map(token => [token.value, token]));
147
- const validatedTokens = convertedTokens.map(token => validTokens.get(token.value) ?? {...token, status: ValidationState.Invalid});
148
- for (const token of validatedTokens) {
149
- tokenStatusCache.set(token.value, token.status);
150
- }
151
- return {validatedTokens, isValidating: false};
152
- } else {
153
- return {validatedTokens: statusUpdatedConvertedTokens, isValidating: true};
154
- }
155
- } else {
156
- return {validatedTokens: statusUpdatedConvertedTokens, isValidating: false};
157
- }
158
- }, [convertedTokens, statusUpdatedConvertedTokens, statusUpdatedConvertedTokenValues, validationQuery, tokenStatusCache]);
159
- }
160
-
161
- function useTokenTitleApplier(convertedTokens: TokenItem[], tokenTitleCache: Map<TokenItem['value'], TokenItem['title']>) {
162
- return useMemo(() => convertedTokens.map(token => syncTokenTitles(token, tokenTitleCache)), [convertedTokens, tokenTitleCache]);
163
- }
@@ -1,17 +0,0 @@
1
- import {useCallback, useMemo} from "@wordpress/element";
2
-
3
- import type {TokenItem} from "../../lib/useful-types";
4
-
5
- export type ValueConverter = {value: string[], setValue(value: TokenItem[]): void};
6
- export function useMultiSingleConversionLayer(
7
- value: string|string[]|undefined, onChange: ((tokens: string[]) => void)|((token: string) => void),
8
- makeTokenFromSuggestion: (suggestion: string) => TokenItem, multiple = true
9
- ): ValueConverter {
10
- return {
11
- value: useMemo(() => value === undefined ? [] : (Array.isArray(value) ? value : [value]), [value]),
12
- setValue: useCallback(tokens => {
13
- const bareTokens = tokens.filter(token => token.status === 'success').map(token => token.value);
14
- onChange((multiple ? bareTokens : bareTokens[0]) as any);
15
- }, [onChange, makeTokenFromSuggestion, multiple])
16
- };
17
- }
@@ -1,6 +0,0 @@
1
- import {useRef} from "@wordpress/element";
2
-
3
- export function useNonRenderingCounter(initialValue = 0): [{ readonly current: number }, () => number] {
4
- const count = useRef(initialValue);
5
- return [count, () => ++count.current];
6
- }
@@ -1,16 +0,0 @@
1
- import {useCallback, useRef} from "@wordpress/element";
2
-
3
- export function useOutputMemoizingFilter<A extends any[], R>(filter: (...args: A) => R, externals?: any[]): typeof filter {
4
- const existingState = useRef<undefined|[typeof filter, A, R]>(undefined);
5
- const memoizedFilter = useCallback((...args: A): R => {
6
- if (existingState.current !== undefined && existingState.current[0] === memoizedFilter
7
- && (existingState.current[1] === args || args.every((a, i) => existingState.current![1][i] === a))
8
- ) {
9
- return existingState.current[2];
10
- }
11
- const res = filter(...args);
12
- existingState.current = [memoizedFilter, args, res];
13
- return res;
14
- }, externals ? [filter, ...externals] : [filter]);
15
- return memoizedFilter;
16
- }
@@ -1,196 +0,0 @@
1
- import {useEffect, useMemo, useState} from "@wordpress/element";
2
-
3
- import {Immer} from "immer";
4
- import {clone} from "../../lib/helpers";
5
-
6
- type Listeners<D> = {
7
- onAdd?: (index: number, value: D) => void,
8
- onChange?: (value: D[]) => void,
9
- onRemove?: (index: number) => void,
10
- onReorder?: (oldIndex: number, newIndex: number) => void
11
- };
12
- type SortableItemsModelArgs<D> = Listeners<D>&{
13
- emptyValue?: D,
14
- value?: D[]
15
- }
16
- export type SortableItemsModelKey = ReturnType<typeof crypto.randomUUID>;
17
- export type KeyedValue<D> = {value: D, key: SortableItemsModelKey};
18
- export type KeyedValues<D> = KeyedValue<D>[];
19
- type ActionDefs<D> = {
20
- add(args: {index?: number, value: D}): void,
21
- remove(args: {key: SortableItemsModelKey}): void,
22
- move(args: {oldIndex: number, newIndex: number}): void,
23
- change(args: {value: D, index: number}): void,
24
-
25
- commit(args: {keyedValues: KeyedValues<D>}): void,
26
- refresh(args: {listeners: Listeners<D>, value?: D[]}): void
27
- };
28
-
29
- type State<D> = {
30
- listeners: Listeners<D>,
31
- keyedValues: KeyedValues<D>,
32
- value?: D[] // This is only used to avoid updating keyedValues on refresh wherever possible
33
- };
34
-
35
- export type IndexedItemActions<D> = {
36
- add(value: D): void,
37
- remove(): void,
38
- moveUp(): void,
39
- moveDown(): void,
40
- change(value: D): void
41
- };
42
- export type IndexedItemActionsMaker<D> = (index: number, key: SortableItemsModelKey) => IndexedItemActions<D>;
43
-
44
- type Actions<D> = {add(value: D, index?: number): void, commit(keyedValues: KeyedValues<D>): void, makeIndexedItemActions: IndexedItemActionsMaker<D>};
45
- export function useSortableItemsModel<D>(args: SortableItemsModelArgs<D>) {
46
- const {mutators, state} = usePseudoReducerBasedStateManagement<D>(args);
47
-
48
- const {emptyValue} = args;
49
- // We use a separate memo for the actions because dispatch should change significantly less-often than state
50
- const actions = useMemo<Actions<D>>(() => {
51
- return ({
52
- add: (value: D, index?: number) => {
53
- mutators.add({value, index});
54
- },
55
- commit: (keyedValues: KeyedValues<D>) => {
56
- mutators.commit({keyedValues});
57
- },
58
- addEmptyValue: emptyValue !== undefined && emptyValue !== null ? (index?: number) => {
59
- mutators.add({value: typeof emptyValue === 'object' ? {...emptyValue} : emptyValue, index});
60
- } : undefined,
61
- makeIndexedItemActions(index: number, key: SortableItemsModelKey) {
62
- return useMemo(() => ({
63
- add: (value: D) => {
64
- mutators.add({value, index});
65
- },
66
- remove: () => {
67
- mutators.remove({key});
68
- },
69
- moveUp: () => {
70
- mutators.move({oldIndex: index, newIndex: index - 1});
71
- },
72
- moveDown: () => {
73
- mutators.move({oldIndex: index, newIndex: index + 1});
74
- },
75
- change: (value: D) => {
76
- mutators.change({value, index});
77
- }
78
- }), [index, key, mutators]);
79
- }
80
- });
81
- }, [emptyValue, mutators]);
82
-
83
- return {...actions, keyedValues: state.keyedValues};
84
- }
85
-
86
- function swap<T>(value: T[], newIndex: number, oldIndex: number): T[] {
87
- const splicedValue = [...value];
88
- if (newIndex > oldIndex) {
89
- splicedValue.splice(newIndex + 1, 0, splicedValue[oldIndex]!);
90
- splicedValue.splice(oldIndex, 1);
91
- } else {
92
- splicedValue.splice(newIndex, 0, ...splicedValue.splice(oldIndex, 1));
93
- }
94
- return splicedValue;
95
- }
96
-
97
- function usePseudoReducerBasedStateManagement<D>({value, onAdd, onChange, onRemove, onReorder}: SortableItemsModelArgs<D>) {
98
- const [state, setState] = useState<State<D>>({keyedValues: [], listeners: {}});
99
-
100
- const mutators = useMemo<ActionDefs<D>>(() => {
101
- return {
102
- add({value, index}) {
103
- const newItemIndex = index ?? state.value?.length ?? 0;
104
- const newValue = state.value?.toSpliced(newItemIndex, 0, value) ?? [value];
105
- onAdd?.(newItemIndex, value);
106
- onChange?.(newValue);
107
- setState({
108
- ...state,
109
- value: newValue,
110
- keyedValues: state.keyedValues.toSpliced(newItemIndex, 0, {value: value, key: crypto.randomUUID()})
111
- });
112
- },
113
- remove({key}) {
114
- const actionIndex = state.keyedValues?.findIndex(({key: k}) => k === key) ?? -1;
115
- if (actionIndex === -1) {
116
- return;
117
- }
118
- const newValue = state.value?.toSpliced(actionIndex, 1) ?? [];
119
- onRemove?.(actionIndex);
120
- onChange?.(newValue);
121
- setState({
122
- ...state,
123
- value: newValue,
124
- keyedValues: state.keyedValues.toSpliced(actionIndex, 1)
125
- });
126
- },
127
- move({oldIndex, newIndex}) {
128
- if (newIndex === oldIndex) {
129
- return;
130
- }
131
- const newValue = swap(state.value ?? [], newIndex, oldIndex);
132
- const changesStart = Math.min(newIndex, oldIndex);
133
- newValue.splice(changesStart, newValue.length, ...newValue.slice(changesStart).map(v => clone(v)));
134
- onReorder?.(oldIndex, newIndex);
135
- onChange?.(newValue);
136
- setState({
137
- ...state,
138
- value: newValue,
139
- keyedValues: swap(state.keyedValues, newIndex, oldIndex).map(({key}, i) => ({
140
- value: newValue[i]!,
141
- key
142
- }))
143
- });
144
- },
145
- change({value, index}) {
146
- const newValue = (state.value ?? []).toSpliced(index, 1, value);
147
- onChange?.(newValue);
148
- setState({
149
- ...state,
150
- value: newValue,
151
- keyedValues: state.keyedValues.toSpliced(index, 1, {...state.keyedValues[index]!, value})
152
- });
153
- },
154
- commit({keyedValues}) {
155
- const newValue = keyedValues.map(({value}) => value);
156
- onChange?.(newValue);
157
- setState({
158
- ...state,
159
- value: newValue,
160
- keyedValues
161
- });
162
- },
163
- refresh(args) {
164
- const prevState = state;
165
- setState((new Immer({autoFreeze: false})).produce(prevState, (draft: State<D>) => {
166
- for (const [key, value] of Object.entries(args.listeners) as [keyof Listeners<D>, any][]) {
167
- draft.listeners[key] = value;
168
- }
169
- if (prevState.value !== args.value) {
170
- draft.value = args.value;
171
- if (args.value !== undefined) {
172
- if (args.value.length !== prevState.keyedValues.length) {
173
- draft.keyedValues = args.value.map((v, i) => {
174
- return {
175
- value: v,
176
- key: prevState.keyedValues[i] !== undefined && prevState.keyedValues[i].value === v ? prevState.keyedValues[i].key : crypto.randomUUID()
177
- };
178
- });
179
- } else {
180
- for (let i = 0; i < args.value.length; i++) {
181
- draft.keyedValues[i]!.value = args.value[i]!;
182
- }
183
- }
184
- }
185
- }
186
- }));
187
- }
188
- };
189
- }, [state, onAdd, onChange, onRemove, onReorder]);
190
-
191
- useEffect(() => {
192
- mutators.refresh({listeners: {onAdd, onChange, onRemove, onReorder}, value});
193
- }, [value, onAdd, onChange, onRemove, onReorder, mutators]);
194
-
195
- return {mutators, state};
196
- }
@@ -1,91 +0,0 @@
1
- import {useCallback, useEffect, useRef, useState} from "@wordpress/element";
2
-
3
- import {useNonRenderingCounter} from "./useNonRenderingCounter";
4
-
5
- import {useReducer} from "react";
6
-
7
- type UseSuggestionsArgs<T> = {
8
- expandOnFocus?: boolean,
9
- getOption?: (value?: string) => Promise<T|undefined>,
10
- getSuggestions(filterValue: string): Promise<T[]>
11
- };
12
- type SuggestionState<T> = {
13
- hasLoadingError: boolean,
14
- suggestions: T[],
15
- requestId: number
16
- };
17
- function useSimpleDebouncer(delay: number) {
18
- const timerId = useRef<ReturnType<typeof setTimeout>>(undefined);
19
- return useCallback((action: () => void) => {
20
- if (timerId.current !== undefined) {
21
- clearTimeout(timerId.current);
22
- }
23
- timerId.current = setTimeout(() => {
24
- timerId.current = undefined;
25
- action();
26
- }, delay);
27
- }, [timerId]);
28
- }
29
-
30
- export function useSuggestions<T>(initialValue: string|null|undefined, {expandOnFocus, getOption, getSuggestions}: UseSuggestionsArgs<T>) {
31
- const suggestionRequestDebouncer = useSimpleDebouncer(200);
32
- const [requestId, nextRequestId] = useNonRenderingCounter();
33
- const [isLoading, setIsLoading] = useState(false);
34
- const [latestSuggestions, dispatchSuggestionsUpdate] = useReducer(useSuggestionSuggestionUpdateReducer<T>,
35
- {hasLoadingError: false, requestId: 0, suggestions: []});
36
- const [input, setInput] = useState(initialValue ?? "");
37
-
38
- useEffect(() => {
39
- const myRequestId = nextRequestId();
40
- if (getOption === undefined) {
41
- dispatchSuggestionsUpdate({hasLoadingError: false, suggestions: [], requestId: myRequestId});
42
- return;
43
- }
44
- if (!expandOnFocus && input.length < 2) {
45
- dispatchSuggestionsUpdate({hasLoadingError: false, suggestions: [], requestId: myRequestId});
46
- } else {
47
- setIsLoading(true);
48
- getOption(input)
49
- .then(option => dispatchSuggestionsUpdate({hasLoadingError: false, suggestions: option ? [option] : [], requestId: myRequestId}))
50
- .catch(err => {
51
- dispatchSuggestionsUpdate({hasLoadingError: true, suggestions: [], requestId: myRequestId});
52
- console.error("An error occurred while loading options:", err);
53
- });
54
- }
55
- }, [expandOnFocus, getOption]);
56
-
57
- useEffect(() => {
58
- const myRequestId = nextRequestId();
59
- if (!expandOnFocus && input.length < 2) {
60
- dispatchSuggestionsUpdate({hasLoadingError: false, suggestions: [], requestId: myRequestId});
61
- } else {
62
- const myRequestId = nextRequestId();
63
- setIsLoading(true);
64
- suggestionRequestDebouncer(() => {
65
- getSuggestions(input)
66
- .then(suggestions => dispatchSuggestionsUpdate({hasLoadingError: false, suggestions, requestId: myRequestId}))
67
- .catch(err => {
68
- dispatchSuggestionsUpdate({hasLoadingError: true, suggestions: [], requestId: myRequestId});
69
- console.error("An error occurred while loading options:", err);
70
- });
71
- });
72
- }
73
- }, [input, suggestionRequestDebouncer]);
74
-
75
- // This is a separate useEffect hook because we don't necessarily want to update the rendered state when a new request is sent out.
76
- useEffect(() => setIsLoading(latestSuggestions.requestId < requestId.current), [latestSuggestions.requestId]);
77
-
78
- return {
79
- hasLoadingError: latestSuggestions.hasLoadingError, isInitializing: latestSuggestions.requestId === 0, isLoading,
80
- input, setInput, suggestions: latestSuggestions.suggestions
81
- }
82
- }
83
- function useSuggestionSuggestionUpdateReducer<T>(state: SuggestionState<T>, action: SuggestionState<T>): SuggestionState<T> {
84
- if (action.requestId <= state.requestId) {
85
- return state;
86
- }
87
- if (action.hasLoadingError) {
88
- return {hasLoadingError: true, requestId: action.requestId, suggestions: [...state.suggestions]};
89
- }
90
- return action;
91
- }