@elementor/editor-controls 4.2.0-839 → 4.2.0-841

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-839",
4
+ "version": "4.2.0-841",
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-839",
44
- "@elementor/editor-elements": "4.2.0-839",
45
- "@elementor/editor-props": "4.2.0-839",
46
- "@elementor/editor-responsive": "4.2.0-839",
47
- "@elementor/editor-ui": "4.2.0-839",
48
- "@elementor/editor-v1-adapters": "4.2.0-839",
49
- "@elementor/env": "4.2.0-839",
50
- "@elementor/events": "4.2.0-839",
51
- "@elementor/http-client": "4.2.0-839",
43
+ "@elementor/editor-current-user": "4.2.0-841",
44
+ "@elementor/editor-elements": "4.2.0-841",
45
+ "@elementor/editor-props": "4.2.0-841",
46
+ "@elementor/editor-responsive": "4.2.0-841",
47
+ "@elementor/editor-ui": "4.2.0-841",
48
+ "@elementor/editor-v1-adapters": "4.2.0-841",
49
+ "@elementor/env": "4.2.0-841",
50
+ "@elementor/events": "4.2.0-841",
51
+ "@elementor/http-client": "4.2.0-841",
52
52
  "@elementor/icons": "~1.75.1",
53
- "@elementor/locations": "4.2.0-839",
54
- "@elementor/query": "4.2.0-839",
55
- "@elementor/session": "4.2.0-839",
53
+ "@elementor/locations": "4.2.0-841",
54
+ "@elementor/query": "4.2.0-841",
55
+ "@elementor/session": "4.2.0-841",
56
56
  "@elementor/ui": "1.37.5",
57
- "@elementor/utils": "4.2.0-839",
58
- "@elementor/wp-media": "4.2.0-839",
57
+ "@elementor/utils": "4.2.0-841",
58
+ "@elementor/wp-media": "4.2.0-841",
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,22 @@
1
+ import * as React from 'react';
2
+ import { type AutocompleteRenderGetTagProps, Chip } from '@elementor/ui';
3
+
4
+ const CHIP_SIZE = 'tiny' as const;
5
+
6
+ export type ChipsListProps< Option > = {
7
+ getLabel: ( option: Option ) => string;
8
+ getTagProps: AutocompleteRenderGetTagProps;
9
+ values: Option[];
10
+ };
11
+
12
+ export function ChipsList< Option >( { getLabel, getTagProps, values }: ChipsListProps< Option > ) {
13
+ return (
14
+ <>
15
+ { values.map( ( option, index ) => {
16
+ const { key, ...tagProps } = getTagProps( { index } );
17
+
18
+ return <Chip key={ key } label={ getLabel( option ) } size={ CHIP_SIZE } { ...tagProps } />;
19
+ } ) }
20
+ </>
21
+ );
22
+ }
@@ -1,9 +1,10 @@
1
1
  import * as React from 'react';
2
2
  import { type SyntheticEvent } from 'react';
