@elementor/editor-controls 1.5.0 → 3.32.0-21
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 +0 -22
- package/dist/index.d.mts +95 -25
- package/dist/index.d.ts +95 -25
- package/dist/index.js +2045 -1041
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1962 -964
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -18
- package/src/components/control-toggle-button-group.tsx +78 -14
- package/src/components/floating-bar.tsx +45 -0
- package/src/components/{font-family-selector.tsx → item-selector.tsx} +62 -50
- package/src/components/repeater.tsx +1 -1
- package/src/components/restricted-link-infotip.tsx +76 -0
- package/src/components/size-control/size-input.tsx +8 -7
- package/src/components/size-control/text-field-inner-selection.tsx +60 -14
- package/src/components/text-field-popover.tsx +30 -7
- package/src/components/unstable-repeater/actions/add-item-action.tsx +50 -0
- package/src/components/unstable-repeater/actions/disable-item-action.tsx +39 -0
- package/src/components/unstable-repeater/actions/duplicate-item-action.tsx +32 -0
- package/src/components/unstable-repeater/actions/remove-item-action.tsx +27 -0
- package/src/components/unstable-repeater/context/repeater-context.tsx +137 -0
- package/src/components/unstable-repeater/header/header.tsx +23 -0
- package/src/components/unstable-repeater/index.ts +5 -0
- package/src/components/unstable-repeater/items/edit-item-popover.tsx +28 -0
- package/src/components/unstable-repeater/items/item.tsx +71 -0
- package/src/components/unstable-repeater/items/items-container.tsx +49 -0
- package/src/components/unstable-repeater/items/use-popover.tsx +26 -0
- package/src/{locations.ts → components/unstable-repeater/locations.ts} +9 -1
- package/src/components/unstable-repeater/types.ts +26 -0
- package/src/components/unstable-repeater/unstable-repeater.tsx +24 -0
- package/src/control-actions/control-actions.tsx +3 -20
- package/src/control-replacements.tsx +41 -0
- package/src/controls/background-control/background-control.tsx +1 -8
- package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +17 -16
- package/src/controls/equal-unequal-sizes-control.tsx +2 -9
- package/src/controls/filter-control/drop-shadow-item-content.tsx +4 -6
- package/src/controls/filter-control/drop-shadow-item-label.tsx +2 -2
- package/src/controls/filter-repeater-control.tsx +149 -110
- package/src/controls/font-family-control/font-family-control.tsx +22 -10
- package/src/controls/key-value-control.tsx +9 -6
- package/src/controls/link-control.tsx +8 -91
- package/src/controls/linked-dimensions-control.tsx +3 -16
- package/src/controls/number-control.tsx +10 -1
- package/src/controls/position-control.tsx +4 -16
- package/src/controls/repeatable-control.tsx +8 -5
- package/src/controls/select-control.tsx +7 -2
- package/src/controls/selection-size-control.tsx +74 -0
- package/src/controls/size-control.tsx +181 -126
- package/src/controls/stroke-control.tsx +2 -2
- package/src/controls/toggle-control.tsx +3 -2
- package/src/controls/transform-control/functions/axis-row.tsx +4 -2
- package/src/controls/transform-control/functions/move.tsx +2 -1
- package/src/controls/transform-control/functions/rotate.tsx +48 -0
- package/src/controls/transform-control/functions/scale-axis-row.tsx +32 -0
- package/src/controls/transform-control/functions/scale.tsx +45 -0
- package/src/controls/transform-control/functions/skew.tsx +43 -0
- package/src/controls/transform-control/transform-content.tsx +60 -23
- package/src/controls/transform-control/transform-icon.tsx +10 -2
- package/src/controls/transform-control/transform-label.tsx +39 -2
- package/src/controls/transform-control/transform-repeater-control.tsx +2 -10
- package/src/controls/transform-control/types.ts +58 -0
- package/src/controls/transform-control/use-transform-tabs-history.tsx +107 -0
- package/src/controls/transition-control/data.ts +34 -0
- package/src/controls/transition-control/transition-repeater-control.tsx +63 -0
- package/src/controls/transition-control/transition-selector.tsx +88 -0
- package/src/controls/unstable-transform-control/unstable-transform-repeater-control.tsx +35 -0
- package/src/hooks/use-filtered-items-list.ts +24 -0
- package/src/hooks/use-size-extended-options.ts +1 -6
- package/src/index.ts +13 -3
- package/src/utils/size-control.ts +12 -6
- package/src/hooks/use-filtered-font-families.ts +0 -24
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { forwardRef, useId } from 'react';
|
|
3
|
-
import { type PropValue } from '@elementor/editor-props';
|
|
3
|
+
import { type PropValue, sizePropTypeUtil } from '@elementor/editor-props';
|
|
4
4
|
import { MenuListItem } from '@elementor/editor-ui';
|
|
5
5
|
import {
|
|
6
6
|
bindMenu,
|
|
@@ -8,11 +8,15 @@ import {
|
|
|
8
8
|
Button,
|
|
9
9
|
InputAdornment,
|
|
10
10
|
Menu,
|
|
11
|
+
styled,
|
|
11
12
|
TextField,
|
|
12
13
|
type TextFieldProps,
|
|
13
14
|
usePopupState,
|
|
14
15
|
} from '@elementor/ui';
|
|
15
16
|
|
|
17
|
+
import { useBoundProp } from '../../bound-prop-context';
|
|
18
|
+
import { DEFAULT_UNIT } from '../../utils/size-control';
|
|
19
|
+
|
|
16
20
|
type TextFieldInnerSelectionProps = {
|
|
17
21
|
placeholder?: string;
|
|
18
22
|
type: string;
|
|
@@ -21,11 +25,11 @@ type TextFieldInnerSelectionProps = {
|
|
|
21
25
|
onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
|
22
26
|
onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
|
23
27
|
onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
|
24
|
-
shouldBlockInput?: boolean;
|
|
25
28
|
inputProps: TextFieldProps[ 'InputProps' ] & {
|
|
26
29
|
endAdornment: React.JSX.Element;
|
|
27
30
|
};
|
|
28
31
|
disabled?: boolean;
|
|
32
|
+
isPopoverOpen?: boolean;
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
export const TextFieldInnerSelection = forwardRef(
|
|
@@ -38,26 +42,33 @@ export const TextFieldInnerSelection = forwardRef(
|
|
|
38
42
|
onBlur,
|
|
39
43
|
onKeyDown,
|
|
40
44
|
onKeyUp,
|
|
41
|
-
shouldBlockInput = false,
|
|
42
45
|
inputProps,
|
|
43
46
|
disabled,
|
|
47
|
+
isPopoverOpen,
|
|
44
48
|
}: TextFieldInnerSelectionProps,
|
|
45
49
|
ref
|
|
46
50
|
) => {
|
|
51
|
+
const { placeholder: boundPropPlaceholder } = useBoundProp( sizePropTypeUtil );
|
|
52
|
+
|
|
53
|
+
const getCursorStyle = () => ( {
|
|
54
|
+
input: { cursor: inputProps.readOnly ? 'default !important' : undefined },
|
|
55
|
+
} );
|
|
56
|
+
|
|
47
57
|
return (
|
|
48
58
|
<TextField
|
|
49
59
|
ref={ ref }
|
|
50
|
-
sx={
|
|
60
|
+
sx={ getCursorStyle() }
|
|
51
61
|
size="tiny"
|
|
52
62
|
fullWidth
|
|
53
|
-
type={
|
|
63
|
+
type={ type }
|
|
54
64
|
value={ value }
|
|
55
|
-
onChange={
|
|
56
|
-
onKeyDown={
|
|
57
|
-
onKeyUp={
|
|
65
|
+
onChange={ onChange }
|
|
66
|
+
onKeyDown={ onKeyDown }
|
|
67
|
+
onKeyUp={ onKeyUp }
|
|
58
68
|
disabled={ disabled }
|
|
59
69
|
onBlur={ onBlur }
|
|
60
|
-
|
|
70
|
+
focused={ isPopoverOpen ? true : undefined }
|
|
71
|
+
placeholder={ placeholder ?? ( String( boundPropPlaceholder?.size ?? '' ) || undefined ) }
|
|
61
72
|
InputProps={ inputProps }
|
|
62
73
|
/>
|
|
63
74
|
);
|
|
@@ -91,17 +102,18 @@ export const SelectionEndAdornment = < T extends string >( {
|
|
|
91
102
|
popupState.close();
|
|
92
103
|
};
|
|
93
104
|
|
|
105
|
+
const { placeholder, showPrimaryColor } = useUnitPlaceholder( value );
|
|
106
|
+
|
|
94
107
|
return (
|
|
95
108
|
<InputAdornment position="end">
|
|
96
|
-
<
|
|
109
|
+
<StyledButton
|
|
110
|
+
isPrimaryColor={ showPrimaryColor }
|
|
97
111
|
size="small"
|
|
98
|
-
color="secondary"
|
|
99
112
|
disabled={ disabled }
|
|
100
|
-
sx={ { font: 'inherit', minWidth: 'initial', textTransform: 'uppercase' } }
|
|
101
113
|
{ ...bindTrigger( popupState ) }
|
|
102
114
|
>
|
|
103
|
-
{ alternativeOptionLabels[ value ] ?? value }
|
|
104
|
-
</
|
|
115
|
+
{ placeholder ?? alternativeOptionLabels[ value ] ?? value }
|
|
116
|
+
</StyledButton>
|
|
105
117
|
|
|
106
118
|
<Menu MenuListProps={ { dense: true } } { ...bindMenu( popupState ) }>
|
|
107
119
|
{ options.map( ( option, index ) => (
|
|
@@ -117,3 +129,37 @@ export const SelectionEndAdornment = < T extends string >( {
|
|
|
117
129
|
</InputAdornment>
|
|
118
130
|
);
|
|
119
131
|
};
|
|
132
|
+
|
|
133
|
+
function useUnitPlaceholder( value: string ) {
|
|
134
|
+
const { value: externalValue, placeholder } = useBoundProp( sizePropTypeUtil );
|
|
135
|
+
const size = externalValue?.size;
|
|
136
|
+
const unit = externalValue?.unit;
|
|
137
|
+
|
|
138
|
+
const isCustomUnitWithSize = value === 'custom' && Boolean( size );
|
|
139
|
+
const isAutoUnit = value === 'auto';
|
|
140
|
+
const showPrimaryColor = isAutoUnit || isCustomUnitWithSize || Boolean( size );
|
|
141
|
+
|
|
142
|
+
if ( ! placeholder ) {
|
|
143
|
+
return {
|
|
144
|
+
placeholder: null,
|
|
145
|
+
showPrimaryColor,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const isMissingUnit = ! unit;
|
|
150
|
+
const showPlaceholder = isMissingUnit && value === DEFAULT_UNIT;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
placeholder: showPlaceholder ? placeholder.unit : undefined,
|
|
154
|
+
showPrimaryColor,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const StyledButton = styled( Button, {
|
|
159
|
+
shouldForwardProp: ( prop ) => prop !== 'isPrimaryColor',
|
|
160
|
+
} )( ( { isPrimaryColor, theme } ) => ( {
|
|
161
|
+
color: isPrimaryColor ? theme.palette.text.primary : theme.palette.text.tertiary,
|
|
162
|
+
font: 'inherit',
|
|
163
|
+
minWidth: 'initial',
|
|
164
|
+
textTransform: 'uppercase',
|
|
165
|
+
} ) );
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { type RefObject } from 'react';
|
|
2
|
+
import { type RefObject, useEffect, useRef } from 'react';
|
|
3
|
+
import { PopoverHeader } from '@elementor/editor-ui';
|
|
4
|
+
import { MathFunctionIcon } from '@elementor/icons';
|
|
3
5
|
import { bindPopover, Popover, type PopupState, TextField } from '@elementor/ui';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
4
7
|
|
|
5
8
|
type Props = {
|
|
6
9
|
popupState: PopupState;
|
|
@@ -10,8 +13,26 @@ type Props = {
|
|
|
10
13
|
onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
|
|
11
14
|
};
|
|
12
15
|
|
|
16
|
+
const SIZE = 'tiny';
|
|
17
|
+
|
|
13
18
|
export const TextFieldPopover = ( props: Props ) => {
|
|
14
19
|
const { popupState, restoreValue, anchorRef, value, onChange } = props;
|
|
20
|
+
const inputRef = useRef< HTMLInputElement >( null );
|
|
21
|
+
|
|
22
|
+
useEffect( () => {
|
|
23
|
+
if ( popupState.isOpen ) {
|
|
24
|
+
requestAnimationFrame( () => {
|
|
25
|
+
if ( inputRef.current ) {
|
|
26
|
+
inputRef.current.focus();
|
|
27
|
+
}
|
|
28
|
+
} );
|
|
29
|
+
}
|
|
30
|
+
}, [ popupState.isOpen ] );
|
|
31
|
+
|
|
32
|
+
const handleClose = () => {
|
|
33
|
+
restoreValue();
|
|
34
|
+
popupState.close();
|
|
35
|
+
};
|
|
15
36
|
|
|
16
37
|
return (
|
|
17
38
|
<Popover
|
|
@@ -21,18 +42,19 @@ export const TextFieldPopover = ( props: Props ) => {
|
|
|
21
42
|
sx: {
|
|
22
43
|
borderRadius: 2,
|
|
23
44
|
width: anchorRef.current?.offsetWidth + 'px',
|
|
24
|
-
p: 1.5,
|
|
25
45
|
},
|
|
26
46
|
},
|
|
27
47
|
} }
|
|
28
48
|
{ ...bindPopover( popupState ) }
|
|
29
49
|
anchorOrigin={ { vertical: 'bottom', horizontal: 'center' } }
|
|
30
50
|
transformOrigin={ { vertical: 'top', horizontal: 'center' } }
|
|
31
|
-
onClose={
|
|
32
|
-
restoreValue();
|
|
33
|
-
popupState.close();
|
|
34
|
-
} }
|
|
51
|
+
onClose={ handleClose }
|
|
35
52
|
>
|
|
53
|
+
<PopoverHeader
|
|
54
|
+
title={ __( 'CSS function', 'elementor' ) }
|
|
55
|
+
onClose={ handleClose }
|
|
56
|
+
icon={ <MathFunctionIcon fontSize={ SIZE } /> }
|
|
57
|
+
/>
|
|
36
58
|
<TextField
|
|
37
59
|
value={ value }
|
|
38
60
|
onChange={ onChange }
|
|
@@ -40,8 +62,9 @@ export const TextFieldPopover = ( props: Props ) => {
|
|
|
40
62
|
type="text"
|
|
41
63
|
fullWidth
|
|
42
64
|
inputProps={ {
|
|
43
|
-
|
|
65
|
+
ref: inputRef,
|
|
44
66
|
} }
|
|
67
|
+
sx={ { pt: 0, pr: 1.5, pb: 1.5, pl: 1.5 } }
|
|
45
68
|
/>
|
|
46
69
|
</Popover>
|
|
47
70
|
);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { PlusIcon } from '@elementor/icons';
|
|
3
|
+
import { IconButton, Tooltip } from '@elementor/ui';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { useRepeaterContext } from '../context/repeater-context';
|
|
7
|
+
|
|
8
|
+
const SIZE = 'tiny';
|
|
9
|
+
|
|
10
|
+
export const AddItemAction = ( {
|
|
11
|
+
disabled = false,
|
|
12
|
+
tooltip = false,
|
|
13
|
+
tooltipContent = null,
|
|
14
|
+
newItemIndex,
|
|
15
|
+
}: {
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
tooltip?: boolean;
|
|
18
|
+
tooltipContent?: React.ReactNode;
|
|
19
|
+
newItemIndex?: number;
|
|
20
|
+
} ) => {
|
|
21
|
+
const { addItem } = useRepeaterContext();
|
|
22
|
+
const shouldShowTooltip = tooltip && tooltipContent;
|
|
23
|
+
|
|
24
|
+
const onClick = () => addItem( { index: newItemIndex } );
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<ConditionalToolTip content={ tooltipContent } shouldShowTooltip={ !! shouldShowTooltip }>
|
|
28
|
+
<IconButton
|
|
29
|
+
size={ SIZE }
|
|
30
|
+
sx={ { ml: 'auto' } }
|
|
31
|
+
disabled={ disabled }
|
|
32
|
+
onClick={ onClick }
|
|
33
|
+
aria-label={ __( 'Add item', 'elementor' ) }
|
|
34
|
+
>
|
|
35
|
+
<PlusIcon fontSize={ SIZE } />
|
|
36
|
+
</IconButton>
|
|
37
|
+
</ConditionalToolTip>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const ConditionalToolTip = ( {
|
|
42
|
+
children,
|
|
43
|
+
content,
|
|
44
|
+
shouldShowTooltip,
|
|
45
|
+
}: React.PropsWithChildren< {
|
|
46
|
+
content: React.ReactNode;
|
|
47
|
+
shouldShowTooltip: boolean;
|
|
48
|
+
} > ) => {
|
|
49
|
+
return shouldShowTooltip ? <Tooltip title={ content }>{ children }</Tooltip> : children;
|
|
50
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { EyeIcon, EyeOffIcon } from '@elementor/icons';
|
|
3
|
+
import { IconButton, Tooltip } from '@elementor/ui';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { useRepeaterContext } from '../context/repeater-context';
|
|
7
|
+
const SIZE = 'tiny';
|
|
8
|
+
|
|
9
|
+
export const DisableItemAction = ( { index = -1 }: { index?: number } ) => {
|
|
10
|
+
const { items, updateItem } = useRepeaterContext();
|
|
11
|
+
|
|
12
|
+
if ( index === -1 ) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const propDisabled = items[ index ]?.disabled ?? false;
|
|
17
|
+
|
|
18
|
+
const toggleLabel = propDisabled ? __( 'Show', 'elementor' ) : __( 'Hide', 'elementor' );
|
|
19
|
+
|
|
20
|
+
const onClick = () => {
|
|
21
|
+
const self = structuredClone( items[ index ] );
|
|
22
|
+
|
|
23
|
+
self.disabled = ! self.disabled;
|
|
24
|
+
|
|
25
|
+
if ( ! self.disabled ) {
|
|
26
|
+
delete self.disabled;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
updateItem( self, index );
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Tooltip title={ toggleLabel } placement="top">
|
|
34
|
+
<IconButton size={ SIZE } onClick={ onClick } aria-label={ toggleLabel }>
|
|
35
|
+
{ propDisabled ? <EyeOffIcon fontSize={ SIZE } /> : <EyeIcon fontSize={ SIZE } /> }
|
|
36
|
+
</IconButton>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { CopyIcon } from '@elementor/icons';
|
|
3
|
+
import { IconButton, Tooltip } from '@elementor/ui';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { useRepeaterContext } from '../context/repeater-context';
|
|
7
|
+
|
|
8
|
+
const SIZE = 'tiny';
|
|
9
|
+
|
|
10
|
+
export const DuplicateItemAction = ( { index = -1 }: { index?: number } ) => {
|
|
11
|
+
const { items, addItem } = useRepeaterContext();
|
|
12
|
+
|
|
13
|
+
if ( index === -1 ) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const duplicateLabel = __( 'Duplicate', 'elementor' );
|
|
18
|
+
|
|
19
|
+
const onClick = () => {
|
|
20
|
+
const newItem = structuredClone( items[ index ] );
|
|
21
|
+
|
|
22
|
+
addItem( { item: newItem, index: index + 1 } );
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Tooltip title={ duplicateLabel } placement="top">
|
|
27
|
+
<IconButton size={ SIZE } onClick={ onClick } aria-label={ duplicateLabel }>
|
|
28
|
+
<CopyIcon fontSize={ SIZE } />
|
|
29
|
+
</IconButton>
|
|
30
|
+
</Tooltip>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { XIcon } from '@elementor/icons';
|
|
3
|
+
import { IconButton, Tooltip } from '@elementor/ui';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { useRepeaterContext } from '../context/repeater-context';
|
|
7
|
+
|
|
8
|
+
const SIZE = 'tiny';
|
|
9
|
+
|
|
10
|
+
export const RemoveItemAction = ( { index = -1 }: { index?: number } ) => {
|
|
11
|
+
const { removeItem } = useRepeaterContext();
|
|
12
|
+
|
|
13
|
+
if ( index === -1 ) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const removeLabel = __( 'Remove', 'elementor' );
|
|
18
|
+
|
|
19
|
+
const onClick = () => removeItem( index );
|
|
20
|
+
return (
|
|
21
|
+
<Tooltip title={ removeLabel } placement="top">
|
|
22
|
+
<IconButton size={ SIZE } onClick={ onClick } aria-label={ removeLabel }>
|
|
23
|
+
<XIcon fontSize={ SIZE } />
|
|
24
|
+
</IconButton>
|
|
25
|
+
</Tooltip>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createContext, useState } from 'react';
|
|
3
|
+
import { type PropTypeUtil, type PropValue } from '@elementor/editor-props';
|
|
4
|
+
|
|
5
|
+
import { useBoundProp } from '../../../bound-prop-context/use-bound-prop';
|
|
6
|
+
import { useSyncExternalState } from '../../../hooks/use-sync-external-state';
|
|
7
|
+
import { type Item } from '../types';
|
|
8
|
+
|
|
9
|
+
type SetterFn< T extends PropValue > = ( prevItems: T[] ) => T[];
|
|
10
|
+
|
|
11
|
+
type AddItem< T > = { item?: T; index?: number };
|
|
12
|
+
|
|
13
|
+
type RepeaterContextType< T extends PropValue > = {
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
openItem: number;
|
|
16
|
+
setOpenItem: ( key: number ) => void;
|
|
17
|
+
items: Item< T >[];
|
|
18
|
+
setItems: ( items: T[] | SetterFn< T > ) => void;
|
|
19
|
+
initial: T;
|
|
20
|
+
uniqueKeys: number[];
|
|
21
|
+
setUniqueKeys: ( keys: number[] ) => void;
|
|
22
|
+
isSortable: boolean;
|
|
23
|
+
generateNextKey: ( source: number[] ) => number;
|
|
24
|
+
addItem: ( config?: AddItem< T > ) => void;
|
|
25
|
+
updateItem: ( item: T, index: number ) => void;
|
|
26
|
+
removeItem: ( index: number ) => void;
|
|
27
|
+
sortItemsByKeys: ( newKeysOrder: number[] ) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const RepeaterContext = createContext< RepeaterContextType< PropValue > | null >( null );
|
|
31
|
+
|
|
32
|
+
const EMPTY_OPEN_ITEM = -1;
|
|
33
|
+
|
|
34
|
+
export const useRepeaterContext = () => {
|
|
35
|
+
const context = React.useContext( RepeaterContext );
|
|
36
|
+
|
|
37
|
+
if ( ! context ) {
|
|
38
|
+
throw new Error( 'useRepeaterContext must be used within a RepeaterContextProvider' );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
isOpen: context.isOpen,
|
|
43
|
+
openItem: context.openItem,
|
|
44
|
+
setOpenItem: context.setOpenItem,
|
|
45
|
+
items: context.items,
|
|
46
|
+
setItems: context.setItems,
|
|
47
|
+
uniqueKeys: context.uniqueKeys,
|
|
48
|
+
setUniqueKeys: context.setUniqueKeys,
|
|
49
|
+
initial: context.initial,
|
|
50
|
+
isSortable: context.isSortable,
|
|
51
|
+
generateNextKey: context.generateNextKey,
|
|
52
|
+
sortItemsByKeys: context.sortItemsByKeys,
|
|
53
|
+
addItem: context.addItem,
|
|
54
|
+
updateItem: context.updateItem,
|
|
55
|
+
removeItem: context.removeItem,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const RepeaterContextProvider = < T extends PropValue = PropValue >( {
|
|
60
|
+
children,
|
|
61
|
+
initial,
|
|
62
|
+
propTypeUtil,
|
|
63
|
+
isSortable = true,
|
|
64
|
+
}: React.PropsWithChildren< { initial: T; propTypeUtil: PropTypeUtil< string, T[] >; isSortable?: boolean } > ) => {
|
|
65
|
+
const { value: repeaterValues, setValue: setRepeaterValues } = useBoundProp( propTypeUtil );
|
|
66
|
+
|
|
67
|
+
const [ items, setItems ] = useSyncExternalState( {
|
|
68
|
+
external: repeaterValues,
|
|
69
|
+
fallback: () => [] as T[],
|
|
70
|
+
setExternal: setRepeaterValues,
|
|
71
|
+
persistWhen: () => true,
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
const [ openItem, setOpenItem ] = useState( EMPTY_OPEN_ITEM );
|
|
75
|
+
const [ uniqueKeys, setUniqueKeys ] = useState( items?.map( ( _, index ) => index ) ?? [] );
|
|
76
|
+
|
|
77
|
+
const isOpen = openItem !== EMPTY_OPEN_ITEM;
|
|
78
|
+
|
|
79
|
+
const sortItemsByKeys = ( keysOrder: number[] ) => {
|
|
80
|
+
setUniqueKeys( keysOrder );
|
|
81
|
+
setItems( ( prevItems ) =>
|
|
82
|
+
keysOrder.map( ( key ) => {
|
|
83
|
+
const index = uniqueKeys.indexOf( key );
|
|
84
|
+
|
|
85
|
+
return prevItems[ index ];
|
|
86
|
+
} )
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const addItem = ( config?: AddItem< T > ) => {
|
|
91
|
+
const item = config?.item ?? initial;
|
|
92
|
+
const index = config?.index ?? items.length;
|
|
93
|
+
const newItems = [ ...items ];
|
|
94
|
+
|
|
95
|
+
newItems.splice( index, 0, item );
|
|
96
|
+
setItems( newItems );
|
|
97
|
+
setUniqueKeys( newItems.map( ( _, i ) => i ) );
|
|
98
|
+
|
|
99
|
+
setOpenItem( index );
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const removeItem = ( index: number ) => {
|
|
103
|
+
setItems( ( prevItems ) => prevItems.filter( ( _, pos ) => pos !== index ) );
|
|
104
|
+
setUniqueKeys( ( prevKeys ) => prevKeys.slice( 0, -1 ) );
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const updateItem = ( updatedItem: T, index: number ) => {
|
|
108
|
+
setItems( ( prevItems ) => prevItems.map( ( item, pos ) => ( pos === index ? updatedItem : item ) ) );
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<RepeaterContext.Provider
|
|
113
|
+
value={ {
|
|
114
|
+
isOpen,
|
|
115
|
+
openItem,
|
|
116
|
+
setOpenItem,
|
|
117
|
+
items: ( items ?? [] ) as Item< T >[],
|
|
118
|
+
setItems: setItems as ( items: PropValue[] | SetterFn< PropValue > ) => void,
|
|
119
|
+
initial,
|
|
120
|
+
uniqueKeys,
|
|
121
|
+
setUniqueKeys,
|
|
122
|
+
isSortable,
|
|
123
|
+
generateNextKey,
|
|
124
|
+
sortItemsByKeys,
|
|
125
|
+
addItem: addItem as ( config?: AddItem< PropValue > ) => void,
|
|
126
|
+
updateItem: updateItem as ( item: PropValue, index: number ) => void,
|
|
127
|
+
removeItem,
|
|
128
|
+
} }
|
|
129
|
+
>
|
|
130
|
+
{ children }
|
|
131
|
+
</RepeaterContext.Provider>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const generateNextKey = ( source: number[] ) => {
|
|
136
|
+
return 1 + Math.max( 0, ...source );
|
|
137
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Stack, Typography } from '@elementor/ui';
|
|
3
|
+
|
|
4
|
+
import { useBoundProp } from '../../../bound-prop-context/use-bound-prop';
|
|
5
|
+
import { ControlAdornments } from '../../../control-adornments/control-adornments';
|
|
6
|
+
import { SlotChildren } from '../../../control-replacements';
|
|
7
|
+
import { AddItemAction } from '../actions/add-item-action';
|
|
8
|
+
import { RepeaterHeaderActionsSlot } from '../locations';
|
|
9
|
+
|
|
10
|
+
export const Header = ( { label, children }: React.PropsWithChildren< { label: string } > ) => {
|
|
11
|
+
const { value } = useBoundProp();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Stack direction="row" justifyContent="start" alignItems="center" gap={ 1 } sx={ { marginInlineEnd: -0.75 } }>
|
|
15
|
+
<Typography component="label" variant="caption" color="text.secondary">
|
|
16
|
+
{ label }
|
|
17
|
+
</Typography>
|
|
18
|
+
<RepeaterHeaderActionsSlot value={ value } />
|
|
19
|
+
<SlotChildren whitelist={ [ AddItemAction ] as React.FC[] }>{ children }</SlotChildren>
|
|
20
|
+
<ControlAdornments />
|
|
21
|
+
</Stack>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Box, Popover, type PopoverProps } from '@elementor/ui';
|
|
3
|
+
|
|
4
|
+
type AddItemPopoverProps = {
|
|
5
|
+
anchorRef: HTMLElement | null;
|
|
6
|
+
setAnchorEl: ( el: HTMLElement | null ) => void;
|
|
7
|
+
popoverProps: Partial< PopoverProps >;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const EditItemPopover = ( { children, anchorRef, setAnchorEl, popoverProps }: AddItemPopoverProps ) => {
|
|
12
|
+
return (
|
|
13
|
+
<Popover
|
|
14
|
+
disablePortal
|
|
15
|
+
slotProps={ {
|
|
16
|
+
paper: {
|
|
17
|
+
ref: setAnchorEl,
|
|
18
|
+
sx: { mt: 0.5, width: anchorRef?.offsetWidth },
|
|
19
|
+
},
|
|
20
|
+
} }
|
|
21
|
+
anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
|
|
22
|
+
anchorEl={ anchorRef }
|
|
23
|
+
{ ...popoverProps }
|
|
24
|
+
>
|
|
25
|
+
<Box>{ children }</Box>
|
|
26
|
+
</Popover>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { type PropValue } from '@elementor/editor-props';
|
|
4
|
+
import { bindTrigger, UnstableTag } from '@elementor/ui';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
import { SlotChildren } from '../../../control-replacements';
|
|
8
|
+
import { DisableItemAction } from '../actions/disable-item-action';
|
|
9
|
+
import { DuplicateItemAction } from '../actions/duplicate-item-action';
|
|
10
|
+
import { RemoveItemAction } from '../actions/remove-item-action';
|
|
11
|
+
import { RepeaterItemActionsSlot, RepeaterItemIconSlot, RepeaterItemLabelSlot } from '../locations';
|
|
12
|
+
import { type ItemProps } from '../types';
|
|
13
|
+
import { EditItemPopover } from './edit-item-popover';
|
|
14
|
+
import { usePopover } from './use-popover';
|
|
15
|
+
|
|
16
|
+
type AnchorEl = HTMLElement | null;
|
|
17
|
+
|
|
18
|
+
export const Item = < T extends PropValue >( {
|
|
19
|
+
Label,
|
|
20
|
+
Icon,
|
|
21
|
+
Content,
|
|
22
|
+
key,
|
|
23
|
+
value,
|
|
24
|
+
index,
|
|
25
|
+
openOnMount,
|
|
26
|
+
children,
|
|
27
|
+
}: React.PropsWithChildren< ItemProps< T > > ) => {
|
|
28
|
+
const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
|
|
29
|
+
const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount as boolean, () => {} );
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<UnstableTag
|
|
34
|
+
key={ key }
|
|
35
|
+
disabled={ false }
|
|
36
|
+
label={
|
|
37
|
+
<RepeaterItemLabelSlot value={ value }>
|
|
38
|
+
<Label value={ value as T } />
|
|
39
|
+
</RepeaterItemLabelSlot>
|
|
40
|
+
}
|
|
41
|
+
showActionsOnHover
|
|
42
|
+
fullWidth
|
|
43
|
+
ref={ setRef }
|
|
44
|
+
variant="outlined"
|
|
45
|
+
aria-label={ __( 'Open item', 'elementor' ) }
|
|
46
|
+
{ ...bindTrigger( popoverState ) }
|
|
47
|
+
startIcon={
|
|
48
|
+
<RepeaterItemIconSlot value={ value }>
|
|
49
|
+
<Icon value={ value as T } />
|
|
50
|
+
</RepeaterItemIconSlot>
|
|
51
|
+
}
|
|
52
|
+
actions={
|
|
53
|
+
<>
|
|
54
|
+
<RepeaterItemActionsSlot index={ index ?? -1 } />
|
|
55
|
+
|
|
56
|
+
<SlotChildren
|
|
57
|
+
whitelist={ [ DuplicateItemAction, DisableItemAction, RemoveItemAction ] as React.FC[] }
|
|
58
|
+
props={ { index } }
|
|
59
|
+
sorted
|
|
60
|
+
>
|
|
61
|
+
{ children }
|
|
62
|
+
</SlotChildren>
|
|
63
|
+
</>
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
<EditItemPopover anchorRef={ ref } setAnchorEl={ setAnchorEl } popoverProps={ popoverProps }>
|
|
67
|
+
<Content anchorEl={ anchorEl } bind={ String( index ) } value={ value as T } />
|
|
68
|
+
</EditItemPopover>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type PropValue } from '@elementor/editor-props';
|
|
3
|
+
|
|
4
|
+
import { SortableItem, SortableProvider } from '../../sortable';
|
|
5
|
+
import { useRepeaterContext } from '../context/repeater-context';
|
|
6
|
+
import { type ItemProps } from '../types';
|
|
7
|
+
|
|
8
|
+
export const ItemsContainer = < T extends PropValue >( {
|
|
9
|
+
itemTemplate,
|
|
10
|
+
children,
|
|
11
|
+
}: React.PropsWithChildren< { itemTemplate: React.ReactNode } > ) => {
|
|
12
|
+
const { items, uniqueKeys, openItem, isSortable, sortItemsByKeys } = useRepeaterContext();
|
|
13
|
+
|
|
14
|
+
if ( ! itemTemplate ) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const onChangeOrder = ( newOrder: number[] ) => {
|
|
19
|
+
sortItemsByKeys( newOrder );
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<SortableProvider value={ uniqueKeys } onChange={ onChangeOrder }>
|
|
25
|
+
{ uniqueKeys?.map( ( key: number, index: number ) => {
|
|
26
|
+
const value = items?.[ index ] as T;
|
|
27
|
+
|
|
28
|
+
if ( ! value ) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<SortableItem id={ key } key={ `sortable-${ key }` } disabled={ ! isSortable }>
|
|
34
|
+
{ React.isValidElement< React.PropsWithChildren< ItemProps< T > > >( itemTemplate )
|
|
35
|
+
? React.cloneElement( itemTemplate, {
|
|
36
|
+
key,
|
|
37
|
+
value,
|
|
38
|
+
index,
|
|
39
|
+
openOnMount: key === openItem,
|
|
40
|
+
children,
|
|
41
|
+
} )
|
|
42
|
+
: null }
|
|
43
|
+
</SortableItem>
|
|
44
|
+
);
|
|
45
|
+
} ) }
|
|
46
|
+
</SortableProvider>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
};
|