@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +117 -0
  3. package/dist/index.d.mts +159 -0
  4. package/dist/index.d.ts +159 -0
  5. package/dist/index.js +1108 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +1062 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +58 -0
  10. package/src/api.ts +36 -0
  11. package/src/components/external/floating-panel-body.tsx +15 -0
  12. package/src/components/external/floating-panel-footer.tsx +22 -0
  13. package/src/components/external/floating-panel-header.tsx +100 -0
  14. package/src/components/external/floating-panel.tsx +10 -0
  15. package/src/components/external/index.ts +4 -0
  16. package/src/components/internal/corner-resize-handle.tsx +69 -0
  17. package/src/components/internal/drag-handle.tsx +29 -0
  18. package/src/components/internal/host.tsx +86 -0
  19. package/src/components/internal/panel-window.tsx +100 -0
  20. package/src/components/internal/resize-handle.tsx +60 -0
  21. package/src/constants.ts +1 -0
  22. package/src/hooks/use-floating-panel-actions.ts +26 -0
  23. package/src/hooks/use-floating-panel-drag.ts +85 -0
  24. package/src/hooks/use-floating-panel-resize.ts +166 -0
  25. package/src/hooks/use-floating-panel-status.ts +17 -0
  26. package/src/hooks/use-floating-panel-z-index.ts +7 -0
  27. package/src/index.ts +15 -0
  28. package/src/init.ts +14 -0
  29. package/src/location.ts +3 -0
  30. package/src/persistence.ts +76 -0
  31. package/src/store/index.ts +2 -0
  32. package/src/store/selectors.ts +61 -0
  33. package/src/store/slice.ts +113 -0
  34. package/src/sync.ts +104 -0
  35. package/src/types.ts +64 -0
  36. package/src/utils/clamp.ts +3 -0
  37. package/src/utils/corner-position.ts +149 -0
  38. package/src/utils/direction.ts +3 -0
  39. package/src/utils/drag-math.ts +49 -0
  40. package/src/utils/resize-math.ts +107 -0
  41. 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
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { createLocation } from '@elementor/locations';
2
+
3
+ export const { inject: injectIntoFloatingPanels, useInjections: useFloatingPanelsInjections } = createLocation();
@@ -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,2 @@
1
+ export { slice } from './slice';
2
+ export * from './selectors';
@@ -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
+ );