@elementor/editor-controls 0.21.0 → 0.25.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.
- package/CHANGELOG.md +56 -0
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +215 -110
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +193 -88
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/bound-prop-context/prop-context.tsx +3 -0
- package/src/bound-prop-context/prop-key-context.tsx +3 -1
- package/src/bound-prop-context/use-bound-prop.ts +41 -2
- package/src/components/repeater.tsx +9 -6
- package/src/components/sortable.tsx +6 -6
- package/src/components/text-field-inner-selection.tsx +21 -5
- package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +12 -1
- package/src/controls/font-family-control/font-family-control.tsx +4 -0
- package/src/controls/link-control.tsx +95 -23
- package/src/controls/number-control.tsx +7 -0
- package/src/controls/size-control.tsx +25 -15
- package/src/controls/toggle-control.tsx +3 -3
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": "0.
|
|
4
|
+
"version": "0.25.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@elementor/editor-current-user": "0.3.0",
|
|
44
|
-
"@elementor/editor-elements": "0.
|
|
45
|
-
"@elementor/editor-props": "0.
|
|
46
|
-
"@elementor/editor-ui": "0.
|
|
44
|
+
"@elementor/editor-elements": "0.8.1",
|
|
45
|
+
"@elementor/editor-props": "0.12.0",
|
|
46
|
+
"@elementor/editor-ui": "0.7.1",
|
|
47
47
|
"@elementor/env": "0.3.5",
|
|
48
48
|
"@elementor/http": "0.1.4",
|
|
49
49
|
"@elementor/icons": "1.37.0",
|
|
@@ -14,6 +14,7 @@ type PropContext< T extends PropValue, P extends PropType > = {
|
|
|
14
14
|
setValue: SetValue< T >;
|
|
15
15
|
value: T | null;
|
|
16
16
|
propType: P;
|
|
17
|
+
placeholder?: T;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
const PropContext = createContext< PropContext< PropValue, PropType > | null >( null );
|
|
@@ -27,6 +28,7 @@ export const PropProvider = < T extends PropValue, P extends PropType >( {
|
|
|
27
28
|
value,
|
|
28
29
|
setValue,
|
|
29
30
|
propType,
|
|
31
|
+
placeholder,
|
|
30
32
|
}: PropProviderProps< T, P > ) => {
|
|
31
33
|
return (
|
|
32
34
|
<PropContext.Provider
|
|
@@ -34,6 +36,7 @@ export const PropProvider = < T extends PropValue, P extends PropType >( {
|
|
|
34
36
|
value,
|
|
35
37
|
propType,
|
|
36
38
|
setValue: setValue as SetValue< PropValue >,
|
|
39
|
+
placeholder,
|
|
37
40
|
} }
|
|
38
41
|
>
|
|
39
42
|
{ children }
|
|
@@ -19,6 +19,7 @@ export type PropKeyContextValue< T, P > = {
|
|
|
19
19
|
setValue: SetValue< T >;
|
|
20
20
|
value: T;
|
|
21
21
|
propType: P;
|
|
22
|
+
placeholder?: T;
|
|
22
23
|
path: PropKey[];
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -60,12 +61,13 @@ const ObjectPropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
|
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
const value = context.value?.[ bind ];
|
|
64
|
+
const placeholder = context.placeholder?.[ bind ];
|
|
63
65
|
|
|
64
66
|
const propType = context.propType.shape[ bind ];
|
|
65
67
|
|
|
66
68
|
return (
|
|
67
69
|
<PropKeyContext.Provider
|
|
68
|
-
value={ { ...context, value, setValue, bind, propType, path: [ ...( path ?? [] ), bind ] } }
|
|
70
|
+
value={ { ...context, value, setValue, placeholder, bind, propType, path: [ ...( path ?? [] ), bind ] } }
|
|
69
71
|
>
|
|
70
72
|
{ children }
|
|
71
73
|
</PropKeyContext.Provider>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import {
|
|
2
3
|
type CreateOptions,
|
|
3
4
|
type PropKey,
|
|
@@ -15,7 +16,9 @@ type UseBoundProp< TValue extends PropValue > = {
|
|
|
15
16
|
setValue: SetValue< TValue | null >;
|
|
16
17
|
value: TValue;
|
|
17
18
|
propType: PropType;
|
|
19
|
+
placeholder?: TValue;
|
|
18
20
|
path: PropKey[];
|
|
21
|
+
restoreValue: () => void;
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
export function useBoundProp< T extends PropValue = PropValue >(): PropKeyContextValue< T, PropType >;
|
|
@@ -29,12 +32,18 @@ export function useBoundProp< TKey extends string, TValue extends PropValue >(
|
|
|
29
32
|
) {
|
|
30
33
|
const propKeyContext = usePropKeyContext();
|
|
31
34
|
|
|
35
|
+
const { isValid, validate, restoreValue } = useValidation( propKeyContext.propType );
|
|
36
|
+
|
|
32
37
|
// allow using the hook without a propTypeUtil, with no modifications or validations.
|
|
33
38
|
if ( ! propTypeUtil ) {
|
|
34
39
|
return propKeyContext;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
function setValue( value: TValue | null, options: CreateOptions, meta: { bind?: PropKey } ) {
|
|
43
|
+
if ( ! validate( value ) ) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
if ( value === null ) {
|
|
39
48
|
return propKeyContext?.setValue( null, options, meta );
|
|
40
49
|
}
|
|
@@ -45,15 +54,45 @@ export function useBoundProp< TKey extends string, TValue extends PropValue >(
|
|
|
45
54
|
const propType = resolveUnionPropType( propKeyContext.propType, propTypeUtil.key );
|
|
46
55
|
|
|
47
56
|
const value = propTypeUtil.extract( propKeyContext.value ?? propType.default ?? null );
|
|
57
|
+
const placeholder = propTypeUtil.extract( propKeyContext.placeholder ?? null );
|
|
48
58
|
|
|
49
59
|
return {
|
|
50
60
|
...propKeyContext,
|
|
51
|
-
setValue,
|
|
52
|
-
value,
|
|
53
61
|
propType,
|
|
62
|
+
setValue,
|
|
63
|
+
value: isValid ? value : null,
|
|
64
|
+
restoreValue,
|
|
65
|
+
placeholder,
|
|
54
66
|
};
|
|
55
67
|
}
|
|
56
68
|
|
|
69
|
+
const useValidation = ( propType: PropType ) => {
|
|
70
|
+
const [ isValid, setIsValid ] = useState( true );
|
|
71
|
+
|
|
72
|
+
// If the value does not pass the prop type validation, set the isValid state to false.
|
|
73
|
+
// This will prevent the value from being set in the model, and its fallback will be used instead.
|
|
74
|
+
const validate = ( value: PropValue | null ) => {
|
|
75
|
+
let valid = true;
|
|
76
|
+
|
|
77
|
+
if ( propType.settings.required && value === null ) {
|
|
78
|
+
valid = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setIsValid( valid );
|
|
82
|
+
|
|
83
|
+
return valid;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const restoreValue = () => setIsValid( true );
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
isValid,
|
|
90
|
+
setIsValid,
|
|
91
|
+
validate,
|
|
92
|
+
restoreValue,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
57
96
|
// utils
|
|
58
97
|
const resolveUnionPropType = ( propType: PropType, key: string ): PropType => {
|
|
59
98
|
let resolvedPropType = propType;
|
|
@@ -48,6 +48,8 @@ type RepeaterProps< T > = {
|
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
const EMPTY_OPEN_ITEM = -1;
|
|
52
|
+
|
|
51
53
|
export const Repeater = < T, >( {
|
|
52
54
|
label,
|
|
53
55
|
itemSettings,
|
|
@@ -56,7 +58,7 @@ export const Repeater = < T, >( {
|
|
|
56
58
|
values: repeaterValues = [],
|
|
57
59
|
setValues: setRepeaterValues,
|
|
58
60
|
}: RepeaterProps< Item< T > > ) => {
|
|
59
|
-
const [ openItem, setOpenItem ] = useState(
|
|
61
|
+
const [ openItem, setOpenItem ] = useState( EMPTY_OPEN_ITEM );
|
|
60
62
|
|
|
61
63
|
const [ items, setItems ] = useSyncExternalState( {
|
|
62
64
|
external: repeaterValues,
|
|
@@ -166,7 +168,6 @@ export const Repeater = < T, >( {
|
|
|
166
168
|
return (
|
|
167
169
|
<SortableItem id={ key } key={ `sortable-${ key }` }>
|
|
168
170
|
<RepeaterItem
|
|
169
|
-
bind={ String( index ) }
|
|
170
171
|
disabled={ value?.disabled }
|
|
171
172
|
label={ <itemSettings.Label value={ value } /> }
|
|
172
173
|
startIcon={ <itemSettings.Icon value={ value } /> }
|
|
@@ -174,6 +175,7 @@ export const Repeater = < T, >( {
|
|
|
174
175
|
duplicateItem={ () => duplicateRepeaterItem( index ) }
|
|
175
176
|
toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
|
|
176
177
|
openOnMount={ openOnAdd && openItem === key }
|
|
178
|
+
onOpen={ () => setOpenItem( EMPTY_OPEN_ITEM ) }
|
|
177
179
|
>
|
|
178
180
|
{ ( props ) => (
|
|
179
181
|
<itemSettings.Content { ...props } value={ value } bind={ String( index ) } />
|
|
@@ -190,7 +192,6 @@ export const Repeater = < T, >( {
|
|
|
190
192
|
|
|
191
193
|
type RepeaterItemProps = {
|
|
192
194
|
label: React.ReactNode;
|
|
193
|
-
bind: string;
|
|
194
195
|
disabled?: boolean;
|
|
195
196
|
startIcon: UnstableTagProps[ 'startIcon' ];
|
|
196
197
|
removeItem: () => void;
|
|
@@ -198,6 +199,7 @@ type RepeaterItemProps = {
|
|
|
198
199
|
toggleDisableItem: () => void;
|
|
199
200
|
children: ( { anchorEl }: { anchorEl: AnchorEl } ) => React.ReactNode;
|
|
200
201
|
openOnMount: boolean;
|
|
202
|
+
onOpen: () => void;
|
|
201
203
|
};
|
|
202
204
|
|
|
203
205
|
const RepeaterItem = ( {
|
|
@@ -209,9 +211,10 @@ const RepeaterItem = ( {
|
|
|
209
211
|
duplicateItem,
|
|
210
212
|
toggleDisableItem,
|
|
211
213
|
openOnMount,
|
|
214
|
+
onOpen,
|
|
212
215
|
}: RepeaterItemProps ) => {
|
|
213
216
|
const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
|
|
214
|
-
const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount );
|
|
217
|
+
const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount, onOpen );
|
|
215
218
|
|
|
216
219
|
const duplicateLabel = __( 'Duplicate', 'elementor' );
|
|
217
220
|
const toggleLabel = disabled ? __( 'Show', 'elementor' ) : __( 'Hide', 'elementor' );
|
|
@@ -247,7 +250,6 @@ const RepeaterItem = ( {
|
|
|
247
250
|
</Tooltip>
|
|
248
251
|
</>
|
|
249
252
|
}
|
|
250
|
-
sx={ { backgroundColor: 'background.paper' } }
|
|
251
253
|
/>
|
|
252
254
|
<Popover
|
|
253
255
|
disablePortal
|
|
@@ -267,7 +269,7 @@ const RepeaterItem = ( {
|
|
|
267
269
|
);
|
|
268
270
|
};
|
|
269
271
|
|
|
270
|
-
const usePopover = ( openOnMount: boolean ) => {
|
|
272
|
+
const usePopover = ( openOnMount: boolean, onOpen: () => void ) => {
|
|
271
273
|
const [ ref, setRef ] = useState< HTMLElement | null >( null );
|
|
272
274
|
|
|
273
275
|
const popoverState = usePopupState( { variant: 'popover' } );
|
|
@@ -277,6 +279,7 @@ const usePopover = ( openOnMount: boolean ) => {
|
|
|
277
279
|
useEffect( () => {
|
|
278
280
|
if ( openOnMount && ref ) {
|
|
279
281
|
popoverState.open( ref );
|
|
282
|
+
onOpen?.();
|
|
280
283
|
}
|
|
281
284
|
// eslint-disable-next-line react-compiler/react-compiler
|
|
282
285
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -34,16 +34,11 @@ export const SortableItem = ( { id, children }: SortableItemProps ): React.React
|
|
|
34
34
|
triggerProps,
|
|
35
35
|
itemStyle,
|
|
36
36
|
triggerStyle,
|
|
37
|
-
isDragOverlay,
|
|
38
37
|
showDropIndication,
|
|
39
38
|
dropIndicationStyle,
|
|
40
39
|
}: UnstableSortableItemRenderProps ) => {
|
|
41
40
|
return (
|
|
42
|
-
<StyledListItem
|
|
43
|
-
{ ...itemProps }
|
|
44
|
-
style={ itemStyle }
|
|
45
|
-
sx={ { backgroundColor: isDragOverlay ? 'background.paper' : undefined } }
|
|
46
|
-
>
|
|
41
|
+
<StyledListItem { ...itemProps } style={ itemStyle }>
|
|
47
42
|
<SortableTrigger { ...triggerProps } style={ triggerStyle } />
|
|
48
43
|
{ children }
|
|
49
44
|
{ showDropIndication && <StyledDivider style={ dropIndicationStyle } /> }
|
|
@@ -72,6 +67,11 @@ const StyledListItem = styled( ListItem )`
|
|
|
72
67
|
transform: translate( -75%, -50% );
|
|
73
68
|
}
|
|
74
69
|
|
|
70
|
+
&[aria-describedby=''] > .MuiTag-root {
|
|
71
|
+
background-color: ${ ( { theme } ) => theme.palette.background.paper };
|
|
72
|
+
box-shadow: ${ ( { theme } ) => theme.shadows[ 3 ] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
75
|
&:hover {
|
|
76
76
|
& .class-item-sortable-trigger {
|
|
77
77
|
visibility: visible;
|
|
@@ -9,25 +9,41 @@ type TextFieldInnerSelectionProps = {
|
|
|
9
9
|
type: string;
|
|
10
10
|
value: PropValue;
|
|
11
11
|
onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
|
|
12
|
+
onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
|
13
|
+
onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
|
12
14
|
endAdornment: React.ReactNode;
|
|
13
15
|
startAdornment?: React.ReactNode;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
export const TextFieldInnerSelection = forwardRef(
|
|
17
|
-
(
|
|
19
|
+
(
|
|
20
|
+
{
|
|
21
|
+
placeholder,
|
|
22
|
+
type,
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
onBlur,
|
|
26
|
+
onKeyDown,
|
|
27
|
+
endAdornment,
|
|
28
|
+
startAdornment,
|
|
29
|
+
}: TextFieldInnerSelectionProps,
|
|
30
|
+
ref
|
|
31
|
+
) => {
|
|
18
32
|
return (
|
|
19
33
|
<TextField
|
|
34
|
+
ref={ ref }
|
|
20
35
|
size="tiny"
|
|
21
36
|
fullWidth
|
|
22
37
|
type={ type }
|
|
23
38
|
value={ value }
|
|
24
39
|
onChange={ onChange }
|
|
40
|
+
onKeyDown={ onKeyDown }
|
|
41
|
+
onBlur={ onBlur }
|
|
25
42
|
placeholder={ placeholder }
|
|
26
43
|
InputProps={ {
|
|
27
44
|
endAdornment,
|
|
28
45
|
startAdornment,
|
|
29
46
|
} }
|
|
30
|
-
ref={ ref }
|
|
31
47
|
/>
|
|
32
48
|
);
|
|
33
49
|
}
|
|
@@ -58,11 +74,11 @@ export const SelectionEndAdornment = < T extends string >( {
|
|
|
58
74
|
<InputAdornment position="end">
|
|
59
75
|
<Button
|
|
60
76
|
size="small"
|
|
61
|
-
color="
|
|
62
|
-
sx={ { font: 'inherit', minWidth: 'initial' } }
|
|
77
|
+
color="secondary"
|
|
78
|
+
sx={ { font: 'inherit', minWidth: 'initial', textTransform: 'uppercase' } }
|
|
63
79
|
{ ...bindTrigger( popupState ) }
|
|
64
80
|
>
|
|
65
|
-
{ value
|
|
81
|
+
{ value }
|
|
66
82
|
</Button>
|
|
67
83
|
|
|
68
84
|
<Menu MenuListProps={ { dense: true } } { ...bindMenu( popupState ) }>
|
package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx
CHANGED
|
@@ -271,7 +271,7 @@ const useImage = ( image: BackgroundImageOverlay ) => {
|
|
|
271
271
|
const { data: attachment } = useWpMediaAttachment( imageSrc.id?.value || null );
|
|
272
272
|
|
|
273
273
|
if ( imageSrc.id ) {
|
|
274
|
-
const imageFileTypeExtension = attachment?.
|
|
274
|
+
const imageFileTypeExtension = getFileExtensionFromFilename( attachment?.filename );
|
|
275
275
|
imageTitle = `${ attachment?.title }${ imageFileTypeExtension }` || null;
|
|
276
276
|
imageUrl = attachment?.url || null;
|
|
277
277
|
} else if ( imageSrc.url ) {
|
|
@@ -282,6 +282,17 @@ const useImage = ( image: BackgroundImageOverlay ) => {
|
|
|
282
282
|
return { imageTitle, imageUrl };
|
|
283
283
|
};
|
|
284
284
|
|
|
285
|
+
const getFileExtensionFromFilename = ( filename?: string ) => {
|
|
286
|
+
if ( ! filename ) {
|
|
287
|
+
return '';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// get the substring after the last . in the filename
|
|
291
|
+
const extension = filename.substring( filename.lastIndexOf( '.' ) + 1 );
|
|
292
|
+
|
|
293
|
+
return `.${ extension }`;
|
|
294
|
+
};
|
|
295
|
+
|
|
285
296
|
const getGradientValue = ( value: BackgroundOverlayItemPropValue ) => {
|
|
286
297
|
const gradient = value.value;
|
|
287
298
|
|
|
@@ -86,6 +86,8 @@ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyCo
|
|
|
86
86
|
|
|
87
87
|
<Box px={ 1.5 } pb={ 1 }>
|
|
88
88
|
<TextField
|
|
89
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
90
|
+
autoFocus
|
|
89
91
|
fullWidth
|
|
90
92
|
size={ SIZE }
|
|
91
93
|
value={ searchValue }
|
|
@@ -280,6 +282,8 @@ const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
|
|
|
280
282
|
top: 0,
|
|
281
283
|
left: 0,
|
|
282
284
|
width: '100%',
|
|
285
|
+
display: 'flex',
|
|
286
|
+
alignItems: 'center',
|
|
283
287
|
},
|
|
284
288
|
'& > [role="option"]': {
|
|
285
289
|
...theme.typography.caption,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { useMemo, useState } from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import { type PropsWithChildren, useMemo, useState } from 'react';
|
|
3
|
+
import { getLinkInLinkRestriction, type LinkInLinkRestriction, selectElement } from '@elementor/editor-elements';
|
|
4
4
|
import {
|
|
5
5
|
booleanPropTypeUtil,
|
|
6
6
|
linkPropTypeUtil,
|
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
stringPropTypeUtil,
|
|
10
10
|
urlPropTypeUtil,
|
|
11
11
|
} from '@elementor/editor-props';
|
|
12
|
+
import { InfoTipCard } from '@elementor/editor-ui';
|
|
12
13
|
import { type HttpResponse, httpService } from '@elementor/http';
|
|
13
|
-
import { MinusIcon, PlusIcon } from '@elementor/icons';
|
|
14
|
+
import { AlertTriangleIcon, MinusIcon, PlusIcon } from '@elementor/icons';
|
|
14
15
|
import { useSessionStorage } from '@elementor/session';
|
|
15
|
-
import { Collapse, Divider, Grid, IconButton, Stack, Switch } from '@elementor/ui';
|
|
16
|
+
import { Box, Collapse, Divider, Grid, IconButton, Infotip, Stack, Switch } from '@elementor/ui';
|
|
16
17
|
import { debounce } from '@elementor/utils';
|
|
17
18
|
import { __ } from '@wordpress/i18n';
|
|
18
19
|
|
|
@@ -49,11 +50,15 @@ type LinkSessionValue = {
|
|
|
49
50
|
type Response = HttpResponse< { value: FlatOption[] | CategorizedOption[] } >;
|
|
50
51
|
|
|
51
52
|
const SIZE = 'tiny';
|
|
53
|
+
const learnMoreButton = {
|
|
54
|
+
label: __( 'Learn More', 'elementor' ),
|
|
55
|
+
href: 'https://go.elementor.com/element-link-inside-link-infotip',
|
|
56
|
+
};
|
|
52
57
|
|
|
53
58
|
export const LinkControl = createControl( ( props: Props ) => {
|
|
54
59
|
const { value, path, setValue, ...propContext } = useBoundProp( linkPropTypeUtil );
|
|
55
60
|
const [ linkSessionValue, setLinkSessionValue ] = useSessionStorage< LinkSessionValue >( path.join( '/' ) );
|
|
56
|
-
const [
|
|
61
|
+
const [ isActive, setIsActive ] = useState( !! value );
|
|
57
62
|
|
|
58
63
|
const {
|
|
59
64
|
allowCustomValues,
|
|
@@ -63,20 +68,22 @@ export const LinkControl = createControl( ( props: Props ) => {
|
|
|
63
68
|
context: { elementId },
|
|
64
69
|
} = props || {};
|
|
65
70
|
|
|
71
|
+
const [ linkInLinkRestriction, setLinkInLinkRestriction ] = useState( getLinkInLinkRestriction( elementId ) );
|
|
66
72
|
const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
|
|
67
73
|
generateFirstLoadedOption( value )
|
|
68
74
|
);
|
|
75
|
+
const shouldDisableAddingLink = ! isActive && linkInLinkRestriction.shouldRestrict;
|
|
69
76
|
|
|
70
77
|
const onEnabledChange = () => {
|
|
71
|
-
|
|
78
|
+
setLinkInLinkRestriction( getLinkInLinkRestriction( elementId ) );
|
|
72
79
|
|
|
73
|
-
if ( shouldRestrict && !
|
|
80
|
+
if ( linkInLinkRestriction.shouldRestrict && ! isActive ) {
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
setValue(
|
|
79
|
-
setLinkSessionValue( { value, meta: { isEnabled: !
|
|
84
|
+
setIsActive( ( prevState ) => ! prevState );
|
|
85
|
+
setValue( isActive ? null : linkSessionValue?.value ?? null );
|
|
86
|
+
setLinkSessionValue( { value, meta: { isEnabled: ! isActive } } );
|
|
80
87
|
};
|
|
81
88
|
|
|
82
89
|
const onOptionChange = ( newValue: number | null ) => {
|
|
@@ -145,13 +152,16 @@ export const LinkControl = createControl( ( props: Props ) => {
|
|
|
145
152
|
} }
|
|
146
153
|
>
|
|
147
154
|
<ControlFormLabel>{ __( 'Link', 'elementor' ) }</ControlFormLabel>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
<ConditionalInfoTip isVisible={ ! isActive } linkInLinkRestriction={ linkInLinkRestriction }>
|
|
156
|
+
<ToggleIconControl
|
|
157
|
+
disabled={ shouldDisableAddingLink }
|
|
158
|
+
active={ isActive }
|
|
159
|
+
onIconClick={ onEnabledChange }
|
|
160
|
+
label={ __( 'Toggle link', 'elementor' ) }
|
|
161
|
+
/>
|
|
162
|
+
</ConditionalInfoTip>
|
|
153
163
|
</Stack>
|
|
154
|
-
<Collapse in={
|
|
164
|
+
<Collapse in={ isActive } timeout="auto" unmountOnExit>
|
|
155
165
|
<Stack gap={ 1.5 }>
|
|
156
166
|
<PropKeyProvider bind={ 'destination' }>
|
|
157
167
|
<ControlActions>
|
|
@@ -167,7 +177,7 @@ export const LinkControl = createControl( ( props: Props ) => {
|
|
|
167
177
|
</ControlActions>
|
|
168
178
|
</PropKeyProvider>
|
|
169
179
|
<PropKeyProvider bind={ 'isTargetBlank' }>
|
|
170
|
-
<SwitchControl />
|
|
180
|
+
<SwitchControl disabled={ ! value } />
|
|
171
181
|
</PropKeyProvider>
|
|
172
182
|
</Stack>
|
|
173
183
|
</Collapse>
|
|
@@ -177,34 +187,43 @@ export const LinkControl = createControl( ( props: Props ) => {
|
|
|
177
187
|
} );
|
|
178
188
|
|
|
179
189
|
type ToggleIconControlProps = {
|
|
180
|
-
|
|
190
|
+
disabled: boolean;
|
|
191
|
+
active: boolean;
|
|
181
192
|
onIconClick: () => void;
|
|
182
193
|
label?: string;
|
|
183
194
|
};
|
|
184
195
|
|
|
185
|
-
const ToggleIconControl = ( {
|
|
196
|
+
const ToggleIconControl = ( { disabled, active, onIconClick, label }: ToggleIconControlProps ) => {
|
|
186
197
|
return (
|
|
187
|
-
<IconButton size={ SIZE } onClick={ onIconClick } aria-label={ label }>
|
|
188
|
-
{
|
|
198
|
+
<IconButton size={ SIZE } onClick={ onIconClick } aria-label={ label } disabled={ disabled }>
|
|
199
|
+
{ active ? <MinusIcon fontSize={ SIZE } /> : <PlusIcon fontSize={ SIZE } /> }
|
|
189
200
|
</IconButton>
|
|
190
201
|
);
|
|
191
202
|
};
|
|
192
203
|
|
|
193
204
|
// @TODO Should be refactored in ED-16323
|
|
194
|
-
const SwitchControl = () => {
|
|
205
|
+
const SwitchControl = ( { disabled }: { disabled: boolean } ) => {
|
|
195
206
|
const { value = false, setValue } = useBoundProp( booleanPropTypeUtil );
|
|
196
207
|
|
|
197
208
|
const onClick = () => {
|
|
198
209
|
setValue( ! value );
|
|
199
210
|
};
|
|
200
211
|
|
|
212
|
+
const inputProps = disabled
|
|
213
|
+
? {
|
|
214
|
+
style: {
|
|
215
|
+
opacity: 0,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
: {};
|
|
219
|
+
|
|
201
220
|
return (
|
|
202
221
|
<Grid container alignItems="center" flexWrap="nowrap" justifyContent="space-between">
|
|
203
222
|
<Grid item>
|
|
204
223
|
<ControlFormLabel>{ __( 'Open in a new tab', 'elementor' ) }</ControlFormLabel>
|
|
205
224
|
</Grid>
|
|
206
225
|
<Grid item>
|
|
207
|
-
<Switch checked={ value } onClick={ onClick } />
|
|
226
|
+
<Switch checked={ value } onClick={ onClick } disabled={ disabled } inputProps={ inputProps } />
|
|
208
227
|
</Grid>
|
|
209
228
|
</Grid>
|
|
210
229
|
);
|
|
@@ -248,3 +267,56 @@ function generateFirstLoadedOption( unionValue: LinkPropValue[ 'value' ] | null
|
|
|
248
267
|
]
|
|
249
268
|
: [];
|
|
250
269
|
}
|
|
270
|
+
|
|
271
|
+
interface ConditionalInfoTipType extends PropsWithChildren {
|
|
272
|
+
linkInLinkRestriction: LinkInLinkRestriction;
|
|
273
|
+
isVisible: boolean;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const ConditionalInfoTip: React.FC< ConditionalInfoTipType > = ( { linkInLinkRestriction, isVisible, children } ) => {
|
|
277
|
+
const { shouldRestrict, reason, elementId } = linkInLinkRestriction;
|
|
278
|
+
|
|
279
|
+
const handleTakeMeClick = () => {
|
|
280
|
+
if ( elementId ) {
|
|
281
|
+
selectElement( elementId );
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return shouldRestrict && isVisible ? (
|
|
286
|
+
<Infotip
|
|
287
|
+
placement="right"
|
|
288
|
+
content={
|
|
289
|
+
<InfoTipCard
|
|
290
|
+
content={ INFOTIP_CONTENT[ reason ] }
|
|
291
|
+
svgIcon={ <AlertTriangleIcon /> }
|
|
292
|
+
learnMoreButton={ learnMoreButton }
|
|
293
|
+
ctaButton={ {
|
|
294
|
+
label: __( 'Take me there', 'elementor' ),
|
|
295
|
+
onClick: handleTakeMeClick,
|
|
296
|
+
} }
|
|
297
|
+
/>
|
|
298
|
+
}
|
|
299
|
+
>
|
|
300
|
+
<Box>{ children }</Box>
|
|
301
|
+
</Infotip>
|
|
302
|
+
) : (
|
|
303
|
+
<>{ children }</>
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const INFOTIP_CONTENT = {
|
|
308
|
+
descendant: (
|
|
309
|
+
<>
|
|
310
|
+
{ __( 'To add a link to this container,', 'elementor' ) }
|
|
311
|
+
<br />
|
|
312
|
+
{ __( 'first remove the link from the elements inside of it.', 'elementor' ) }
|
|
313
|
+
</>
|
|
314
|
+
),
|
|
315
|
+
ancestor: (
|
|
316
|
+
<>
|
|
317
|
+
{ __( 'To add a link to this element,', 'elementor' ) }
|
|
318
|
+
<br />
|
|
319
|
+
{ __( 'first remove the link from its parent container.', 'elementor' ) }
|
|
320
|
+
</>
|
|
321
|
+
),
|
|
322
|
+
};
|
|
@@ -9,6 +9,8 @@ import { createControl } from '../create-control';
|
|
|
9
9
|
const isEmptyOrNaN = ( value?: string | number | null ) =>
|
|
10
10
|
value === null || value === undefined || value === '' || Number.isNaN( Number( value ) );
|
|
11
11
|
|
|
12
|
+
const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
|
|
13
|
+
|
|
12
14
|
export const NumberControl = createControl(
|
|
13
15
|
( {
|
|
14
16
|
placeholder,
|
|
@@ -49,6 +51,11 @@ export const NumberControl = createControl(
|
|
|
49
51
|
onChange={ handleChange }
|
|
50
52
|
placeholder={ placeholder }
|
|
51
53
|
inputProps={ { step } }
|
|
54
|
+
onKeyDown={ ( event: KeyboardEvent ) => {
|
|
55
|
+
if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
}
|
|
58
|
+
} }
|
|
52
59
|
/>
|
|
53
60
|
</ControlActions>
|
|
54
61
|
);
|
|
@@ -25,7 +25,7 @@ type SizeControlProps = {
|
|
|
25
25
|
|
|
26
26
|
export const SizeControl = createControl(
|
|
27
27
|
( { units = defaultUnits, extendedValues = [], placeholder, startIcon }: SizeControlProps ) => {
|
|
28
|
-
const { value: sizeValue, setValue: setSizeValue } = useBoundProp( sizePropTypeUtil );
|
|
28
|
+
const { value: sizeValue, setValue: setSizeValue, restoreValue } = useBoundProp( sizePropTypeUtil );
|
|
29
29
|
|
|
30
30
|
const [ state, setState ] = useSyncExternalState( {
|
|
31
31
|
external: sizeValue,
|
|
@@ -50,21 +50,21 @@ export const SizeControl = createControl(
|
|
|
50
50
|
} ) );
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const
|
|
54
|
-
size: state.size,
|
|
55
|
-
unit: state.unit,
|
|
56
|
-
placeholder,
|
|
57
|
-
startIcon,
|
|
58
|
-
units,
|
|
59
|
-
extendedValues,
|
|
60
|
-
handleSizeChange,
|
|
61
|
-
handleUnitChange,
|
|
62
|
-
};
|
|
53
|
+
const Input = extendedValues?.length ? ExtendedSizeInput : SizeInput;
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
return (
|
|
56
|
+
<Input
|
|
57
|
+
size={ state.size }
|
|
58
|
+
unit={ state.unit }
|
|
59
|
+
placeholder={ placeholder }
|
|
60
|
+
startIcon={ startIcon }
|
|
61
|
+
units={ units }
|
|
62
|
+
extendedValues={ extendedValues }
|
|
63
|
+
handleSizeChange={ handleSizeChange }
|
|
64
|
+
handleUnitChange={ handleUnitChange }
|
|
65
|
+
onBlur={ restoreValue }
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
68
|
}
|
|
69
69
|
);
|
|
70
70
|
|
|
@@ -99,16 +99,20 @@ type SizeInputProps = {
|
|
|
99
99
|
startIcon?: React.ReactNode;
|
|
100
100
|
units: Unit[];
|
|
101
101
|
extendedValues?: ExtendedValue[];
|
|
102
|
+
onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
|
102
103
|
handleUnitChange: ( unit: Unit ) => void;
|
|
103
104
|
handleSizeChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
|
|
104
105
|
};
|
|
105
106
|
|
|
107
|
+
const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
|
|
108
|
+
|
|
106
109
|
const SizeInput = ( {
|
|
107
110
|
units,
|
|
108
111
|
handleUnitChange,
|
|
109
112
|
handleSizeChange,
|
|
110
113
|
placeholder,
|
|
111
114
|
startIcon,
|
|
115
|
+
onBlur,
|
|
112
116
|
size,
|
|
113
117
|
unit,
|
|
114
118
|
}: SizeInputProps ) => {
|
|
@@ -129,6 +133,12 @@ const SizeInput = ( {
|
|
|
129
133
|
type="number"
|
|
130
134
|
value={ Number.isNaN( size ) ? '' : size }
|
|
131
135
|
onChange={ handleSizeChange }
|
|
136
|
+
onBlur={ onBlur }
|
|
137
|
+
onKeyDown={ ( event ) => {
|
|
138
|
+
if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
}
|
|
141
|
+
} }
|
|
132
142
|
/>
|
|
133
143
|
</ControlActions>
|
|
134
144
|
);
|