@elementor/editor-controls 4.0.0-609 → 4.0.0-621

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-609",
4
+ "version": "4.0.0-621",
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-609",
44
- "@elementor/editor-elements": "4.0.0-609",
45
- "@elementor/editor-props": "4.0.0-609",
46
- "@elementor/editor-responsive": "4.0.0-609",
47
- "@elementor/editor-ui": "4.0.0-609",
48
- "@elementor/editor-v1-adapters": "4.0.0-609",
49
- "@elementor/env": "4.0.0-609",
50
- "@elementor/http-client": "4.0.0-609",
43
+ "@elementor/editor-current-user": "4.0.0-621",
44
+ "@elementor/editor-elements": "4.0.0-621",
45
+ "@elementor/editor-props": "4.0.0-621",
46
+ "@elementor/editor-responsive": "4.0.0-621",
47
+ "@elementor/editor-ui": "4.0.0-621",
48
+ "@elementor/editor-v1-adapters": "4.0.0-621",
49
+ "@elementor/env": "4.0.0-621",
50
+ "@elementor/http-client": "4.0.0-621",
51
51
  "@elementor/icons": "^1.68.0",
52
- "@elementor/locations": "4.0.0-609",
53
- "@elementor/events": "4.0.0-609",
54
- "@elementor/query": "4.0.0-609",
55
- "@elementor/session": "4.0.0-609",
52
+ "@elementor/locations": "4.0.0-621",
53
+ "@elementor/events": "4.0.0-621",
54
+ "@elementor/query": "4.0.0-621",
55
+ "@elementor/session": "4.0.0-621",
56
56
  "@elementor/ui": "1.36.17",
57
- "@elementor/utils": "4.0.0-609",
58
- "@elementor/wp-media": "4.0.0-609",
57
+ "@elementor/utils": "4.0.0-621",
58
+ "@elementor/wp-media": "4.0.0-621",
59
59
  "@wordpress/i18n": "^5.13.0",
60
60
  "@monaco-editor/react": "^4.7.0",
61
61
  "dayjs": "^1.11.18",
@@ -1,14 +1,12 @@
1
1
  import * as React from 'react';
2
- import { useRef } from 'react';
3
2
  import { MathFunctionIcon } from '@elementor/icons';
4
3
  import { Box, InputAdornment, type PopupState } from '@elementor/ui';
5
4
 
6
5
  import ControlActions from '../../control-actions/control-actions';
6
+ import { useTypingBuffer } from '../../hooks/use-typing-buffer';
7
7
  import { type ExtendedOption, isUnitExtendedOption, type Unit } from '../../utils/size-control';
8
8
  import { SelectionEndAdornment, TextFieldInnerSelection } from '../size-control/text-field-inner-selection';
9
9
 
