@elementor/editor-editing-panel 0.2.0 → 0.4.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 +22 -0
- package/dist/index.js +143 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +136 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/utils.ts +16 -5
- package/src/components/__tests__/editing-panel.test.tsx +5 -5
- package/src/components/controls/__tests__/control-context.test.tsx +14 -0
- package/src/components/controls/__tests__/settings-control.test.tsx +67 -0
- package/src/components/controls/control-context.ts +20 -0
- package/src/components/controls/control-types/__tests__/select-control.test.tsx +67 -0
- package/src/components/controls/control-types/__tests__/text-control.test.tsx +29 -0
- package/src/components/controls/control-types/select-control.tsx +26 -0
- package/src/components/controls/control-types/text-control.tsx +19 -0
- package/src/components/controls/settings-control.tsx +53 -0
- package/src/components/editing-panel.tsx +44 -8
- package/src/contexts/settings-controls.tsx +32 -0
- package/src/hooks/__tests__/use-widget-settings.test.ts +81 -0
- package/src/hooks/use-widget-settings.ts +16 -0
- package/src/sync/__tests__/get-container.test.ts +40 -0
- package/src/sync/__tests__/should-use-v2-panel.test.ts +8 -8
- package/src/sync/get-container.ts +8 -0
- package/src/sync/types.ts +10 -4
- package/src/sync/update-settings.ts +14 -0
- package/src/types.ts +29 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { SettingsControl } from '../settings-control';
|
|
4
|
+
import { useControl } from '../control-context';
|
|
5
|
+
import { updateSettings } from '../../../sync/update-settings';
|
|
6
|
+
import { useWidgetSettings } from '../../../hooks/use-widget-settings';
|
|
7
|
+
|
|
8
|
+
jest.mock( '../../../sync/update-settings' );
|
|
9
|
+
jest.mock( '../../../hooks/use-widget-settings' );
|
|
10
|
+
|
|
11
|
+
describe( 'SettingsControl', () => {
|
|
12
|
+
beforeEach( () => {
|
|
13
|
+
jest.mocked( useWidgetSettings ).mockReturnValue( 'Hello, World!' );
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
it( 'should set the initial value', () => {
|
|
17
|
+
// Arrange.
|
|
18
|
+
const elementID = '1-heading';
|
|
19
|
+
const bind = 'text';
|
|
20
|
+
|
|
21
|
+
// Act.
|
|
22
|
+
render(
|
|
23
|
+
<SettingsControl bind={ bind } elementID={ elementID }>
|
|
24
|
+
<MockControl />
|
|
25
|
+
</SettingsControl>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Assert.
|
|
29
|
+
expect( screen.getByRole( 'textbox', { name: bind } ) ).toHaveValue( 'Hello, World!' );
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
it( 'should pass the updated payload when input value changes', () => {
|
|
33
|
+
// Arrange.
|
|
34
|
+
const elementID = '1-heading';
|
|
35
|
+
const bind = 'text';
|
|
36
|
+
|
|
37
|
+
// Act.
|
|
38
|
+
render(
|
|
39
|
+
<SettingsControl bind={ bind } elementID={ elementID }>
|
|
40
|
+
<MockControl />
|
|
41
|
+
</SettingsControl>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const input = screen.getByRole( 'textbox', { name: bind } );
|
|
45
|
+
const newValue = 'Goodbye, World!';
|
|
46
|
+
|
|
47
|
+
fireEvent.change( input, { target: { value: newValue } } );
|
|
48
|
+
|
|
49
|
+
// Assert.
|
|
50
|
+
expect( jest.mocked( updateSettings ) ).toHaveBeenCalledWith( {
|
|
51
|
+
id: elementID,
|
|
52
|
+
props: { [ bind ]: newValue },
|
|
53
|
+
} );
|
|
54
|
+
} );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
const MockControl = () => {
|
|
58
|
+
const { value, setValue, bind } = useControl<string>( '' );
|
|
59
|
+
|
|
60
|
+
const handleChange = ( event: React.ChangeEvent<HTMLInputElement> ) => {
|
|
61
|
+
setValue( event.target.value );
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<input type="text" aria-label={ bind } value={ value } onChange={ handleChange } />
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { PropKey, PropValue } from '../../types';
|
|
3
|
+
|
|
4
|
+
export type ControlContext<T extends PropValue> = null | {
|
|
5
|
+
bind: PropKey;
|
|
6
|
+
setValue: ( value: T ) => void;
|
|
7
|
+
value: T;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ControlContext = createContext<ControlContext<PropValue>>( null );
|
|
11
|
+
|
|
12
|
+
export function useControl<T extends PropValue>( defaultValue?: T ) {
|
|
13
|
+
const controlContext = useContext<ControlContext<T>>( ControlContext as never );
|
|
14
|
+
|
|
15
|
+
if ( ! controlContext ) {
|
|
16
|
+
throw new Error( 'useControl must be used within a ControlContext' );
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { ...controlContext, value: controlContext.value ?? defaultValue };
|
|
20
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { ControlContext } from '../../control-context';
|
|
4
|
+
import { SelectControl } from '../select-control';
|
|
5
|
+
|
|
6
|
+
describe( 'SelectControl', () => {
|
|
7
|
+
it( 'should pass the updated payload when select value changes', () => {
|
|
8
|
+
// Arrange.
|
|
9
|
+
const setValue = jest.fn();
|
|
10
|
+
const options = [
|
|
11
|
+
{ label: 'Option 1', value: 'value1' },
|
|
12
|
+
{ label: 'Option 2', value: 'value2' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// Act.
|
|
16
|
+
render(
|
|
17
|
+
<ControlContext.Provider value={ { setValue, value: 'value1', bind: 'select' } }>
|
|
18
|
+
<SelectControl options={ options } />
|
|
19
|
+
</ControlContext.Provider>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const select = screen.getByRole( 'combobox' );
|
|
23
|
+
|
|
24
|
+
// Assert.
|
|
25
|
+
expect( screen.getByText( 'Option 1' ) ).toBeInTheDocument();
|
|
26
|
+
expect( screen.queryByText( 'Option 2' ) ).not.toBeInTheDocument();
|
|
27
|
+
|
|
28
|
+
// Act.
|
|
29
|
+
fireEvent.mouseDown( select );
|
|
30
|
+
|
|
31
|
+
const option2 = screen.getByText( 'Option 2' );
|
|
32
|
+
|
|
33
|
+
fireEvent.click( option2 );
|
|
34
|
+
|
|
35
|
+
// Assert.
|
|
36
|
+
expect( setValue ).toHaveBeenCalledWith( 'value2' );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'should disable the select options', () => {
|
|
40
|
+
// Arrange.
|
|
41
|
+
const setValue = jest.fn();
|
|
42
|
+
const options = [
|
|
43
|
+
{ label: 'Option 1', value: 'value1' },
|
|
44
|
+
{ label: 'Option 2', value: 'value2', disabled: true },
|
|
45
|
+
{ label: 'Option 3', value: 'value3', disabled: false },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Act.
|
|
49
|
+
render(
|
|
50
|
+
<ControlContext.Provider value={ { setValue, value: '', bind: 'select' } }>
|
|
51
|
+
<SelectControl options={ options } />
|
|
52
|
+
</ControlContext.Provider>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const select = screen.getByRole( 'combobox' );
|
|
56
|
+
|
|
57
|
+
// Act.
|
|
58
|
+
fireEvent.mouseDown( select );
|
|
59
|
+
|
|
60
|
+
const option2 = screen.getByText( 'Option 2' );
|
|
61
|
+
const option3 = screen.getByText( 'Option 3' );
|
|
62
|
+
|
|
63
|
+
// Assert.
|
|
64
|
+
expect( option2 ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
65
|
+
expect( option3 ).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
66
|
+
} );
|
|
67
|
+
} );
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { TextControl } from '../text-control';
|
|
4
|
+
import { ControlContext } from '../../control-context';
|
|
5
|
+
|
|
6
|
+
describe( 'TextControl', () => {
|
|
7
|
+
it( 'should pass the updated payload when input value changes', () => {
|
|
8
|
+
// Arrange.
|
|
9
|
+
const setValue = jest.fn();
|
|
10
|
+
|
|
11
|
+
// Act.
|
|
12
|
+
render(
|
|
13
|
+
<ControlContext.Provider value={ { setValue, value: 'Hi', bind: 'text' } }>
|
|
14
|
+
<TextControl placeholder="type text here" />
|
|
15
|
+
</ControlContext.Provider>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const input = screen.getByRole( 'textbox' );
|
|
19
|
+
|
|
20
|
+
// Assert.
|
|
21
|
+
expect( input ).toHaveValue( 'Hi' );
|
|
22
|
+
|
|
23
|
+
// Act.
|
|
24
|
+
fireEvent.input( input, { target: { value: 'Cool Heading!' } } );
|
|
25
|
+
|
|
26
|
+
// Assert.
|
|
27
|
+
expect( setValue ).toHaveBeenCalledWith( 'Cool Heading!' );
|
|
28
|
+
} );
|
|
29
|
+
} );
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { MenuItem, Select, SelectChangeEvent } from '@elementor/ui';
|
|
3
|
+
import { useControl } from '../control-context';
|
|
4
|
+
import { PropValue } from '../../../types';
|
|
5
|
+
|
|
6
|
+
export type SelectControlProps<T> = {
|
|
7
|
+
options: Array<{ label: string; value: T; disabled?: boolean }>
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const SelectControl = <T extends PropValue>( { options }: SelectControlProps<T> ) => {
|
|
11
|
+
const { value, setValue } = useControl<T>();
|
|
12
|
+
|
|
13
|
+
const handleChange = ( event: SelectChangeEvent<T> ) => {
|
|
14
|
+
setValue( event.target.value as T );
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Select size="tiny" value={ value ?? '' } onChange={ handleChange }>
|
|
19
|
+
{ options.map( ( option ) => (
|
|
20
|
+
<MenuItem key={ option.value } value={ option.value } disabled={ option.disabled }>
|
|
21
|
+
{ option.label }
|
|
22
|
+
</MenuItem>
|
|
23
|
+
) ) }
|
|
24
|
+
</Select>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { TextareaAutosize } from '@elementor/ui';
|
|
3
|
+
import { useControl } from '../control-context';
|
|
4
|
+
|
|
5
|
+
export type TextControlProps = {
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const TextControl = ( { placeholder }: TextControlProps ) => {
|
|
10
|
+
const { value, setValue } = useControl<string>( '' );
|
|
11
|
+
|
|
12
|
+
const handleChange = ( event: React.ChangeEvent<HTMLInputElement> ) => {
|
|
13
|
+
setValue( event.target.value );
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<TextareaAutosize size="tiny" minRows={ 3 }value={ value } onChange={ handleChange } placeholder={ placeholder } />
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ControlContext } from './control-context';
|
|
3
|
+
import { Stack, Typography } from '@elementor/ui';
|
|
4
|
+
import { updateSettings } from '../../sync/update-settings';
|
|
5
|
+
import { useWidgetSettings } from '../../hooks/use-widget-settings';
|
|
6
|
+
import { PropKey, PropValue } from '../../types';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
bind: PropKey;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
elementID: Element['id'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SettingsControl = ( { bind, children, elementID }: Props ) => {
|
|
15
|
+
const value = useWidgetSettings( { id: elementID, bind } );
|
|
16
|
+
|
|
17
|
+
const setValue = ( newValue: PropValue ) => {
|
|
18
|
+
updateSettings( {
|
|
19
|
+
id: elementID,
|
|
20
|
+
props: {
|
|
21
|
+
[ bind ]: newValue,
|
|
22
|
+
},
|
|
23
|
+
} );
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<ControlContext.Provider value={ { setValue, value, bind } }>
|
|
28
|
+
{ children }
|
|
29
|
+
</ControlContext.Provider>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const Label = ( { children }: { children: React.ReactNode } ) => {
|
|
34
|
+
return <Typography component="label" variant="caption">{ children }</Typography>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const Container = ( { children }: { children: React.ReactNode} ) => (
|
|
38
|
+
<Stack
|
|
39
|
+
spacing={ 1 }
|
|
40
|
+
flexDirection="row"
|
|
41
|
+
alignItems="center"
|
|
42
|
+
justifyContent="space-between"
|
|
43
|
+
flexWrap="wrap"
|
|
44
|
+
sx={ { px: 2 } }
|
|
45
|
+
>
|
|
46
|
+
{ children }
|
|
47
|
+
</Stack>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
SettingsControl.Label = Label;
|
|
51
|
+
SettingsControl.Container = Container;
|
|
52
|
+
|
|
53
|
+
export { SettingsControl };
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Panel,
|
|
4
|
-
PanelBody,
|
|
5
|
-
PanelHeader,
|
|
6
|
-
PanelHeaderTitle,
|
|
7
|
-
} from '@elementor/editor-panels';
|
|
8
2
|
import { __ } from '@wordpress/i18n';
|
|
9
3
|
import useSelectedElements from '../hooks/use-selected-elements';
|
|
10
4
|
import useElementType from '../hooks/use-element-type';
|
|
5
|
+
import { Panel, PanelBody, PanelHeader, PanelHeaderTitle } from '@elementor/editor-panels';
|
|
6
|
+
import { SelectControl } from './controls/control-types/select-control';
|
|
7
|
+
import { TextControl } from './controls/control-types/text-control';
|
|
8
|
+
import { SettingsControl } from '../components/controls/settings-control';
|
|
9
|
+
import { Stack } from '@elementor/ui';
|
|
10
|
+
import { ElementContext } from '../contexts/settings-controls';
|
|
11
|
+
|
|
12
|
+
const controlTypes = {
|
|
13
|
+
select: SelectControl,
|
|
14
|
+
text: TextControl,
|
|
15
|
+
};
|
|
11
16
|
|
|
12
17
|
export const EditingPanel = () => {
|
|
13
18
|
const elements = useSelectedElements();
|
|
14
|
-
|
|
19
|
+
|
|
20
|
+
const selectedElement = elements[ 0 ];
|
|
21
|
+
|
|
22
|
+
const elementType = useElementType( selectedElement?.type );
|
|
15
23
|
|
|
16
24
|
if ( elements.length !== 1 || ! elementType ) {
|
|
17
25
|
return null;
|
|
@@ -25,7 +33,35 @@ export const EditingPanel = () => {
|
|
|
25
33
|
<PanelHeader>
|
|
26
34
|
<PanelHeaderTitle>{ panelTitle }</PanelHeaderTitle>
|
|
27
35
|
</PanelHeader>
|
|
28
|
-
<PanelBody
|
|
36
|
+
<PanelBody>
|
|
37
|
+
<ElementContext element={ selectedElement }>
|
|
38
|
+
<Stack spacing={ 2 }>
|
|
39
|
+
{
|
|
40
|
+
elementType.controls.map( ( control ) => {
|
|
41
|
+
if ( control.type === 'control' ) {
|
|
42
|
+
const ControlComponent = controlTypes[ control.value.type as keyof typeof controlTypes ];
|
|
43
|
+
|
|
44
|
+
if ( ! ControlComponent ) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<SettingsControl key={ control.value.bind } bind={ control.value.bind } elementID={ elements[ 0 ].id }>
|
|
50
|
+
<SettingsControl.Container>
|
|
51
|
+
<SettingsControl.Label>{ control.value.label }</SettingsControl.Label>
|
|
52
|
+
{ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ }
|
|
53
|
+
<ControlComponent { ...control.value.props as any } />
|
|
54
|
+
</SettingsControl.Container>
|
|
55
|
+
</SettingsControl>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
} )
|
|
61
|
+
}
|
|
62
|
+
</Stack>
|
|
63
|
+
</ElementContext>
|
|
64
|
+
</PanelBody>
|
|
29
65
|
</Panel>
|
|
30
66
|
);
|
|
31
67
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createContext, ReactNode, useContext } from 'react';
|
|
3
|
+
import { Element } from '../types';
|
|
4
|
+
|
|
5
|
+
type ContextValue = {
|
|
6
|
+
element: Element;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Context = createContext<ContextValue | null>( null );
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
element: Element;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ElementContext( { children, element }: Props ) {
|
|
17
|
+
return (
|
|
18
|
+
<Context.Provider value={ { element } }>
|
|
19
|
+
{ children }
|
|
20
|
+
</Context.Provider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useElementContext() {
|
|
25
|
+
const context = useContext( Context );
|
|
26
|
+
|
|
27
|
+
if ( ! context ) {
|
|
28
|
+
throw new Error( 'useElementContext must be used within a ElementContextProvider' );
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return context;
|
|
32
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { useWidgetSettings } from '../use-widget-settings';
|
|
3
|
+
import { ExtendedWindow } from '../../sync/types';
|
|
4
|
+
import { mockV1Element } from '../../__tests__/utils';
|
|
5
|
+
import { dispatchCommandAfter } from 'test-utils';
|
|
6
|
+
import getContainer from '../../sync/get-container';
|
|
7
|
+
|
|
8
|
+
jest.mock( '../../sync/get-container' );
|
|
9
|
+
|
|
10
|
+
describe( 'useWidgetSettings', () => {
|
|
11
|
+
beforeEach( () => {
|
|
12
|
+
const extendedWindow = window as unknown as ExtendedWindow;
|
|
13
|
+
|
|
14
|
+
extendedWindow.elementor = {
|
|
15
|
+
widgetsCache: {
|
|
16
|
+
'v1-heading': {
|
|
17
|
+
controls: [],
|
|
18
|
+
title: 'Heading',
|
|
19
|
+
},
|
|
20
|
+
'v2-heading': {
|
|
21
|
+
controls: [],
|
|
22
|
+
atomic_controls: [],
|
|
23
|
+
title: 'Heading',
|
|
24
|
+
},
|
|
25
|
+
'v2-container': {
|
|
26
|
+
controls: [],
|
|
27
|
+
atomic_controls: [],
|
|
28
|
+
title: 'Container',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'should return the value of the setting', () => {
|
|
35
|
+
// Arrange.
|
|
36
|
+
const bind = 'title';
|
|
37
|
+
const element = { id: 'element-id' };
|
|
38
|
+
jest.mocked( getContainer ).mockReturnValue( mockV1Element( { settings: { [ bind ]: 'Hello, World!' } } ) );
|
|
39
|
+
|
|
40
|
+
// Act.
|
|
41
|
+
const result = renderHook( () => useWidgetSettings( { id: element.id, bind } ) ).result.current;
|
|
42
|
+
|
|
43
|
+
// Assert.
|
|
44
|
+
expect( result ).toEqual( 'Hello, World!' );
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
it( 'should return null if the setting is not found', () => {
|
|
48
|
+
// Arrange.
|
|
49
|
+
const bind = 'title';
|
|
50
|
+
const element = { id: 'element-id' };
|
|
51
|
+
jest.mocked( getContainer ).mockReturnValue( mockV1Element( { settings: {} } ) );
|
|
52
|
+
|
|
53
|
+
// Act.
|
|
54
|
+
const result = renderHook( () => useWidgetSettings( { id: element.id, bind } ) ).result.current;
|
|
55
|
+
|
|
56
|
+
// Assert.
|
|
57
|
+
expect( result ).toEqual( null );
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
it( 'should update the state value on settings change', () => {
|
|
61
|
+
// Arrange.
|
|
62
|
+
const bind = 'title';
|
|
63
|
+
const element = { id: 'element-id' };
|
|
64
|
+
jest.mocked( getContainer ).mockReturnValue( mockV1Element( { settings: { [ bind ]: 'Hello, World!' } } ) );
|
|
65
|
+
|
|
66
|
+
// Act.
|
|
67
|
+
const { result } = renderHook( () => useWidgetSettings( { id: element.id, bind } ) );
|
|
68
|
+
|
|
69
|
+
// Assert.
|
|
70
|
+
expect( result.current ).toEqual( 'Hello, World!' );
|
|
71
|
+
|
|
72
|
+
// Act.
|
|
73
|
+
act( () => {
|
|
74
|
+
jest.mocked( getContainer ).mockReturnValue( mockV1Element( { settings: { [ bind ]: 'Goodbye, World!' } } ) );
|
|
75
|
+
dispatchCommandAfter( 'document/elements/settings' );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
// Assert.
|
|
79
|
+
expect( result.current ).toEqual( 'Goodbye, World!' );
|
|
80
|
+
} );
|
|
81
|
+
} );
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { commandEndEvent, __privateUseListenTo as useListenTo } from '@elementor/editor-v1-adapters';
|
|
2
|
+
import { PropValue } from '../types';
|
|
3
|
+
import getContainer from '../sync/get-container';
|
|
4
|
+
|
|
5
|
+
export const useWidgetSettings = ( { id, bind }: { id: string; bind: string } ): PropValue => {
|
|
6
|
+
return useListenTo(
|
|
7
|
+
commandEndEvent( 'document/elements/settings' ),
|
|
8
|
+
() => {
|
|
9
|
+
const container = getContainer( id );
|
|
10
|
+
const value = container?.settings?.get( bind ) ?? null;
|
|
11
|
+
|
|
12
|
+
return value;
|
|
13
|
+
},
|
|
14
|
+
[]
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import getContainer from '../get-container';
|
|
2
|
+
import { ExtendedWindow } from '../types';
|
|
3
|
+
|
|
4
|
+
describe( 'getContainer', () => {
|
|
5
|
+
const mockGetContainer = jest.fn();
|
|
6
|
+
|
|
7
|
+
beforeEach( () => {
|
|
8
|
+
const extendedWindow = window as unknown as ExtendedWindow;
|
|
9
|
+
|
|
10
|
+
extendedWindow.elementor = {
|
|
11
|
+
getContainer: mockGetContainer,
|
|
12
|
+
};
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
it( 'should return the container', () => {
|
|
16
|
+
// Arrange.
|
|
17
|
+
const elementID = '1-heading';
|
|
18
|
+
const container = { id: elementID };
|
|
19
|
+
mockGetContainer.mockReturnValue( container );
|
|
20
|
+
|
|
21
|
+
// Act.
|
|
22
|
+
const result = getContainer( elementID );
|
|
23
|
+
|
|
24
|
+
// Assert.
|
|
25
|
+
expect( result ).toBe( container );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'should return null if the container is not found', () => {
|
|
29
|
+
// Arrange.
|
|
30
|
+
const elementID = '1-heading';
|
|
31
|
+
const container = undefined;
|
|
32
|
+
mockGetContainer.mockReturnValue( container );
|
|
33
|
+
|
|
34
|
+
// Act.
|
|
35
|
+
const result = getContainer( elementID );
|
|
36
|
+
|
|
37
|
+
// Assert.
|
|
38
|
+
expect( result ).toBe( null );
|
|
39
|
+
} );
|
|
40
|
+
} );
|
|
@@ -41,7 +41,7 @@ describe( 'shouldUseV2Panel', () => {
|
|
|
41
41
|
|
|
42
42
|
it( 'should return true for v2 element', () => {
|
|
43
43
|
// Arrange.
|
|
44
|
-
getElements.mockReturnValue( [ mockV1Element( { widgetType: 'v2-heading' } ) ] );
|
|
44
|
+
getElements.mockReturnValue( [ mockV1Element( { model: { widgetType: 'v2-heading' } } ) ] );
|
|
45
45
|
|
|
46
46
|
// Assert.
|
|
47
47
|
expect( shouldUseV2Panel() ).toBe( true );
|
|
@@ -49,7 +49,7 @@ describe( 'shouldUseV2Panel', () => {
|
|
|
49
49
|
|
|
50
50
|
it( 'should return true for v2 element', () => {
|
|
51
51
|
// Arrange.
|
|
52
|
-
getElements.mockReturnValue( [ mockV1Element( { elType: 'v2-container' } ) ] );
|
|
52
|
+
getElements.mockReturnValue( [ mockV1Element( { model: { elType: 'v2-container' } } ) ] );
|
|
53
53
|
|
|
54
54
|
// Assert.
|
|
55
55
|
expect( shouldUseV2Panel() ).toBe( true );
|
|
@@ -57,7 +57,7 @@ describe( 'shouldUseV2Panel', () => {
|
|
|
57
57
|
|
|
58
58
|
it( 'should return false for v1 element', () => {
|
|
59
59
|
// Arrange.
|
|
60
|
-
getElements.mockReturnValue( [ mockV1Element( { widgetType: 'v1-heading' } ) ] );
|
|
60
|
+
getElements.mockReturnValue( [ mockV1Element( { model: { widgetType: 'v1-heading' } } ) ] );
|
|
61
61
|
|
|
62
62
|
// Assert.
|
|
63
63
|
expect( shouldUseV2Panel() ).toBe( false );
|
|
@@ -65,7 +65,7 @@ describe( 'shouldUseV2Panel', () => {
|
|
|
65
65
|
|
|
66
66
|
it( 'should return false for non-existing element', () => {
|
|
67
67
|
// Arrange.
|
|
68
|
-
getElements.mockReturnValue( [ mockV1Element( { widgetType: 'non-existing' } ) ] );
|
|
68
|
+
getElements.mockReturnValue( [ mockV1Element( { model: { widgetType: 'non-existing' } } ) ] );
|
|
69
69
|
|
|
70
70
|
// Assert.
|
|
71
71
|
expect( shouldUseV2Panel() ).toBe( false );
|
|
@@ -74,8 +74,8 @@ describe( 'shouldUseV2Panel', () => {
|
|
|
74
74
|
it( 'should return false if there is more than 1 selected element with mixed versions', () => {
|
|
75
75
|
// Arrange.
|
|
76
76
|
getElements.mockReturnValue( [
|
|
77
|
-
mockV1Element( { widgetType: 'v2-heading' } ),
|
|
78
|
-
mockV1Element( { widgetType: 'v1-heading' } ),
|
|
77
|
+
mockV1Element( { model: { widgetType: 'v2-heading' } } ),
|
|
78
|
+
mockV1Element( { model: { widgetType: 'v1-heading' } } ),
|
|
79
79
|
] );
|
|
80
80
|
|
|
81
81
|
// Assert.
|
|
@@ -85,8 +85,8 @@ describe( 'shouldUseV2Panel', () => {
|
|
|
85
85
|
it( 'should return false if there is more than 1 selected element with same version', () => {
|
|
86
86
|
// Arrange.
|
|
87
87
|
getElements.mockReturnValue( [
|
|
88
|
-
mockV1Element( { widgetType: 'v2-heading' } ),
|
|
89
|
-
mockV1Element( { widgetType: 'v2-container' } ),
|
|
88
|
+
mockV1Element( { model: { widgetType: 'v2-heading' } } ),
|
|
89
|
+
mockV1Element( { model: { widgetType: 'v2-container' } } ),
|
|
90
90
|
] );
|
|
91
91
|
|
|
92
92
|
// Assert.
|
package/src/sync/types.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
import { AtomicControl, PropValue } from '../types';
|
|
2
|
+
|
|
1
3
|
export type ExtendedWindow = Window & {
|
|
2
4
|
elementor?: {
|
|
3
5
|
selection?: {
|
|
4
6
|
getElements: () => V1Element[];
|
|
5
7
|
},
|
|
6
8
|
widgetsCache?: Record<string, {
|
|
7
|
-
atomic_controls?:
|
|
9
|
+
atomic_controls?: AtomicControl[],
|
|
8
10
|
controls: object,
|
|
9
11
|
title: string,
|
|
10
|
-
}
|
|
12
|
+
}>,
|
|
13
|
+
getContainer?: ( id: string ) => V1Element
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export type V1Element = {
|
|
15
|
-
model:
|
|
18
|
+
model: V1Model<V1ElementModelProps>,
|
|
19
|
+
settings?: V1Model<V1ElementSettingsProps>,
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export type V1ElementModelProps = {
|
|
@@ -21,6 +25,8 @@ export type V1ElementModelProps = {
|
|
|
21
25
|
id: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
type
|
|
28
|
+
export type V1ElementSettingsProps = Record<string, PropValue>;
|
|
29
|
+
|
|
30
|
+
type V1Model<T> = {
|
|
25
31
|
get: <K extends keyof T>( key: K ) => T[K],
|
|
26
32
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
|
|
2
|
+
import { Props } from '../types';
|
|
3
|
+
import getContainer from './get-container';
|
|
4
|
+
|
|
5
|
+
export const updateSettings = ( { id, props }: { id: string, props: Props } ) => {
|
|
6
|
+
const container = getContainer( id );
|
|
7
|
+
|
|
8
|
+
runCommand( 'document/elements/settings', {
|
|
9
|
+
container,
|
|
10
|
+
settings: {
|
|
11
|
+
...props,
|
|
12
|
+
},
|
|
13
|
+
} );
|
|
14
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,34 @@ export type Element = {
|
|
|
5
5
|
|
|
6
6
|
export type ElementType = {
|
|
7
7
|
key: string;
|
|
8
|
-
controls:
|
|
8
|
+
controls: AtomicControl[];
|
|
9
9
|
title: string;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
export type AtomicControl = {
|
|
13
|
+
type: 'control',
|
|
14
|
+
value: {
|
|
15
|
+
bind: string,
|
|
16
|
+
label: string,
|
|
17
|
+
description?: string,
|
|
18
|
+
type: string,
|
|
19
|
+
props: Record<string, unknown>,
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type MaybeArray<T> = T | T[];
|
|
24
|
+
|
|
25
|
+
type TransformablePropValue = {
|
|
26
|
+
$$type: string,
|
|
27
|
+
value: unknown,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type PlainPropValue = MaybeArray<string | number | boolean | object | null | undefined>;
|
|
31
|
+
|
|
32
|
+
export type PropValue = PlainPropValue | TransformablePropValue;
|
|
33
|
+
|
|
34
|
+
export type PropKey = string;
|
|
35
|
+
|
|
36
|
+
export type Props = Record<PropKey, PropValue>;
|
|
37
|
+
|
|
38
|
+
export type PlainProps = Record<PropKey, PlainPropValue>;
|