3
3
  import { stringArrayPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
4
- import { Autocomplete, Chip, TextField } from '@elementor/ui';
4
+ import { Autocomplete, TextField } from '@elementor/ui';
5
5
 
6
6
  import { useBoundProp } from '../bound-prop-context';
7
+ import { ChipsList } from '../components/chips-list';
7
8
  import ControlActions from '../control-actions/control-actions';
8
9
  import { createControl } from '../create-control';
9
10
 
@@ -47,12 +48,13 @@ export const ChipsControl = createControl( ( { options }: ChipsControlProps ) =>
47
48
  getOptionLabel={ ( option ) => option.label }
48
49
  isOptionEqualToValue={ ( option, val ) => option.value === val.value }
49
50
  renderInput={ ( params ) => <TextField { ...params } /> }
50
- renderTags={ ( values, getTagProps ) =>
51
- values.map( ( option, index ) => {
52
- const { key, ...chipProps } = getTagProps( { index } );
53
- return <Chip key={ key } size="tiny" label={ option.label } { ...chipProps } />;
54
- } )
55
- }
51
+ renderTags={ ( tagValues, getTagProps ) => (
52
+ <ChipsList
53
+ getLabel={ ( option ) => option.label }
54
+ getTagProps={ getTagProps }
55
+ values={ tagValues }
56
+ />
57
+ ) }
56
58
  />
57
59
  </ControlActions>
58
60
  );
@@ -0,0 +1,110 @@
1
+ import * as React from 'react';
2
+ import { type SyntheticEvent, useMemo } from 'react';
3
+ import {
4
+ createArrayPropUtils,
5
+ numberPropTypeUtil,
6
+ queryPropTypeUtil,
7
+ type QueryPropValue,
8
+ stringPropTypeUtil,
9
+ } from '@elementor/editor-props';
10
+ import { Autocomplete, TextField } from '@elementor/ui';
11
+ import { __ } from '@wordpress/i18n';
12
+
13
+ import { useBoundProp } from '../bound-prop-context';
14
+ import { ChipsList } from '../components/chips-list';
15
+ import ControlActions from '../control-actions/control-actions';
16
+ import { createControl } from '../create-control';
17
+ import { extractFlatOptionFromQueryValue, useQueryAutocomplete } from '../hooks/use-query-autocomplete';
18
+
19
+ type QueryChipsControlProps = {
20
+ queryOptions: {
21
+ params: Record< string, unknown >;
22
+ url: string;
23
+ };
24
+ placeholder?: string;
25
+ minInputLength?: number;
26
+ };
27
+
28
+ type ChipOption = {
29
+ id: string;
30
+ label: string;
31
+ };
32
+
33
+ const queryArrayPropTypeUtil = createArrayPropUtils( queryPropTypeUtil.key, queryPropTypeUtil.schema );
34
+
35
+ const SIZE = 'tiny';
36
+
37
+ export const QueryChipsControl = createControl( ( props: QueryChipsControlProps ) => {
38
+ const { queryOptions, placeholder, minInputLength = 2 } = props;
39
+ const { value, setValue, disabled } = useBoundProp( queryArrayPropTypeUtil );
40
+
41
+ const selectedChips = useMemo< ChipOption[] >( () => extractChips( value ), [ value ] );
42
+
43
+ const excludeIds = useMemo(
44
+ () =>
45
+ selectedChips.map( ( chip ) => Number( chip.id ) ).filter( ( id ): id is number => Number.isFinite( id ) ),
46
+ [ selectedChips ]
47
+ );
48
+
49
+ const { options, updateOptions } = useQueryAutocomplete( {
50
+ url: queryOptions.url,
51
+ params: queryOptions.params,
52
+ minInputLength,
53
+ excludeIds,
54
+ } );
55
+
56
+ const handleChange = ( _: SyntheticEvent, newValue: ChipOption[] ) => {
57
+ setValue(
58
+ newValue.map( ( option ) =>
59
+ queryPropTypeUtil.create( {
60
+ id: numberPropTypeUtil.create( Number( option.id ) ),
61
+ label: stringPropTypeUtil.create( option.label ),
62
+ } )
63
+ )
64
+ );
65
+ };
66
+
67
+ const handleInputChange = ( _: SyntheticEvent, term: string ) => {
68
+ updateOptions( term || null );
69
+ };
70
+
71
+ return (
72
+ <ControlActions>
73
+ <Autocomplete
74
+ multiple
75
+ fullWidth
76
+ disableClearable
77
+ forcePopupIcon={ false }
78
+ disabled={ disabled }
79
+ size={ SIZE }
80
+ value={ selectedChips }
81
+ options={ options as ChipOption[] }
82
+ onChange={ handleChange }
83
+ onInputChange={ handleInputChange }
84
+ getOptionLabel={ ( option ) => option.label }
85
+ isOptionEqualToValue={ ( option, val ) => option.id === val.id }
86
+ filterOptions={ ( opts ) => opts }
87
+ renderTags={ ( tagValues, getTagProps ) => (
88
+ <ChipsList
89
+ getLabel={ ( option ) => option.label }
90
+ getTagProps={ getTagProps }
91
+ values={ tagValues }
92
+ />
93
+ ) }
94
+ renderInput={ ( params ) => (
95
+ <TextField { ...params } placeholder={ placeholder ?? __( 'Search', 'elementor' ) } />
96
+ ) }
97
+ />
98
+ </ControlActions>
99
+ );
100
+ } );
101
+
102
+ function extractChips( value: QueryPropValue[] | null | undefined ): ChipOption[] {
103
+ if ( ! value ) {
104
+ return [];
105
+ }
106
+
107
+ return value
108
+ .map( ( item ) => extractFlatOptionFromQueryValue( item?.value ) )
109
+ .filter( ( chip ): chip is ChipOption => chip !== null );
110
+ }
@@ -1,27 +1,13 @@
1
1
  import * as React from 'react';
2
- import { useMemo, useState } from 'react';
3
- import {
4
- numberPropTypeUtil,
5
- queryPropTypeUtil,
6
- type QueryPropValue,
7
- stringPropTypeUtil,
8
- urlPropTypeUtil,
9
- } from '@elementor/editor-props';
10
- import { type HttpResponse, httpService } from '@elementor/http-client';
2
+ import { numberPropTypeUtil, queryPropTypeUtil, stringPropTypeUtil, urlPropTypeUtil } from '@elementor/editor-props';
11
3
  import { SearchIcon } from '@elementor/icons';
12
- import { debounce } from '@elementor/utils';
13
4
  import { __ } from '@wordpress/i18n';
14
5
 
15
6
  import { useBoundProp } from '../bound-prop-context';
16
- import {
17
- Autocomplete,
18
- type CategorizedOption,
19
- findMatchingOption,
20
- type FlatOption,
21
- isCategorizedOptionPool,
22
- } from '../components/autocomplete';
7
+ import { Autocomplete, findMatchingOption } from '../components/autocomplete';
23
8
  import ControlActions from '../control-actions/control-actions';
24
9
  import { createControl } from '../create-control';
10
+ import { useQueryAutocomplete } from '../hooks/use-query-autocomplete';
25
11
  import { type DestinationProp } from './link-control';
26
12
 
27
13
  type Props = {
@@ -36,10 +22,6 @@ type Props = {
36
22
  ariaLabel?: string;
37
23
  };
38
24
 
39
- type Response = HttpResponse< { value: FlatOption[] | CategorizedOption[] } >;
40
-
41
- type FetchOptionsParams = Record< string, unknown > & { term: string };
42
-
43
25
  export const QueryControl = createControl( ( props: Props ) => {
44
26
  const { value: queryValue, setValue: setQueryValue } = useBoundProp( queryPropTypeUtil );
45
27
  const { value: urlValue, setValue: setUrlValue, placeholder: urlPlaceholder } = useBoundProp( urlPropTypeUtil );
@@ -53,9 +35,12 @@ export const QueryControl = createControl( ( props: Props ) => {
53
35
  ariaLabel,
54
36
  } = props || {};
55
37
 
56
- const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
57
- generateFirstLoadedOption( queryValue )
58
- );
38
+ const { options, updateOptions } = useQueryAutocomplete( {
39
+ url,
40
+ params,
41
+ minInputLength,
42
+ initialQueryValue: queryValue,
43
+ } );
59
44
 
60
45
  const onOptionChange = ( newValue: number | null ) => {
61
46
  if ( newValue === null ) {
@@ -89,28 +74,6 @@ export const QueryControl = createControl( ( props: Props ) => {
89
74
  updateOptions( newValue );
90
75
  };
91
76
 
92
- const updateOptions = ( newValue: string | null ) => {
93
- setOptions( [] );
94
-
95
- if ( ! newValue || ! url || newValue.length < minInputLength ) {
96
- return;
97
- }
98
-
99
- debounceFetch( { ...params, term: newValue } );
100
- };
101
-
102
- const debounceFetch = useMemo(
103
- () =>
104
- debounce(
105
- ( queryParams: FetchOptionsParams ) =>
106
- fetchOptions( url, queryParams ).then( ( newOptions ) => {
107
- setOptions( formatOptions( newOptions ) );
108
- } ),
109
- 400
110
- ),
111
- [ url ]
112
- );
113
-
114
77
  const displayValue = queryValue?.id?.value ?? urlValue;
115
78
 
116
79
  return (
@@ -132,38 +95,3 @@ export const QueryControl = createControl( ( props: Props ) => {
132
95
  </ControlActions>
133
96
  );
134
97
  } );
135
-
136
- async function fetchOptions( ajaxUrl: string, params: FetchOptionsParams ) {
137
- if ( ! params || ! ajaxUrl ) {
138
- return [];
139
- }
140
-
141
- try {
142
- const { data: response } = await httpService().get< Response >( ajaxUrl, { params } );
143
-
144
- return response.data.value;
145
- } catch {
146
- return [];
147
- }
148
- }
149
-
150
- function formatOptions( options: FlatOption[] | CategorizedOption[] ): FlatOption[] | CategorizedOption[] {
151
- const compareKey = isCategorizedOptionPool( options ) ? 'groupLabel' : 'label';
152
-
153
- return options.sort( ( a, b ) =>
154
- a[ compareKey ] && b[ compareKey ] ? a[ compareKey ].localeCompare( b[ compareKey ] ) : 0
155
- );
156
- }
157
-
158
- function generateFirstLoadedOption( queryValue: QueryPropValue[ 'value' ] | null ): FlatOption[] {
159
- const id = queryValue?.id?.value;
160
- const label = queryValue?.label?.value;
161
-
162
- const option = [];
163
-
164
- if ( id && label ) {
165
- option.push( { id: id.toString(), label } );
166
- }
167
-
168
- return option;
169
- }
@@ -0,0 +1,118 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { numberPropTypeUtil, type QueryPropValue, stringPropTypeUtil } from '@elementor/editor-props';
3
+ import { type HttpResponse, httpService } from '@elementor/http-client';
4
+ import { debounce } from '@elementor/utils';
5
+
6
+ import { type CategorizedOption, type FlatOption, isCategorizedOptionPool } from '../components/autocomplete';
7
+
8
+ type Response = HttpResponse< { value: FlatOption[] | CategorizedOption[] } >;
9
+
10
+ type FetchOptionsParams = Record< string, unknown > & { term: string };
11
+
12
+ type UseQueryAutocompleteOptions = {
13
+ url: string;
14
+ params?: Record< string, unknown >;
15
+ minInputLength?: number;
16
+ initialQueryValue?: QueryPropValue[ 'value' ] | null;
17
+ excludeIds?: number[];
18
+ };
19
+
20
+ export type UseQueryAutocompleteResult = {
21
+ options: FlatOption[] | CategorizedOption[];
22
+ updateOptions: ( term: string | null ) => void;
23
+ };
24
+
25
+ export function useQueryAutocomplete( {
26
+ url,
27
+ params = {},
28
+ minInputLength = 2,
29
+ initialQueryValue = null,
30
+ excludeIds,
31
+ }: UseQueryAutocompleteOptions ): UseQueryAutocompleteResult {
32
+ const excludeIdSet = useMemo( () => new Set( ( excludeIds ?? [] ).map( String ) ), [ excludeIds ] );
33
+
34
+ const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
35
+ generateFirstLoadedOption( initialQueryValue )
36
+ );
37
+
38
+ const debounceFetch = useMemo(
39
+ () =>
40
+ debounce(
41
+ ( queryParams: FetchOptionsParams ) =>
42
+ fetchOptions( url, queryParams ).then( ( newOptions ) => {
43
+ setOptions( formatOptions( filterExcludedOptions( newOptions, excludeIdSet ) ) );
44
+ } ),
45
+ 400
46
+ ),
47
+ [ url, excludeIdSet ]
48
+ );
49
+
50
+ const updateOptions = ( term: string | null ) => {
51
+ setOptions( [] );
52
+
53
+ if ( ! term || ! url || term.length < minInputLength ) {
54
+ return;
55
+ }
56
+
57
+ debounceFetch( { ...params, term } );
58
+ };
59
+
60
+ return { options, updateOptions };
61
+ }
62
+
63
+ async function fetchOptions( ajaxUrl: string, params: FetchOptionsParams ) {
64
+ if ( ! params || ! ajaxUrl ) {
65
+ return [];
66
+ }
67
+
68
+ try {
69
+ const { data: response } = await httpService().get< Response >( ajaxUrl, { params } );
70
+
71
+ return response.data.value;
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ function formatOptions( options: FlatOption[] | CategorizedOption[] ): FlatOption[] | CategorizedOption[] {
78
+ const compareKey = isCategorizedOptionPool( options ) ? 'groupLabel' : 'label';
79
+
80
+ return options.sort( ( a, b ) =>
81
+ a[ compareKey ] && b[ compareKey ] ? a[ compareKey ].localeCompare( b[ compareKey ] ) : 0
82
+ );
83
+ }
84
+
85
+ function filterExcludedOptions(
86
+ options: FlatOption[] | CategorizedOption[],
87
+ excludeIdSet: Set< string >
88
+ ): FlatOption[] | CategorizedOption[] {
89
+ if ( excludeIdSet.size === 0 ) {
90
+ return options;
91
+ }
92
+
93
+ return options.filter( ( option ) => ! excludeIdSet.has( String( option.id ) ) ) as
94
+ | FlatOption[]
95
+ | CategorizedOption[];
96
+ }
97
+
98
+ export function extractFlatOptionFromQueryValue(
99
+ queryValue: QueryPropValue[ 'value' ] | null | undefined
100
+ ): FlatOption | null {
101
+ const id = numberPropTypeUtil.extract( queryValue?.id );
102
+ const label = stringPropTypeUtil.extract( queryValue?.label );
103
+
104
+ if ( id === null ) {
105
+ return null;
106
+ }
107
+
108
+ return {
109
+ id: String( id ),
110
+ label: label || String( id ),
111
+ };
112
+ }
113
+
114
+ function generateFirstLoadedOption( queryValue: QueryPropValue[ 'value' ] | null ): FlatOption[] {
115
+ const option = extractFlatOptionFromQueryValue( queryValue );
116
+
117
+ return option ? [ option ] : [];
118
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export { ItemSelector } from './components/item-selector';
20
20
  export { UrlControl } from './controls/url-control';
21
21
  export { LinkControl } from './controls/link-control';
22
22
  export { HtmlTagControl } from './controls/html-tag-control';
23
+ export { QueryChipsControl } from './controls/query-chips-control';
23
24
  export { QueryControl } from './controls/query-control';
24
25
  export { GapControl } from './controls/gap-control';
25
26
  export { AspectRatioControl } from './controls/aspect-ratio-control';