@elementor/editor-editing-panel 1.6.0 → 1.8.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/CHANGELOG.md +46 -0
- package/dist/index.js +652 -401
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +665 -409
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -6
- package/src/components/add-or-remove-content.tsx +2 -2
- package/src/components/css-class-selector.tsx +198 -51
- package/src/components/editable-field.tsx +158 -0
- package/src/components/editing-panel.tsx +17 -14
- package/src/components/multi-combobox.tsx +184 -0
- package/src/components/settings-tab.tsx +28 -25
- package/src/components/style-sections/border-section/border-field.tsx +14 -17
- package/src/components/style-sections/position-section/position-field.tsx +1 -0
- package/src/components/style-tab.tsx +32 -29
- package/src/controls-registry/create-top-level-object-type.ts +14 -0
- package/src/controls-registry/settings-field.tsx +12 -14
- package/src/controls-registry/styles-field.tsx +17 -5
- package/src/css-classes.ts +15 -7
- package/src/dynamics/components/dynamic-selection-control.tsx +1 -1
- package/src/dynamics/components/dynamic-selection.tsx +3 -4
- package/src/dynamics/dynamic-control.tsx +16 -11
- package/src/dynamics/hooks/use-dynamic-tag.ts +2 -3
- package/src/dynamics/hooks/use-prop-dynamic-action.tsx +1 -4
- package/src/dynamics/hooks/use-prop-dynamic-tags.ts +3 -6
- package/src/dynamics/utils.ts +1 -1
- package/src/hooks/use-styles-fields.ts +1 -0
- package/src/hooks/use-unapply-class.ts +25 -0
- package/src/components/multi-combobox/index.ts +0 -3
- package/src/components/multi-combobox/multi-combobox.tsx +0 -122
- package/src/components/multi-combobox/types.ts +0 -29
- package/src/components/multi-combobox/use-combobox-actions.ts +0 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-editing-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Elementor Team",
|
|
6
6
|
"homepage": "https://elementor.com/",
|
|
@@ -40,17 +40,18 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@elementor/editor": "0.17.2",
|
|
43
|
-
"@elementor/editor-controls": "0.
|
|
44
|
-
"@elementor/editor-elements": "0.
|
|
43
|
+
"@elementor/editor-controls": "0.6.0",
|
|
44
|
+
"@elementor/editor-elements": "0.4.1",
|
|
45
45
|
"@elementor/menus": "0.1.2",
|
|
46
|
-
"@elementor/editor-props": "0.
|
|
46
|
+
"@elementor/editor-props": "0.7.0",
|
|
47
47
|
"@elementor/editor-panels": "0.10.2",
|
|
48
48
|
"@elementor/editor-responsive": "0.12.4",
|
|
49
|
-
"@elementor/editor-styles": "0.
|
|
50
|
-
"@elementor/editor-styles-repository": "0.
|
|
49
|
+
"@elementor/editor-styles": "0.5.1",
|
|
50
|
+
"@elementor/editor-styles-repository": "0.4.0",
|
|
51
51
|
"@elementor/editor-v1-adapters": "0.8.5",
|
|
52
52
|
"@elementor/icons": "^1.20.0",
|
|
53
53
|
"@elementor/schema": "0.1.2",
|
|
54
|
+
"@elementor/session": "0.1.0",
|
|
54
55
|
"@elementor/ui": "^1.22.0",
|
|
55
56
|
"@elementor/utils": "0.3.0",
|
|
56
57
|
"@wordpress/i18n": "^5.13.0"
|
|
@@ -25,11 +25,11 @@ export const AddOrRemoveContent = ( { isAdded, label, onAdd, onRemove, children
|
|
|
25
25
|
>
|
|
26
26
|
<ControlLabel>{ label }</ControlLabel>
|
|
27
27
|
{ isAdded ? (
|
|
28
|
-
<IconButton size={ SIZE } onClick={ onRemove }>
|
|
28
|
+
<IconButton size={ SIZE } onClick={ onRemove } aria-label="Remove">
|
|
29
29
|
<MinusIcon fontSize={ SIZE } />
|
|
30
30
|
</IconButton>
|
|
31
31
|
) : (
|
|
32
|
-
<IconButton size={ SIZE } onClick={ onAdd }>
|
|
32
|
+
<IconButton size={ SIZE } onClick={ onAdd } aria-label="Add">
|
|
33
33
|
<PlusIcon fontSize={ SIZE } />
|
|
34
34
|
</IconButton>
|
|
35
35
|
) }
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { useId, useRef } from 'react';
|
|
3
|
-
import { updateSettings, useElementSetting } from '@elementor/editor-elements';
|
|
3
|
+
import { getElementSetting, updateSettings, useElementSetting } from '@elementor/editor-elements';
|
|
4
4
|
import { classesPropTypeUtil, type ClassesPropValue } from '@elementor/editor-props';
|
|
5
5
|
import { type StyleDefinitionID } from '@elementor/editor-styles';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ELEMENTS_STYLES_PROVIDER_KEY,
|
|
8
|
+
stylesRepository,
|
|
9
|
+
type UpdateActionPayload,
|
|
10
|
+
useAllStylesByProvider,
|
|
11
|
+
useCreateActionsByProvider,
|
|
12
|
+
} from '@elementor/editor-styles-repository';
|
|
7
13
|
import { DotsVerticalIcon } from '@elementor/icons';
|
|
8
14
|
import {
|
|
9
15
|
type AutocompleteRenderGetTagProps,
|
|
@@ -23,18 +29,24 @@ import { useElement } from '../contexts/element-context';
|
|
|
23
29
|
import { useStyle } from '../contexts/style-context';
|
|
24
30
|
import { ConditionalTooltipWrapper } from './conditional-tooltip-wrapper';
|
|
25
31
|
import { CssClassMenu } from './css-class-menu';
|
|
26
|
-
import {
|
|
32
|
+
import { EditableField, EditableFieldProvider, useEditableField } from './editable-field';
|
|
33
|
+
import { type Action, MultiCombobox, type Option } from './multi-combobox';
|
|
27
34
|
|
|
28
35
|
const ID = 'elementor-css-class-selector';
|
|
29
36
|
const TAGS_LIMIT = 8;
|
|
30
37
|
|
|
38
|
+
type StyleDefOption = Option & {
|
|
39
|
+
color: 'primary' | 'global';
|
|
40
|
+
provider: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
31
43
|
const EMPTY_OPTION = {
|
|
32
44
|
label: __( 'local', 'elementor' ),
|
|
33
45
|
value: '',
|
|
34
46
|
fixed: true,
|
|
35
47
|
color: 'primary',
|
|
36
48
|
provider: ELEMENTS_STYLES_PROVIDER_KEY,
|
|
37
|
-
} satisfies
|
|
49
|
+
} satisfies StyleDefOption;
|
|
38
50
|
|
|
39
51
|
/**
|
|
40
52
|
* Applied - Classes applied to an element.
|
|
@@ -43,10 +55,12 @@ const EMPTY_OPTION = {
|
|
|
43
55
|
|
|
44
56
|
export function CssClassSelector() {
|
|
45
57
|
const options = useOptions();
|
|
46
|
-
const [ appliedIds, setAppliedIds ] = useAppliedClassesIds();
|
|
47
58
|
|
|
59
|
+
const { value: appliedIds, setValue: setAppliedIds, pushValue: pushAppliedId } = useAppliedClassesIds();
|
|
48
60
|
const { id: activeId, setId: setActiveId } = useStyle();
|
|
49
61
|
|
|
62
|
+
const actions = useCreateActions( { pushAppliedId, setActiveId } );
|
|
63
|
+
|
|
50
64
|
const handleApply = useHandleApply( appliedIds, setAppliedIds );
|
|
51
65
|
const handleActivate = ( { value }: Option ) => setActiveId( value );
|
|
52
66
|
|
|
@@ -65,23 +79,42 @@ export function CssClassSelector() {
|
|
|
65
79
|
selected={ applied }
|
|
66
80
|
onSelect={ handleApply }
|
|
67
81
|
limitTags={ TAGS_LIMIT }
|
|
68
|
-
|
|
82
|
+
actions={ actions }
|
|
83
|
+
getLimitTagsText={ ( more ) => (
|
|
84
|
+
<Chip size="tiny" variant="standard" label={ `+${ more }` } clickable />
|
|
85
|
+
) }
|
|
69
86
|
renderTags={ ( values, getTagProps ) =>
|
|
70
87
|
values.map( ( value, index ) => {
|
|
71
88
|
const chipProps = getTagProps( { index } );
|
|
72
89
|
const isActive = value.value === active?.value;
|
|
73
90
|
|
|
91
|
+
const renameLabel = ( newLabel: string ) => {
|
|
92
|
+
return updateClassByProvider( value.provider, { label: newLabel, id: value.value } );
|
|
93
|
+
};
|
|
94
|
+
|
|
74
95
|
return (
|
|
75
|
-
<
|
|
96
|
+
<EditableFieldProvider
|
|
76
97
|
key={ chipProps.key }
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
value={ value.label }
|
|
99
|
+
onSubmit={ renameLabel }
|
|
100
|
+
editable={ value.provider !== ELEMENTS_STYLES_PROVIDER_KEY }
|
|
101
|
+
validation={ ( newLabel ) =>
|
|
102
|
+
renameValidation(
|
|
103
|
+
newLabel,
|
|
104
|
+
options.filter( ( option ) => option.value !== value.value )
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
>
|
|
108
|
+
<CssClassItem
|
|
109
|
+
label={ value.label }
|
|
110
|
+
id={ value.value }
|
|
111
|
+
isActive={ isActive }
|
|
112
|
+
isGlobal={ value.color === 'global' }
|
|
113
|
+
color={ isActive && value.color ? value.color : 'default' }
|
|
114
|
+
chipProps={ chipProps }
|
|
115
|
+
onClickActive={ () => handleActivate( value ) }
|
|
116
|
+
/>
|
|
117
|
+
</EditableFieldProvider>
|
|
85
118
|
);
|
|
86
119
|
} )
|
|
87
120
|
}
|
|
@@ -100,70 +133,174 @@ type CssClassItemProps = {
|
|
|
100
133
|
onClickActive: ( id: string ) => void;
|
|
101
134
|
};
|
|
102
135
|
|
|
103
|
-
|
|
136
|
+
const CHIP_SIZE = 'tiny';
|
|
137
|
+
|
|
138
|
+
export function CssClassItem( {
|
|
139
|
+
id,
|
|
140
|
+
label,
|
|
141
|
+
isActive,
|
|
142
|
+
isGlobal,
|
|
143
|
+
color: colorProp,
|
|
144
|
+
chipProps,
|
|
145
|
+
onClickActive,
|
|
146
|
+
}: CssClassItemProps ) {
|
|
104
147
|
const { meta } = useStyle();
|
|
105
148
|
// TODO - resolve the useId issue with invalid characters upon CSS selectors (EDS-1089)
|
|
106
149
|
const popupId = useId().replace( /:/g, '_' );
|
|
107
150
|
const popupState = usePopupState( { variant: 'popover', popupId } );
|
|
108
151
|
const chipRef = useRef< Element >( null );
|
|
109
152
|
const { onDelete, ...chipGroupProps } = chipProps;
|
|
153
|
+
const { isEditing, openEditMode, error, submitting } = useEditableField();
|
|
154
|
+
|
|
155
|
+
const color = error ? 'error' : colorProp;
|
|
110
156
|
|
|
111
157
|
return (
|
|
112
158
|
<CssClassItemProvider styleId={ id } isActive={ isActive } isGlobal={ isGlobal }>
|
|
113
159
|
<UnstableChipGroup ref={ chipRef } { ...chipGroupProps } aria-label={ `Edit ${ label }` } role="group">
|
|
114
160
|
<Chip
|
|
115
|
-
|
|
116
|
-
size=
|
|
117
|
-
label={
|
|
161
|
+
disabled={ submitting }
|
|
162
|
+
size={ CHIP_SIZE }
|
|
163
|
+
label={
|
|
164
|
+
<EditableField
|
|
165
|
+
onDoubleClick={ () => {
|
|
166
|
+
if ( ! isActive ) {
|
|
167
|
+
openEditMode();
|
|
168
|
+
}
|
|
169
|
+
} }
|
|
170
|
+
onClick={ () => {
|
|
171
|
+
if ( isActive ) {
|
|
172
|
+
openEditMode();
|
|
173
|
+
}
|
|
174
|
+
} }
|
|
175
|
+
>
|
|
176
|
+
<ConditionalTooltipWrapper maxWidth="10ch" title={ label } />
|
|
177
|
+
</EditableField>
|
|
178
|
+
}
|
|
118
179
|
variant={ isActive && ! meta.state ? 'filled' : 'standard' }
|
|
119
180
|
color={ color }
|
|
120
181
|
onClick={ () => onClickActive( id ) }
|
|
121
182
|
aria-pressed={ isActive }
|
|
183
|
+
sx={ {
|
|
184
|
+
'&.Mui-focusVisible': {
|
|
185
|
+
boxShadow: 'none !important',
|
|
186
|
+
},
|
|
187
|
+
} }
|
|
122
188
|
/>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
189
|
+
{ ! isEditing && (
|
|
190
|
+
<Chip
|
|
191
|
+
disabled={ submitting }
|
|
192
|
+
size={ CHIP_SIZE }
|
|
193
|
+
label={
|
|
194
|
+
<Stack direction="row" gap={ 0.5 } alignItems="center">
|
|
195
|
+
{ isActive && meta.state && <Typography variant="inherit">{ meta.state }</Typography> }
|
|
196
|
+
<DotsVerticalIcon fontSize="inherit" />
|
|
197
|
+
</Stack>
|
|
198
|
+
}
|
|
199
|
+
variant="filled"
|
|
200
|
+
color={ color }
|
|
201
|
+
{ ...bindTrigger( popupState ) }
|
|
202
|
+
aria-label={ __( 'Open CSS Class Menu', 'elementor' ) }
|
|
203
|
+
/>
|
|
204
|
+
) }
|
|
137
205
|
</UnstableChipGroup>
|
|
138
206
|
<CssClassMenu popupState={ popupState } containerRef={ chipRef } />
|
|
139
207
|
</CssClassItemProvider>
|
|
140
208
|
);
|
|
141
209
|
}
|
|
142
210
|
|
|
211
|
+
const updateClassByProvider = ( provider: string, data: UpdateActionPayload ) => {
|
|
212
|
+
const providerInstance = stylesRepository.getProviderByKey( provider );
|
|
213
|
+
|
|
214
|
+
if ( ! providerInstance ) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return providerInstance.actions.update?.( data );
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const VALID_SELECTOR_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
222
|
+
|
|
223
|
+
const renameValidation = ( newLabel: string, options: Option[] ) => {
|
|
224
|
+
if ( isNameExist( newLabel, options ) ) {
|
|
225
|
+
return __( 'Existing name', 'elementor' );
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if ( isCharactersNotSupported( newLabel ) ) {
|
|
229
|
+
return __( 'Format is not valid', 'elementor' );
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const isNameExist = ( newLabel: string, options: Option[] ) => {
|
|
234
|
+
if ( ! options?.length ) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return options.some( ( option ) => option.label.toLowerCase() === newLabel.toLowerCase() );
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const isCharactersNotSupported = ( newLabel: string ) => ! VALID_SELECTOR_REGEX.test( newLabel );
|
|
242
|
+
|
|
143
243
|
function useOptions() {
|
|
144
244
|
const { element } = useElement();
|
|
145
245
|
|
|
146
|
-
return useAllStylesByProvider( { elementId: element.id } ).flatMap<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
246
|
+
return useAllStylesByProvider( { elementId: element.id } ).flatMap< StyleDefOption >(
|
|
247
|
+
( [ provider, styleDefs ] ) => {
|
|
248
|
+
const isElements = provider.key === ELEMENTS_STYLES_PROVIDER_KEY;
|
|
249
|
+
|
|
250
|
+
// Add empty local option for elements, as fallback.
|
|
251
|
+
if ( isElements && styleDefs.length === 0 ) {
|
|
252
|
+
return [ EMPTY_OPTION ];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return styleDefs.map( ( styleDef ) => {
|
|
256
|
+
return {
|
|
257
|
+
label: styleDef.label,
|
|
258
|
+
value: styleDef.id,
|
|
259
|
+
fixed: isElements,
|
|
260
|
+
color: isElements ? 'primary' : 'global',
|
|
261
|
+
provider: provider.key,
|
|
262
|
+
group: provider.labels?.plural,
|
|
263
|
+
};
|
|
264
|
+
} );
|
|
152
265
|
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
153
268
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
269
|
+
function useCreateActions( {
|
|
270
|
+
pushAppliedId,
|
|
271
|
+
setActiveId,
|
|
272
|
+
}: {
|
|
273
|
+
pushAppliedId: ( id: StyleDefinitionID ) => void;
|
|
274
|
+
setActiveId: ( id: StyleDefinitionID ) => void;
|
|
275
|
+
} ) {
|
|
276
|
+
return useCreateActionsByProvider().map( ( [ provider, create ] ): Action< StyleDefOption > => {
|
|
277
|
+
return {
|
|
278
|
+
// translators: %s is the label of the new class.
|
|
279
|
+
label: ( value ) => __( 'Create new "%s"', 'elementor' ).replace( '%s', value ),
|
|
280
|
+
apply: async ( value ) => {
|
|
281
|
+
const created = await create( { label: value } );
|
|
282
|
+
|
|
283
|
+
if ( ! created ) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
pushAppliedId( created.id );
|
|
288
|
+
setActiveId( created.id );
|
|
289
|
+
},
|
|
290
|
+
condition: ( options, inputValue ) => {
|
|
291
|
+
const isUniqueLabel = ! options.some(
|
|
292
|
+
( option ) => option.label.toLowerCase() === inputValue.toLowerCase()
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
return !! inputValue && isUniqueLabel;
|
|
296
|
+
},
|
|
297
|
+
// translators: %s is the singular label of css class provider (e.g "Global CSS Class").
|
|
298
|
+
group: __( 'Create New %s', 'elementor' ).replace( '%s', provider.labels?.singular ?? '' ),
|
|
299
|
+
};
|
|
163
300
|
} );
|
|
164
301
|
}
|
|
165
302
|
|
|
166
|
-
function useAppliedOptions( options:
|
|
303
|
+
function useAppliedOptions( options: StyleDefOption[], appliedIds: StyleDefinitionID[] ) {
|
|
167
304
|
const applied = options.filter( ( option ) => appliedIds.includes( option.value ) );
|
|
168
305
|
|
|
169
306
|
const hasElementsProviderStyleApplied = applied.some(
|
|
@@ -192,13 +329,23 @@ function useAppliedClassesIds() {
|
|
|
192
329
|
} );
|
|
193
330
|
};
|
|
194
331
|
|
|
195
|
-
|
|
332
|
+
const pushValue = ( id: StyleDefinitionID ) => {
|
|
333
|
+
const ids = getElementSetting< ClassesPropValue >( element.id, currentClassesProp )?.value || [];
|
|
334
|
+
|
|
335
|
+
setValue( [ ...ids, id ] );
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
value,
|
|
340
|
+
setValue,
|
|
341
|
+
pushValue,
|
|
342
|
+
};
|
|
196
343
|
}
|
|
197
344
|
|
|
198
345
|
function useHandleApply( appliedIds: StyleDefinitionID[], setAppliedIds: ( ids: StyleDefinitionID[] ) => void ) {
|
|
199
346
|
const { id: activeId, setId: setActiveId } = useStyle();
|
|
200
347
|
|
|
201
|
-
return ( selectedOptions:
|
|
348
|
+
return ( selectedOptions: StyleDefOption[] ) => {
|
|
202
349
|
const selectedValues = selectedOptions
|
|
203
350
|
.map( ( { value } ) => value )
|
|
204
351
|
.filter( ( value ) => value !== EMPTY_OPTION.value );
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type ComponentProps, createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Tooltip } from '@elementor/ui';
|
|
4
|
+
|
|
5
|
+
type EditableFieldContext = {
|
|
6
|
+
isEditing: boolean;
|
|
7
|
+
openEditMode: () => void;
|
|
8
|
+
closeEditMode: () => void;
|
|
9
|
+
onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
|
|
10
|
+
value: string;
|
|
11
|
+
error?: string | null;
|
|
12
|
+
submit: ( value: string ) => Promise< void >;
|
|
13
|
+
editable?: boolean;
|
|
14
|
+
submitting: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const Context = createContext< EditableFieldContext | null >( null );
|
|
18
|
+
|
|
19
|
+
export type EditableFieldProviderProps = React.PropsWithChildren< {
|
|
20
|
+
value: string;
|
|
21
|
+
onSubmit: ( value: string ) => unknown | Promise< unknown >;
|
|
22
|
+
validation?: ( value: string ) => string | undefined | null;
|
|
23
|
+
editable?: boolean;
|
|
24
|
+
} >;
|
|
25
|
+
|
|
26
|
+
export const EditableFieldProvider = ( {
|
|
27
|
+
children,
|
|
28
|
+
value,
|
|
29
|
+
onSubmit,
|
|
30
|
+
validation,
|
|
31
|
+
editable,
|
|
32
|
+
}: EditableFieldProviderProps ) => {
|
|
33
|
+
const [ isEditing, setIsEditing ] = useState( false );
|
|
34
|
+
const [ submitting, setSubmitting ] = useState( false );
|
|
35
|
+
const [ error, setError ] = useState< string | null | undefined >( null );
|
|
36
|
+
|
|
37
|
+
const openEditMode = () => {
|
|
38
|
+
setIsEditing( true );
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const closeEditMode = () => {
|
|
42
|
+
setError( null );
|
|
43
|
+
setIsEditing( false );
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const submit = async ( newValue: string ) => {
|
|
47
|
+
if ( ! error ) {
|
|
48
|
+
setSubmitting( true );
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await onSubmit( newValue );
|
|
52
|
+
} finally {
|
|
53
|
+
setSubmitting( false );
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
closeEditMode();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const onChange = ( event: React.ChangeEvent< HTMLSpanElement > ) => {
|
|
61
|
+
const { innerText: newValue } = event.target;
|
|
62
|
+
|
|
63
|
+
if ( validation ) {
|
|
64
|
+
setError( validation( newValue ) );
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Context.Provider
|
|
70
|
+
value={ {
|
|
71
|
+
isEditing,
|
|
72
|
+
openEditMode,
|
|
73
|
+
closeEditMode,
|
|
74
|
+
onChange,
|
|
75
|
+
value,
|
|
76
|
+
error,
|
|
77
|
+
submit,
|
|
78
|
+
editable,
|
|
79
|
+
submitting,
|
|
80
|
+
} }
|
|
81
|
+
>
|
|
82
|
+
{ children }
|
|
83
|
+
</Context.Provider>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type EditableFieldProps = ComponentProps< 'div' >;
|
|
88
|
+
|
|
89
|
+
export const EditableField = ( { children, ...props }: EditableFieldProps ) => {
|
|
90
|
+
const ref = useRef< HTMLElement >( null );
|
|
91
|
+
const { isEditing, closeEditMode, value, onChange, error, submit, editable } = useEditableField();
|
|
92
|
+
|
|
93
|
+
useEffect( () => {
|
|
94
|
+
if ( isEditing ) {
|
|
95
|
+
ref.current?.focus();
|
|
96
|
+
selectAll();
|
|
97
|
+
}
|
|
98
|
+
}, [ isEditing ] );
|
|
99
|
+
|
|
100
|
+
const handleKeyDown = ( event: React.KeyboardEvent ) => {
|
|
101
|
+
event.stopPropagation();
|
|
102
|
+
|
|
103
|
+
if ( [ 'Escape' ].includes( event.key ) ) {
|
|
104
|
+
return closeEditMode();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if ( [ 'Enter' ].includes( event.key ) ) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
return submit( ( event.target as HTMLElement ).innerText );
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const selectAll = () => {
|
|
114
|
+
const selection = getSelection();
|
|
115
|
+
|
|
116
|
+
if ( ! selection || ! ref.current ) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const range = document.createRange();
|
|
121
|
+
range.selectNodeContents( ref.current );
|
|
122
|
+
|
|
123
|
+
selection.removeAllRanges();
|
|
124
|
+
selection.addRange( range );
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if ( ! editable ) {
|
|
128
|
+
return children;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Tooltip open={ !! error } title={ error } placement="top">
|
|
133
|
+
{ /* eslint-disable-next-line jsx-a11y/no-static-element-interactions */ }
|
|
134
|
+
<div onKeyDown={ handleKeyDown } { ...props }>
|
|
135
|
+
<span
|
|
136
|
+
ref={ ref }
|
|
137
|
+
role="textbox"
|
|
138
|
+
onInput={ onChange }
|
|
139
|
+
contentEditable={ isEditing }
|
|
140
|
+
suppressContentEditableWarning
|
|
141
|
+
onBlur={ closeEditMode }
|
|
142
|
+
>
|
|
143
|
+
{ isEditing ? value : children }
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
</Tooltip>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const useEditableField = () => {
|
|
151
|
+
const contextValue = useContext( Context );
|
|
152
|
+
|
|
153
|
+
if ( ! contextValue ) {
|
|
154
|
+
throw new Error( 'useEditableField must be used within a EditableFieldProvider' );
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return contextValue;
|
|
158
|
+
};
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { ControlActionsProvider, ControlReplacementProvider } from '@elementor/editor-controls';
|
|
3
3
|
import { useSelectedElement } from '@elementor/editor-elements';
|
|
4
4
|
import { Panel, PanelBody, PanelHeader, PanelHeaderTitle } from '@elementor/editor-panels';
|
|
5
|
+
import { SessionStorageProvider } from '@elementor/session';
|
|
5
6
|
import { ErrorBoundary } from '@elementor/ui';
|
|
6
7
|
import { __ } from '@wordpress/i18n';
|
|
7
8
|
|
|
@@ -27,20 +28,22 @@ export const EditingPanel = () => {
|
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
30
|
<ErrorBoundary fallback={ <EditorPanelErrorFallback /> }>
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
<SessionStorageProvider prefix={ 'elementor' }>
|
|
32
|
+
<Panel>
|
|
33
|
+
<PanelHeader>
|
|
34
|
+
<PanelHeaderTitle>{ panelTitle }</PanelHeaderTitle>
|
|
35
|
+
</PanelHeader>
|
|
36
|
+
<PanelBody>
|
|
37
|
+
<ControlActionsProvider items={ menuItems }>
|
|
38
|
+
<ControlReplacementProvider { ...controlReplacement }>
|
|
39
|
+
<ElementProvider element={ element } elementType={ elementType }>
|
|
40
|
+
<EditingPanelTabs />
|
|
41
|
+
</ElementProvider>
|
|
42
|
+
</ControlReplacementProvider>
|
|
43
|
+
</ControlActionsProvider>
|
|
44
|
+
</PanelBody>
|
|
45
|
+
</Panel>
|
|
46
|
+
</SessionStorageProvider>
|
|
44
47
|
</ErrorBoundary>
|
|
45
48
|
);
|
|
46
49
|
};
|