@elementor/editor-panels 0.1.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 +11 -0
- package/LICENSE +674 -0
- package/README.md +76 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +318 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
- package/src/__tests__/api.test.tsx +145 -0
- package/src/__tests__/sync.test.tsx +129 -0
- package/src/api.ts +67 -0
- package/src/components/external/index.ts +4 -0
- package/src/components/external/panel-body.tsx +16 -0
- package/src/components/external/panel-header-title.tsx +21 -0
- package/src/components/external/panel-header.tsx +20 -0
- package/src/components/external/panel.tsx +24 -0
- package/src/components/internal/panels.tsx +18 -0
- package/src/components/internal/portal.tsx +18 -0
- package/src/hooks/use-open-panel-injection.ts +14 -0
- package/src/index.ts +6 -0
- package/src/init.ts +13 -0
- package/src/location.ts +6 -0
- package/src/store/index.ts +2 -0
- package/src/store/selectors.ts +6 -0
- package/src/store/slice.ts +22 -0
- package/src/sync.ts +141 -0
package/src/api.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
import { injectIntoPanels } from './location';
|
|
3
|
+
import { selectOpenId, slice } from './store';
|
|
4
|
+
import { useSelector, useDispatch } from '@elementor/store';
|
|
5
|
+
import { useV1PanelStatus } from './sync';
|
|
6
|
+
|
|
7
|
+
export type PanelDeclaration = {
|
|
8
|
+
id: string;
|
|
9
|
+
component: ComponentType;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createPanel( { id, component }: PanelDeclaration ) {
|
|
13
|
+
const usePanelStatus = createUseStatus( id );
|
|
14
|
+
const usePanelActions = createUseActions( id, usePanelStatus );
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
panel: {
|
|
18
|
+
id,
|
|
19
|
+
component,
|
|
20
|
+
},
|
|
21
|
+
usePanelStatus,
|
|
22
|
+
usePanelActions,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerPanel( { id, component }: Pick<PanelDeclaration, 'id' | 'component'> ) {
|
|
27
|
+
injectIntoPanels( {
|
|
28
|
+
id,
|
|
29
|
+
filler: component,
|
|
30
|
+
} );
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createUseStatus( id: PanelDeclaration['id'] ) {
|
|
34
|
+
return () => {
|
|
35
|
+
const openPanelId = useSelector( selectOpenId );
|
|
36
|
+
const v1PanelStatus = useV1PanelStatus();
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
isOpen: openPanelId === id && v1PanelStatus.isActive,
|
|
40
|
+
isBlocked: v1PanelStatus.isBlocked,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createUseActions( id: PanelDeclaration['id'], useStatus: ReturnType<typeof createUseStatus> ) {
|
|
46
|
+
return () => {
|
|
47
|
+
const dispatch = useDispatch();
|
|
48
|
+
const { isBlocked } = useStatus();
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
open: async () => {
|
|
52
|
+
if ( isBlocked ) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
dispatch( slice.actions.open( id ) );
|
|
57
|
+
},
|
|
58
|
+
close: async () => {
|
|
59
|
+
if ( isBlocked ) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
dispatch( slice.actions.close( id ) );
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Box, BoxProps } from '@elementor/ui';
|
|
3
|
+
|
|
4
|
+
export default function PanelBody( { children, sx, ...props }: BoxProps ) {
|
|
5
|
+
return (
|
|
6
|
+
<Box
|
|
7
|
+
component="main"
|
|
8
|
+
sx={ {
|
|
9
|
+
overflowY: 'auto',
|
|
10
|
+
height: '100%',
|
|
11
|
+
...sx,
|
|
12
|
+
} }
|
|
13
|
+
{ ...props }
|
|
14
|
+
>{ children }</Box>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Typography, TypographyProps, styled } from '@elementor/ui';
|
|
3
|
+
|
|
4
|
+
const Title = styled( Typography )( ( { theme } ) => ( {
|
|
5
|
+
'&.MuiTypography-root': {
|
|
6
|
+
fontWeight: 'bold',
|
|
7
|
+
fontSize: theme.typography.body1.fontSize,
|
|
8
|
+
},
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
11
|
+
export default function PanelHeaderTitle( { children, ...props }: TypographyProps ) {
|
|
12
|
+
return (
|
|
13
|
+
<Title
|
|
14
|
+
component="h2"
|
|
15
|
+
variant="body1"
|
|
16
|
+
{ ...props }
|
|
17
|
+
>
|
|
18
|
+
{ children }
|
|
19
|
+
</Title>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Box, BoxProps, Divider, styled } from '@elementor/ui';
|
|
3
|
+
|
|
4
|
+
const Header = styled( Box )( ( { theme } ) => ( {
|
|
5
|
+
height: theme?.sizing?.[ '600' ] || '48px',
|
|
6
|
+
display: 'flex',
|
|
7
|
+
alignItems: 'center',
|
|
8
|
+
justifyContent: 'center',
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
11
|
+
export default function PanelHeader( { children, ...props }: BoxProps ) {
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<Header component="header" { ...props }>
|
|
15
|
+
{ children }
|
|
16
|
+
</Header>
|
|
17
|
+
<Divider />
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Drawer, DrawerProps } from '@elementor/ui';
|
|
3
|
+
|
|
4
|
+
export default function Panel( { children, sx, ...props }: DrawerProps ) {
|
|
5
|
+
return (
|
|
6
|
+
<Drawer
|
|
7
|
+
open={ true }
|
|
8
|
+
variant="persistent"
|
|
9
|
+
anchor="left"
|
|
10
|
+
PaperProps={ {
|
|
11
|
+
sx: {
|
|
12
|
+
position: 'relative',
|
|
13
|
+
width: '100%',
|
|
14
|
+
bgcolor: 'background.default',
|
|
15
|
+
border: 'none',
|
|
16
|
+
},
|
|
17
|
+
} }
|
|
18
|
+
sx={ { height: '100%', ...sx } }
|
|
19
|
+
{ ...props }
|
|
20
|
+
>
|
|
21
|
+
{ children }
|
|
22
|
+
</Drawer>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import useOpenPanelInjection from '../../hooks/use-open-panel-injection';
|
|
3
|
+
import Portal from './portal';
|
|
4
|
+
|
|
5
|
+
export default function Panels() {
|
|
6
|
+
const openPanel = useOpenPanelInjection();
|
|
7
|
+
const Component = openPanel?.filler ?? null;
|
|
8
|
+
|
|
9
|
+
if ( ! Component ) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Portal>
|
|
15
|
+
<Component />
|
|
16
|
+
</Portal>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Portal as BasePortal, PortalProps } from '@elementor/ui';
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { getPortalContainer } from '../../sync';
|
|
5
|
+
|
|
6
|
+
type Props = Omit<PortalProps, 'container'>;
|
|
7
|
+
|
|
8
|
+
export default function Portal( props: Props ) {
|
|
9
|
+
const containerRef = useRef( getPortalContainer );
|
|
10
|
+
|
|
11
|
+
if ( ! containerRef.current ) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<BasePortal container={ containerRef.current } { ...props } />
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useSelector } from '@elementor/store';
|
|
2
|
+
import { usePanelsInjections } from '../location';
|
|
3
|
+
import { selectOpenId } from '../store';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
export default function useOpenPanelInjection() {
|
|
7
|
+
const injections = usePanelsInjections();
|
|
8
|
+
const openId = useSelector( selectOpenId );
|
|
9
|
+
|
|
10
|
+
return useMemo(
|
|
11
|
+
() => injections.find( ( injection ) => openId === injection.id ),
|
|
12
|
+
[ injections, openId ]
|
|
13
|
+
);
|
|
14
|
+
}
|
package/src/index.ts
ADDED
package/src/init.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { injectIntoTop } from '@elementor/editor';
|
|
2
|
+
import { registerSlice } from '@elementor/store';
|
|
3
|
+
import Panels from './components/internal/panels';
|
|
4
|
+
import { sync } from './sync';
|
|
5
|
+
import { slice } from './store';
|
|
6
|
+
|
|
7
|
+
export default function init() {
|
|
8
|
+
sync();
|
|
9
|
+
|
|
10
|
+
registerSlice( slice );
|
|
11
|
+
|
|
12
|
+
injectIntoTop( { id: 'panels', filler: Panels } );
|
|
13
|
+
}
|
package/src/location.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createSlice, PayloadAction } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
const initialState: {
|
|
4
|
+
openId: string | null;
|
|
5
|
+
} = {
|
|
6
|
+
openId: null,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default createSlice( {
|
|
10
|
+
name: 'panels',
|
|
11
|
+
initialState,
|
|
12
|
+
reducers: {
|
|
13
|
+
open( state, action: PayloadAction<string> ) {
|
|
14
|
+
state.openId = action.payload;
|
|
15
|
+
},
|
|
16
|
+
close( state, action: PayloadAction<string | undefined> ) {
|
|
17
|
+
if ( ! action.payload || state.openId === action.payload ) {
|
|
18
|
+
state.openId = null;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
} );
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
openRoute,
|
|
3
|
+
listenTo,
|
|
4
|
+
routeCloseEvent,
|
|
5
|
+
useRouteStatus,
|
|
6
|
+
routeOpenEvent,
|
|
7
|
+
windowEvent,
|
|
8
|
+
registerRoute,
|
|
9
|
+
isRouteActive,
|
|
10
|
+
} from '@elementor/editor-v1-adapters';
|
|
11
|
+
import { dispatch, getState, subscribe as originalSubscribe } from '@elementor/store';
|
|
12
|
+
import { selectOpenId, slice } from './store';
|
|
13
|
+
|
|
14
|
+
const V2_PANEL = 'panel/v2';
|
|
15
|
+
|
|
16
|
+
export function getPortalContainer() {
|
|
17
|
+
return document.querySelector( '#elementor-panel-inner' );
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useV1PanelStatus() {
|
|
21
|
+
// For now supporting only panels that are not part of the kit and not in preview mode.
|
|
22
|
+
return useRouteStatus( V2_PANEL, {
|
|
23
|
+
blockOnKitRoutes: true,
|
|
24
|
+
blockOnPreviewMode: true,
|
|
25
|
+
} );
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sync() {
|
|
29
|
+
// Register the V2 panel route on panel init.
|
|
30
|
+
listenTo(
|
|
31
|
+
windowEvent( 'elementor/panel/init' ),
|
|
32
|
+
() => registerRoute( V2_PANEL )
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// On empty route open, hide V1 panel elements.
|
|
36
|
+
listenTo(
|
|
37
|
+
routeOpenEvent( V2_PANEL ),
|
|
38
|
+
() => {
|
|
39
|
+
getV1PanelElements().forEach( ( el ) => {
|
|
40
|
+
el.setAttribute( 'hidden', 'hidden' );
|
|
41
|
+
el.setAttribute( 'aria-hidden', 'true' );
|
|
42
|
+
} );
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// On empty route close, close the V2 panel.
|
|
47
|
+
listenTo(
|
|
48
|
+
routeCloseEvent( V2_PANEL ),
|
|
49
|
+
() => selectOpenId( getState() ) && dispatch( slice.actions.close() )
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// On empty route close, show V1 panel elements.
|
|
53
|
+
listenTo(
|
|
54
|
+
routeCloseEvent( V2_PANEL ),
|
|
55
|
+
() => {
|
|
56
|
+
getV1PanelElements().forEach( ( el ) => {
|
|
57
|
+
el.removeAttribute( 'hidden' );
|
|
58
|
+
el.removeAttribute( 'aria-hidden' );
|
|
59
|
+
} );
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// On V2 panel open, open the V2 panel route.
|
|
64
|
+
listenTo(
|
|
65
|
+
windowEvent( 'elementor/panel/init' ),
|
|
66
|
+
() => subscribe( {
|
|
67
|
+
on: ( state ) => selectOpenId( state ),
|
|
68
|
+
when: ( { prev, current } ) => !! ( ! prev && current ), // is panel opened
|
|
69
|
+
callback: () => openRoute( V2_PANEL ),
|
|
70
|
+
} )
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// On V2 panel close, close the V2 panel route.
|
|
74
|
+
listenTo(
|
|
75
|
+
windowEvent( 'elementor/panel/init' ),
|
|
76
|
+
() => subscribe( {
|
|
77
|
+
on: ( state ) => selectOpenId( state ),
|
|
78
|
+
when: ( { prev, current } ) => !! ( ! current && prev ), // is panel closed
|
|
79
|
+
callback: () => isRouteActive( V2_PANEL ) && openRoute( getDefaultRoute() ),
|
|
80
|
+
} )
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getV1PanelElements() {
|
|
85
|
+
const v1ElementsSelector = [
|
|
86
|
+
'#elementor-panel-header-wrapper',
|
|
87
|
+
'#elementor-panel-content-wrapper',
|
|
88
|
+
'#elementor-panel-state-loading',
|
|
89
|
+
'#elementor-panel-footer',
|
|
90
|
+
].join( ', ' );
|
|
91
|
+
|
|
92
|
+
return document.querySelectorAll( v1ElementsSelector );
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getDefaultRoute() {
|
|
96
|
+
type ExtendedWindow = Window & {
|
|
97
|
+
elementor?: {
|
|
98
|
+
documents?: {
|
|
99
|
+
getCurrent?: () => {
|
|
100
|
+
config?: {
|
|
101
|
+
panel?: {
|
|
102
|
+
default_route?: string,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const defaultRoute = ( window as unknown as ExtendedWindow )
|
|
111
|
+
?.elementor
|
|
112
|
+
?.documents
|
|
113
|
+
?.getCurrent?.()
|
|
114
|
+
?.config
|
|
115
|
+
?.panel
|
|
116
|
+
?.default_route;
|
|
117
|
+
|
|
118
|
+
return defaultRoute || 'panel/elements/categories';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function subscribe<TVal>( {
|
|
122
|
+
on,
|
|
123
|
+
when,
|
|
124
|
+
callback,
|
|
125
|
+
}: {
|
|
126
|
+
on: ( state: ReturnType<typeof getState> ) => TVal,
|
|
127
|
+
when: ( { prev, current }: { prev: TVal, current: TVal } ) => boolean,
|
|
128
|
+
callback: ( { prev, current }: { prev: TVal, current: TVal } ) => void,
|
|
129
|
+
} ) {
|
|
130
|
+
let prev: TVal;
|
|
131
|
+
|
|
132
|
+
originalSubscribe( () => {
|
|
133
|
+
const current = on( getState() );
|
|
134
|
+
|
|
135
|
+
if ( when( { prev, current } ) ) {
|
|
136
|
+
callback( { prev, current } );
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
prev = current;
|
|
140
|
+
} );
|
|
141
|
+
}
|