@elementor/editor-controls 3.33.0-98 → 3.35.0-324
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/dist/index.d.mts +276 -85
- package/dist/index.d.ts +276 -85
- package/dist/index.js +2491 -1783
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2304 -1592
- package/dist/index.mjs.map +1 -1
- package/package.json +31 -17
- package/src/bound-prop-context/prop-context.tsx +7 -1
- package/src/bound-prop-context/use-bound-prop.ts +19 -5
- package/src/components/autocomplete.tsx +34 -3
- package/src/components/conditional-control-infotip.tsx +64 -0
- package/src/components/{unstable-repeater → control-repeater}/actions/disable-item-action.tsx +2 -2
- package/src/components/{unstable-repeater → control-repeater}/actions/duplicate-item-action.tsx +10 -4
- package/src/components/{unstable-repeater → control-repeater}/actions/remove-item-action.tsx +2 -2
- package/src/components/control-repeater/context/item-context.tsx +8 -0
- package/src/components/{unstable-repeater → control-repeater}/context/repeater-context.tsx +24 -15
- package/src/components/control-repeater/control-repeater.tsx +29 -0
- package/src/components/{unstable-repeater → control-repeater}/index.ts +1 -2
- package/src/components/{unstable-repeater → control-repeater}/items/edit-item-popover.tsx +6 -20
- package/src/components/control-repeater/items/item.tsx +75 -0
- package/src/components/{unstable-repeater → control-repeater}/items/items-container.tsx +8 -13
- package/src/components/{unstable-repeater → control-repeater}/locations.ts +0 -4
- package/src/components/{unstable-repeater → control-repeater}/types.ts +1 -2
- package/src/components/control-toggle-button-group.tsx +79 -69
- package/src/components/enable-unfiltered-modal.tsx +1 -26
- package/src/components/icon-buttons/clear-icon-button.tsx +23 -0
- package/src/components/inline-editor-toolbar.tsx +137 -0
- package/src/components/inline-editor.tsx +111 -0
- package/src/components/item-selector.tsx +10 -4
- package/src/components/{unstable-repeater/header/header.tsx → repeater/repeater-header.tsx} +4 -12
- package/src/components/repeater/repeater-popover.tsx +19 -0
- package/src/components/repeater/repeater-tag.tsx +16 -0
- package/src/components/repeater/repeater.tsx +405 -0
- package/src/components/{sortable.tsx → repeater/sortable.tsx} +1 -1
- package/src/components/size-control/size-input.tsx +20 -14
- package/src/components/size-control/text-field-inner-selection.tsx +15 -2
- package/src/control-adornments/control-adornments-context.tsx +5 -4
- package/src/control-replacements.tsx +12 -47
- package/src/controls/background-control/background-control.tsx +43 -12
- package/src/controls/background-control/background-gradient-color-control.tsx +5 -8
- package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +18 -13
- package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +25 -16
- package/src/controls/box-shadow-repeater-control.tsx +38 -21
- package/src/controls/color-control.tsx +3 -1
- package/src/controls/date-time-control.tsx +108 -0
- package/src/controls/filter-control/drop-shadow/drop-shadow-item-content.tsx +1 -0
- package/src/controls/filter-control/drop-shadow/drop-shadow-item-label.tsx +10 -6
- package/src/controls/filter-control/filter-content.tsx +1 -1
- package/src/controls/filter-control/filter-repeater-control.tsx +24 -21
- package/src/controls/filter-control/single-size/single-size-item-content.tsx +1 -1
- package/src/controls/filter-control/single-size/single-size-item-label.tsx +2 -1
- package/src/controls/font-family-control/font-family-control.tsx +66 -55
- package/src/controls/html-tag-control.tsx +90 -0
- package/src/controls/image-media-control.tsx +2 -2
- package/src/controls/inline-editing-control.tsx +18 -0
- package/src/controls/key-value-control.tsx +8 -2
- package/src/controls/link-control.tsx +23 -123
- package/src/controls/query-control.tsx +168 -0
- package/src/controls/repeatable-control.tsx +62 -27
- package/src/controls/select-control-wrapper.tsx +57 -0
- package/src/controls/select-control.tsx +9 -5
- package/src/controls/selection-size-control.tsx +13 -2
- package/src/controls/size-control.tsx +43 -25
- package/src/controls/svg-media-control.tsx +33 -10
- package/src/controls/text-area-control.tsx +5 -1
- package/src/controls/text-control.tsx +5 -0
- package/src/controls/toggle-control.tsx +11 -2
- package/src/controls/transform-control/functions/axis-row.tsx +1 -0
- package/src/controls/transform-control/transform-icon.tsx +2 -2
- package/src/controls/transform-control/transform-label.tsx +15 -32
- package/src/controls/transform-control/transform-repeater-control.tsx +42 -36
- package/src/controls/transform-control/{transform-base-control.tsx → transform-settings-control.tsx} +2 -2
- package/src/controls/transform-control/use-transform-tabs-history.tsx +1 -1
- package/src/controls/transition-control/data.ts +16 -1
- package/src/controls/transition-control/trainsition-events.ts +2 -2
- package/src/controls/transition-control/transition-repeater-control.tsx +137 -13
- package/src/controls/transition-control/transition-selector.tsx +37 -14
- package/src/controls/url-control.tsx +21 -16
- package/src/create-control.tsx +3 -2
- package/src/hooks/use-filtered-items-list.ts +3 -2
- package/src/hooks/use-repeatable-control-context.ts +3 -0
- package/src/hooks/use-sync-external-state.tsx +0 -1
- package/src/index.ts +21 -5
- package/src/utils/convert-toggle-options-to-atomic.tsx +33 -0
- package/src/utils/escape-html-attr.ts +11 -0
- package/src/components/css-code-editor/css-editor.styles.ts +0 -52
- package/src/components/css-code-editor/css-editor.tsx +0 -142
- package/src/components/css-code-editor/css-validation.ts +0 -75
- package/src/components/css-code-editor/resize-handle.tsx +0 -55
- package/src/components/css-code-editor/visual-content-change-protection.ts +0 -69
- package/src/components/repeater.tsx +0 -343
- package/src/components/unstable-repeater/items/item.tsx +0 -77
- package/src/components/unstable-repeater/unstable-repeater.tsx +0 -26
- /package/src/components/{unstable-repeater → control-repeater}/actions/tooltip-add-item-action.tsx +0 -0
|
@@ -19,64 +19,75 @@ export type FontCategory = {
|
|
|
19
19
|
type FontFamilyControlProps = {
|
|
20
20
|
fontFamilies: FontCategory[];
|
|
21
21
|
sectionWidth: number;
|
|
22
|
+
ariaLabel?: string;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
export const FontFamilyControl = createControl(
|
|
25
|
-
|
|
25
|
+
export const FontFamilyControl = createControl(
|
|
26
|
+
( { fontFamilies, sectionWidth, ariaLabel }: FontFamilyControlProps ) => {
|
|
27
|
+
const {
|
|
28
|
+
value: fontFamily,
|
|
29
|
+
setValue: setFontFamily,
|
|
30
|
+
disabled,
|
|
31
|
+
placeholder,
|
|
32
|
+
} = useBoundProp( stringPropTypeUtil );
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
const popoverState = usePopupState( { variant: 'popover' } );
|
|
35
|
+
const isShowingPlaceholder = ! fontFamily && placeholder;
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
const mapFontSubs = React.useMemo< Category[] >( () => {
|
|
38
|
+
return fontFamilies.map( ( { label, fonts } ) => ( {
|
|
39
|
+
label,
|
|
40
|
+
items: fonts,
|
|
41
|
+
} ) );
|
|
42
|
+
}, [ fontFamilies ] );
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<ControlActions>
|
|
47
|
+
<UnstableTag
|
|
48
|
+
id="font-family-control"
|
|
49
|
+
variant="outlined"
|
|
50
|
+
label={ fontFamily || placeholder }
|
|
51
|
+
endIcon={ <ChevronDownIcon fontSize="tiny" /> }
|
|
52
|
+
{ ...bindTrigger( popoverState ) }
|
|
53
|
+
fullWidth
|
|
54
|
+
disabled={ disabled }
|
|
55
|
+
aria-label={ ariaLabel }
|
|
56
|
+
sx={
|
|
57
|
+
isShowingPlaceholder
|
|
58
|
+
? {
|
|
59
|
+
'& .MuiTag-label': {
|
|
60
|
+
color: ( theme ) => theme.palette.text.tertiary,
|
|
61
|
+
},
|
|
62
|
+
textTransform: 'capitalize',
|
|
63
|
+
}
|
|
64
|
+
: undefined
|
|
65
|
+
}
|
|
66
|
+
/>
|
|
67
|
+
</ControlActions>
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
69
|
+
<Popover
|
|
70
|
+
disablePortal
|
|
71
|
+
disableScrollLock
|
|
72
|
+
anchorOrigin={ { vertical: 'bottom', horizontal: 'right' } }
|
|
73
|
+
transformOrigin={ { vertical: 'top', horizontal: 'right' } }
|
|
74
|
+
sx={ { my: 1.5 } }
|
|
75
|
+
{ ...bindPopover( popoverState ) }
|
|
76
|
+
>
|
|
77
|
+
<ItemSelector
|
|
78
|
+
id="font-family-selector"
|
|
79
|
+
itemsList={ mapFontSubs }
|
|
80
|
+
selectedItem={ fontFamily }
|
|
81
|
+
onItemChange={ setFontFamily }
|
|
82
|
+
onClose={ popoverState.close }
|
|
83
|
+
sectionWidth={ sectionWidth }
|
|
84
|
+
title={ __( 'Font family', 'elementor' ) }
|
|
85
|
+
itemStyle={ ( item ) => ( { fontFamily: item.value } ) }
|
|
86
|
+
onDebounce={ enqueueFont }
|
|
87
|
+
icon={ TextIcon as React.ElementType< { fontSize: string } > }
|
|
88
|
+
/>
|
|
89
|
+
</Popover>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { getElementLabel } from '@elementor/editor-elements';
|
|
3
|
+
import { stringPropTypeUtil, type StringPropValue } from '@elementor/editor-props';
|
|
4
|
+
import { MenuListItem } from '@elementor/editor-ui';
|
|
5
|
+
import { Select, type SelectChangeEvent, styled, Typography } from '@elementor/ui';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
7
|
+
|
|
8
|
+
import { useBoundProp } from '../bound-prop-context';
|
|
9
|
+
import { ConditionalControlInfotip } from '../components/conditional-control-infotip';
|
|
10
|
+
import ControlActions from '../control-actions/control-actions';
|
|
11
|
+
import { createControl } from '../create-control';
|
|
12
|
+
|
|
13
|
+
export type SelectOption = {
|
|
14
|
+
label: string;
|
|
15
|
+
value: StringPropValue[ 'value' ];
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Props = {
|
|
20
|
+
options: SelectOption[];
|
|
21
|
+
onChange?: ( newValue: string | null, previousValue: string | null | undefined ) => void;
|
|
22
|
+
fallbackLabels?: Record< string, string >;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const StyledSelect = styled( Select )( () => ( { '.MuiSelect-select.Mui-disabled': { cursor: 'not-allowed' } } ) );
|
|
26
|
+
|
|
27
|
+
export const HtmlTagControl = createControl( ( { options, onChange, fallbackLabels = {} }: Props ) => {
|
|
28
|
+
const { value, setValue, disabled, placeholder } = useBoundProp( stringPropTypeUtil );
|
|
29
|
+
const handleChange = ( event: SelectChangeEvent< StringPropValue[ 'value' ] > ) => {
|
|
30
|
+
const newValue = event.target.value || null;
|
|
31
|
+
|
|
32
|
+
onChange?.( newValue, value );
|
|
33
|
+
setValue( newValue );
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const elementLabel = getElementLabel() ?? 'element';
|
|
37
|
+
const infoTipProps = {
|
|
38
|
+
title: __( 'HTML Tag', 'elementor' ),
|
|
39
|
+
/* translators: %s is the element name. */
|
|
40
|
+
description: __(
|
|
41
|
+
`The tag is locked to 'a' tag because this %s has a link. To pick a different tag, remove the link first.`,
|
|
42
|
+
'elementor'
|
|
43
|
+
).replace( '%s', elementLabel ),
|
|
44
|
+
isEnabled: !! disabled,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const renderValue = ( selectedValue: string | null ) => {
|
|
48
|
+
if ( selectedValue ) {
|
|
49
|
+
return findOptionByValue( selectedValue )?.label || fallbackLabels[ selectedValue ] || selectedValue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ( ! placeholder ) {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const placeholderOption = findOptionByValue( placeholder );
|
|
57
|
+
const displayText = placeholderOption?.label || placeholder;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Typography component="span" variant="caption" color="text.tertiary">
|
|
61
|
+
{ displayText }
|
|
62
|
+
</Typography>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const findOptionByValue = ( searchValue: string | null ) => options.find( ( opt ) => opt.value === searchValue );
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ControlActions>
|
|
70
|
+
<ConditionalControlInfotip { ...infoTipProps }>
|
|
71
|
+
<StyledSelect
|
|
72
|
+
sx={ { overflow: 'hidden', cursor: disabled ? 'not-allowed' : undefined } }
|
|
73
|
+
displayEmpty
|
|
74
|
+
size="tiny"
|
|
75
|
+
renderValue={ renderValue }
|
|
76
|
+
value={ value ?? '' }
|
|
77
|
+
onChange={ handleChange }
|
|
78
|
+
disabled={ disabled }
|
|
79
|
+
fullWidth
|
|
80
|
+
>
|
|
81
|
+
{ options.map( ( { label, ...props } ) => (
|
|
82
|
+
<MenuListItem key={ props.value } { ...props } value={ props.value ?? '' }>
|
|
83
|
+
{ label }
|
|
84
|
+
</MenuListItem>
|
|
85
|
+
) ) }
|
|
86
|
+
</StyledSelect>
|
|
87
|
+
</ConditionalControlInfotip>
|
|
88
|
+
</ControlActions>
|
|
89
|
+
);
|
|
90
|
+
} );
|
|
@@ -14,7 +14,7 @@ type ImageMediaControlProps = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export const ImageMediaControl = createControl( ( { mediaTypes = [ 'image' ] }: ImageMediaControlProps ) => {
|
|
17
|
-
const { value, setValue } = useBoundProp( imageSrcPropTypeUtil );
|
|
17
|
+
const { value, setValue, propType } = useBoundProp( imageSrcPropTypeUtil );
|
|
18
18
|
const { id, url } = value ?? {};
|
|
19
19
|
|
|
20
20
|
const { data: attachment, isFetching } = useWpMediaAttachment( id?.value || null );
|
|
@@ -38,7 +38,7 @@ export const ImageMediaControl = createControl( ( { mediaTypes = [ 'image' ] }:
|
|
|
38
38
|
return (
|
|
39
39
|
<ControlActions>
|
|
40
40
|
<Card variant="outlined">
|
|
41
|
-
<CardMedia image={ src } sx={ { height: 150 } }>
|
|
41
|
+
<CardMedia image={ src } sx={ { height: propType.meta.isDynamic ? 134 : 150 } }>
|
|
42
42
|
{ isFetching ? (
|
|
43
43
|
<Stack justifyContent="center" alignItems="center" width="100%" height="100%">
|
|
44
44
|
<CircularProgress />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { htmlPropTypeUtil } from '@elementor/editor-props';
|
|
3
|
+
|
|
4
|
+
import { useBoundProp } from '../bound-prop-context';
|
|
5
|
+
import { InlineEditor } from '../components/inline-editor';
|
|
6
|
+
import ControlActions from '../control-actions/control-actions';
|
|
7
|
+
import { createControl } from '../create-control';
|
|
8
|
+
|
|
9
|
+
export const InlineEditingControl = createControl( () => {
|
|
10
|
+
const { value, setValue } = useBoundProp( htmlPropTypeUtil );
|
|
11
|
+
const handleChange = ( newValue: unknown ) => setValue( newValue as string );
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<ControlActions>
|
|
15
|
+
<InlineEditor value={ value || '' } setValue={ handleChange } />
|
|
16
|
+
</ControlActions>
|
|
17
|
+
);
|
|
18
|
+
} );
|
|
@@ -13,6 +13,7 @@ import { __ } from '@wordpress/i18n';
|
|
|
13
13
|
|
|
14
14
|
import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
|
|
15
15
|
import { createControl } from '../create-control';
|
|
16
|
+
import { escapeHtmlAttr } from '../utils/escape-html-attr';
|
|
16
17
|
import { TextControl } from './text-control';
|
|
17
18
|
|
|
18
19
|
type KeyValueControlProps = {
|
|
@@ -21,6 +22,7 @@ type KeyValueControlProps = {
|
|
|
21
22
|
regexKey?: string;
|
|
22
23
|
regexValue?: string;
|
|
23
24
|
validationErrorMessage?: string;
|
|
25
|
+
escapeHtml?: boolean;
|
|
24
26
|
getHelperText?: ( key: string, value: string ) => { keyHelper?: string; valueHelper?: string };
|
|
25
27
|
};
|
|
26
28
|
|
|
@@ -115,7 +117,11 @@ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {}
|
|
|
115
117
|
{ keyLabel }
|
|
116
118
|
</FormLabel>
|
|
117
119
|
<PropKeyProvider bind={ 'key' }>
|
|
118
|
-
<TextControl
|
|
120
|
+
<TextControl
|
|
121
|
+
inputValue={ props.escapeHtml ? escapeHtmlAttr( sessionState.key ) : sessionState.key }
|
|
122
|
+
error={ !! keyError }
|
|
123
|
+
helperText={ keyHelper }
|
|
124
|
+
/>
|
|
119
125
|
</PropKeyProvider>
|
|
120
126
|
{ !! keyError && <FormHelperText error>{ keyError }</FormHelperText> }
|
|
121
127
|
</Grid>
|
|
@@ -125,7 +131,7 @@ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {}
|
|
|
125
131
|
</FormLabel>
|
|
126
132
|
<PropKeyProvider bind={ 'value' }>
|
|
127
133
|
<TextControl
|
|
128
|
-
inputValue={ sessionState.value }
|
|
134
|
+
inputValue={ props.escapeHtml ? escapeHtmlAttr( sessionState.value ) : sessionState.value }
|
|
129
135
|
error={ !! valueError }
|
|
130
136
|
inputDisabled={ !! keyError }
|
|
131
137
|
helperText={ valueHelper }
|
|
@@ -1,44 +1,31 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { useState } from 'react';
|
|
3
3
|
import { getLinkInLinkRestriction } from '@elementor/editor-elements';
|
|
4
|
-
import {
|
|
5
|
-
linkPropTypeUtil,
|
|
6
|
-
type LinkPropValue,
|
|
7
|
-
numberPropTypeUtil,
|
|
8
|
-
stringPropTypeUtil,
|
|
9
|
-
urlPropTypeUtil,
|
|
10
|
-
} from '@elementor/editor-props';
|
|
11
|
-
import { type HttpResponse, httpService } from '@elementor/http-client';
|
|
4
|
+
import { linkPropTypeUtil, type LinkPropValue } from '@elementor/editor-props';
|
|
12
5
|
import { MinusIcon, PlusIcon } from '@elementor/icons';
|
|
13
6
|
import { useSessionStorage } from '@elementor/session';
|
|
14
7
|
import { Collapse, Grid, IconButton, Stack } from '@elementor/ui';
|
|
15
|
-
import { debounce } from '@elementor/utils';
|
|
16
8
|
import { __ } from '@wordpress/i18n';
|
|
17
9
|
|
|
18
10
|
import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
|
|
19
|
-
import {
|
|
20
|
-
Autocomplete,
|
|
21
|
-
type CategorizedOption,
|
|
22
|
-
findMatchingOption,
|
|
23
|
-
type FlatOption,
|
|
24
|
-
isCategorizedOptionPool,
|
|
25
|
-
} from '../components/autocomplete';
|
|
26
11
|
import { ControlFormLabel } from '../components/control-form-label';
|
|
12
|
+
import { ControlLabel } from '../components/control-label';
|
|
27
13
|
import { RestrictedLinkInfotip } from '../components/restricted-link-infotip';
|
|
28
|
-
import ControlActions from '../control-actions/control-actions';
|
|
29
14
|
import { createControl } from '../create-control';
|
|
30
15
|
import { type ControlProps } from '../utils/types';
|
|
16
|
+
import { QueryControl } from './query-control';
|
|
31
17
|
import { SwitchControl } from './switch-control';
|
|
32
18
|
|
|
33
19
|
type Props = ControlProps< {
|
|
34
20
|
queryOptions: {
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
params: Record< string, unknown >;
|
|
22
|
+
url: string;
|
|
37
23
|
};
|
|
38
24
|
allowCustomValues?: boolean;
|
|
39
25
|
minInputLength?: number;
|
|
40
26
|
placeholder?: string;
|
|
41
27
|
label?: string;
|
|
28
|
+
ariaLabel?: string;
|
|
42
29
|
} >;
|
|
43
30
|
|
|
44
31
|
type LinkSessionValue = {
|
|
@@ -48,7 +35,7 @@ type LinkSessionValue = {
|
|
|
48
35
|
};
|
|
49
36
|
};
|
|
50
37
|
|
|
51
|
-
type
|
|
38
|
+
export type DestinationProp = LinkPropValue[ 'value' ][ 'destination' ];
|
|
52
39
|
|
|
53
40
|
const SIZE = 'tiny';
|
|
54
41
|
|
|
@@ -58,18 +45,16 @@ export const LinkControl = createControl( ( props: Props ) => {
|
|
|
58
45
|
const [ isActive, setIsActive ] = useState( !! value );
|
|
59
46
|
|
|
60
47
|
const {
|
|
61
|
-
allowCustomValues,
|
|
62
|
-
queryOptions
|
|
48
|
+
allowCustomValues = true,
|
|
49
|
+
queryOptions,
|
|
63
50
|
placeholder,
|
|
64
51
|
minInputLength = 2,
|
|
65
52
|
context: { elementId },
|
|
66
53
|
label = __( 'Link', 'elementor' ),
|
|
54
|
+
ariaLabel,
|
|
67
55
|
} = props || {};
|
|
68
56
|
|
|
69
57
|
const [ linkInLinkRestriction, setLinkInLinkRestriction ] = useState( getLinkInLinkRestriction( elementId ) );
|
|
70
|
-
const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
|
|
71
|
-
generateFirstLoadedOption( value )
|
|
72
|
-
);
|
|
73
58
|
const shouldDisableAddingLink = ! isActive && linkInLinkRestriction.shouldRestrict;
|
|
74
59
|
|
|
75
60
|
const onEnabledChange = () => {
|
|
@@ -96,60 +81,17 @@ export const LinkControl = createControl( ( props: Props ) => {
|
|
|
96
81
|
} );
|
|
97
82
|
};
|
|
98
83
|
|
|
99
|
-
const
|
|
84
|
+
const onSaveValueToSession = ( newValue: DestinationProp[ 'value' ] | null ) => {
|
|
100
85
|
const valueToSave: LinkPropValue[ 'value' ] | null = newValue
|
|
101
86
|
? {
|
|
102
87
|
...value,
|
|
103
|
-
destination:
|
|
104
|
-
label: stringPropTypeUtil.create( findMatchingOption( options, newValue )?.label || null ),
|
|
88
|
+
destination: newValue,
|
|
105
89
|
}
|
|
106
90
|
: null;
|
|
107
91
|
|
|
108
|
-
|
|
92
|
+
setLinkSessionValue( { ...linkSessionValue, value: valueToSave } );
|
|
109
93
|
};
|
|
110
94
|
|
|
111
|
-
const onTextChange = ( newValue: string | null ) => {
|
|
112
|
-
newValue = newValue?.trim() || '';
|
|
113
|
-
|
|
114
|
-
const valueToSave: LinkPropValue[ 'value' ] | null = newValue
|
|
115
|
-
? {
|
|
116
|
-
...value,
|
|
117
|
-
destination: urlPropTypeUtil.create( newValue ),
|
|
118
|
-
label: stringPropTypeUtil.create( '' ),
|
|
119
|
-
}
|
|
120
|
-
: null;
|
|
121
|
-
|
|
122
|
-
onSaveNewValue( valueToSave );
|
|
123
|
-
updateOptions( newValue );
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const onSaveNewValue = ( newValue: LinkPropValue[ 'value' ] | null ) => {
|
|
127
|
-
setValue( newValue );
|
|
128
|
-
setLinkSessionValue( { ...linkSessionValue, value: newValue } );
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const updateOptions = ( newValue: string | null ) => {
|
|
132
|
-
setOptions( [] );
|
|
133
|
-
|
|
134
|
-
if ( ! newValue || ! endpoint || newValue.length < minInputLength ) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
debounceFetch( { ...requestParams, term: newValue } );
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const debounceFetch = useMemo(
|
|
142
|
-
() =>
|
|
143
|
-
debounce(
|
|
144
|
-
( params: FetchOptionsParams ) =>
|
|
145
|
-
fetchOptions( endpoint, params ).then( ( newOptions ) => {
|
|
146
|
-
setOptions( formatOptions( newOptions ) );
|
|
147
|
-
} ),
|
|
148
|
-
400
|
|
149
|
-
),
|
|
150
|
-
[ endpoint ]
|
|
151
|
-
);
|
|
152
|
-
|
|
153
95
|
return (
|
|
154
96
|
<PropProvider { ...propContext } value={ value } setValue={ setValue }>
|
|
155
97
|
<Stack gap={ 1.5 }>
|
|
@@ -161,7 +103,7 @@ export const LinkControl = createControl( ( props: Props ) => {
|
|
|
161
103
|
marginInlineEnd: -0.75,
|
|
162
104
|
} }
|
|
163
105
|
>
|
|
164
|
-
<
|
|
106
|
+
<ControlLabel>{ label }</ControlLabel>
|
|
165
107
|
<RestrictedLinkInfotip isVisible={ ! isActive } linkInLinkRestriction={ linkInLinkRestriction }>
|
|
166
108
|
<ToggleIconControl
|
|
167
109
|
disabled={ shouldDisableAddingLink }
|
|
@@ -174,17 +116,14 @@ export const LinkControl = createControl( ( props: Props ) => {
|
|
|
174
116
|
<Collapse in={ isActive } timeout="auto" unmountOnExit>
|
|
175
117
|
<Stack gap={ 1.5 }>
|
|
176
118
|
<PropKeyProvider bind={ 'destination' }>
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
minInputLength={ minInputLength }
|
|
186
|
-
/>
|
|
187
|
-
</ControlActions>
|
|
119
|
+
<QueryControl
|
|
120
|
+
queryOptions={ queryOptions }
|
|
121
|
+
allowCustomValues={ allowCustomValues }
|
|
122
|
+
minInputLength={ minInputLength }
|
|
123
|
+
placeholder={ placeholder }
|
|
124
|
+
onSetValue={ onSaveValueToSession }
|
|
125
|
+
ariaLabel={ ariaLabel || label }
|
|
126
|
+
/>
|
|
188
127
|
</PropKeyProvider>
|
|
189
128
|
<PropKeyProvider bind={ 'isTargetBlank' }>
|
|
190
129
|
<Grid container alignItems="center" flexWrap="nowrap" justifyContent="space-between">
|
|
@@ -217,42 +156,3 @@ const ToggleIconControl = ( { disabled, active, onIconClick, label }: ToggleIcon
|
|
|
217
156
|
</IconButton>
|
|
218
157
|
);
|
|
219
158
|
};
|
|
220
|
-
|
|
221
|
-
type FetchOptionsParams = Record< string, unknown > & { term: string };
|
|
222
|
-
|
|
223
|
-
async function fetchOptions( ajaxUrl: string, params: FetchOptionsParams ) {
|
|
224
|
-
if ( ! params || ! ajaxUrl ) {
|
|
225
|
-
return [];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const { data: response } = await httpService().get< Response >( ajaxUrl, { params } );
|
|
230
|
-
|
|
231
|
-
return response.data.value;
|
|
232
|
-
} catch {
|
|
233
|
-
return [];
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function formatOptions( options: FlatOption[] | CategorizedOption[] ): FlatOption[] | CategorizedOption[] {
|
|
238
|
-
const compareKey = isCategorizedOptionPool( options ) ? 'groupLabel' : 'label';
|
|
239
|
-
|
|
240
|
-
return options.sort( ( a, b ) =>
|
|
241
|
-
a[ compareKey ] && b[ compareKey ] ? a[ compareKey ].localeCompare( b[ compareKey ] ) : 0
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function generateFirstLoadedOption( unionValue: LinkPropValue[ 'value' ] | null ): FlatOption[] {
|
|
246
|
-
const value = unionValue?.destination?.value;
|
|
247
|
-
const label = unionValue?.label?.value;
|
|
248
|
-
const type = unionValue?.destination?.$$type || 'url';
|
|
249
|
-
|
|
250
|
-
return value && label && type === 'number'
|
|
251
|
-
? [
|
|
252
|
-
{
|
|
253
|
-
id: value.toString(),
|
|
254
|
-
label,
|
|
255
|
-
},
|
|
256
|
-
]
|
|
257
|
-
: [];
|
|
258
|
-
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { numberPropTypeUtil, stringPropTypeUtil, urlPropTypeUtil } from '@elementor/editor-props';
|
|
4
|
+
import { type HttpResponse, httpService } from '@elementor/http-client';
|
|
5
|
+
import { SearchIcon } from '@elementor/icons';
|
|
6
|
+
import { debounce } from '@elementor/utils';
|
|
7
|
+
import { __ } from '@wordpress/i18n';
|
|
8
|
+
|
|
9
|
+
import { useBoundProp } from '../bound-prop-context';
|
|
10
|
+
import {
|
|
11
|
+
Autocomplete,
|
|
12
|
+
type CategorizedOption,
|
|
13
|
+
findMatchingOption,
|
|
14
|
+
type FlatOption,
|
|
15
|
+
isCategorizedOptionPool,
|
|
16
|
+
} from '../components/autocomplete';
|
|
17
|
+
import ControlActions from '../control-actions/control-actions';
|
|
18
|
+
import { createControl } from '../create-control';
|
|
19
|
+
import { type DestinationProp } from './link-control';
|
|
20
|
+
|
|
21
|
+
type Props = {
|
|
22
|
+
queryOptions: {
|
|
23
|
+
params: Record< string, unknown >;
|
|
24
|
+
url: string;
|
|
25
|
+
};
|
|
26
|
+
allowCustomValues?: boolean;
|
|
27
|
+
minInputLength?: number;
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
onSetValue?: ( value: DestinationProp | null ) => void;
|
|
30
|
+
ariaLabel?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type Response = HttpResponse< { value: FlatOption[] | CategorizedOption[] } >;
|
|
34
|
+
|
|
35
|
+
type FetchOptionsParams = Record< string, unknown > & { term: string };
|
|
36
|
+
|
|
37
|
+
export const QueryControl = createControl( ( props: Props ) => {
|
|
38
|
+
const { value, setValue } = useBoundProp< DestinationProp >();
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
allowCustomValues = true,
|
|
42
|
+
queryOptions: { url, params = {} },
|
|
43
|
+
placeholder,
|
|
44
|
+
minInputLength = 2,
|
|
45
|
+
onSetValue,
|
|
46
|
+
ariaLabel,
|
|
47
|
+
} = props || {};
|
|
48
|
+
|
|
49
|
+
const normalizedPlaceholder = placeholder || __( 'Search', 'elementor' );
|
|
50
|
+
|
|
51
|
+
const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
|
|
52
|
+
generateFirstLoadedOption( value?.value )
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const onOptionChange = ( newValue: number | null ) => {
|
|
56
|
+
if ( newValue === null ) {
|
|
57
|
+
setValue( null );
|
|
58
|
+
onSetValue?.( null );
|
|
59
|
+
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const valueToSave = {
|
|
64
|
+
$$type: 'query',
|
|
65
|
+
value: {
|
|
66
|
+
id: numberPropTypeUtil.create( newValue ),
|
|
67
|
+
label: stringPropTypeUtil.create( findMatchingOption( options, newValue )?.label || null ),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
setValue( valueToSave );
|
|
72
|
+
onSetValue?.( valueToSave );
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const onTextChange = ( newValue: string | null ) => {
|
|
76
|
+
if ( ! newValue ) {
|
|
77
|
+
setValue( null );
|
|
78
|
+
onSetValue?.( null );
|
|
79
|
+
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const newLinkValue = newValue?.trim() || '';
|
|
84
|
+
const valueToSave = newLinkValue ? urlPropTypeUtil.create( newLinkValue ) : null;
|
|
85
|
+
|
|
86
|
+
setValue( valueToSave );
|
|
87
|
+
onSetValue?.( valueToSave );
|
|
88
|
+
updateOptions( newValue );
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const updateOptions = ( newValue: string | null ) => {
|
|
92
|
+
setOptions( [] );
|
|
93
|
+
|
|
94
|
+
if ( ! newValue || ! url || newValue.length < minInputLength ) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
debounceFetch( { ...params, term: newValue } );
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const debounceFetch = useMemo(
|
|
102
|
+
() =>
|
|
103
|
+
debounce(
|
|
104
|
+
( queryParams: FetchOptionsParams ) =>
|
|
105
|
+
fetchOptions( url, queryParams ).then( ( newOptions ) => {
|
|
106
|
+
setOptions( formatOptions( newOptions ) );
|
|
107
|
+
} ),
|
|
108
|
+
400
|
|
109
|
+
),
|
|
110
|
+
[ url ]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<ControlActions>
|
|
115
|
+
<Autocomplete
|
|
116
|
+
options={ options }
|
|
117
|
+
allowCustomValues={ allowCustomValues }
|
|
118
|
+
placeholder={ normalizedPlaceholder }
|
|
119
|
+
startAdornment={ <SearchIcon fontSize="tiny" /> }
|
|
120
|
+
value={ value?.value?.id?.value || value?.value }
|
|
121
|
+
onOptionChange={ onOptionChange }
|
|
122
|
+
onTextChange={ onTextChange }
|
|
123
|
+
minInputLength={ minInputLength }
|
|
124
|
+
disablePortal={ false }
|
|
125
|
+
inputProps={ {
|
|
126
|
+
...( ariaLabel ? { 'aria-label': ariaLabel } : {} ),
|
|
127
|
+
} }
|
|
128
|
+
/>
|
|
129
|
+
</ControlActions>
|
|
130
|
+
);
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
async function fetchOptions( ajaxUrl: string, params: FetchOptionsParams ) {
|
|
134
|
+
if ( ! params || ! ajaxUrl ) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const { data: response } = await httpService().get< Response >( ajaxUrl, { params } );
|
|
140
|
+
|
|
141
|
+
return response.data.value;
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatOptions( options: FlatOption[] | CategorizedOption[] ): FlatOption[] | CategorizedOption[] {
|
|
148
|
+
const compareKey = isCategorizedOptionPool( options ) ? 'groupLabel' : 'label';
|
|
149
|
+
|
|
150
|
+
return options.sort( ( a, b ) =>
|
|
151
|
+
a[ compareKey ] && b[ compareKey ] ? a[ compareKey ].localeCompare( b[ compareKey ] ) : 0
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function generateFirstLoadedOption( unionValue: DestinationProp | null ): FlatOption[] {
|
|
156
|
+
const value = unionValue?.id?.value;
|
|
157
|
+
const label = unionValue?.label?.value;
|
|
158
|
+
const type = unionValue?.id?.$$type || 'url';
|
|
159
|
+
|
|
160
|
+
return value && label && type === 'number'
|
|
161
|
+
? [
|
|
162
|
+
{
|
|
163
|
+
id: value.toString(),
|
|
164
|
+
label,
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
: [];
|
|
168
|
+
}
|