@elementor/editor-floating-panels 4.3.0-970
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 +5 -0
- package/README.md +117 -0
- package/dist/index.d.mts +159 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +1108 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1062 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
- package/src/api.ts +36 -0
- package/src/components/external/floating-panel-body.tsx +15 -0
- package/src/components/external/floating-panel-footer.tsx +22 -0
- package/src/components/external/floating-panel-header.tsx +100 -0
- package/src/components/external/floating-panel.tsx +10 -0
- package/src/components/external/index.ts +4 -0
- package/src/components/internal/corner-resize-handle.tsx +69 -0
- package/src/components/internal/drag-handle.tsx +29 -0
- package/src/components/internal/host.tsx +86 -0
- package/src/components/internal/panel-window.tsx +100 -0
- package/src/components/internal/resize-handle.tsx +60 -0
- package/src/constants.ts +1 -0
- package/src/hooks/use-floating-panel-actions.ts +26 -0
- package/src/hooks/use-floating-panel-drag.ts +85 -0
- package/src/hooks/use-floating-panel-resize.ts +166 -0
- package/src/hooks/use-floating-panel-status.ts +17 -0
- package/src/hooks/use-floating-panel-z-index.ts +7 -0
- package/src/index.ts +15 -0
- package/src/init.ts +14 -0
- package/src/location.ts +3 -0
- package/src/persistence.ts +76 -0
- package/src/store/index.ts +2 -0
- package/src/store/selectors.ts +61 -0
- package/src/store/slice.ts +113 -0
- package/src/sync.ts +104 -0
- package/src/types.ts +64 -0
- package/src/utils/clamp.ts +3 -0
- package/src/utils/corner-position.ts +149 -0
- package/src/utils/direction.ts +3 -0
- package/src/utils/drag-math.ts +49 -0
- package/src/utils/resize-math.ts +107 -0
- package/src/utils/viewport-bounds.ts +7 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import { ThemeProvider } from '@elementor/editor-ui';
|
|
4
|
+
import { __useSelector as useSelector } from '@elementor/store';
|
|
5
|
+
import { Box, Fade, Paper } from '@elementor/ui';
|
|
6
|
+
|
|
7
|
+
import { usePanelResizeInteraction } from '../../hooks/use-floating-panel-resize';
|
|
8
|
+
import { type GlobalState, selectIsResizable } from '../../store/selectors';
|
|
9
|
+
import { type LogicalPosition, type LogicalSize } from '../../types';
|
|
10
|
+
import { type PanelCorner, positionToCssInsets } from '../../utils/corner-position';
|
|
11
|
+
import { type ResizeCorner, type ResizeEdge } from '../../utils/resize-math';
|
|
12
|
+
import CornerResizeHandle from './corner-resize-handle';
|
|
13
|
+
import ResizeHandle from './resize-handle';
|
|
14
|
+
|
|
15
|
+
const FADE_ENTER_MS = 225;
|
|
16
|
+
const FADE_EXIT_MS = 195;
|
|
17
|
+
|
|
18
|
+
const RESIZE_EDGES: ResizeEdge[] = [ 'inline-start', 'inline-end', 'block-start', 'block-end' ];
|
|
19
|
+
|
|
20
|
+
const RESIZE_CORNERS: ResizeCorner[] = [
|
|
21
|
+
'block-start-inline-start',
|
|
22
|
+
'block-start-inline-end',
|
|
23
|
+
'block-end-inline-start',
|
|
24
|
+
'block-end-inline-end',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
type Props = {
|
|
28
|
+
panelId: string;
|
|
29
|
+
corner: PanelCorner;
|
|
30
|
+
position: LogicalPosition;
|
|
31
|
+
size: LogicalSize;
|
|
32
|
+
title?: string;
|
|
33
|
+
zIndex: number;
|
|
34
|
+
visible: boolean;
|
|
35
|
+
onFocus: () => void;
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function PanelResizeHandles( { panelId }: { panelId: string } ) {
|
|
40
|
+
const { getResizeHandleProps } = usePanelResizeInteraction( panelId );
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
{ RESIZE_EDGES.map( ( edge ) => (
|
|
45
|
+
<ResizeHandle key={ edge } edge={ edge } { ...getResizeHandleProps( edge ) } />
|
|
46
|
+
) ) }
|
|
47
|
+
{ RESIZE_CORNERS.map( ( corner ) => (
|
|
48
|
+
<CornerResizeHandle key={ corner } corner={ corner } { ...getResizeHandleProps( corner ) } />
|
|
49
|
+
) ) }
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function PanelWindow( {
|
|
55
|
+
panelId,
|
|
56
|
+
corner,
|
|
57
|
+
position,
|
|
58
|
+
size,
|
|
59
|
+
title,
|
|
60
|
+
zIndex,
|
|
61
|
+
visible,
|
|
62
|
+
onFocus,
|
|
63
|
+
children,
|
|
64
|
+
}: Props ) {
|
|
65
|
+
const isResizable = useSelector( ( state: GlobalState ) => selectIsResizable( state, panelId ) );
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Fade in={ visible } timeout={ { enter: FADE_ENTER_MS, exit: FADE_EXIT_MS } }>
|
|
69
|
+
<Paper
|
|
70
|
+
component="aside"
|
|
71
|
+
data-floating-panel={ panelId }
|
|
72
|
+
elevation={ 0 }
|
|
73
|
+
aria-label={ title || panelId }
|
|
74
|
+
aria-hidden={ ! visible }
|
|
75
|
+
inert={ ! visible ? '' : undefined }
|
|
76
|
+
onMouseDown={ onFocus }
|
|
77
|
+
onFocusCapture={ onFocus }
|
|
78
|
+
sx={ {
|
|
79
|
+
position: 'fixed',
|
|
80
|
+
...positionToCssInsets( corner, position ),
|
|
81
|
+
inlineSize: `${ size.inlineSize }px`,
|
|
82
|
+
blockSize: `${ size.blockSize }px`,
|
|
83
|
+
zIndex,
|
|
84
|
+
display: 'flex',
|
|
85
|
+
flexDirection: 'column',
|
|
86
|
+
pointerEvents: visible ? 'auto' : 'none',
|
|
87
|
+
bgcolor: 'var(--e-a-bg-default)',
|
|
88
|
+
color: 'var(--e-a-color-txt)',
|
|
89
|
+
border: 'var(--e-a-border)',
|
|
90
|
+
boxShadow: `0 2px 20px 0 rgba(0, 0, 0, 0.1)`,
|
|
91
|
+
} }
|
|
92
|
+
>
|
|
93
|
+
<ThemeProvider>
|
|
94
|
+
<Box sx={ { display: 'flex', flexDirection: 'column', height: '100%' } }>{ children }</Box>
|
|
95
|
+
</ThemeProvider>
|
|
96
|
+
{ isResizable && <PanelResizeHandles panelId={ panelId } /> }
|
|
97
|
+
</Paper>
|
|
98
|
+
</Fade>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type PointerEventHandler } from 'react';
|
|
3
|
+
import { Box } from '@elementor/ui';
|
|
4
|
+
|
|
5
|
+
import { type ResizeEdge } from '../../utils/resize-math';
|
|
6
|
+
|
|
7
|
+
const HANDLE_THICKNESS_PX = 8;
|
|
8
|
+
|
|
9
|
+
const EDGE_SX: Record< ResizeEdge, object > = {
|
|
10
|
+
'inline-start': {
|
|
11
|
+
insetBlockStart: 0,
|
|
12
|
+
insetBlockEnd: 0,
|
|
13
|
+
insetInlineStart: 0,
|
|
14
|
+
inlineSize: `${ HANDLE_THICKNESS_PX }px`,
|
|
15
|
+
cursor: 'ew-resize',
|
|
16
|
+
},
|
|
17
|
+
'inline-end': {
|
|
18
|
+
insetBlockStart: 0,
|
|
19
|
+
insetBlockEnd: 0,
|
|
20
|
+
insetInlineEnd: 0,
|
|
21
|
+
inlineSize: `${ HANDLE_THICKNESS_PX }px`,
|
|
22
|
+
cursor: 'ew-resize',
|
|
23
|
+
},
|
|
24
|
+
'block-start': {
|
|
25
|
+
insetInlineStart: 0,
|
|
26
|
+
insetInlineEnd: 0,
|
|
27
|
+
insetBlockStart: 0,
|
|
28
|
+
blockSize: `${ HANDLE_THICKNESS_PX }px`,
|
|
29
|
+
cursor: 'ns-resize',
|
|
30
|
+
},
|
|
31
|
+
'block-end': {
|
|
32
|
+
insetInlineStart: 0,
|
|
33
|
+
insetInlineEnd: 0,
|
|
34
|
+
insetBlockEnd: 0,
|
|
35
|
+
blockSize: `${ HANDLE_THICKNESS_PX }px`,
|
|
36
|
+
cursor: 'ns-resize',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type Props = {
|
|
41
|
+
edge: ResizeEdge;
|
|
42
|
+
onPointerDown: PointerEventHandler< HTMLElement >;
|
|
43
|
+
onPointerMove: PointerEventHandler< HTMLElement >;
|
|
44
|
+
onPointerUp: PointerEventHandler< HTMLElement >;
|
|
45
|
+
onPointerCancel: PointerEventHandler< HTMLElement >;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default function ResizeHandle( { edge, onPointerDown, onPointerMove, onPointerUp, onPointerCancel }: Props ) {
|
|
49
|
+
return (
|
|
50
|
+
<Box
|
|
51
|
+
data-resize-edge={ edge }
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
onPointerDown={ onPointerDown }
|
|
54
|
+
onPointerMove={ onPointerMove }
|
|
55
|
+
onPointerUp={ onPointerUp }
|
|
56
|
+
onPointerCancel={ onPointerCancel }
|
|
57
|
+
sx={ { position: 'absolute', touchAction: 'none', zIndex: 1, ...EDGE_SX[ edge ] } }
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const FLOATING_PANEL_Z_INDEX_BASE = 1000;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { __useDispatch as useDispatch, __useSelector as useSelector } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { type GlobalState, selectIsOpen } from '../store/selectors';
|
|
4
|
+
import { slice } from '../store/slice';
|
|
5
|
+
import { type LogicalPosition, type LogicalSize } from '../types';
|
|
6
|
+
|
|
7
|
+
export function useFloatingPanelActions( id: string ) {
|
|
8
|
+
const dispatch = useDispatch();
|
|
9
|
+
const isOpen = useSelector( ( state: GlobalState ) => selectIsOpen( state, id ) );
|
|
10
|
+
|
|
11
|
+
const open = () => {
|
|
12
|
+
dispatch( slice.actions.open( id ) );
|
|
13
|
+
dispatch( slice.actions.bringToFront( id ) );
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const close = () => dispatch( slice.actions.close( id ) );
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
open,
|
|
20
|
+
close,
|
|
21
|
+
toggle: () => ( isOpen ? close() : open() ),
|
|
22
|
+
setPosition: ( position: LogicalPosition ) => dispatch( slice.actions.setPosition( { id, position } ) ),
|
|
23
|
+
setSize: ( size: LogicalSize ) => dispatch( slice.actions.setSize( { id, size } ) ),
|
|
24
|
+
focus: () => dispatch( slice.actions.bringToFront( id ) ),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type PointerEvent as ReactPointerEvent, useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { type LogicalPosition } from '../types';
|
|
4
|
+
import { activePositionChanged, getDragBounds, type PanelCorner } from '../utils/corner-position';
|
|
5
|
+
import { isRtl } from '../utils/direction';
|
|
6
|
+
import { applyDragDelta, type DragBounds, physicalToLogicalDelta } from '../utils/drag-math';
|
|
7
|
+
import { APP_BAR_HEIGHT_PX, getSidePanelInlineSize } from '../utils/viewport-bounds';
|
|
8
|
+
import { useFloatingPanelActions } from './use-floating-panel-actions';
|
|
9
|
+
import { useFloatingPanelStatus } from './use-floating-panel-status';
|
|
10
|
+
|
|
11
|
+
type DragSession = {
|
|
12
|
+
pointerId: number;
|
|
13
|
+
startClientX: number;
|
|
14
|
+
startClientY: number;
|
|
15
|
+
startPosition: LogicalPosition;
|
|
16
|
+
lastDispatchedPosition: LogicalPosition;
|
|
17
|
+
bounds: DragBounds;
|
|
18
|
+
corner: PanelCorner;
|
|
19
|
+
isRtl: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function useFloatingPanelDrag( id: string ) {
|
|
23
|
+
const sessionRef = useRef< DragSession | null >( null );
|
|
24
|
+
const { corner, position, size } = useFloatingPanelStatus( id );
|
|
25
|
+
const { setPosition } = useFloatingPanelActions( id );
|
|
26
|
+
|
|
27
|
+
const onPointerDown = useCallback(
|
|
28
|
+
( event: ReactPointerEvent< HTMLElement > ) => {
|
|
29
|
+
if ( ! corner || ! position || ! size ) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
event.currentTarget.setPointerCapture( event.pointerId );
|
|
34
|
+
|
|
35
|
+
sessionRef.current = {
|
|
36
|
+
pointerId: event.pointerId,
|
|
37
|
+
startClientX: event.clientX,
|
|
38
|
+
startClientY: event.clientY,
|
|
39
|
+
startPosition: position,
|
|
40
|
+
lastDispatchedPosition: position,
|
|
41
|
+
bounds: getDragBounds(
|
|
42
|
+
size,
|
|
43
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
44
|
+
getSidePanelInlineSize(),
|
|
45
|
+
APP_BAR_HEIGHT_PX
|
|
46
|
+
),
|
|
47
|
+
corner,
|
|
48
|
+
isRtl: isRtl(),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
[ corner, position, size ]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const onPointerMove = useCallback(
|
|
55
|
+
( event: ReactPointerEvent< HTMLElement > ) => {
|
|
56
|
+
const session = sessionRef.current;
|
|
57
|
+
|
|
58
|
+
if ( ! session || session.pointerId !== event.pointerId ) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const physical = { dx: event.clientX - session.startClientX, dy: event.clientY - session.startClientY };
|
|
63
|
+
const logical = physicalToLogicalDelta( physical, session.isRtl );
|
|
64
|
+
const nextPosition = applyDragDelta( session.corner, session.startPosition, logical, session.bounds );
|
|
65
|
+
|
|
66
|
+
if ( activePositionChanged( session.corner, session.lastDispatchedPosition, nextPosition ) ) {
|
|
67
|
+
setPosition( nextPosition );
|
|
68
|
+
session.lastDispatchedPosition = nextPosition;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[ setPosition ]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const clearSession = useCallback( ( event: ReactPointerEvent< HTMLElement > ) => {
|
|
75
|
+
const session = sessionRef.current;
|
|
76
|
+
|
|
77
|
+
if ( ! session || session.pointerId !== event.pointerId ) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
sessionRef.current = null;
|
|
82
|
+
}, [] );
|
|
83
|
+
|
|
84
|
+
return { onPointerDown, onPointerMove, onPointerUp: clearSession, onPointerCancel: clearSession };
|
|
85
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { type PointerEvent as ReactPointerEvent, useCallback, useRef } from 'react';
|
|
2
|
+
import { __useSelector as useSelector } from '@elementor/store';
|
|
3
|
+
|
|
4
|
+
import { type GlobalState, selectMinSize } from '../store/selectors';
|
|
5
|
+
import { type LogicalPosition, type LogicalSize } from '../types';
|
|
6
|
+
import {
|
|
7
|
+
activePositionChanged,
|
|
8
|
+
fromStartAnchoredPosition,
|
|
9
|
+
type PanelCorner,
|
|
10
|
+
toStartAnchoredPosition,
|
|
11
|
+
} from '../utils/corner-position';
|
|
12
|
+
import { isRtl } from '../utils/direction';
|
|
13
|
+
import { physicalToLogicalDelta } from '../utils/drag-math';
|
|
14
|
+
import { applyResize, type ResizeBounds, type ResizeDirection } from '../utils/resize-math';
|
|
15
|
+
import { APP_BAR_HEIGHT_PX, getSidePanelInlineSize } from '../utils/viewport-bounds';
|
|
16
|
+
import { useFloatingPanelActions } from './use-floating-panel-actions';
|
|
17
|
+
import { useFloatingPanelStatus } from './use-floating-panel-status';
|
|
18
|
+
|
|
19
|
+
type ResizeSession = {
|
|
20
|
+
pointerId: number;
|
|
21
|
+
direction: ResizeDirection;
|
|
22
|
+
startClientX: number;
|
|
23
|
+
startClientY: number;
|
|
24
|
+
startPosition: LogicalPosition;
|
|
25
|
+
startSize: LogicalSize;
|
|
26
|
+
lastPosition: LogicalPosition;
|
|
27
|
+
lastSize: LogicalSize;
|
|
28
|
+
bounds: ResizeBounds;
|
|
29
|
+
corner: PanelCorner;
|
|
30
|
+
isRtl: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function getResizeBounds(
|
|
34
|
+
corner: PanelCorner,
|
|
35
|
+
position: LogicalPosition,
|
|
36
|
+
currentSize: LogicalSize,
|
|
37
|
+
minSize: LogicalSize
|
|
38
|
+
): ResizeBounds {
|
|
39
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
40
|
+
const startAnchored = toStartAnchoredPosition( corner, position, currentSize, viewport );
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
minBlockSize: minSize.blockSize,
|
|
44
|
+
maxBlockSize: window.innerHeight - startAnchored.insetBlockStart,
|
|
45
|
+
minInlineSize: minSize.inlineSize,
|
|
46
|
+
maxInlineSize: window.innerWidth - startAnchored.insetInlineStart,
|
|
47
|
+
minInlineStart: getSidePanelInlineSize(),
|
|
48
|
+
minBlockStart: APP_BAR_HEIGHT_PX,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ResizeHandlePointerHandlers = {
|
|
53
|
+
onPointerDown: ( event: ReactPointerEvent< HTMLElement > ) => void;
|
|
54
|
+
onPointerMove: ( event: ReactPointerEvent< HTMLElement > ) => void;
|
|
55
|
+
onPointerUp: ( event: ReactPointerEvent< HTMLElement > ) => void;
|
|
56
|
+
onPointerCancel: ( event: ReactPointerEvent< HTMLElement > ) => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function usePanelResizeInteraction( id: string ) {
|
|
60
|
+
const sessionRef = useRef< ResizeSession | null >( null );
|
|
61
|
+
const { corner, position, size } = useFloatingPanelStatus( id );
|
|
62
|
+
const minSize = useSelector( ( state: GlobalState ) => selectMinSize( state, id ) );
|
|
63
|
+
const { setPosition, setSize } = useFloatingPanelActions( id );
|
|
64
|
+
|
|
65
|
+
const onPointerDown = useCallback(
|
|
66
|
+
( direction: ResizeDirection, event: ReactPointerEvent< HTMLElement > ) => {
|
|
67
|
+
if ( ! corner || ! position || ! size || ! minSize ) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
event.currentTarget.setPointerCapture( event.pointerId );
|
|
72
|
+
|
|
73
|
+
sessionRef.current = {
|
|
74
|
+
pointerId: event.pointerId,
|
|
75
|
+
direction,
|
|
76
|
+
startClientX: event.clientX,
|
|
77
|
+
startClientY: event.clientY,
|
|
78
|
+
startPosition: position,
|
|
79
|
+
startSize: size,
|
|
80
|
+
lastPosition: position,
|
|
81
|
+
lastSize: size,
|
|
82
|
+
bounds: getResizeBounds( corner, position, size, minSize ),
|
|
83
|
+
corner,
|
|
84
|
+
isRtl: isRtl(),
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
[ corner, position, size, minSize ]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const onPointerMove = useCallback(
|
|
91
|
+
( event: ReactPointerEvent< HTMLElement > ) => {
|
|
92
|
+
const session = sessionRef.current;
|
|
93
|
+
|
|
94
|
+
if ( ! session || session.pointerId !== event.pointerId ) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const physical = { dx: event.clientX - session.startClientX, dy: event.clientY - session.startClientY };
|
|
99
|
+
const logical = physicalToLogicalDelta( physical, session.isRtl );
|
|
100
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
101
|
+
const startAnchoredPosition = toStartAnchoredPosition(
|
|
102
|
+
session.corner,
|
|
103
|
+
session.startPosition,
|
|
104
|
+
session.startSize,
|
|
105
|
+
viewport
|
|
106
|
+
);
|
|
107
|
+
const resizeInput = { ...session.startPosition, ...startAnchoredPosition };
|
|
108
|
+
const next = applyResize(
|
|
109
|
+
session.direction,
|
|
110
|
+
resizeInput,
|
|
111
|
+
session.startSize,
|
|
112
|
+
logical.inlineDelta,
|
|
113
|
+
logical.blockDelta,
|
|
114
|
+
session.bounds
|
|
115
|
+
);
|
|
116
|
+
const nextPosition = fromStartAnchoredPosition(
|
|
117
|
+
session.corner,
|
|
118
|
+
{
|
|
119
|
+
insetBlockStart: next.position.insetBlockStart,
|
|
120
|
+
insetInlineStart: next.position.insetInlineStart,
|
|
121
|
+
},
|
|
122
|
+
next.size,
|
|
123
|
+
viewport
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if ( activePositionChanged( session.corner, session.lastPosition, nextPosition ) ) {
|
|
127
|
+
setPosition( nextPosition );
|
|
128
|
+
session.lastPosition = nextPosition;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const sizeChanged =
|
|
132
|
+
next.size.inlineSize !== session.lastSize.inlineSize ||
|
|
133
|
+
next.size.blockSize !== session.lastSize.blockSize;
|
|
134
|
+
|
|
135
|
+
if ( sizeChanged ) {
|
|
136
|
+
setSize( next.size );
|
|
137
|
+
session.lastSize = next.size;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[ setPosition, setSize ]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const clearSession = useCallback( ( event: ReactPointerEvent< HTMLElement > ) => {
|
|
144
|
+
const session = sessionRef.current;
|
|
145
|
+
|
|
146
|
+
if ( ! session || session.pointerId !== event.pointerId ) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
sessionRef.current = null;
|
|
151
|
+
}, [] );
|
|
152
|
+
|
|
153
|
+
const getResizeHandleProps = useCallback(
|
|
154
|
+
( direction: ResizeDirection ): ResizeHandlePointerHandlers => {
|
|
155
|
+
return {
|
|
156
|
+
onPointerDown: ( event ) => onPointerDown( direction, event ),
|
|
157
|
+
onPointerMove,
|
|
158
|
+
onPointerUp: clearSession,
|
|
159
|
+
onPointerCancel: clearSession,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
[ onPointerDown, onPointerMove, clearSession ]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return { getResizeHandleProps };
|
|
166
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { __createSelector, __useSelector as useSelector } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { type GlobalState, selectCorner, selectIsOpen, selectPosition, selectSize } from '../store/selectors';
|
|
4
|
+
|
|
5
|
+
const selectStatus = __createSelector(
|
|
6
|
+
[
|
|
7
|
+
( state: GlobalState, id: string ) => selectIsOpen( state, id ),
|
|
8
|
+
( state: GlobalState, id: string ) => selectCorner( state, id ),
|
|
9
|
+
( state: GlobalState, id: string ) => selectPosition( state, id ),
|
|
10
|
+
( state: GlobalState, id: string ) => selectSize( state, id ),
|
|
11
|
+
],
|
|
12
|
+
( isOpen, corner, position, size ) => ( { isOpen, corner, position, size } )
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export function useFloatingPanelStatus( id: string ) {
|
|
16
|
+
return useSelector( ( state: GlobalState ) => selectStatus( state, id ) );
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { __useSelector as useSelector } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { type GlobalState, resolveOverlayZIndex } from '../store/selectors';
|
|
4
|
+
|
|
5
|
+
export function useFloatingPanelZIndex( panelId: string ): number {
|
|
6
|
+
return useSelector( ( state: GlobalState ) => resolveOverlayZIndex( state, panelId ) );
|
|
7
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { createFloatingPanel, registerFloatingPanel } from './api';
|
|
2
|
+
export * from './components/external';
|
|
3
|
+
export { useFloatingPanelActions } from './hooks/use-floating-panel-actions';
|
|
4
|
+
export { useFloatingPanelStatus } from './hooks/use-floating-panel-status';
|
|
5
|
+
export { useFloatingPanelZIndex } from './hooks/use-floating-panel-z-index';
|
|
6
|
+
export { init } from './init';
|
|
7
|
+
export type {
|
|
8
|
+
FloatingPanelDeclaration,
|
|
9
|
+
FloatingPanelDefaults,
|
|
10
|
+
FloatingPanelHeaderAction,
|
|
11
|
+
FloatingPanelState,
|
|
12
|
+
LogicalPosition,
|
|
13
|
+
LogicalSize,
|
|
14
|
+
PanelCorner,
|
|
15
|
+
} from './types';
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { injectIntoTop } from '@elementor/editor';
|
|
2
|
+
import { __registerSlice } from '@elementor/store';
|
|
3
|
+
|
|
4
|
+
import FloatingPanelsHost from './components/internal/host';
|
|
5
|
+
import { slice } from './store/slice';
|
|
6
|
+
import { sync } from './sync';
|
|
7
|
+
|
|
8
|
+
export function init() {
|
|
9
|
+
sync();
|
|
10
|
+
|
|
11
|
+
__registerSlice( slice );
|
|
12
|
+
|
|
13
|
+
injectIntoTop( { id: 'floating-panels', component: FloatingPanelsHost } );
|
|
14
|
+
}
|
package/src/location.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type FloatingPanelState, type PanelCorner } from './types';
|
|
2
|
+
|
|
3
|
+
export const PERSISTENCE_STORAGE_KEY = 'elementor_floating_panels_state';
|
|
4
|
+
|
|
5
|
+
const VALID_PANEL_CORNERS = new Set< PanelCorner >( [
|
|
6
|
+
'block-start-inline-start',
|
|
7
|
+
'block-start-inline-end',
|
|
8
|
+
'block-end-inline-start',
|
|
9
|
+
'block-end-inline-end',
|
|
10
|
+
] );
|
|
11
|
+
|
|
12
|
+
type PersistedState = Record< string, FloatingPanelState >;
|
|
13
|
+
|
|
14
|
+
export function encodePersistedState( state: PersistedState ): string {
|
|
15
|
+
return JSON.stringify( state );
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function decodePersistedState( raw: string | null | undefined ): PersistedState {
|
|
19
|
+
if ( ! raw ) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let parsed: unknown;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse( raw );
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ( typeof parsed !== 'object' || parsed === null ) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result: PersistedState = {};
|
|
36
|
+
|
|
37
|
+
for ( const [ id, value ] of Object.entries( parsed as Record< string, unknown > ) ) {
|
|
38
|
+
if ( isPanelState( value ) ) {
|
|
39
|
+
result[ id ] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isPanelState( value: unknown ): value is FloatingPanelState {
|
|
47
|
+
if ( typeof value !== 'object' || value === null ) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const v = value as Record< string, unknown >;
|
|
52
|
+
|
|
53
|
+
if ( typeof v.size !== 'object' || v.size === null ) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ( typeof v.position !== 'object' || v.position === null ) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const size = v.size as Record< string, unknown >;
|
|
62
|
+
const position = v.position as Record< string, unknown >;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
typeof v.isOpen === 'boolean' &&
|
|
66
|
+
typeof v.zIndex === 'number' &&
|
|
67
|
+
typeof v.corner === 'string' &&
|
|
68
|
+
VALID_PANEL_CORNERS.has( v.corner as PanelCorner ) &&
|
|
69
|
+
typeof size.inlineSize === 'number' &&
|
|
70
|
+
typeof size.blockSize === 'number' &&
|
|
71
|
+
typeof position.insetInlineStart === 'number' &&
|
|
72
|
+
typeof position.insetInlineEnd === 'number' &&
|
|
73
|
+
typeof position.insetBlockStart === 'number' &&
|
|
74
|
+
typeof position.insetBlockEnd === 'number'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { __createSelector } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { FLOATING_PANEL_Z_INDEX_BASE } from '../constants';
|
|
4
|
+
import { type FloatingPanelsSliceState } from './slice';
|
|
5
|
+
|
|
6
|
+
export type GlobalState = { floatingPanels: FloatingPanelsSliceState };
|
|
7
|
+
|
|
8
|
+
export function selectPanelState( state: GlobalState, id: string ) {
|
|
9
|
+
return state.floatingPanels.byId[ id ];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolvePanelZIndex( state: GlobalState, id: string ): number {
|
|
13
|
+
return FLOATING_PANEL_Z_INDEX_BASE + ( state.floatingPanels.byId[ id ]?.zIndex ?? 0 );
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveOverlayZIndex( state: GlobalState, id: string ): number {
|
|
17
|
+
return resolvePanelZIndex( state, id ) + 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function selectIsOpen( state: GlobalState, id: string ): boolean {
|
|
21
|
+
return state.floatingPanels.byId[ id ]?.isOpen ?? false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function selectCorner( state: GlobalState, id: string ) {
|
|
25
|
+
return state.floatingPanels.byId[ id ]?.corner;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function selectPosition( state: GlobalState, id: string ) {
|
|
29
|
+
return state.floatingPanels.byId[ id ]?.position;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function selectSize( state: GlobalState, id: string ) {
|
|
33
|
+
return state.floatingPanels.byId[ id ]?.size;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function selectMinSize( state: GlobalState, id: string ) {
|
|
37
|
+
return state.floatingPanels.minSizeById[ id ];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function selectIsDraggable( state: GlobalState, id: string ): boolean {
|
|
41
|
+
return state.floatingPanels.isDraggableById[ id ] ?? false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function selectIsResizable( state: GlobalState, id: string ): boolean {
|
|
45
|
+
return state.floatingPanels.isResizableById[ id ] ?? false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function selectPanelTitle( state: GlobalState, id: string ): string | undefined {
|
|
49
|
+
return state.floatingPanels.titlesById[ id ];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function selectTopZIndex( state: GlobalState ): number {
|
|
53
|
+
return state.floatingPanels.topZIndex;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const selectOpenPanelIds = __createSelector( [ ( state: GlobalState ) => state.floatingPanels.byId ], ( byId ) =>
|
|
57
|
+
Object.entries( byId )
|
|
58
|
+
.filter( ( [ , panel ] ) => panel.isOpen )
|
|
59
|
+
.sort( ( [ , a ], [ , b ] ) => a.zIndex - b.zIndex )
|
|
60
|
+
.map( ( [ id ] ) => id )
|
|
61
|
+
);
|