10
- const RESTRICTED_KEYBOARD_SHORTCUT_UNITS = [ 'auto' ];
11
-
12
10
  type SizeInputProps = {
13
11
  unit: Unit | ExtendedOption;
14
12
  size: number | string;
@@ -44,12 +42,25 @@ export const SizeInput = ( {
44
42
  id,
45
43
  ariaLabel,
46
44
  }: SizeInputProps ) => {
47
- const unitInputBufferRef = useRef( '' );
45
+ const { appendKey, startsWith } = useTypingBuffer();
46
+
48
47
  const inputType = isUnitExtendedOption( unit ) ? 'text' : 'number';
49
48
  const inputValue = ! isUnitExtendedOption( unit ) && Number.isNaN( size ) ? '' : size ?? '';
50
49
 
51
- const handleKeyUp = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
52
- const { key } = event;
50
+ const handleKeyDown = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
51
+ const { key, altKey, ctrlKey, metaKey } = event;
52
+
53
+ if ( altKey || ctrlKey || metaKey ) {
54
+ return;
55
+ }
56
+
57
+ if ( isUnitExtendedOption( unit ) && ! isNaN( Number( key ) ) ) {
58
+ const defaultUnit = units?.[ 0 ];
59
+ if ( defaultUnit ) {
60
+ handleUnitChange( defaultUnit );
61
+ }
62
+ return;
63
+ }
53
64
 
54
65
  if ( ! /^[a-zA-Z%]$/.test( key ) ) {
55
66
  return;
@@ -58,13 +69,9 @@ export const SizeInput = ( {
58
69
  event.preventDefault();
59
70
 
60
71
  const newChar = key.toLowerCase();
61
- const updatedBuffer = ( unitInputBufferRef.current + newChar ).slice( -3 );
62
- unitInputBufferRef.current = updatedBuffer;
72
+ const updatedBuffer = appendKey( newChar );
63
73
 
64
- const matchedUnit =
65
- units.find( ( u ) => ! RESTRICTED_KEYBOARD_SHORTCUT_UNITS.includes( u ) && u.includes( updatedBuffer ) ) ||
66
- units.find( ( u ) => ! RESTRICTED_KEYBOARD_SHORTCUT_UNITS.includes( u ) && u.startsWith( newChar ) ) ||
67
- units.find( ( u ) => ! RESTRICTED_KEYBOARD_SHORTCUT_UNITS.includes( u ) && u.includes( newChar ) );
74
+ const matchedUnit = units.find( ( u ) => startsWith( u, updatedBuffer ) );
68
75
 
69
76
  if ( matchedUnit ) {
70
77
  handleUnitChange( matchedUnit );
@@ -118,7 +125,7 @@ export const SizeInput = ( {
118
125
  type={ inputType }
119
126
  value={ inputValue }
120
127
  onChange={ handleSizeChange }
121
- onKeyUp={ handleKeyUp }
128
+ onKeyDown={ handleKeyDown }
122
129
  onBlur={ onBlur }
123
130
  InputProps={ InputProps }
124
131
  inputProps={ { min, step: 'any', 'aria-label': ariaLabel } }
@@ -29,6 +29,12 @@ export const TextFieldPopover = ( props: Props ) => {
29
29
  }
30
30
  }, [ popupState.isOpen ] );
31
31
 
32
+ const handleKeyPress = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
33
+ if ( event.key.toLowerCase() === 'enter' ) {
34
+ handleClose();
35
+ }
36
+ };
37
+
32
38
  const handleClose = () => {
33
39
  restoreValue();
34
40
  popupState.close();
@@ -58,6 +64,7 @@ export const TextFieldPopover = ( props: Props ) => {
58
64
  <TextField
59
65
  value={ value }
60
66
  onChange={ onChange }
67
+ onKeyPress={ handleKeyPress }
61
68
  size="tiny"
62
69
  type="text"
63
70
  fullWidth
@@ -25,7 +25,14 @@ const RATIO_OPTIONS = [
25
25
  const CUSTOM_RATIO = 'custom';
26
26
 
27
27
  export const AspectRatioControl = createControl( ( { label }: { label: string } ) => {
28
- const { value: aspectRatioValue, setValue: setAspectRatioValue, disabled } = useBoundProp( stringPropTypeUtil );
28
+ const {
29
+ value: currentPropValue,
30
+ setValue: setAspectRatioValue,
31
+ disabled,
32
+ placeholder: externalPlaceholder,
33
+ } = useBoundProp( stringPropTypeUtil );
34
+
35
+ const aspectRatioValue = currentPropValue ?? externalPlaceholder;
29
36
 
30
37
  const isCustomSelected =
31
38
  aspectRatioValue && ! RATIO_OPTIONS.some( ( option ) => option.value === aspectRatioValue );
@@ -87,6 +94,9 @@ export const AspectRatioControl = createControl( ( { label }: { label: string }
87
94
  }
88
95
  };
89
96
 
97
+ const lookup = currentPropValue ?? externalPlaceholder;
98
+ const selectedOption = RATIO_OPTIONS.find( ( option ) => option.value === lookup );
99
+
90
100
  return (
91
101
  <ControlActions>
92
102
  <Stack direction="column" gap={ 2 }>
@@ -102,6 +112,7 @@ export const AspectRatioControl = createControl( ( { label }: { label: string }
102
112
  disabled={ disabled }
103
113
  value={ selectedValue }
104
114
  onChange={ handleSelectChange }
115
+ renderValue={ isCustomSelected ? undefined : () => selectedOption?.label }
105
116
  fullWidth
106
117
  >
107
118
  { [ ...RATIO_OPTIONS, { label: __( 'Custom', 'elementor' ), value: CUSTOM_RATIO } ].map(
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { numberPropTypeUtil } from '@elementor/editor-props';
2
+ import { numberPropTypeUtil, type PropType } from '@elementor/editor-props';
3
3
  import { InputAdornment } from '@elementor/ui';
4
4
 
5
5
  import { useBoundProp } from '../bound-prop-context';
@@ -10,6 +10,13 @@ import { createControl } from '../create-control';
10
10
  const isEmptyOrNaN = ( value?: string | number | null ) =>
11
11
  value === null || value === undefined || value === '' || Number.isNaN( Number( value ) );
12
12
 
13
+ const renderSuffix = ( propType: PropType ) => {
14
+ if ( propType.meta?.suffix ) {
15
+ return <InputAdornment position="end">{ propType.meta.suffix }</InputAdornment>;
16
+ }
17
+ return <></>;
18
+ };
19
+
13
20
  export const NumberControl = createControl(
14
21
  ( {
15
22
  placeholder: labelPlaceholder,
@@ -26,7 +33,7 @@ export const NumberControl = createControl(
26
33
  shouldForceInt?: boolean;
27
34
  startIcon?: React.ReactNode;
28
35
  } ) => {
29
- const { value, setValue, placeholder, disabled, restoreValue } = useBoundProp( numberPropTypeUtil );
36
+ const { value, setValue, placeholder, disabled, restoreValue, propType } = useBoundProp( numberPropTypeUtil );
30
37
 
31
38
  const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
32
39
  const {
@@ -68,6 +75,7 @@ export const NumberControl = createControl(
68
75
  { startIcon }
69
76
  </InputAdornment>
70
77
  ) : undefined,
78
+ endAdornment: renderSuffix( propType ),
71
79
  } }
72
80
  />
73
81
  </ControlActions>
@@ -17,7 +17,7 @@ import { useUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
17
17
  const TILE_SIZE = 8;
18
18
  const TILE_WHITE = 'transparent';
19
19
  const TILE_BLACK = '#c1c1c1';
20
- const TILES_GRADIENT_FORMULA = `linear-gradient(45deg, ${ TILE_BLACK } 25%, ${ TILE_WHITE } 0, ${ TILE_WHITE } 75%, ${ TILE_BLACK } 0, ${ TILE_BLACK })`;
20
+ export const TILES_GRADIENT_FORMULA = `linear-gradient(45deg, ${ TILE_BLACK } 25%, ${ TILE_WHITE } 0, ${ TILE_WHITE } 75%, ${ TILE_BLACK } 0, ${ TILE_BLACK })`;
21
21
 
22
22
  const StyledCard = styled( Card )`
23
23
  background-color: white;
@@ -13,7 +13,6 @@ import { EditItemPopover } from '../../components/control-repeater/items/edit-it
13
13
  import { RepeaterHeader } from '../../components/repeater/repeater-header';
14
14
  import { ControlAdornments } from '../../control-adornments/control-adornments';
15
15
  import { createControl } from '../../create-control';
16
- import { useElementCanHaveChildren } from '../../hooks/use-element-can-have-children';
17
16
  import { initialRotateValue, initialScaleValue, initialSkewValue, initialTransformValue } from './initial-values';
18
17
  import { TransformContent } from './transform-content';
19
18
  import { TransformIcon } from './transform-icon';
@@ -22,25 +21,26 @@ import { TransformSettingsControl } from './transform-settings-control';
22
21
 
23
22
  const SIZE = 'tiny';
24
23
 
25
- export const TransformRepeaterControl = createControl( () => {
26
- const context = useBoundProp( transformPropTypeUtil );
27
- const headerRef = useRef< HTMLDivElement >( null );
28
- const popupState = usePopupState( { variant: 'popover' } );
29
- const showChildrenPerspective = useElementCanHaveChildren();
24
+ export const TransformRepeaterControl = createControl(
25
+ ( { showChildrenPerspective }: { showChildrenPerspective: boolean } ) => {
26
+ const context = useBoundProp( transformPropTypeUtil );
27
+ const headerRef = useRef< HTMLDivElement >( null );
28
+ const popupState = usePopupState( { variant: 'popover' } );
30
29
 
31
- return (
32
- <PropProvider { ...context }>
33
- <TransformSettingsControl
34
- popupState={ popupState }
35
- anchorRef={ headerRef }
36
- showChildrenPerspective={ showChildrenPerspective }
37
- />
38
- <PropKeyProvider bind={ 'transform-functions' }>
39
- <Repeater headerRef={ headerRef } propType={ context.propType } popupState={ popupState } />
40
- </PropKeyProvider>
41
- </PropProvider>
42
- );
43
- } );
30
+ return (
31
+ <PropProvider { ...context }>
32
+ <TransformSettingsControl
33
+ popupState={ popupState }
34
+ anchorRef={ headerRef }
35
+ showChildrenPerspective={ showChildrenPerspective }
36
+ />
37
+ <PropKeyProvider bind={ 'transform-functions' }>
38
+ <Repeater headerRef={ headerRef } propType={ context.propType } popupState={ popupState } />
39
+ </PropKeyProvider>
40
+ </PropProvider>
41
+ );
42
+ }
43
+ );
44
44
 
45
45
  const ToolTip = (
46
46
  <Box
@@ -1,5 +1,5 @@
1
1
  import { type KeyValuePropValue, type SizePropValue } from '@elementor/editor-props';
2
- import { isVersionGreaterOrEqual } from '@elementor/utils';
2
+ import { hasProInstalled, isVersionGreaterOrEqual } from '@elementor/utils';
3
3
  import { __ } from '@wordpress/i18n';
4
4
 
5
5
  export type TransitionProperty = {
@@ -47,9 +47,7 @@ const getIsSiteRtl = () => {
47
47
 
48
48
  // TODO: Remove this after version 4.01 is released
49
49
  const shouldExtendTransitionProperties = (): boolean => {
50
- const hasProInstalled = !! window.elementorPro;
51
-
52
- if ( ! hasProInstalled ) {
50
+ if ( ! hasProInstalled() ) {
53
51
  return false;
54
52
  }
55
53
 
@@ -0,0 +1,102 @@
1
+ import * as React from 'react';
2
+ import { videoSrcPropTypeUtil } from '@elementor/editor-props';
3
+ import { UploadIcon } from '@elementor/icons';
4
+ import { Button, Card, CardMedia, CardOverlay, CircularProgress, Stack } from '@elementor/ui';
5
+ import { useWpMediaAttachment, useWpMediaFrame } from '@elementor/wp-media';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { useBoundProp } from '../bound-prop-context';
9
+ import ControlActions from '../control-actions/control-actions';
10
+ import { createControl } from '../create-control';
11
+ import { TILES_GRADIENT_FORMULA } from './svg-media-control';
12
+
13
+ const PLACEHOLDER_IMAGE = window.elementorCommon?.config?.urls?.assets + '/shapes/play-triangle.svg';
14
+
15
+ export const VideoMediaControl = createControl( () => {
16
+ const { value, setValue } = useBoundProp( videoSrcPropTypeUtil );
17
+ const { id, url } = value ?? {};
18
+
19
+ const { data: attachment, isFetching } = useWpMediaAttachment( id?.value || null );
20
+ const videoUrl = attachment?.url ?? url?.value ?? null;
21
+
22
+ const { open } = useWpMediaFrame( {
23
+ mediaTypes: [ 'video' ],
24
+ multiple: false,
25
+ selected: id?.value || null,
26
+ onSelect: ( selectedAttachment ) => {
27
+ setValue( {
28
+ id: {
29
+ $$type: 'video-attachment-id',
30
+ value: selectedAttachment.id,
31
+ },
32
+ url: null,
33
+ } );
34
+ },
35
+ } );
36
+
37
+ return (
38
+ <ControlActions>
39
+ <Card variant="outlined">
40
+ <CardMedia
41
+ sx={ {
42
+ height: 140,
43
+ backgroundColor: 'white',
44
+ backgroundSize: '8px 8px',
45
+ backgroundPosition: '0 0, 4px 4px',
46
+ backgroundRepeat: 'repeat',
47
+ backgroundImage: `${ TILES_GRADIENT_FORMULA }, ${ TILES_GRADIENT_FORMULA }`,
48
+ display: 'flex',
49
+ justifyContent: 'center',
50
+ alignItems: 'center',
51
+ } }
52
+ >
53
+ <VideoPreview isFetching={ isFetching } videoUrl={ videoUrl } />
54
+ </CardMedia>
55
+ <CardOverlay>
56
+ <Stack gap={ 1 }>
57
+ <Button
58
+ size="tiny"
59
+ color="inherit"
60
+ variant="outlined"
61
+ onClick={ () => open( { mode: 'browse' } ) }
62
+ >
63
+ { __( 'Select video', 'elementor' ) }
64
+ </Button>
65
+ <Button
66
+ size="tiny"
67
+ variant="text"
68
+ color="inherit"
69
+ startIcon={ <UploadIcon /> }
70
+ onClick={ () => open( { mode: 'upload' } ) }
71
+ >
72
+ { __( 'Upload', 'elementor' ) }
73
+ </Button>
74
+ </Stack>
75
+ </CardOverlay>
76
+ </Card>
77
+ </ControlActions>
78
+ );
79
+ } );
80
+
81
+ const VideoPreview = ( { isFetching = false, videoUrl }: { isFetching?: boolean; videoUrl?: string } ) => {
82
+ if ( isFetching ) {
83
+ return <CircularProgress />;
84
+ }
85
+
86
+ if ( videoUrl ) {
87
+ return (
88
+ <video
89
+ src={ videoUrl }
90
+ muted
91
+ preload="metadata"
92
+ style={ {
93
+ width: '100%',
94
+ height: '100%',
95
+ objectFit: 'cover',
96
+ pointerEvents: 'none',
97
+ } }
98
+ />
99
+ );
100
+ }
101
+ return <img src={ PLACEHOLDER_IMAGE } alt="No video selected" />;
102
+ };
@@ -0,0 +1,52 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export type UseTypingBufferOptions = {
4
+ limit?: number;
5
+ timeout?: number;
6
+ };
7
+
8
+ export function useTypingBuffer( options: UseTypingBufferOptions = {} ) {
9
+ const { limit = 3, timeout = 600 } = options;
10
+
11
+ const inputBufferRef = useRef( '' );
12
+ const timeoutRef = useRef< ReturnType< typeof setTimeout > | null >( null );
13
+
14
+ const appendKey = ( key: string ) => {
15
+ inputBufferRef.current = ( inputBufferRef.current + key ).slice( -limit );
16
+
17
+ if ( timeoutRef.current ) {
18
+ clearTimeout( timeoutRef.current );
19
+ }
20
+
21
+ timeoutRef.current = setTimeout( () => {
22
+ inputBufferRef.current = '';
23
+ timeoutRef.current = null;
24
+ }, timeout );
25
+
26
+ return inputBufferRef.current;
27
+ };
28
+
29
+ const startsWith = ( haystack: string, needle: string ) => {
30
+ // At least 2 characters in needle for longer haystack.
31
+ if ( 3 < haystack.length && 2 > needle.length ) {
32
+ return false;
33
+ }
34
+ return haystack.startsWith( needle );
35
+ };
36
+
37
+ useEffect( () => {
38
+ return () => {
39
+ inputBufferRef.current = '';
40
+ if ( timeoutRef.current ) {
41
+ clearTimeout( timeoutRef.current );
42
+ timeoutRef.current = null;
43
+ }
44
+ };
45
+ }, [] );
46
+
47
+ return {
48
+ buffer: inputBufferRef.current,
49
+ appendKey,
50
+ startsWith,
51
+ };
52
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export { QueryControl } from './controls/query-control';
23
23
  export { GapControl } from './controls/gap-control';
24
24
  export { AspectRatioControl } from './controls/aspect-ratio-control';
25
25
  export { SvgMediaControl } from './controls/svg-media-control';
26
+ export { VideoMediaControl } from './controls/video-media-control';
26
27
  export { BackgroundControl } from './controls/background-control/background-control';
27
28
  export { SwitchControl } from './controls/switch-control';
28
29
  export { RepeatableControl } from './controls/repeatable-control';
@@ -66,7 +67,14 @@ export type { ControlActionsItems } from './control-actions/control-actions-cont
66
67
  export type { AdornmentComponent } from './control-adornments/control-adornments-context';
67
68
  export type { PropProviderProps } from './bound-prop-context';
68
69
  export type { SetValue, SetValueMeta } from './bound-prop-context/prop-context';
69
- export type { ExtendedOption, Unit, LengthUnit, AngleUnit, TimeUnit } from './utils/size-control';
70
+ export {
71
+ isUnitExtendedOption,
72
+ type ExtendedOption,
73
+ type Unit,
74
+ type LengthUnit,
75
+ type AngleUnit,
76
+ type TimeUnit,
77
+ } from './utils/size-control';
70
78
  export type { ToggleControlProps } from './controls/toggle-control';
71
79
  export type { FontCategory } from './controls/font-family-control/font-family-control';
72
80
  export type { InlineEditorToolbarProps } from './components/inline-editor-toolbar';
@@ -96,5 +104,5 @@ export {
96
104
 
97
105
  // hooks
98
106
  export { useSyncExternalState } from './hooks/use-sync-external-state';
99
- export { useElementCanHaveChildren } from './hooks/use-element-can-have-children';
100
107
  export { useFontFamilies } from './hooks/use-font-families';
108
+ export { useTypingBuffer, type UseTypingBufferOptions } from './hooks/use-typing-buffer';
@@ -1,17 +0,0 @@
1
- import { useMemo } from 'react';
2
- import { getContainer, useSelectedElement } from '@elementor/editor-elements';
3
-
4
- export const useElementCanHaveChildren = (): boolean => {
5
- const { element } = useSelectedElement();
6
- const elementId = element?.id || '';
7
-
8
- return useMemo( () => {
9
- const container = getContainer( elementId );
10
-
11
- if ( ! container ) {
12
- return false;
13
- }
14
-
15
- return container.model.get( 'elType' ) !== 'widget';
16
- }, [ elementId ] );
17
- };