@elementor/editor-controls 4.0.0-649 → 4.0.0-659

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.0.0-649",
4
+ "version": "4.0.0-659",
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.0.0-649",
44
- "@elementor/editor-elements": "4.0.0-649",
45
- "@elementor/editor-props": "4.0.0-649",
46
- "@elementor/editor-responsive": "4.0.0-649",
47
- "@elementor/editor-ui": "4.0.0-649",
48
- "@elementor/editor-v1-adapters": "4.0.0-649",
49
- "@elementor/env": "4.0.0-649",
50
- "@elementor/http-client": "4.0.0-649",
43
+ "@elementor/editor-current-user": "4.0.0-659",
44
+ "@elementor/editor-elements": "4.0.0-659",
45
+ "@elementor/editor-props": "4.0.0-659",
46
+ "@elementor/editor-responsive": "4.0.0-659",
47
+ "@elementor/editor-ui": "4.0.0-659",
48
+ "@elementor/editor-v1-adapters": "4.0.0-659",
49
+ "@elementor/env": "4.0.0-659",
50
+ "@elementor/http-client": "4.0.0-659",
51
51
  "@elementor/icons": "^1.68.0",
52
- "@elementor/locations": "4.0.0-649",
53
- "@elementor/events": "4.0.0-649",
54
- "@elementor/query": "4.0.0-649",
55
- "@elementor/session": "4.0.0-649",
52
+ "@elementor/locations": "4.0.0-659",
53
+ "@elementor/events": "4.0.0-659",
54
+ "@elementor/query": "4.0.0-659",
55
+ "@elementor/session": "4.0.0-659",
56
56
  "@elementor/ui": "1.36.17",
57
- "@elementor/utils": "4.0.0-649",
58
- "@elementor/wp-media": "4.0.0-649",
57
+ "@elementor/utils": "4.0.0-659",
58
+ "@elementor/wp-media": "4.0.0-659",
59
59
  "@wordpress/i18n": "^5.13.0",
60
60
  "@monaco-editor/react": "^4.7.0",
61
61
  "dayjs": "^1.11.18",
@@ -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,3 @@
1
+ export const getExtendedUnits = () => {
2
+ return window.elementor?.config?.size_units?.extended_units ?? [];
3
+ };
@@ -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,8 @@
1
+ import { getExtendedUnits } from '../sync/get-units';
2
+ import { type SizeUnit } from '../types';
3
+
4
+ export const isExtendedUnit = ( unit: SizeUnit ) => {
5
+ const extendedUnits = getExtendedUnits();
6
+
7
+ return extendedUnits.includes( unit );
8
+ };
@@ -0,0 +1,11 @@
1
+ export const isNumericValue = ( value: unknown ): boolean => {
2
+ if ( typeof value === 'number' ) {
3
+ return ! isNaN( value );
4
+ }
5
+
6
+ if ( typeof value === 'string' ) {
7
+ return value.trim() !== '' && ! isNaN( Number( value ) );
8
+ }
9
+
10
+ return false;
11
+ };
@@ -0,0 +1,84 @@
1
+ import { type SizePropValue } from '@elementor/editor-props';
2
+
3
+ import { type SizeUnit } from '../types';
4
+ import { isExtendedUnit } from './is-extended-unit';
5
+
6
+ type SizeValue = SizePropValue[ 'value' ];
7
+
8
+ type ResolverContext< U > = {
9
+ units: U[];
10
+ defaultUnit?: U;
11
+ };
12
+
13
+ const DEFAULT_SIZE = '';
14
+
15
+ export const EXTENDED_UNITS = {
16
+ auto: 'auto',
17
+ custom: 'custom',
18
+ } as const;
19
+
20
+ export const resolveSizeValue = < TValue extends SizeValue | null, TUnit extends SizeValue[ 'unit' ] >(
21
+ value: TValue,
22
+ context: ResolverContext< TUnit >
23
+ ) => {
24
+ if ( ! value ) {
25
+ return value;
26
+ }
27
+
28
+ const { units, defaultUnit } = context;
29
+
30
+ const unit = resolveFallbackUnit( value.unit as TUnit, units, defaultUnit );
31
+
32
+ if ( unit === EXTENDED_UNITS.auto ) {
33
+ return { size: DEFAULT_SIZE, unit };
34
+ }
35
+
36
+ if ( unit === EXTENDED_UNITS.custom ) {
37
+ return { size: String( value.size ?? DEFAULT_SIZE ), unit };
38
+ }
39
+
40
+ return {
41
+ size: sanitizeSize( value.size ) ?? DEFAULT_SIZE,
42
+ unit,
43
+ };
44
+ };
45
+
46
+ export const resolveSizeOnUnitChange = (
47
+ size: SizeValue[ 'size' ],
48
+ unit: SizeValue[ 'unit' ]
49
+ ): SizeValue[ 'size' ] => {
50
+ return isExtendedUnit( unit ) ? DEFAULT_SIZE : size;
51
+ };
52
+
53
+ export const createDefaultSizeValue = < T extends SizeValue >( units: SizeUnit[], defaultUnit?: SizeUnit ): T => {
54
+ let unit = units[ 0 ];
55
+
56
+ if ( defaultUnit !== undefined ) {
57
+ unit = resolveFallbackUnit( defaultUnit, units ) as SizeUnit;
58
+ }
59
+
60
+ return { size: DEFAULT_SIZE, unit } as T;
61
+ };
62
+
63
+ const resolveFallbackUnit = < TUnit extends SizeUnit >(
64
+ unit: TUnit,
65
+ units: readonly TUnit[],
66
+ defaultUnit?: TUnit
67
+ ): TUnit | string => {
68
+ if ( units.includes( unit ) ) {
69
+ return unit;
70
+ }
71
+
72
+ if ( defaultUnit && units.includes( defaultUnit ) ) {
73
+ return defaultUnit;
74
+ }
75
+
76
+ return units[ 0 ] ?? '';
77
+ };
78
+
79
+ const sanitizeSize = ( size: SizeValue[ 'size' ] ): SizeValue[ 'size' ] => {
80
+ if ( typeof size === 'number' && isNaN( size ) ) {
81
+ return DEFAULT_SIZE;
82
+ }
83
+ return size;
84
+ };