@elementor/editor-controls 0.12.0 → 0.13.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/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.12.0",
4
+ "version": "0.13.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -16,7 +16,9 @@ import {
16
16
  } from '@elementor/ui';
17
17
  import { __ } from '@wordpress/i18n';
18
18
 
19
+ import { useSyncExternalState } from '../hooks/use-sync-external-state';
19
20
  import { SectionContent } from './section-content';
21
+ import { SortableItem, SortableProvider } from './sortable';
20
22
 
21
23
  const SIZE = 'tiny';
22
24
 
@@ -50,32 +52,61 @@ export const Repeater = < T, >( {
50
52
  values: repeaterValues = [],
51
53
  setValues: setRepeaterValues,
52
54
  }: RepeaterProps< Item< T > > ) => {
55
+ const [ items, setItems ] = useSyncExternalState( {
56
+ external: repeaterValues,
57
+ // @ts-expect-error - as long as persistWhen => true, value will never be null
58
+ setExternal: setRepeaterValues,
59
+ persistWhen: () => true,
60
+ } );
61
+
62
+ const [ uniqueKeys, setUniqueKeys ] = useState( items.map( ( _, index ) => index ) );
63
+
64
+ const generateNextKey = ( source: number[] ) => {
65
+ return 1 + Math.max( 0, ...source );
66
+ };
67
+
53
68
  const addRepeaterItem = () => {
54
69
  const newItem = structuredClone( itemSettings.initialValues );
70
+ const newKey = generateNextKey( uniqueKeys );
55
71
 
56
72
  if ( addToBottom ) {
57
- return setRepeaterValues( [ ...repeaterValues, newItem ] );
73
+ setItems( [ ...items, newItem ] );
74
+ setUniqueKeys( [ ...uniqueKeys, newKey ] );
75
+ } else {
76
+ setItems( [ newItem, ...items ] );
77
+ setUniqueKeys( [ newKey, ...uniqueKeys ] );
58
78
  }
59
-
60
- setRepeaterValues( [ newItem, ...repeaterValues ] );
61
79
  };
62
80
 
63
81
  const duplicateRepeaterItem = ( index: number ) => {
64
- setRepeaterValues( [
65
- ...repeaterValues.slice( 0, index ),
66
- structuredClone( repeaterValues[ index ] ),
67
- ...repeaterValues.slice( index ),
68
- ] );
82
+ const newItem = structuredClone( items[ index ] );
83
+ const newKey = generateNextKey( uniqueKeys );
84
+
85
+ // Insert the new (cloned item) at the next spot (after the current index)
86
+ const atPosition = 1 + index;
87
+
88
+ setItems( [ ...items.slice( 0, atPosition ), newItem, ...items.slice( atPosition ) ] );
89
+ setUniqueKeys( [ ...uniqueKeys.slice( 0, atPosition ), newKey, ...uniqueKeys.slice( atPosition ) ] );
69
90
  };
70
91
 
71
92
  const removeRepeaterItem = ( index: number ) => {
72
- setRepeaterValues( repeaterValues.filter( ( _, i ) => i !== index ) );
93
+ setUniqueKeys(
94
+ uniqueKeys.filter( ( _, pos ) => {
95
+ return pos !== index;
96
+ } )
97
+ );
98
+
99
+ setItems(
100
+ items.filter( ( _, pos ) => {
101
+ return pos !== index;
102
+ } )
103
+ );
73
104
  };
74
105
 
75
106
  const toggleDisableRepeaterItem = ( index: number ) => {
76
- setRepeaterValues(
77
- repeaterValues.map( ( value, i ) => {
78
- if ( i === index ) {
107
+ setItems(
108
+ items.map( ( value, pos ) => {
109
+ if ( pos === index ) {
79
110
  const { disabled, ...rest } = value;
80
111
 
81
112
  // If the items should not be disabled, remove the disabled property.
@@ -87,6 +118,16 @@ export const Repeater = < T, >( {
87
118
  );
88
119
  };
89
120
 
121
+ const onChangeOrder = ( reorderedKeys: number[] ) => {
122
+ setUniqueKeys( reorderedKeys );
123
+ setItems( ( prevItems ) => {
124
+ return reorderedKeys.map( ( keyValue ) => {
125
+ const index = uniqueKeys.indexOf( keyValue );
126
+ return prevItems[ index ];
127
+ } );
128
+ } );
129
+ };
130
+
90
131
  return (
91
132
  <SectionContent>
92
133
  <Stack direction="row" justifyContent="space-between" alignItems="center">
@@ -97,22 +138,35 @@ export const Repeater = < T, >( {
97
138
  <PlusIcon fontSize={ SIZE } />
98
139
  </IconButton>
99
140
  </Stack>
100
- <Stack gap={ 1 } sx={ { '&:empty': { display: 'none' } } }>
101
- { repeaterValues.map( ( value, index ) => (
102
- <RepeaterItem
103
- key={ index }
104
- bind={ String( index ) }
105
- disabled={ value.disabled }
106
- label={ <itemSettings.Label value={ value } /> }
107
- startIcon={ <itemSettings.Icon value={ value } /> }
108
- removeItem={ () => removeRepeaterItem( index ) }
109
- duplicateItem={ () => duplicateRepeaterItem( index ) }
110
- toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
111
- >
112
- { ( props ) => <itemSettings.Content { ...props } value={ value } bind={ String( index ) } /> }
113
- </RepeaterItem>
114
- ) ) }
115
- </Stack>
141
+ { 0 < uniqueKeys.length && (
142
+ <SortableProvider value={ uniqueKeys } onChange={ onChangeOrder }>
143
+ { uniqueKeys.map( ( key, index ) => {
144
+ const value = items[ index ];
145
+
146
+ if ( ! value ) {
147
+ return null;
148
+ }
149
+
150
+ return (
151
+ <SortableItem id={ key } key={ `sortable-${ key }` }>
152
+ <RepeaterItem
153
+ bind={ String( index ) }
154
+ disabled={ value?.disabled }
155
+ label={ <itemSettings.Label value={ value } /> }
156
+ startIcon={ <itemSettings.Icon value={ value } /> }
157
+ removeItem={ () => removeRepeaterItem( index ) }
158
+ duplicateItem={ () => duplicateRepeaterItem( index ) }
159
+ toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
160
+ >
161
+ { ( props ) => (
162
+ <itemSettings.Content { ...props } value={ value } bind={ String( index ) } />
163
+ ) }
164
+ </RepeaterItem>
165
+ </SortableItem>
166
+ );
167
+ } ) }
168
+ </SortableProvider>
169
+ ) }
116
170
  </SectionContent>
117
171
  );
118
172
  };
@@ -151,6 +205,7 @@ const RepeaterItem = ( {
151
205
  <UnstableTag
152
206
  label={ label }
153
207
  showActionsOnHover
208
+ fullWidth
154
209
  ref={ controlRef }
155
210
  variant="outlined"
156
211
  aria-label={ __( 'Open item', 'elementor' ) }
@@ -0,0 +1,108 @@
1
+ import * as React from 'react';
2
+ import { GripVerticalIcon } from '@elementor/icons';
3
+ import {
4
+ Divider,
5
+ List,
6
+ ListItem,
7
+ styled,
8
+ UnstableSortableItem,
9
+ type UnstableSortableItemProps,
10
+ type UnstableSortableItemRenderProps,
11
+ UnstableSortableProvider,
12
+ type UnstableSortableProviderProps,
13
+ } from '@elementor/ui';
14
+
15
+ export const SortableProvider = < T extends number >( props: UnstableSortableProviderProps< T > ) => {
16
+ return (
17
+ <List sx={ { p: 0, m: 0 } }>
18
+ <UnstableSortableProvider
19
+ restrictAxis={ true }
20
+ disableDragOverlay={ false }
21
+ variant={ 'static' }
22
+ { ...props }
23
+ />
24
+ </List>
25
+ );
26
+ };
27
+
28
+ type SortableItemProps = {
29
+ id: UnstableSortableItemProps[ 'id' ];
30
+ children: React.ReactNode;
31
+ };
32
+
33
+ export const SortableItem = ( { id, children }: SortableItemProps ): React.ReactNode => {
34
+ return (
35
+ <UnstableSortableItem
36
+ id={ id }
37
+ render={ ( {
38
+ itemProps,
39
+ triggerProps,
40
+ itemStyle,
41
+ triggerStyle,
42
+ isDragOverlay,
43
+ showDropIndication,
44
+ dropIndicationStyle,
45
+ }: UnstableSortableItemRenderProps ) => {
46
+ return (
47
+ <StyledListItem
48
+ { ...itemProps }
49
+ style={ itemStyle }
50
+ sx={ { backgroundColor: isDragOverlay ? 'background.paper' : undefined } }
51
+ >
52
+ <SortableTrigger { ...triggerProps } style={ triggerStyle } />
53
+ { children }
54
+ { showDropIndication && <StyledDivider style={ dropIndicationStyle } /> }
55
+ </StyledListItem>
56
+ );
57
+ } }
58
+ />
59
+ );
60
+ };
61
+
62
+ const StyledListItem = styled( ListItem )`
63
+ position: relative;
64
+ margin-inline: 0px;
65
+ padding-inline: 0px;
66
+ padding-block: ${ ( { theme } ) => theme.spacing( 0.5 ) };
67
+
68
+ & .class-item-sortable-trigger {
69
+ color: ${ ( { theme } ) => theme.palette.action.active };
70
+ height: 100%;
71
+ display: flex;
72
+ align-items: center;
73
+ visibility: hidden;
74
+ position: absolute;
75
+ top: 50%;
76
+ padding-inline-end: ${ ( { theme } ) => theme.spacing( 0.5 ) };
77
+ transform: translate( -75%, -50% );
78
+ }
79
+
80
+ &:hover {
81
+ & .class-item-sortable-trigger {
82
+ visibility: visible;
83
+ }
84
+ }
85
+ `;
86
+
87
+ const SortableTrigger = ( props: React.HTMLAttributes< HTMLDivElement > ) => (
88
+ <div { ...props } role="button" className="class-item-sortable-trigger">
89
+ <GripVerticalIcon fontSize="tiny" />
90
+ </div>
91
+ );
92
+
93
+ const StyledDivider = styled( Divider )`
94
+ height: 0px;
95
+ border: none;
96
+ overflow: visible;
97
+
98
+ &:after {
99
+ --height: 2px;
100
+ content: '';
101
+ display: block;
102
+ width: 100%;
103
+ height: var( --height );
104
+ margin-block: calc( -1 * var( --height ) / 2 );
105
+ border-radius: ${ ( { theme } ) => theme.spacing( 0.5 ) };
106
+ background-color: ${ ( { theme } ) => theme.palette.text.primary };
107
+ }
108
+ `;
@@ -23,7 +23,7 @@ export const TextAreaControl = createControl( ( { placeholder }: Props ) => {
23
23
  size="tiny"
24
24
  multiline
25
25
  fullWidth
26
- rows={ 5 }
26
+ minRows={ 5 }
27
27
  value={ value ?? '' }
28
28
  onChange={ handleChange }
29
29
  placeholder={ placeholder }