@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/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,4 @@
1
+ export { default as Panel } from './panel';
2
+ export { default as PanelHeader } from './panel-header';
3
+ export { default as PanelHeaderTitle } from './panel-header-title';
4
+ export { default as PanelBody } from './panel-body';
@@ -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
@@ -0,0 +1,6 @@
1
+ import init from './init';
2
+
3
+ export { createPanel, registerPanel } from './api';
4
+ export * from './components/external';
5
+
6
+ init();
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
+ }
@@ -0,0 +1,6 @@
1
+ import { createLocation } from '@elementor/locations';
2
+
3
+ export const {
4
+ inject: injectIntoPanels,
5
+ useInjections: usePanelsInjections,
6
+ } = createLocation();
@@ -0,0 +1,2 @@
1
+ export { selectOpenId } from './selectors';
2
+ export { default as slice } from './slice';
@@ -0,0 +1,6 @@
1
+ import { SliceState } from '@elementor/store';
2
+ import slice from './slice';
3
+
4
+ type State = SliceState<typeof slice>;
5
+
6
+ export const selectOpenId = ( state: State ) => state.panels.openId;
@@ -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
+ }