@elementor/editor-controls 3.32.0-68 → 3.32.0-69

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": "3.32.0-68",
4
+ "version": "3.32.0-69",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,21 +40,21 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "3.32.0-68",
44
- "@elementor/editor-elements": "3.32.0-68",
45
- "@elementor/editor-props": "3.32.0-68",
46
- "@elementor/editor-responsive": "3.32.0-68",
47
- "@elementor/editor-ui": "3.32.0-68",
48
- "@elementor/editor-v1-adapters": "3.32.0-68",
49
- "@elementor/env": "3.32.0-68",
50
- "@elementor/http-client": "3.32.0-68",
43
+ "@elementor/editor-current-user": "3.32.0-69",
44
+ "@elementor/editor-elements": "3.32.0-69",
45
+ "@elementor/editor-props": "3.32.0-69",
46
+ "@elementor/editor-responsive": "3.32.0-69",
47
+ "@elementor/editor-ui": "3.32.0-69",
48
+ "@elementor/editor-v1-adapters": "3.32.0-69",
49
+ "@elementor/env": "3.32.0-69",
50
+ "@elementor/http-client": "3.32.0-69",
51
51
  "@elementor/icons": "^1.51.1",
52
- "@elementor/locations": "3.32.0-68",
53
- "@elementor/query": "3.32.0-68",
54
- "@elementor/session": "3.32.0-68",
52
+ "@elementor/locations": "3.32.0-69",
53
+ "@elementor/query": "3.32.0-69",
54
+ "@elementor/session": "3.32.0-69",
55
55
  "@elementor/ui": "1.36.8",
56
- "@elementor/utils": "3.32.0-68",
57
- "@elementor/wp-media": "3.32.0-68",
56
+ "@elementor/utils": "3.32.0-69",
57
+ "@elementor/wp-media": "3.32.0-69",
58
58
  "@wordpress/i18n": "^5.13.0",
59
59
  "@monaco-editor/react": "^4.7.0"
60
60
  },
@@ -4,8 +4,9 @@ import { type CreateOptions, type PropKey, type PropType, type PropValue } from
4
4
 
5
5
  import { HookOutsideProviderError } from './errors';
6
6
 
