@elementor/editor-controls 4.0.0-manual → 4.1.0-684
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 +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +87 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +94 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -15
- package/src/components/promotions/attributes-control.tsx +2 -1
- package/src/components/promotions/display-conditions-control.tsx +2 -1
- package/src/components/promotions/promotion-trigger.tsx +14 -4
- package/src/controls/chips-control.tsx +1 -1
- package/src/controls/number-control.tsx +8 -2
- package/src/controls/size-control/hooks/use-size-unit-keyboard.tsx +55 -0
- package/src/controls/size-control/hooks/use-size-value.ts +75 -0
- package/src/controls/size-control/size-component.tsx +73 -0
- package/src/controls/size-control/size-field.tsx +89 -0
- package/src/controls/size-control/sync/get-units.ts +3 -0
- package/src/controls/size-control/types.ts +9 -0
- package/src/controls/size-control/ui/size-input.tsx +68 -0
- package/src/controls/size-control/ui/text-field-popover.tsx +78 -0
- package/src/controls/size-control/ui/unit-selector.tsx +80 -0
- package/src/controls/size-control/utils/is-extended-unit.ts +8 -0
- package/src/controls/size-control/utils/is-numeric-value.ts +11 -0
- package/src/controls/size-control/utils/resolve-size-value.ts +84 -0
- package/src/controls/transition-control/transition-selector.tsx +7 -0
- package/src/hooks/use-font-families.ts +22 -25
- package/src/index.ts +2 -0
- package/src/utils/tracking.ts +61 -0
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.
|
|
4
|
+
"version": "4.1.0-684",
|
|
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.
|
|
44
|
-
"@elementor/editor-elements": "4.
|
|
45
|
-
"@elementor/editor-props": "4.
|
|
46
|
-
"@elementor/editor-responsive": "4.
|
|
47
|
-
"@elementor/editor-ui": "4.
|
|
48
|
-
"@elementor/editor-v1-adapters": "4.
|
|
49
|
-
"@elementor/env": "4.
|
|
50
|
-
"@elementor/http-client": "4.
|
|
43
|
+
"@elementor/editor-current-user": "4.1.0-684",
|
|
44
|
+
"@elementor/editor-elements": "4.1.0-684",
|
|
45
|
+
"@elementor/editor-props": "4.1.0-684",
|
|
46
|
+
"@elementor/editor-responsive": "4.1.0-684",
|
|
47
|
+
"@elementor/editor-ui": "4.1.0-684",
|
|
48
|
+
"@elementor/editor-v1-adapters": "4.1.0-684",
|
|
49
|
+
"@elementor/env": "4.1.0-684",
|
|
50
|
+
"@elementor/http-client": "4.1.0-684",
|
|
51
51
|
"@elementor/icons": "^1.68.0",
|
|
52
|
-
"@elementor/locations": "4.
|
|
53
|
-
"@elementor/events": "4.
|
|
54
|
-
"@elementor/query": "4.
|
|
55
|
-
"@elementor/session": "4.
|
|
52
|
+
"@elementor/locations": "4.1.0-684",
|
|
53
|
+
"@elementor/events": "4.1.0-684",
|
|
54
|
+
"@elementor/query": "4.1.0-684",
|
|
55
|
+
"@elementor/session": "4.1.0-684",
|
|
56
56
|
"@elementor/ui": "1.36.17",
|
|
57
|
-
"@elementor/utils": "4.
|
|
58
|
-
"@elementor/wp-media": "4.
|
|
57
|
+
"@elementor/utils": "4.1.0-684",
|
|
58
|
+
"@elementor/wp-media": "4.1.0-684",
|
|
59
59
|
"@wordpress/i18n": "^5.13.0",
|
|
60
60
|
"@monaco-editor/react": "^4.7.0",
|
|
61
61
|
"dayjs": "^1.11.18",
|
|
@@ -8,6 +8,7 @@ import { createControl } from '../../create-control';
|
|
|
8
8
|
import { PromotionTrigger, type PromotionTriggerRef } from './promotion-trigger';
|
|
9
9
|
|
|
10
10
|
const ARIA_LABEL = __( 'Attributes', 'elementor' );
|
|
11
|
+
const TRACKING_DATA = { target_name: 'attributes', location_l2: 'general' } as const;
|
|
11
12
|
|
|
12
13
|
export const AttributesControl = createControl( () => {
|
|
13
14
|
const triggerRef = useRef< PromotionTriggerRef >( null );
|
|
@@ -21,7 +22,7 @@ export const AttributesControl = createControl( () => {
|
|
|
21
22
|
alignItems: 'center',
|
|
22
23
|
} }
|
|
23
24
|
>
|
|
24
|
-
<PromotionTrigger ref={ triggerRef } promotionKey="attributes" />
|
|
25
|
+
<PromotionTrigger ref={ triggerRef } promotionKey="attributes" trackingData={ TRACKING_DATA } />
|
|
25
26
|
<Tooltip title={ ARIA_LABEL } placement="top">
|
|
26
27
|
<PlusIcon
|
|
27
28
|
aria-label={ ARIA_LABEL }
|
|
@@ -8,6 +8,7 @@ import { createControl } from '../../create-control';
|
|
|
8
8
|
import { PromotionTrigger, type PromotionTriggerRef } from './promotion-trigger';
|
|
9
9
|
|
|
10
10
|
const ARIA_LABEL = __( 'Display Conditions', 'elementor' );
|
|
11
|
+
const TRACKING_DATA = { target_name: 'display_conditions', location_l2: 'general' } as const;
|
|
11
12
|
|
|
12
13
|
export const DisplayConditionsControl = createControl( () => {
|
|
13
14
|
const triggerRef = useRef< PromotionTriggerRef >( null );
|
|
@@ -21,7 +22,7 @@ export const DisplayConditionsControl = createControl( () => {
|
|
|
21
22
|
alignItems: 'center',
|
|
22
23
|
} }
|
|
23
24
|
>
|
|
24
|
-
<PromotionTrigger ref={ triggerRef } promotionKey="displayConditions" />
|
|
25
|
+
<PromotionTrigger ref={ triggerRef } promotionKey="displayConditions" trackingData={ TRACKING_DATA } />
|
|
25
26
|
<Tooltip title={ ARIA_LABEL } placement="top">
|
|
26
27
|
<IconButton
|
|
27
28
|
size="tiny"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { forwardRef, type ReactNode, useImperativeHandle, useState } from 'react';
|
|
2
|
+
import { forwardRef, type ReactNode, useCallback, useImperativeHandle, useState } from 'react';
|
|
3
3
|
import { PromotionChip, PromotionInfotip } from '@elementor/editor-ui';
|
|
4
4
|
import { Box } from '@elementor/ui';
|
|
5
5
|
|
|
6
|
+
import { type PromotionTrackingData, trackUpgradePromotionClick, trackViewPromotion } from '../../utils/tracking';
|
|
6
7
|
import type { V4PromotionData, V4PromotionKey } from './types';
|
|
7
8
|
|
|
8
9
|
function getV4Promotion( key: V4PromotionKey ): V4PromotionData | undefined {
|
|
@@ -12,6 +13,7 @@ function getV4Promotion( key: V4PromotionKey ): V4PromotionData | undefined {
|
|
|
12
13
|
type PromotionTriggerProps = {
|
|
13
14
|
promotionKey: V4PromotionKey;
|
|
14
15
|
children?: ReactNode;
|
|
16
|
+
trackingData: PromotionTrackingData;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
export type PromotionTriggerRef = {
|
|
@@ -19,13 +21,20 @@ export type PromotionTriggerRef = {
|
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export const PromotionTrigger = forwardRef< PromotionTriggerRef, PromotionTriggerProps >(
|
|
22
|
-
( { promotionKey, children }, ref ) => {
|
|
24
|
+
( { promotionKey, children, trackingData }, ref ) => {
|
|
23
25
|
const [ isOpen, setIsOpen ] = useState( false );
|
|
24
26
|
const promotion = getV4Promotion( promotionKey );
|
|
25
27
|
|
|
26
|
-
const toggle = (
|
|
28
|
+
const toggle = useCallback( () => {
|
|
29
|
+
setIsOpen( ( prev ) => {
|
|
30
|
+
if ( ! prev ) {
|
|
31
|
+
trackViewPromotion( trackingData );
|
|
32
|
+
}
|
|
33
|
+
return ! prev;
|
|
34
|
+
} );
|
|
35
|
+
}, [ trackingData ] );
|
|
27
36
|
|
|
28
|
-
useImperativeHandle( ref, () => ( { toggle } ), [] );
|
|
37
|
+
useImperativeHandle( ref, () => ( { toggle } ), [ toggle ] );
|
|
29
38
|
|
|
30
39
|
return (
|
|
31
40
|
<>
|
|
@@ -40,6 +49,7 @@ export const PromotionTrigger = forwardRef< PromotionTriggerRef, PromotionTrigge
|
|
|
40
49
|
e.stopPropagation();
|
|
41
50
|
setIsOpen( false );
|
|
42
51
|
} }
|
|
52
|
+
onCtaClick={ () => trackUpgradePromotionClick( trackingData ) }
|
|
43
53
|
>
|
|
44
54
|
<Box
|
|
45
55
|
onClick={ ( e: MouseEvent ) => {
|
|
@@ -31,7 +31,7 @@ export const ChipsControl = createControl( ( { options }: ChipsControlProps ) =>
|
|
|
31
31
|
|
|
32
32
|
const handleChange = ( _: SyntheticEvent, newValue: ChipsOption[] ) => {
|
|
33
33
|
const values = newValue.map( ( option ) => stringPropTypeUtil.create( option.value ) );
|
|
34
|
-
setValue( values
|
|
34
|
+
setValue( values );
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
return (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { numberPropTypeUtil, type PropType } from '@elementor/editor-props';
|
|
3
|
-
import { InputAdornment } from '@elementor/ui';
|
|
3
|
+
import { InputAdornment, Typography } from '@elementor/ui';
|
|
4
4
|
|
|
5
5
|
import { useBoundProp } from '../bound-prop-context';
|
|
6
6
|
import { NumberInput } from '../components/number-input';
|
|
@@ -12,7 +12,13 @@ const isEmptyOrNaN = ( value?: string | number | null ) =>
|
|
|
12
12
|
|
|
13
13
|
const renderSuffix = ( propType: PropType ) => {
|
|
14
14
|
if ( propType.meta?.suffix ) {
|
|
15
|
-
return
|
|
15
|
+
return (
|
|
16
|
+
<InputAdornment position="end">
|
|
17
|
+
<Typography variant="caption" color="text.secondary">
|
|
18
|
+
{ propType.meta.suffix as string }
|
|
19
|
+
</Typography>
|
|
20
|
+
</InputAdornment>
|
|
21
|
+
);
|
|
16
22
|
}
|
|
17
23
|
return <></>;
|
|
18
24
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useTypingBuffer } from '../../../hooks/use-typing-buffer';
|
|
4
|
+
import { type SizeUnit } from '../types';
|
|
5
|
+
import { isExtendedUnit } from '../utils/is-extended-unit';
|
|
6
|
+
import { isNumericValue } from '../utils/is-numeric-value';
|
|
7
|
+
|
|
8
|
+
const UNIT_KEY_PATTERN = /^[a-zA-Z%]$/;
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
unit: SizeUnit;
|
|
12
|
+
units: SizeUnit[];
|
|
13
|
+
onUnitChange: ( unit: SizeUnit ) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const useSizeUnitKeyboard = ( { unit, units, onUnitChange }: Props ) => {
|
|
17
|
+
const { appendKey, startsWith } = useTypingBuffer();
|
|
18
|
+
|
|
19
|
+
const onUnitKeyDown = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
|
|
20
|
+
if ( units.length === 0 ) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { key, altKey, ctrlKey, metaKey } = event;
|
|
25
|
+
|
|
26
|
+
if ( altKey || ctrlKey || metaKey ) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if ( isExtendedUnit( unit ) && isNumericValue( key ) ) {
|
|
31
|
+
const [ defaultUnit ] = units;
|
|
32
|
+
|
|
33
|
+
if ( defaultUnit ) {
|
|
34
|
+
onUnitChange( defaultUnit );
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if ( ! UNIT_KEY_PATTERN.test( key ) ) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
|
|
46
|
+
const updatedBuffer = appendKey( key.toLowerCase() );
|
|
47
|
+
const matchedUnit = units.find( ( u ) => startsWith( u, updatedBuffer ) );
|
|
48
|
+
|
|
49
|
+
if ( matchedUnit ) {
|
|
50
|
+
onUnitChange( matchedUnit );
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return { onUnitKeyDown };
|
|
55
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { type SizePropValue } from '@elementor/editor-props';
|
|
3
|
+
|
|
4
|
+
import { useSyncExternalState } from '../../../hooks/use-sync-external-state';
|
|
5
|
+
import { type SizeUnit } from '../types';
|
|
6
|
+
import { isExtendedUnit } from '../utils/is-extended-unit';
|
|
7
|
+
import { createDefaultSizeValue, resolveSizeOnUnitChange, resolveSizeValue } from '../utils/resolve-size-value';
|
|
8
|
+
|
|
9
|
+
type SizeValue = SizePropValue[ 'value' ];
|
|
10
|
+
|
|
11
|
+
type UseSizeValueProps< T, U > = {
|
|
12
|
+
value: T | null;
|
|
13
|
+
onChange: ( value: T ) => void;
|
|
14
|
+
units: U[];
|
|
15
|
+
defaultUnit?: U;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const useSizeValue = < T extends SizeValue, U extends SizeUnit >( {
|
|
19
|
+
value,
|
|
20
|
+
onChange,
|
|
21
|
+
units,
|
|
22
|
+
defaultUnit,
|
|
23
|
+
}: UseSizeValueProps< T, U > ) => {
|
|
24
|
+
const resolvedValue = useMemo(
|
|
25
|
+
() => resolveSizeValue( value, { units, defaultUnit } ),
|
|
26
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
+
[ value?.size, value?.unit, defaultUnit ]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const [ sizeValue, setSizeValue ] = useSyncExternalState< T >( {
|
|
31
|
+
external: resolvedValue as T,
|
|
32
|
+
setExternal: ( newState ) => {
|
|
33
|
+
// TODO we need to check behaviour that low level doesn't set to null only the high level components size component
|
|
34
|
+
// This will fix the issue of if size is empty string '' it gets sends to the model
|
|
35
|
+
// but on blur the size component set to null.
|
|
36
|
+
// But we need to test this behaviour
|
|
37
|
+
if ( newState !== null ) {
|
|
38
|
+
onChange( newState );
|
|
39
|
+
}
|
|
40
|
+
}, // TODO we will need to handle options, meta if context need them
|
|
41
|
+
persistWhen: ( next ) => hasChanged( next, resolvedValue as T ),
|
|
42
|
+
fallback: () => createDefaultSizeValue< T >( units, defaultUnit ),
|
|
43
|
+
} );
|
|
44
|
+
|
|
45
|
+
const setSize = ( newSize: string ) => {
|
|
46
|
+
if ( isExtendedUnit( sizeValue.unit ) ) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const trimmed = newSize.trim();
|
|
51
|
+
const parsed = Number( trimmed );
|
|
52
|
+
|
|
53
|
+
const newState = {
|
|
54
|
+
...sizeValue,
|
|
55
|
+
size: trimmed && ! isNaN( parsed ) ? parsed : '',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
setSizeValue( newState );
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const setUnit = ( unit: SizeValue[ 'unit' ] ) => {
|
|
62
|
+
setSizeValue( { unit, size: resolveSizeOnUnitChange( sizeValue.size, unit ) } as T );
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
size: sizeValue.size,
|
|
67
|
+
unit: sizeValue.unit,
|
|
68
|
+
setSize,
|
|
69
|
+
setUnit,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const hasChanged = ( next?: SizeValue | null, current?: SizeValue | null ): boolean => {
|
|
74
|
+
return next?.size !== current?.size || next?.unit !== current?.unit;
|
|
75
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type RefObject, useEffect } from 'react';
|
|
3
|
+
import type { SizePropValue } from '@elementor/editor-props';
|
|
4
|
+
import { usePopupState } from '@elementor/ui';
|
|
5
|
+
|
|
6
|
+
import { SizeField, type SizeFieldProps } from './size-field';
|
|
7
|
+
import { TextFieldPopover } from './ui/text-field-popover';
|
|
8
|
+
import { EXTENDED_UNITS } from './utils/resolve-size-value';
|
|
9
|
+
|
|
10
|
+
type SizeValue = SizePropValue[ 'value' ];
|
|
11
|
+
|
|
12
|
+
type SizeUnit = SizePropValue[ 'value' ][ 'unit' ];
|
|
13
|
+
|
|
14
|
+
type Props = SizeFieldProps< SizeValue, SizeUnit > & {
|
|
15
|
+
anchorRef?: RefObject< HTMLDivElement | null >;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const SizeComponent = ( { anchorRef, ...sizeFieldProps }: Props ) => {
|
|
19
|
+
const popupState = usePopupState( { variant: 'popover' } );
|
|
20
|
+
|
|
21
|
+
const isCustomUnit = sizeFieldProps?.value?.unit === EXTENDED_UNITS.custom;
|
|
22
|
+
const hasCustomUnitOption = sizeFieldProps.units.includes( EXTENDED_UNITS.custom );
|
|
23
|
+
|
|
24
|
+
useEffect( () => {
|
|
25
|
+
if ( isCustomUnit && anchorRef?.current && ! popupState.isOpen ) {
|
|
26
|
+
popupState.open( anchorRef?.current );
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
}, [ anchorRef, isCustomUnit ] );
|
|
31
|
+
|
|
32
|
+
const handleCustomSizeChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
|
|
33
|
+
sizeFieldProps.onChange( {
|
|
34
|
+
size: event.target.value,
|
|
35
|
+
unit: EXTENDED_UNITS.custom,
|
|
36
|
+
} );
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleSizeFieldClick = ( event: React.MouseEvent ) => {
|
|
40
|
+
if ( ( event.target as HTMLElement ).closest( 'input' ) && isCustomUnit ) {
|
|
41
|
+
popupState.open( anchorRef?.current );
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const popupAttributes = {
|
|
46
|
+
'aria-controls': popupState.isOpen ? popupState.popupId : undefined,
|
|
47
|
+
'aria-haspopup': true,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<SizeField
|
|
53
|
+
{ ...sizeFieldProps }
|
|
54
|
+
InputProps={ {
|
|
55
|
+
...popupAttributes,
|
|
56
|
+
onClick: handleSizeFieldClick,
|
|
57
|
+
} }
|
|
58
|
+
unitSelectorProps={ {
|
|
59
|
+
menuItemsAttributes: hasCustomUnitOption ? { custom: popupAttributes } : undefined,
|
|
60
|
+
} }
|
|
61
|
+
/>
|
|
62
|
+
{ popupState.isOpen && anchorRef?.current && (
|
|
63
|
+
<TextFieldPopover
|
|
64
|
+
popupState={ popupState }
|
|
65
|
+
anchorRef={ anchorRef as RefObject< HTMLDivElement > }
|
|
66
|
+
value={ String( sizeFieldProps?.value?.size ?? '' ) }
|
|
67
|
+
onChange={ handleCustomSizeChange }
|
|
68
|
+
onClose={ () => {} }
|
|
69
|
+
/>
|
|
70
|
+
) }
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { PropValue, SizePropValue } from '@elementor/editor-props';
|
|
3
|
+
import { MathFunctionIcon } from '@elementor/icons';
|
|
4
|
+
import { InputAdornment, type TextFieldProps } from '@elementor/ui';
|
|
5
|
+
|
|
6
|
+
import { useSizeUnitKeyboard } from './hooks/use-size-unit-keyboard';
|
|
7
|
+
import { useSizeValue } from './hooks/use-size-value';
|
|
8
|
+
import { type SizeUnit } from './types';
|
|
9
|
+
import { SizeInput } from './ui/size-input';
|
|
10
|
+
import { UnitSelector, type UnitSelectorProps } from './ui/unit-selector';
|
|
11
|
+
import { isExtendedUnit } from './utils/is-extended-unit';
|
|
12
|
+
|
|
13
|
+
export type SizeFieldProps< TValue extends SizePropValue[ 'value' ], TUnit extends SizeUnit > = {
|
|
14
|
+
units: TUnit[];
|
|
15
|
+
value: TValue | null;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
defaultUnit?: SizeUnit;
|
|
18
|
+
startIcon?: React.ReactNode;
|
|
19
|
+
onChange: ( value: TValue ) => void;
|
|
20
|
+
onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
|
21
|
+
onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
InputProps?: TextFieldProps[ 'InputProps' ];
|
|
24
|
+
unitSelectorProps?: Partial< UnitSelectorProps< TUnit > >;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const UNIT_DISPLAY_LABELS_OVERRIDES: Partial< Record< SizeUnit, React.ReactNode > > = {
|
|
28
|
+
custom: <MathFunctionIcon fontSize="tiny" />,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const SizeField = < T extends SizePropValue[ 'value' ], U extends SizeUnit >( {
|
|
32
|
+
value,
|
|
33
|
+
disabled,
|
|
34
|
+
InputProps,
|
|
35
|
+
defaultUnit,
|
|
36
|
+
startIcon,
|
|
37
|
+
onKeyDown,
|
|
38
|
+
onChange,
|
|
39
|
+
onBlur,
|
|
40
|
+
units,
|
|
41
|
+
unitSelectorProps,
|
|
42
|
+
}: SizeFieldProps< T, U > ) => {
|
|
43
|
+
const { size, unit, setSize, setUnit } = useSizeValue( { value, onChange, units, defaultUnit } );
|
|
44
|
+
const { onUnitKeyDown } = useSizeUnitKeyboard( { unit, onUnitChange: setUnit, units } );
|
|
45
|
+
|
|
46
|
+
const handleKeyDown = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
|
|
47
|
+
onUnitKeyDown( event );
|
|
48
|
+
|
|
49
|
+
onKeyDown?.( event );
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const inputType = isExtendedUnit( unit ) ? 'text' : 'number';
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<SizeInput
|
|
56
|
+
type={ inputType }
|
|
57
|
+
value={ size }
|
|
58
|
+
onBlur={ onBlur }
|
|
59
|
+
onKeyDown={ handleKeyDown }
|
|
60
|
+
onChange={ ( event ) => setSize( event.target.value ) }
|
|
61
|
+
InputProps={ {
|
|
62
|
+
...InputProps,
|
|
63
|
+
autoComplete: 'off',
|
|
64
|
+
readOnly: isExtendedUnit( unit ),
|
|
65
|
+
startAdornment: startIcon && (
|
|
66
|
+
<InputAdornment position="start" disabled={ disabled }>
|
|
67
|
+
{ startIcon }
|
|
68
|
+
</InputAdornment>
|
|
69
|
+
),
|
|
70
|
+
endAdornment: (
|
|
71
|
+
<InputAdornment position="end">
|
|
72
|
+
<UnitSelector< U >
|
|
73
|
+
options={ units }
|
|
74
|
+
value={ unit as U }
|
|
75
|
+
onSelect={ ( newUnit ) => setUnit( newUnit ) }
|
|
76
|
+
isActive={ hasValue( size ) }
|
|
77
|
+
{ ...unitSelectorProps }
|
|
78
|
+
optionLabelOverrides={ UNIT_DISPLAY_LABELS_OVERRIDES }
|
|
79
|
+
/>
|
|
80
|
+
</InputAdornment>
|
|
81
|
+
),
|
|
82
|
+
} }
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const hasValue = ( value: PropValue ): boolean => {
|
|
88
|
+
return Boolean( value ) || value === 0;
|
|
89
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type LengthUnit = 'px' | '%' | 'em' | 'rem' | 'vw' | 'vh' | 'ch';
|
|
2
|
+
type AngleUnit = 'deg' | 'rad' | 'grad' | 'turn';
|
|
3
|
+
type TimeUnit = 's' | 'ms';
|
|
4
|
+
|
|
5
|
+
type ExtendedSizeOption = 'auto' | 'custom';
|
|
6
|
+
|
|
7
|
+
type Unit = LengthUnit | AngleUnit | TimeUnit;
|
|
8
|
+
|
|
9
|
+
export type SizeUnit = Unit | ExtendedSizeOption;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import type { PropValue } from '@elementor/editor-props';
|
|
4
|
+
import type { TextFieldProps } from '@elementor/ui';
|
|
5
|
+
|
|
6
|
+
import { NumberInput } from '../../../components/number-input';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
type: 'number' | 'text';
|
|
10
|
+
value: PropValue;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
focused?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
id?: string;
|
|
15
|
+
onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
|
|
16
|
+
onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
|
17
|
+
onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
|
18
|
+
onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
|
19
|
+
InputProps: TextFieldProps[ 'InputProps' ] & {
|
|
20
|
+
endAdornment: React.JSX.Element;
|
|
21
|
+
};
|
|
22
|
+
inputProps?: TextFieldProps[ 'inputProps' ];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SizeInput = forwardRef(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
id,
|
|
29
|
+
type,
|
|
30
|
+
value,
|
|
31
|
+
onBlur,
|
|
32
|
+
onKeyUp,
|
|
33
|
+
focused,
|
|
34
|
+
disabled,
|
|
35
|
+
onChange,
|
|
36
|
+
onKeyDown,
|
|
37
|
+
InputProps,
|
|
38
|
+
inputProps,
|
|
39
|
+
placeholder,
|
|
40
|
+
}: Props,
|
|
41
|
+
ref
|
|
42
|
+
) => {
|
|
43
|
+
return (
|
|
44
|
+
<NumberInput
|
|
45
|
+
id={ id }
|
|
46
|
+
ref={ ref }
|
|
47
|
+
size="tiny"
|
|
48
|
+
fullWidth
|
|
49
|
+
type={ type }
|
|
50
|
+
value={ value }
|
|
51
|
+
placeholder={ placeholder }
|
|
52
|
+
onKeyUp={ onKeyUp }
|
|
53
|
+
focused={ focused }
|
|
54
|
+
disabled={ disabled }
|
|
55
|
+
onKeyDown={ onKeyDown }
|
|
56
|
+
onInput={ onChange }
|
|
57
|
+
onBlur={ onBlur }
|
|
58
|
+
InputProps={ InputProps }
|
|
59
|
+
inputProps={ inputProps }
|
|
60
|
+
sx={ getCursorStyle( InputProps?.readOnly ?? false ) }
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const getCursorStyle = ( readOnly: boolean ) => ( {
|
|
67
|
+
input: { cursor: readOnly ? 'default !important' : undefined },
|
|
68
|
+
} );
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type RefObject, useEffect, useRef } from 'react';
|
|
3
|
+
import { PopoverHeader } from '@elementor/editor-ui';
|
|
4
|
+
import { MathFunctionIcon } from '@elementor/icons';
|
|
5
|
+
import { bindPopover, Popover, type PopupState, TextField } from '@elementor/ui';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
popupState: PopupState;
|
|
10
|
+
anchorRef: RefObject< HTMLDivElement | null >;
|
|
11
|
+
onClose?: () => void;
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SIZE = 'tiny';
|
|
17
|
+
|
|
18
|
+
export const TextFieldPopover = ( { popupState, anchorRef, value, onChange, onClose }: Props ) => {
|
|
19
|
+
const inputRef = useRef< HTMLInputElement >( null );
|
|
20
|
+
|
|
21
|
+
useEffect( () => {
|
|
22
|
+
if ( popupState.isOpen ) {
|
|
23
|
+
requestAnimationFrame( () => {
|
|
24
|
+
if ( inputRef.current ) {
|
|
25
|
+
inputRef.current.focus();
|
|
26
|
+
}
|
|
27
|
+
} );
|
|
28
|
+
}
|
|
29
|
+
}, [ popupState.isOpen ] );
|
|
30
|
+
|
|
31
|
+
const handleKeyDown = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
|
|
32
|
+
if ( event.key.toLowerCase() === 'enter' ) {
|
|
33
|
+
handleClose();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleClose = () => {
|
|
38
|
+
onClose?.();
|
|
39
|
+
|
|
40
|
+
popupState.close();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Popover
|
|
45
|
+
disablePortal
|
|
46
|
+
slotProps={ {
|
|
47
|
+
paper: {
|
|
48
|
+
sx: {
|
|
49
|
+
borderRadius: 2,
|
|
50
|
+
width: anchorRef.current?.offsetWidth + 'px',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
} }
|
|
54
|
+
{ ...bindPopover( popupState ) }
|
|
55
|
+
anchorOrigin={ { vertical: 'bottom', horizontal: 'center' } }
|
|
56
|
+
transformOrigin={ { vertical: 'top', horizontal: 'center' } }
|
|
57
|
+
onClose={ handleClose }
|
|
58
|
+
>
|
|
59
|
+
<PopoverHeader
|
|
60
|
+
title={ __( 'CSS function', 'elementor' ) }
|
|
61
|
+
onClose={ handleClose }
|
|
62
|
+
icon={ <MathFunctionIcon fontSize={ SIZE } /> }
|
|
63
|
+
/>
|
|
64
|
+
<TextField
|
|
65
|
+
value={ value }
|
|
66
|
+
onChange={ onChange }
|
|
67
|
+
onKeyDown={ handleKeyDown }
|
|
68
|
+
size="tiny"
|
|
69
|
+
type="text"
|
|
70
|
+
fullWidth
|
|
71
|
+
inputProps={ {
|
|
72
|
+
ref: inputRef,
|
|
73
|
+
} }
|
|
74
|
+
sx={ { pt: 0, pr: 1.5, pb: 1.5, pl: 1.5 } }
|
|
75
|
+
/>
|
|
76
|
+
</Popover>
|
|
77
|
+
);
|
|
78
|
+
};
|