@elementor/editor-controls 4.2.0-856 → 4.2.0-857

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-controls",
3
3
  "description": "This package contains the controls model and utils for the Elementor editor",
4
- "version": "4.2.0-856",
4
+ "version": "4.2.0-857",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,22 +40,22 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "4.2.0-856",
44
- "@elementor/editor-elements": "4.2.0-856",
45
- "@elementor/editor-props": "4.2.0-856",
46
- "@elementor/editor-responsive": "4.2.0-856",
47
- "@elementor/editor-ui": "4.2.0-856",
48
- "@elementor/editor-v1-adapters": "4.2.0-856",
49
- "@elementor/env": "4.2.0-856",
50
- "@elementor/events": "4.2.0-856",
51
- "@elementor/http-client": "4.2.0-856",
43
+ "@elementor/editor-current-user": "4.2.0-857",
44
+ "@elementor/editor-elements": "4.2.0-857",
45
+ "@elementor/editor-props": "4.2.0-857",
46
+ "@elementor/editor-responsive": "4.2.0-857",
47
+ "@elementor/editor-ui": "4.2.0-857",
48
+ "@elementor/editor-v1-adapters": "4.2.0-857",
49
+ "@elementor/env": "4.2.0-857",
50
+ "@elementor/events": "4.2.0-857",
51
+ "@elementor/http-client": "4.2.0-857",
52
52
  "@elementor/icons": "~1.75.1",
53
- "@elementor/locations": "4.2.0-856",
54
- "@elementor/query": "4.2.0-856",
55
- "@elementor/session": "4.2.0-856",
53
+ "@elementor/locations": "4.2.0-857",
54
+ "@elementor/query": "4.2.0-857",
55
+ "@elementor/session": "4.2.0-857",
56
56
  "@elementor/ui": "1.37.5",
57
- "@elementor/utils": "4.2.0-856",
58
- "@elementor/wp-media": "4.2.0-856",
57
+ "@elementor/utils": "4.2.0-857",
58
+ "@elementor/wp-media": "4.2.0-857",
59
59
  "@monaco-editor/react": "^4.7.0",
60
60
  "@tiptap/extension-bold": "^3.11.1",
61
61
  "@tiptap/extension-document": "^3.11.1",