7
- type SetValueMeta = {
7
+ export type SetValueMeta = {
8
8
  bind?: PropKey;
9
+ validation?: ( value: PropValue ) => boolean;
9
10
  };
10
11
 
11
12
  export type SetValue< T > = ( value: T, options?: CreateOptions, meta?: SetValueMeta ) => void;
@@ -8,7 +8,7 @@ import {
8
8
  } from '@elementor/editor-props';
9
9
 
10
10
  import { MissingPropTypeError } from './errors';
11
- import { type SetValue } from './prop-context';
11
+ import { type SetValue, type SetValueMeta } from './prop-context';
12
12
  import { type PropKeyContextValue, usePropKeyContext } from './prop-key-context';
13
13
 
14
14
  type UseBoundProp< TValue extends PropValue > = {
@@ -46,8 +46,8 @@ export function useBoundProp< TKey extends string, TValue extends PropValue >(
46
46
  return { ...propKeyContext, disabled } as PropKeyContextValue< PropValue, PropType >;
47
47
  }
48
48
 
49
- function setValue( value: TValue | null, options: CreateOptions, meta: { bind?: PropKey } ) {
50
- if ( ! validate( value ) ) {
49
+ function setValue( value: TValue | null, options: CreateOptions, meta?: SetValueMeta ) {
50
+ if ( ! validate( value, meta?.validation ) ) {
51
51
  return;
52
52
  }
53
53
 
@@ -79,13 +79,17 @@ const useValidation = ( propType: PropType ) => {
79
79
 
80
80
  // If the value does not pass the prop type validation, set the isValid state to false.
81
81
  // This will prevent the value from being set in the model, and its fallback will be used instead.
82
- const validate = ( value: PropValue | null ) => {
82
+ const validate = ( value: PropValue | null, validation?: ( value: PropValue ) => boolean ) => {
83
83
  let valid = true;
84
84
 
85
85
  if ( propType.settings.required && value === null ) {
86
86
  valid = false;
87
87
  }
88
88
 
89
+ if ( validation && ! validation( value ) ) {
90
+ valid = false;
91
+ }
92
+
89
93
  setIsValid( valid );
90
94
 
91
95
  return valid;
@@ -0,0 +1,41 @@
1
+ import * as React from 'react';
2
+ import { forwardRef, useState } from 'react';
3
+ import { TextField, type TextFieldProps } from '@elementor/ui';
4
+
5
+ const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+' ];
6
+
7
+ export const NumberInput = forwardRef( ( props: TextFieldProps, ref ) => {
8
+ const [ key, setKey ] = useState< number >( 0 );
9
+
10
+ const handleKeyDown = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
11
+ blockRestrictedKeys( event, props.inputProps?.min );
12
+
13
+ props.onKeyDown?.( event );
14
+ };
15
+
16
+ const handleBlur = ( event: React.FocusEvent< HTMLInputElement > ) => {
17
+ props.onBlur?.( event );
18
+
19
+ const { valid } = event.target.validity;
20
+
21
+ // HTML number input quirk: invalid input (e.g. "-9-") returns value="" but displays "-9-" to user,
22
+ // so when we revert to last valid value we must re-mount the component to actually display it.
23
+ if ( ! valid ) {
24
+ setKey( ( prev ) => prev + 1 );
25
+ }
26
+ };
27
+
28
+ return <TextField { ...props } ref={ ref } key={ key } onKeyDown={ handleKeyDown } onBlur={ handleBlur } />;
29
+ } );
30
+
31
+ function blockRestrictedKeys( event: React.KeyboardEvent< HTMLInputElement >, min: number ) {
32
+ const restrictedInputKeys = [ ...RESTRICTED_INPUT_KEYS ];
33
+
34
+ if ( min >= 0 ) {
35
+ restrictedInputKeys.push( '-' );
36
+ }
37
+
38
+ if ( restrictedInputKeys.includes( event.key ) ) {
39
+ event.preventDefault();
40
+ }
41
+ }
@@ -20,10 +20,9 @@ type SizeInputProps = {
20
20
  handleSizeChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
21
21
  popupState: PopupState;
22
22
  disabled?: boolean;
23
+ min?: number;
23
24
  };
24
25
 
25
- const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
26
-
27
26
  export const SizeInput = ( {
28
27
  units,
29
28
  handleUnitChange,
@@ -37,6 +36,7 @@ export const SizeInput = ( {
37
36
  unit,
38
37
  popupState,
39
38
  disabled,
39
+ min,
40
40
  }: SizeInputProps ) => {
41
41
  const unitInputBufferRef = useRef( '' );
42
42
  const inputType = isUnitExtendedOption( unit ) ? 'text' : 'number';
@@ -70,7 +70,7 @@ export const SizeInput = ( {
70
70
  'aria-haspopup': true,
71
71
  };
72
72
 
73
- const inputProps = {
73
+ const InputProps = {
74
74
  ...popupAttributes,
75
75
  readOnly: isUnitExtendedOption( unit ),
76
76
  autoComplete: 'off',
@@ -110,14 +110,10 @@ export const SizeInput = ( {
110
110
  type={ inputType }
111
111
  value={ inputValue }
112
112
  onChange={ handleSizeChange }
113
- onKeyDown={ ( event ) => {
114
- if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
115
- event.preventDefault();
116
- }
117
- } }
118
113
  onKeyUp={ handleKeyUp }
119
114
  onBlur={ onBlur }
120
- inputProps={ inputProps }
115
+ InputProps={ InputProps }
116
+ inputProps={ { min, step: 'any' } }
121
117
  isPopoverOpen={ popupState.isOpen }
122
118
  />
123
119
  </Box>
@@ -9,13 +9,13 @@ import {
9
9
  InputAdornment,
10
10
  Menu,
11
11
  styled,
12
- TextField,
13
12
  type TextFieldProps,
14
13
  usePopupState,
15
14
  } from '@elementor/ui';
16
15
 
17
16
  import { useBoundProp } from '../../bound-prop-context';
18
17
  import { DEFAULT_UNIT } from '../../utils/size-control';
18
+ import { NumberInput } from '../number-input';
19
19
 
20
20
  type TextFieldInnerSelectionProps = {
21
21
  placeholder?: string;
@@ -25,9 +25,10 @@ type TextFieldInnerSelectionProps = {
25
25
  onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
26
26
  onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
27
27
  onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
28
- inputProps: TextFieldProps[ 'InputProps' ] & {
28
+ InputProps: TextFieldProps[ 'InputProps' ] & {
29
29
  endAdornment: React.JSX.Element;
30
30
  };
31
+ inputProps?: TextFieldProps[ 'inputProps' ];
31
32
  disabled?: boolean;
32
33
  isPopoverOpen?: boolean;
33
34
  };
@@ -42,6 +43,7 @@ export const TextFieldInnerSelection = forwardRef(
42
43
  onBlur,
43
44
  onKeyDown,
44
45
  onKeyUp,
46
+ InputProps,
45
47
  inputProps,
46
48
  disabled,
47
49
  isPopoverOpen,
@@ -51,25 +53,26 @@ export const TextFieldInnerSelection = forwardRef(
51
53
  const { placeholder: boundPropPlaceholder } = useBoundProp( sizePropTypeUtil );
52
54
 
53
55
  const getCursorStyle = () => ( {
54
- input: { cursor: inputProps.readOnly ? 'default !important' : undefined },
56
+ input: { cursor: InputProps.readOnly ? 'default !important' : undefined },
55
57
  } );
56
58
 
57
59
  return (
58
- <TextField
60
+ <NumberInput
59
61
  ref={ ref }
60
62
  sx={ getCursorStyle() }
61
63
  size="tiny"
62
64
  fullWidth
63
65
  type={ type }
64
66
  value={ value }
65
- onChange={ onChange }
67
+ onInput={ onChange }
66
68
  onKeyDown={ onKeyDown }
67
69
  onKeyUp={ onKeyUp }
68
70
  disabled={ disabled }
69
71
  onBlur={ onBlur }
70
72
  focused={ isPopoverOpen ? true : undefined }
71
73
  placeholder={ placeholder ?? ( String( boundPropPlaceholder?.size ?? '' ) || undefined ) }
72
- InputProps={ inputProps }
74
+ InputProps={ InputProps }
75
+ inputProps={ inputProps }
73
76
  />
74
77
  );
75
78
  }
@@ -86,6 +86,7 @@ export const BackgroundImageOverlayPosition = () => {
86
86
  <SizeControl
87
87
  startIcon={ <LetterXIcon fontSize={ 'tiny' } /> }
88
88
  anchorRef={ rowRef }
89
+ min={ -Number.MAX_SAFE_INTEGER }
89
90
  />
90
91
  </PropKeyProvider>
91
92
  </Grid>
@@ -94,6 +95,7 @@ export const BackgroundImageOverlayPosition = () => {
94
95
  <SizeControl
95
96
  startIcon={ <LetterYIcon fontSize={ 'tiny' } /> }
96
97
  anchorRef={ rowRef }
98
+ min={ -Number.MAX_SAFE_INTEGER }
97
99
  />
98
100
  </PropKeyProvider>
99
101
  </Grid>
@@ -18,10 +18,12 @@ export const LinkedDimensionsControl = createControl(
18
18
  label,
19
19
  isSiteRtl = false,
20
20
  extendedOptions,
21
+ min,
21
22
  }: {
22
23
  label: string;
23
24
  isSiteRtl?: boolean;
24
25
  extendedOptions?: ExtendedOption[];
26
+ min?: number;
25
27
  } ) => {
26
28
  const {
27
29
  value: sizeValue,
@@ -110,6 +112,7 @@ export const LinkedDimensionsControl = createControl(
110
112
  isLinked={ isLinked }
111
113
  extendedOptions={ extendedOptions }
112
114
  anchorRef={ gridRowRefs[ index ] }
115
+ min={ min }
113
116
  />
114
117
  </Grid>
115
118
  </Grid>
@@ -127,20 +130,34 @@ const Control = ( {
127
130
  isLinked,
128
131
  extendedOptions,
129
132
  anchorRef,
133
+ min,
130
134
  }: {
131
135
  bind: PropKey;
132
136
  startIcon: React.ReactNode;
133
137
  isLinked: boolean;
134
138
  extendedOptions?: ExtendedOption[];
135
139
  anchorRef: RefObject< HTMLDivElement >;
140
+ min?: number;
136
141
  } ) => {
137
142
  if ( isLinked ) {
138
- return <SizeControl startIcon={ startIcon } extendedOptions={ extendedOptions } anchorRef={ anchorRef } />;
143
+ return (
144
+ <SizeControl
145
+ startIcon={ startIcon }
146
+ extendedOptions={ extendedOptions }
147
+ anchorRef={ anchorRef }
148
+ min={ min }
149
+ />
150
+ );
139
151
  }
140
152
 
141
153
  return (
142
154
  <PropKeyProvider bind={ bind }>
143
- <SizeControl startIcon={ startIcon } extendedOptions={ extendedOptions } anchorRef={ anchorRef } />
155
+ <SizeControl
156
+ startIcon={ startIcon }
157
+ extendedOptions={ extendedOptions }
158
+ anchorRef={ anchorRef }
159
+ min={ min }
160
+ />
144
161
  </PropKeyProvider>
145
162
  );
146
163
  };
@@ -1,21 +1,20 @@
1
1
  import * as React from 'react';
2
2
  import { numberPropTypeUtil } from '@elementor/editor-props';
3
- import { InputAdornment, TextField } from '@elementor/ui';
3
+ import { InputAdornment } from '@elementor/ui';
4
4
 
5
5
  import { useBoundProp } from '../bound-prop-context';
6
+ import { NumberInput } from '../components/number-input';
6
7
  import ControlActions from '../control-actions/control-actions';
7
8
  import { createControl } from '../create-control';
8
9
 
9
10
  const isEmptyOrNaN = ( value?: string | number | null ) =>
10
11
  value === null || value === undefined || value === '' || Number.isNaN( Number( value ) );
11
12
 
12
- const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
13
-
14
13
  export const NumberControl = createControl(
15
14
  ( {
16
15
  placeholder: labelPlaceholder,
17
- max = Number.MAX_VALUE,
18
- min = -Number.MAX_VALUE,
16
+ max = Number.MAX_SAFE_INTEGER,
17
+ min = -Number.MAX_SAFE_INTEGER,
19
18
  step = 1,
20
19
  shouldForceInt = false,
21
20
  startIcon,
@@ -27,33 +26,38 @@ export const NumberControl = createControl(
27
26
  shouldForceInt?: boolean;
28
27
  startIcon?: React.ReactNode;
29
28
  } ) => {
30
- const { value, setValue, placeholder, disabled } = useBoundProp( numberPropTypeUtil );
29
+ const { value, setValue, placeholder, disabled, restoreValue } = useBoundProp( numberPropTypeUtil );
31
30
 
32
31
  const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
33
- const eventValue: string = event.target.value;
32
+ const {
33
+ value: eventValue,
34
+ validity: { valid: isInputValid },
35
+ } = event.target;
34
36
 
35
- if ( isEmptyOrNaN( eventValue ) ) {
36
- setValue( null );
37
+ let updatedValue;
37
38
 
38
- return;
39
+ if ( isEmptyOrNaN( eventValue ) ) {
40
+ updatedValue = null;
41
+ } else {
42
+ const formattedValue = shouldForceInt ? +parseInt( eventValue ) : Number( eventValue );
43
+ updatedValue = Math.min( Math.max( formattedValue, min ), max );
39
44
  }
40
45
 
41
- const formattedValue = shouldForceInt ? +parseInt( eventValue ) : Number( eventValue );
42
-
43
- setValue( Math.min( Math.max( formattedValue, min ), max ) );
46
+ setValue( updatedValue, undefined, { validation: () => isInputValid } );
44
47
  };
45
48
 
46
49
  return (
47
50
  <ControlActions>
48
- <TextField
51
+ <NumberInput
49
52
  size="tiny"
50
53
  type="number"
51
54
  fullWidth
52
55
  disabled={ disabled }
53
56
  value={ isEmptyOrNaN( value ) ? '' : value }
54
- onChange={ handleChange }
57
+ onInput={ handleChange }
58
+ onBlur={ restoreValue }
55
59
  placeholder={ labelPlaceholder ?? ( isEmptyOrNaN( placeholder ) ? '' : String( placeholder ) ) }
56
- inputProps={ { step } }
60
+ inputProps={ { step, min } }
57
61
  InputProps={ {
58
62
  startAdornment: startIcon ? (
59
63
  <InputAdornment position="start" disabled={ disabled }>
@@ -61,11 +65,6 @@ export const NumberControl = createControl(
61
65
  </InputAdornment>
62
66
  ) : undefined,
63
67
  } }
64
- onKeyDown={ ( event: KeyboardEvent ) => {
65
- if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
66
- event.preventDefault();
67
- }
68
- } }
69
68
  />
70
69
  </ControlActions>
71
70
  );
@@ -80,12 +80,18 @@ export const PositionControl = () => {
80
80
  <Grid container spacing={ 1.5 }>
81
81
  <Grid item xs={ 6 }>
82
82
  <PropKeyProvider bind={ 'x' }>
83
- <SizeControl startIcon={ <LetterXIcon fontSize={ 'tiny' } /> } />
83
+ <SizeControl
84
+ startIcon={ <LetterXIcon fontSize={ 'tiny' } /> }
85
+ min={ -Number.MAX_SAFE_INTEGER }
86
+ />
84
87
  </PropKeyProvider>
85
88
  </Grid>
86
89
  <Grid item xs={ 6 }>
87
90
  <PropKeyProvider bind={ 'y' }>
88
- <SizeControl startIcon={ <LetterYIcon fontSize={ 'tiny' } /> } />
91
+ <SizeControl
92
+ startIcon={ <LetterYIcon fontSize={ 'tiny' } /> }
93
+ min={ -Number.MAX_SAFE_INTEGER }
94
+ />
89
95
  </PropKeyProvider>
90
96
  </Grid>
91
97
  </Grid>
@@ -39,6 +39,7 @@ type BaseSizeControlProps = {
39
39
  extendedOptions?: ExtendedOption[];
40
40
  disableCustom?: boolean;
41
41
  anchorRef?: RefObject< HTMLDivElement | null >;
42
+ min?: number;
42
43
  };
43
44
 
44
45
  type LengthSizeControlProps = BaseSizeControlProps &
@@ -86,6 +87,7 @@ export const SizeControl = createControl(
86
87
  anchorRef,
87
88
  extendedOptions,
88
89
  disableCustom,
90
+ min = 0,
89
91
  }: Omit< SizeControlProps, 'variant' > & { variant?: SizeVariant } ) => {
90
92
  const {
91
93
  value: sizeValue,
@@ -104,7 +106,8 @@ export const SizeControl = createControl(
104
106
 
105
107
  const [ state, setState ] = useSyncExternalState( {
106
108
  external: internalState,
107
- setExternal: ( newState: State | null ) => setSizeValue( extractValueFromState( newState ) ),
109
+ setExternal: ( newState: State | null, options, meta ) =>
110
+ setSizeValue( extractValueFromState( newState ), options, meta ),
108
111
  persistWhen: ( newState ) => {
109
112
  if ( ! newState?.unit ) {
110
113
  return false;
@@ -135,7 +138,8 @@ export const SizeControl = createControl(
135
138
  };
136
139
 
137
140
  const handleSizeChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
138
- const { value: size } = event.target;
141
+ const size = event.target.value;
142
+ const isInputValid = event.target.validity.valid;
139
143
 
140
144
  if ( controlUnit === 'auto' ) {
141
145
  setState( ( prev ) => ( { ...prev, unit: controlUnit } ) );
@@ -143,11 +147,15 @@ export const SizeControl = createControl(
143
147
  return;
144
148
  }
145
149
 
146
- setState( ( prev ) => ( {
147
- ...prev,
148
- [ controlUnit === 'custom' ? 'custom' : 'numeric' ]: formatSize( size, controlUnit ),
149
- unit: controlUnit,
150
- } ) );
150
+ setState(
151
+ ( prev ) => ( {
152
+ ...prev,
153
+ [ controlUnit === 'custom' ? 'custom' : 'numeric' ]: formatSize( size, controlUnit ),
154
+ unit: controlUnit,
155
+ } ),
156
+ undefined,
157
+ { validation: () => isInputValid }
158
+ );
151
159
  };
152
160
 
153
161
  const onInputClick = ( event: React.MouseEvent ) => {
@@ -207,6 +215,7 @@ export const SizeControl = createControl(
207
215
  onBlur={ restoreValue }
208
216
  onClick={ onInputClick }
209
217
  popupState={ popupState }
218
+ min={ min }
210
219
  />
211
220
  { anchorRef?.current && (
212
221
  <TextFieldPopover
@@ -31,6 +31,7 @@ export const AxisRow = ( { label, bind, startIcon, anchorRef, units, variant = '
31
31
  startIcon={ startIcon }
32
32
  units={ units }
33
33
  variant={ variant }
34
+ min={ -Number.MAX_SAFE_INTEGER }
34
35
  />
35
36
  </PropKeyProvider>
36
37
  </Grid>
@@ -1,8 +1,11 @@
1
1
  import { useEffect, useState } from 'react';
2
+ import { type CreateOptions } from '@elementor/editor-props';
3
+
4
+ import { type SetValueMeta } from '../bound-prop-context';
2
5
 
3
6
  type UseInternalStateOptions< TValue > = {
4
7
  external: TValue | null;
5
- setExternal: ( value: TValue | null ) => void;
8
+ setExternal: ( value: TValue | null, options?: CreateOptions, meta?: SetValueMeta ) => void;
6
9
  persistWhen: ( value: TValue | null ) => boolean;
7
10
  fallback: ( value: TValue | null ) => TValue;
8
11
  };
@@ -40,12 +43,12 @@ export const useSyncExternalState = < TValue, >( {
40
43
 
41
44
  type SetterFunc = ( value: TValue ) => TValue;
42
45
 
43
- const setInternalValue = ( setter: SetterFunc | TValue ) => {
46
+ const setInternalValue = ( setter: SetterFunc | TValue, options?: CreateOptions, meta?: SetValueMeta ) => {
44
47
  const setterFn = ( typeof setter === 'function' ? setter : () => setter ) as SetterFunc;
45
48
  const updated = setterFn( internal );
46
49
 
47
50
  setInternal( updated );
48
- setExternal( toExternal( updated ) );
51
+ setExternal( toExternal( updated ), options, meta );
49
52
  };
50
53
 
51
54
  return [ internal, setInternalValue ] as const;