@@ -0,0 +1,243 @@
1
+ import * as React from 'react';
2
+ import { useMemo, useRef } from 'react';
3
+ import {
4
+ type CreateOptions,
5
+ type PropKey,
6
+ queryFilterArrayPropTypeUtil,
7
+ type QueryFilterKeyConfig,
8
+ queryFilterPropTypeUtil,
9
+ type QueryFilterPropValue,
10
+ type QueryPropValue,
11
+ stringPropTypeUtil,
12
+ } from '@elementor/editor-props';
13
+ import { PlusIcon } from '@elementor/icons';
14
+ import { Box, Grid, IconButton } from '@elementor/ui';
15
+ import { __, sprintf } from '@wordpress/i18n';
16
+
17
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
18
+ import { ControlFormLabel } from '../components/control-form-label';
19
+ import { ControlRepeater, Item, ItemsContainer } from '../components/control-repeater';
20
+ import { RemoveItemAction } from '../components/control-repeater/actions/remove-item-action';
21
+ import { useRepeaterContext } from '../components/control-repeater/context/repeater-context';
22
+ import { EditItemPopover } from '../components/control-repeater/items/edit-item-popover';
23
+ import { PopoverContent } from '../components/popover-content';
24
+ import { PopoverGridContainer } from '../components/popover-grid-container';
25
+ import { RepeaterHeader } from '../components/repeater/repeater-header';
26
+ import { createControl } from '../create-control';
27
+ import { QueryChipsControl } from './query-chips-control';
28
+ import { SelectControl, type SelectOption } from './select-control';
29
+
30
+ type QueryFilterRepeaterControlProps = {
31
+ allowedKeys: string[];
32
+ keyConfig: Record< string, QueryFilterKeyConfig >;
33
+ label?: string;
34
+ chipsPlaceholder?: string;
35
+ };
36
+
37
+ export const QueryFilterRepeaterControl = createControl(
38
+ ( {
39
+ allowedKeys,
40
+ keyConfig,
41
+ label = __( 'Filter', 'elementor' ),
42
+ chipsPlaceholder,
43
+ }: QueryFilterRepeaterControlProps ) => {
44
+ const { propType, value, setValue } = useBoundProp( queryFilterArrayPropTypeUtil );
45
+
46
+ const usedKeys = useMemo( () => getUsedKeys( value ?? [] ), [ value ] );
47
+ const nextAvailableKey = useMemo(
48
+ () => allowedKeys.find( ( key ) => ! usedKeys.has( key ) ) ?? null,
49
+ [ allowedKeys, usedKeys ]
50
+ );
51
+
52
+ const getKeySelectOptions = useMemo(
53
+ () => ( currentKey: string | null ) =>
54
+ allowedKeys.map( ( itemKey ) => ( {
55
+ value: itemKey,
56
+ label: keyConfig[ itemKey ]?.label ?? itemKey,
57
+ disabled: itemKey !== currentKey && usedKeys.has( itemKey ),
58
+ } ) ),
59
+ [ allowedKeys, keyConfig, usedKeys ]
60
+ );
61
+
62
+ const initialFallback = useMemo( () => createItemForKey( allowedKeys[ 0 ] ?? '' ), [ allowedKeys ] );
63
+
64
+ return (
65
+ <PropProvider propType={ propType } value={ value } setValue={ setValue }>
66
+ <ControlRepeater initial={ initialFallback } propTypeUtil={ queryFilterArrayPropTypeUtil }>
67
+ <RepeaterHeader label={ label }>
68
+ <AddFilterItemAction nextAvailableKey={ nextAvailableKey } ariaLabel={ label } />
69
+ </RepeaterHeader>
70
+ <ItemsContainer isSortable={ false }>
71
+ <Item
72
+ Icon={ EmptyIcon }
73
+ actions={ <RemoveItemAction /> }
74
+ Label={ ( { value: itemValue }: { value: QueryFilterPropValue } ) => (
75
+ <ItemLabel value={ itemValue } keyConfig={ keyConfig } />
76
+ ) }
77
+ />
78
+ </ItemsContainer>
79
+ <EditItemPopover>
80
+ <ItemContent
81
+ keyConfig={ keyConfig }
82
+ getKeySelectOptions={ getKeySelectOptions }
83
+ chipsPlaceholder={ chipsPlaceholder }
84
+ />
85
+ </EditItemPopover>
86
+ </ControlRepeater>
87
+ </PropProvider>
88
+ );
89
+ }
90
+ );
91
+
92
+ const AddFilterItemAction = ( {
93
+ nextAvailableKey,
94
+ ariaLabel,
95
+ }: {
96
+ nextAvailableKey: string | null;
97
+ ariaLabel: string;
98
+ } ) => {
99
+ const { addItem } = useRepeaterContext();
100
+ const disabled = nextAvailableKey === null;
101
+
102
+ const onClick = ( ev: React.MouseEvent ) => {
103
+ if ( ! nextAvailableKey ) {
104
+ return;
105
+ }
106
+
107
+ addItem( ev, { item: createItemForKey( nextAvailableKey ), index: 0 } );
108
+ };
109
+
110
+ return (
111
+ <Box component="span" sx={ { cursor: disabled ? 'not-allowed' : 'pointer' } }>
112
+ <IconButton
113
+ size="tiny"
114
+ disabled={ disabled }
115
+ onClick={ onClick }
116
+ /* Translators: %s: Aria label. */
117
+ aria-label={ sprintf( __( 'Add %s item', 'elementor' ), ariaLabel.toLowerCase() ) }
118
+ >
119
+ <PlusIcon fontSize="tiny" />
120
+ </IconButton>
121
+ </Box>
122
+ );
123
+ };
124
+
125
+ const EmptyIcon = () => null;
126
+
127
+ type ItemLabelProps = {
128
+ value: QueryFilterPropValue;
129
+ keyConfig: Record< string, QueryFilterKeyConfig >;
130
+ };
131
+
132
+ const ItemLabel = ( { value, keyConfig }: ItemLabelProps ) => {
133
+ const itemKey = stringPropTypeUtil.extract( value?.value?.key );
134
+ const label = ( itemKey && keyConfig[ itemKey ]?.label ) || __( 'Item', 'elementor' );
135
+ const chipLabels = extractChipLabels( value?.value?.values );
136
+ const suffix = chipLabels.length > 0 ? `: ${ chipLabels.join( ', ' ) }` : '';
137
+
138
+ return (
139
+ <Box component="span">
140
+ { label }
141
+ { suffix }
142
+ </Box>
143
+ );
144
+ };
145
+
146
+ type QueryArrayPropValue = { value?: QueryPropValue[] | null };
147
+
148
+ function extractChipLabels< T extends QueryArrayPropValue | undefined >( chipsProp: T ): string[] {
149
+ const chips = chipsProp?.value ?? [];
150
+
151
+ return chips
152
+ .map( ( chip ) => stringPropTypeUtil.extract( chip?.value?.label ) )
153
+ .filter( ( label ): label is string => !! label );
154
+ }
155
+
156
+ type FilterItemValue = NonNullable< QueryFilterPropValue[ 'value' ] >;
157
+
158
+ const ItemContent = ( {
159
+ keyConfig,
160
+ getKeySelectOptions,
161
+ chipsPlaceholder,
162
+ }: {
163
+ keyConfig: Record< string, QueryFilterKeyConfig >;
164
+ getKeySelectOptions: ( currentKey: string | null ) => SelectOption[];
165
+ chipsPlaceholder?: string;
166
+ } ) => {
167
+ const propContext = useBoundProp( queryFilterPropTypeUtil );
168
+
169
+ const valuesByKeyRef = useRef< Record< string, FilterItemValue[ 'values' ] | null > >( {} );
170
+
171
+ const handleValueChange = ( nextValue: FilterItemValue, options?: CreateOptions, meta?: { bind?: PropKey } ) => {
172
+ if ( meta?.bind !== 'key' ) {
173
+ propContext.setValue( nextValue, options, meta );
174
+ return;
175
+ }
176
+
177
+ const previousKey = stringPropTypeUtil.extract( propContext.value?.key );
178
+ const newKey = stringPropTypeUtil.extract( nextValue?.key );
179
+
180
+ if ( previousKey ) {
181
+ valuesByKeyRef.current[ previousKey ] = propContext.value?.values ?? null;
182
+ }
183
+
184
+ const restoredValues = newKey ? valuesByKeyRef.current[ newKey ] ?? null : null;
185
+
186
+ propContext.setValue( { ...nextValue, values: restoredValues }, options, meta );
187
+ };
188
+
189
+ const currentKey = stringPropTypeUtil.extract( propContext.value?.key );
190
+ const currentKeyConfig = currentKey ? keyConfig[ currentKey ] : undefined;
191
+ const hasValuesField = !! currentKeyConfig?.queryEndpoint;
192
+ const keySelectOptions = useMemo( () => getKeySelectOptions( currentKey ), [ getKeySelectOptions, currentKey ] );
193
+
194
+ return (
195
+ <PopoverContent p={ 1.5 }>
196
+ <PropProvider { ...propContext } setValue={ handleValueChange }>
197
+ <PopoverGridContainer flexWrap="wrap">
198
+ <Grid item xs={ 12 }>
199
+ <ControlFormLabel>{ __( 'Type', 'elementor' ) }</ControlFormLabel>
200
+ </Grid>
201
+ <Grid item xs={ 12 }>
202
+ <PropKeyProvider bind="key">
203
+ <SelectControl options={ keySelectOptions } />
204
+ </PropKeyProvider>
205
+ </Grid>
206
+ </PopoverGridContainer>
207
+ { hasValuesField && currentKeyConfig?.queryEndpoint && (
208
+ <PopoverGridContainer flexWrap="wrap">
209
+ <Grid item xs={ 12 }>
210
+ <ControlFormLabel>{ currentKeyConfig.label }</ControlFormLabel>
211
+ </Grid>
212
+ <Grid item xs={ 12 }>
213
+ <PropKeyProvider bind="values">
214
+ <QueryChipsControl
215
+ queryOptions={ {
216
+ url: currentKeyConfig.queryEndpoint.url,
217
+ params: currentKeyConfig.queryEndpoint.params ?? {},
218
+ } }
219
+ placeholder={ currentKeyConfig.chipsPlaceholder ?? chipsPlaceholder }
220
+ />
221
+ </PropKeyProvider>
222
+ </Grid>
223
+ </PopoverGridContainer>
224
+ ) }
225
+ </PropProvider>
226
+ </PopoverContent>
227
+ );
228
+ };
229
+
230
+ function createItemForKey( key: string ): QueryFilterPropValue {
231
+ return queryFilterPropTypeUtil.create( {
232
+ key: stringPropTypeUtil.create( key ),
233
+ values: null,
234
+ } );
235
+ }
236
+
237
+ function getUsedKeys( items: QueryFilterPropValue[] ): Set< string > {
238
+ const keys = items
239
+ .map( ( item ) => stringPropTypeUtil.extract( item?.value?.key ) )
240
+ .filter( ( key ): key is string => !! key );
241
+
242
+ return new Set( keys );
243
+ }
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export { LinkControl } from './controls/link-control';
22
22
  export { HtmlTagControl } from './controls/html-tag-control';
23
23
  export { QueryChipsControl } from './controls/query-chips-control';
24
24
  export { QueryControl } from './controls/query-control';
25
+ export { QueryFilterRepeaterControl } from './controls/query-filter-repeater-control';
25
26
  export { GapControl } from './controls/gap-control';
26
27
  export { AspectRatioControl } from './controls/aspect-ratio-control';
27
28
  export { SvgMediaControl } from './controls/svg-media-control';