@elementor/editor-default-styles 4.3.0-1042
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/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +800 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +802 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +72 -0
- package/src/allowed-tags.ts +36 -0
- package/src/api.ts +23 -0
- package/src/components/default-styles-panel-content.tsx +307 -0
- package/src/components/populate-store.tsx +34 -0
- package/src/components/tag-chip.tsx +99 -0
- package/src/components/tag-state-menu.tsx +31 -0
- package/src/default-styles-open-gate.tsx +99 -0
- package/src/default-styles-panel.tsx +32 -0
- package/src/default-styles-provider.ts +96 -0
- package/src/hooks/use-default-styles-action-props.ts +15 -0
- package/src/index.ts +1 -0
- package/src/init.ts +44 -0
- package/src/load-default-styles.ts +16 -0
- package/src/panel-interactions.ts +24 -0
- package/src/save-default-styles.ts +57 -0
- package/src/store.ts +141 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { type StyleDefinitionState } from '@elementor/editor-styles';
|
|
4
|
+
import { DotsVerticalIcon } from '@elementor/icons';
|
|
5
|
+
import {
|
|
6
|
+
type AutocompleteRenderGetTagProps,
|
|
7
|
+
bindTrigger,
|
|
8
|
+
Chip,
|
|
9
|
+
Stack,
|
|
10
|
+
type Theme,
|
|
11
|
+
Typography,
|
|
12
|
+
UnstableChipGroup,
|
|
13
|
+
usePopupState,
|
|
14
|
+
} from '@elementor/ui';
|
|
15
|
+
import { __ } from '@wordpress/i18n';
|
|
16
|
+
|
|
17
|
+
import { TagStateMenu } from './tag-state-menu';
|
|
18
|
+
|
|
19
|
+
const CHIP_SIZE = 'tiny';
|
|
20
|
+
|
|
21
|
+
type TagChipProps = {
|
|
22
|
+
label: string;
|
|
23
|
+
chipProps: ReturnType< AutocompleteRenderGetTagProps >;
|
|
24
|
+
activeState: StyleDefinitionState | null;
|
|
25
|
+
onSelectState: ( state: StyleDefinitionState | null ) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function TagChip( { label, chipProps, activeState, onSelectState }: TagChipProps ) {
|
|
29
|
+
const popupState = usePopupState( {
|
|
30
|
+
variant: 'popover',
|
|
31
|
+
popupId: 'tag-state-menu',
|
|
32
|
+
} );
|
|
33
|
+
const [ chipRef, setChipRef ] = useState< HTMLElement | null >( null );
|
|
34
|
+
|
|
35
|
+
const { onDelete: _onDelete, ...chipGroupProps } = chipProps;
|
|
36
|
+
const isShowingState = Boolean( activeState );
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<UnstableChipGroup
|
|
41
|
+
ref={ setChipRef }
|
|
42
|
+
{ ...chipGroupProps }
|
|
43
|
+
aria-label={ `Edit ${ label }` }
|
|
44
|
+
role="group"
|
|
45
|
+
sx={ ( theme: Theme ) => ( {
|
|
46
|
+
'&.MuiChipGroup-root.MuiAutocomplete-tag': {
|
|
47
|
+
margin: theme.spacing( 0.125 ),
|
|
48
|
+
},
|
|
49
|
+
} ) }
|
|
50
|
+
>
|
|
51
|
+
<Chip
|
|
52
|
+
size={ CHIP_SIZE }
|
|
53
|
+
label={ label }
|
|
54
|
+
variant={ isShowingState ? 'standard' : 'filled' }
|
|
55
|
+
shape="rounded"
|
|
56
|
+
color="default"
|
|
57
|
+
onClick={ () => {
|
|
58
|
+
if ( isShowingState ) {
|
|
59
|
+
onSelectState( null );
|
|
60
|
+
}
|
|
61
|
+
} }
|
|
62
|
+
sx={ ( theme: Theme ) => ( {
|
|
63
|
+
lineHeight: 1,
|
|
64
|
+
borderRadius: `${ theme.shape.borderRadius * 0.75 }px`,
|
|
65
|
+
} ) }
|
|
66
|
+
/>
|
|
67
|
+
<Chip
|
|
68
|
+
icon={ isShowingState ? undefined : <DotsVerticalIcon fontSize="tiny" /> }
|
|
69
|
+
size={ CHIP_SIZE }
|
|
70
|
+
label={
|
|
71
|
+
isShowingState ? (
|
|
72
|
+
<Stack direction="row" gap={ 0.5 } alignItems="center">
|
|
73
|
+
<Typography variant="inherit">{ activeState }</Typography>
|
|
74
|
+
<DotsVerticalIcon fontSize="tiny" />
|
|
75
|
+
</Stack>
|
|
76
|
+
) : undefined
|
|
77
|
+
}
|
|
78
|
+
variant="filled"
|
|
79
|
+
shape="rounded"
|
|
80
|
+
color="default"
|
|
81
|
+
{ ...bindTrigger( popupState ) }
|
|
82
|
+
aria-label={ __( 'Open tag state menu', 'elementor' ) }
|
|
83
|
+
sx={ ( theme: Theme ) => ( {
|
|
84
|
+
borderRadius: `${ theme.shape.borderRadius * 0.75 }px`,
|
|
85
|
+
paddingRight: 0,
|
|
86
|
+
...( ! isShowingState ? { paddingLeft: 0 } : {} ),
|
|
87
|
+
'.MuiChip-label': isShowingState ? { paddingRight: 0 } : { padding: 0 },
|
|
88
|
+
} ) }
|
|
89
|
+
/>
|
|
90
|
+
</UnstableChipGroup>
|
|
91
|
+
<TagStateMenu
|
|
92
|
+
popupState={ popupState }
|
|
93
|
+
anchorEl={ chipRef }
|
|
94
|
+
activeState={ activeState }
|
|
95
|
+
onSelectState={ onSelectState }
|
|
96
|
+
/>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { DEFAULT_PSEUDO_STATES, PseudoStateMenuItems } from '@elementor/editor-editing-panel';
|
|
3
|
+
import { type StyleDefinitionState } from '@elementor/editor-styles';
|
|
4
|
+
import { bindMenu, Menu, type PopupState } from '@elementor/ui';
|
|
5
|
+
|
|
6
|
+
type TagStateMenuProps = {
|
|
7
|
+
popupState: PopupState;
|
|
8
|
+
anchorEl: HTMLElement | null;
|
|
9
|
+
activeState: StyleDefinitionState | null;
|
|
10
|
+
onSelectState: ( state: StyleDefinitionState | null ) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function TagStateMenu( { popupState, anchorEl, activeState, onSelectState }: TagStateMenuProps ) {
|
|
14
|
+
return (
|
|
15
|
+
<Menu
|
|
16
|
+
MenuListProps={ { dense: true, sx: { minWidth: '160px' } } }
|
|
17
|
+
{ ...bindMenu( popupState ) }
|
|
18
|
+
anchorEl={ anchorEl }
|
|
19
|
+
anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
|
|
20
|
+
transformOrigin={ { horizontal: 'left', vertical: -4 } }
|
|
21
|
+
disableAutoFocusItem
|
|
22
|
+
>
|
|
23
|
+
<PseudoStateMenuItems
|
|
24
|
+
states={ DEFAULT_PSEUDO_STATES }
|
|
25
|
+
activeState={ activeState }
|
|
26
|
+
onSelectState={ onSelectState }
|
|
27
|
+
onClose={ popupState.close }
|
|
28
|
+
/>
|
|
29
|
+
</Menu>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
__useActiveDocument as useActiveDocument,
|
|
5
|
+
__useActiveDocumentActions as useActiveDocumentActions,
|
|
6
|
+
} from '@elementor/editor-documents';
|
|
7
|
+
import { SaveChangesDialog, ThemeProvider, useDialog } from '@elementor/editor-ui';
|
|
8
|
+
import { __ } from '@wordpress/i18n';
|
|
9
|
+
|
|
10
|
+
import { usePanelActions } from './default-styles-panel';
|
|
11
|
+
|
|
12
|
+
export const EVENT_REQUEST_OPEN_DEFAULT_STYLES = 'elementor/default-styles/request-open';
|
|
13
|
+
|
|
14
|
+
export function DefaultStylesOpenGate() {
|
|
15
|
+
const { open } = usePanelActions();
|
|
16
|
+
const document = useActiveDocument();
|
|
17
|
+
const { save: saveDocument } = useActiveDocumentActions();
|
|
18
|
+
const { open: openSaveDialog, close: closeSaveDialog, isOpen: isSaveDialogOpen } = useDialog();
|
|
19
|
+
|
|
20
|
+
const documentRef = useRef( document );
|
|
21
|
+
documentRef.current = document;
|
|
22
|
+
|
|
23
|
+
const pendingOpenRef = useRef< ( () => void ) | null >( null );
|
|
24
|
+
|
|
25
|
+
const gatedOpen = useCallback(
|
|
26
|
+
( onClean: () => void ) => {
|
|
27
|
+
if ( documentRef.current?.isDirty ) {
|
|
28
|
+
pendingOpenRef.current = onClean;
|
|
29
|
+
openSaveDialog();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onClean();
|
|
34
|
+
},
|
|
35
|
+
[ openSaveDialog ]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const handleSaveAndContinue = useCallback( async () => {
|
|
39
|
+
try {
|
|
40
|
+
await saveDocument();
|
|
41
|
+
closeSaveDialog();
|
|
42
|
+
pendingOpenRef.current?.();
|
|
43
|
+
pendingOpenRef.current = null;
|
|
44
|
+
} catch {
|
|
45
|
+
// Keep dialog open.
|
|
46
|
+
}
|
|
47
|
+
}, [ saveDocument, closeSaveDialog ] );
|
|
48
|
+
|
|
49
|
+
const handleStayHere = useCallback( () => {
|
|
50
|
+
closeSaveDialog();
|
|
51
|
+
pendingOpenRef.current = null;
|
|
52
|
+
}, [ closeSaveDialog ] );
|
|
53
|
+
|
|
54
|
+
useEffect( () => {
|
|
55
|
+
const handler = () => {
|
|
56
|
+
gatedOpen( () => {
|
|
57
|
+
void open();
|
|
58
|
+
} );
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
window.addEventListener( EVENT_REQUEST_OPEN_DEFAULT_STYLES, handler );
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
window.removeEventListener( EVENT_REQUEST_OPEN_DEFAULT_STYLES, handler );
|
|
65
|
+
};
|
|
66
|
+
}, [ gatedOpen, open ] );
|
|
67
|
+
|
|
68
|
+
if ( ! isSaveDialogOpen ) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<ThemeProvider>
|
|
74
|
+
<SaveChangesDialog>
|
|
75
|
+
<SaveChangesDialog.Title>{ __( 'You have unsaved changes', 'elementor' ) }</SaveChangesDialog.Title>
|
|
76
|
+
<SaveChangesDialog.Content>
|
|
77
|
+
<SaveChangesDialog.ContentText sx={ { mb: 2 } }>
|
|
78
|
+
{ __(
|
|
79
|
+
"To open Default Styles, save your page first. You can't continue without saving.",
|
|
80
|
+
'elementor'
|
|
81
|
+
) }
|
|
82
|
+
</SaveChangesDialog.ContentText>
|
|
83
|
+
</SaveChangesDialog.Content>
|
|
84
|
+
<SaveChangesDialog.Actions
|
|
85
|
+
actions={ {
|
|
86
|
+
cancel: {
|
|
87
|
+
label: __( 'Stay here', 'elementor' ),
|
|
88
|
+
action: handleStayHere,
|
|
89
|
+
},
|
|
90
|
+
confirm: {
|
|
91
|
+
label: __( 'Save & Continue', 'elementor' ),
|
|
92
|
+
action: handleSaveAndContinue,
|
|
93
|
+
},
|
|
94
|
+
} }
|
|
95
|
+
/>
|
|
96
|
+
</SaveChangesDialog>
|
|
97
|
+
</ThemeProvider>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { __createPanel as createPanel } from '@elementor/editor-panels';
|
|
3
|
+
import { changeEditMode } from '@elementor/editor-v1-adapters';
|
|
4
|
+
|
|
5
|
+
import { DefaultStylesPanelContent } from './components/default-styles-panel-content';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_STYLES_PANEL_ID = 'default-styles';
|
|
8
|
+
|
|
9
|
+
export const { panel, usePanelStatus, usePanelActions } = createPanel( {
|
|
10
|
+
id: DEFAULT_STYLES_PANEL_ID,
|
|
11
|
+
component: DefaultStylesPanelRoot,
|
|
12
|
+
allowedEditModes: [ 'edit', DEFAULT_STYLES_PANEL_ID ],
|
|
13
|
+
onOpen: () => {
|
|
14
|
+
changeEditMode( DEFAULT_STYLES_PANEL_ID );
|
|
15
|
+
},
|
|
16
|
+
onClose: async () => {
|
|
17
|
+
changeEditMode( 'edit' );
|
|
18
|
+
},
|
|
19
|
+
isOpenPreviousElement: true,
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
function DefaultStylesPanelRoot() {
|
|
23
|
+
const { close } = usePanelActions();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<DefaultStylesPanelContent
|
|
27
|
+
onRequestClose={ () => {
|
|
28
|
+
void close();
|
|
29
|
+
} }
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type StyleDefinition, type StyleDefinitionID } from '@elementor/editor-styles';
|
|
2
|
+
import { createStylesProvider } from '@elementor/editor-styles-repository';
|
|
3
|
+
import {
|
|
4
|
+
__dispatch as dispatch,
|
|
5
|
+
__getState as getState,
|
|
6
|
+
__subscribeWithSelector as subscribeWithSelector,
|
|
7
|
+
} from '@elementor/store';
|
|
8
|
+
import { __ } from '@wordpress/i18n';
|
|
9
|
+
|
|
10
|
+
import { getAllowedDefaultStyleTags } from './allowed-tags';
|
|
11
|
+
import { selectData, slice } from './store';
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_STYLES_PROVIDER_KEY = 'default-styles';
|
|
14
|
+
export const DEFAULT_STYLES_CSS_NAME_PREFIX = 'e-default-';
|
|
15
|
+
|
|
16
|
+
const resolveCssName = ( id: StyleDefinitionID ) => `${ DEFAULT_STYLES_CSS_NAME_PREFIX }${ id }`;
|
|
17
|
+
|
|
18
|
+
const placeholderDefinition = ( id: StyleDefinitionID ): StyleDefinition => ( {
|
|
19
|
+
id,
|
|
20
|
+
label: id,
|
|
21
|
+
type: 'class',
|
|
22
|
+
variants: [],
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
const asClassDefinition = ( style: StyleDefinition ): StyleDefinition => ( {
|
|
26
|
+
...style,
|
|
27
|
+
type: 'class',
|
|
28
|
+
} );
|
|
29
|
+
|
|
30
|
+
export const defaultStylesStylesProvider = createStylesProvider( {
|
|
31
|
+
key: DEFAULT_STYLES_PROVIDER_KEY,
|
|
32
|
+
priority: 15,
|
|
33
|
+
labels: {
|
|
34
|
+
singular: __( 'tag', 'elementor' ),
|
|
35
|
+
plural: __( 'tags', 'elementor' ),
|
|
36
|
+
},
|
|
37
|
+
subscribe: ( cb ) => subscribeWithStates( cb ),
|
|
38
|
+
actions: {
|
|
39
|
+
all: () =>
|
|
40
|
+
getAllowedDefaultStyleTags().map( ( tag ) => {
|
|
41
|
+
const style = selectData( getState() )[ tag ];
|
|
42
|
+
|
|
43
|
+
return style ? asClassDefinition( style ) : placeholderDefinition( tag );
|
|
44
|
+
} ),
|
|
45
|
+
get: ( id ) => {
|
|
46
|
+
const style = selectData( getState() )[ id ];
|
|
47
|
+
|
|
48
|
+
return style ? asClassDefinition( style ) : placeholderDefinition( id );
|
|
49
|
+
},
|
|
50
|
+
resolveCssName,
|
|
51
|
+
update: ( payload ) => {
|
|
52
|
+
dispatch(
|
|
53
|
+
slice.actions.update( {
|
|
54
|
+
style: payload,
|
|
55
|
+
} )
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
delete: ( id ) => {
|
|
59
|
+
dispatch( slice.actions.deleteTag( id ) );
|
|
60
|
+
},
|
|
61
|
+
updateProps: ( args ) => {
|
|
62
|
+
dispatch(
|
|
63
|
+
slice.actions.updateProps( {
|
|
64
|
+
id: args.id,
|
|
65
|
+
meta: args.meta,
|
|
66
|
+
props: args.props,
|
|
67
|
+
mode: args.mode,
|
|
68
|
+
} )
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
updateCustomCss: ( args ) => {
|
|
72
|
+
dispatch(
|
|
73
|
+
slice.actions.updateProps( {
|
|
74
|
+
id: args.id,
|
|
75
|
+
meta: args.meta,
|
|
76
|
+
custom_css: args.custom_css,
|
|
77
|
+
props: {},
|
|
78
|
+
} )
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
const subscribeWithStates = (
|
|
85
|
+
cb: ( previous: Record< string, StyleDefinition >, current: Record< string, StyleDefinition > ) => void
|
|
86
|
+
) => {
|
|
87
|
+
let previousState = selectData( getState() );
|
|
88
|
+
|
|
89
|
+
return subscribeWithSelector(
|
|
90
|
+
( state ) => selectData( state ),
|
|
91
|
+
( currentState ) => {
|
|
92
|
+
cb( previousState, currentState );
|
|
93
|
+
previousState = currentState;
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ActionProps } from '@elementor/editor-app-bar';
|
|
2
|
+
import { TextIcon } from '@elementor/icons';
|
|
3
|
+
import { __ } from '@wordpress/i18n';
|
|
4
|
+
|
|
5
|
+
import { EVENT_REQUEST_OPEN_DEFAULT_STYLES } from '../default-styles-open-gate';
|
|
6
|
+
|
|
7
|
+
export function useDefaultStylesActionProps(): ActionProps {
|
|
8
|
+
return {
|
|
9
|
+
title: __( 'Default Styles', 'elementor' ),
|
|
10
|
+
icon: TextIcon,
|
|
11
|
+
onClick: () => {
|
|
12
|
+
window.dispatchEvent( new CustomEvent( EVENT_REQUEST_OPEN_DEFAULT_STYLES ) );
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { init } from './init';
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { injectIntoLogic } from '@elementor/editor';
|
|
2
|
+
import { toolsMenu } from '@elementor/editor-app-bar';
|
|
3
|
+
import { registerElementPanelDefaults, STYLE_SECTION_NAMES } from '@elementor/editor-editing-panel';
|
|
4
|
+
import { __registerPanel as registerPanel } from '@elementor/editor-panels';
|
|
5
|
+
import { stylesRepository } from '@elementor/editor-styles-repository';
|
|
6
|
+
import { __registerSlice as registerSlice } from '@elementor/store';
|
|
7
|
+
|
|
8
|
+
import { PopulateStore } from './components/populate-store';
|
|
9
|
+
import { DefaultStylesOpenGate } from './default-styles-open-gate';
|
|
10
|
+
import { panel } from './default-styles-panel';
|
|
11
|
+
import { defaultStylesStylesProvider } from './default-styles-provider';
|
|
12
|
+
import { useDefaultStylesActionProps } from './hooks/use-default-styles-action-props';
|
|
13
|
+
import { slice } from './store';
|
|
14
|
+
|
|
15
|
+
export function init() {
|
|
16
|
+
registerSlice( slice );
|
|
17
|
+
|
|
18
|
+
registerElementPanelDefaults( 'default-style', {
|
|
19
|
+
defaultTab: 'style',
|
|
20
|
+
defaultSectionsExpanded: {
|
|
21
|
+
style: [ ...STYLE_SECTION_NAMES ],
|
|
22
|
+
},
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
registerPanel( panel );
|
|
26
|
+
|
|
27
|
+
stylesRepository.register( defaultStylesStylesProvider );
|
|
28
|
+
|
|
29
|
+
toolsMenu.registerAction( {
|
|
30
|
+
id: 'default-styles-button',
|
|
31
|
+
priority: 4,
|
|
32
|
+
useProps: useDefaultStylesActionProps,
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
injectIntoLogic( {
|
|
36
|
+
id: 'default-styles-populate-store',
|
|
37
|
+
component: PopulateStore,
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
injectIntoLogic( {
|
|
41
|
+
id: 'default-styles-open-gate',
|
|
42
|
+
component: DefaultStylesOpenGate,
|
|
43
|
+
} );
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { __dispatch as dispatch } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { apiClient } from './api';
|
|
4
|
+
import { slice } from './store';
|
|
5
|
+
|
|
6
|
+
export async function loadDefaultStyles() {
|
|
7
|
+
const response = await apiClient.all();
|
|
8
|
+
|
|
9
|
+
const items = response.data.data;
|
|
10
|
+
|
|
11
|
+
dispatch(
|
|
12
|
+
slice.actions.load( {
|
|
13
|
+
data: items,
|
|
14
|
+
} )
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type ExtendedWindow = Window & {
|
|
2
|
+
$e?: {
|
|
3
|
+
components?: {
|
|
4
|
+
get?: ( name: 'panel' ) =>
|
|
5
|
+
| {
|
|
6
|
+
blockUserInteractions?: () => void;
|
|
7
|
+
unblockUserInteractions?: () => void;
|
|
8
|
+
}
|
|
9
|
+
| undefined;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function blockPanelInteractions() {
|
|
15
|
+
const extendedWindow = window as unknown as ExtendedWindow;
|
|
16
|
+
|
|
17
|
+
extendedWindow.$e?.components?.get?.( 'panel' )?.blockUserInteractions?.();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function unblockPanelInteractions() {
|
|
21
|
+
const extendedWindow = window as unknown as ExtendedWindow;
|
|
22
|
+
|
|
23
|
+
extendedWindow.$e?.components?.get?.( 'panel' )?.unblockUserInteractions?.();
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
|
|
2
|
+
import { hash } from '@elementor/utils';
|
|
3
|
+
|
|
4
|
+
import { type AllowedHtmlTag } from './allowed-tags';
|
|
5
|
+
import { apiClient } from './api';
|
|
6
|
+
import { selectData, selectInitialData, selectIsDirty, slice, type StateWithDefaultStyles } from './store';
|
|
7
|
+
|
|
8
|
+
function getChangedTags( state: StateWithDefaultStyles ): AllowedHtmlTag[] {
|
|
9
|
+
const current = selectData( state );
|
|
10
|
+
const initial = selectInitialData( state );
|
|
11
|
+
|
|
12
|
+
const changed: AllowedHtmlTag[] = [];
|
|
13
|
+
|
|
14
|
+
Object.keys( current ).forEach( ( tag ) => {
|
|
15
|
+
const currentStyle = current[ tag ];
|
|
16
|
+
const initialStyle = initial[ tag ];
|
|
17
|
+
|
|
18
|
+
if ( ! initialStyle || hash( currentStyle ) !== hash( initialStyle ) ) {
|
|
19
|
+
changed.push( tag as AllowedHtmlTag );
|
|
20
|
+
}
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
Object.keys( initial ).forEach( ( tag ) => {
|
|
24
|
+
if ( ! current[ tag ] ) {
|
|
25
|
+
changed.push( tag as AllowedHtmlTag );
|
|
26
|
+
}
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
return changed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function saveDefaultStyles() {
|
|
33
|
+
const state = getState() as StateWithDefaultStyles;
|
|
34
|
+
|
|
35
|
+
if ( ! selectIsDirty( state ) ) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = selectData( state );
|
|
40
|
+
const changedTags = getChangedTags( state );
|
|
41
|
+
|
|
42
|
+
await Promise.all(
|
|
43
|
+
changedTags.map( async ( tag ) => {
|
|
44
|
+
const style = data[ tag ];
|
|
45
|
+
|
|
46
|
+
if ( ! style || style.variants.length === 0 ) {
|
|
47
|
+
await apiClient.delete( tag );
|
|
48
|
+
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await apiClient.put( tag, style.variants );
|
|
53
|
+
} )
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
dispatch( slice.actions.commit() );
|
|
57
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { type Props } from '@elementor/editor-props';
|
|
2
|
+
import {
|
|
3
|
+
type CustomCss,
|
|
4
|
+
getVariantByMeta,
|
|
5
|
+
type StyleDefinition,
|
|
6
|
+
type StyleDefinitionID,
|
|
7
|
+
type StyleDefinitionVariant,
|
|
8
|
+
} from '@elementor/editor-styles';
|
|
9
|
+
import { type UpdateActionPayload } from '@elementor/editor-styles-repository';
|
|
10
|
+
import {
|
|
11
|
+
__createSelector as createSelector,
|
|
12
|
+
__createSlice as createSlice,
|
|
13
|
+
type PayloadAction,
|
|
14
|
+
type SliceState,
|
|
15
|
+
} from '@elementor/store';
|
|
16
|
+
|
|
17
|
+
type DefaultStylesState = {
|
|
18
|
+
data: Record< StyleDefinitionID, StyleDefinition >;
|
|
19
|
+
initialData: Record< StyleDefinitionID, StyleDefinition >;
|
|
20
|
+
isDirty: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const initialState: DefaultStylesState = {
|
|
24
|
+
data: {},
|
|
25
|
+
initialData: {},
|
|
26
|
+
isDirty: false,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type StateWithDefaultStyles = SliceState< typeof slice >;
|
|
30
|
+
|
|
31
|
+
const SLICE_NAME = 'defaultStyles';
|
|
32
|
+
|
|
33
|
+
export const slice = createSlice( {
|
|
34
|
+
name: SLICE_NAME,
|
|
35
|
+
initialState,
|
|
36
|
+
reducers: {
|
|
37
|
+
load(
|
|
38
|
+
state,
|
|
39
|
+
{
|
|
40
|
+
payload: { data },
|
|
41
|
+
}: PayloadAction< {
|
|
42
|
+
data: Record< StyleDefinitionID, StyleDefinition >;
|
|
43
|
+
} >
|
|
44
|
+
) {
|
|
45
|
+
state.initialData = structuredClone( data );
|
|
46
|
+
state.data = structuredClone( data );
|
|
47
|
+
state.isDirty = false;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
update( state, { payload }: PayloadAction< { style: UpdateActionPayload } > ) {
|
|
51
|
+
state.data[ payload.style.id ] = {
|
|
52
|
+
...state.data[ payload.style.id ],
|
|
53
|
+
...payload.style,
|
|
54
|
+
} as StyleDefinition;
|
|
55
|
+
state.isDirty = true;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
updateProps(
|
|
59
|
+
state,
|
|
60
|
+
{
|
|
61
|
+
payload,
|
|
62
|
+
}: PayloadAction< {
|
|
63
|
+
id: StyleDefinitionID;
|
|
64
|
+
meta: StyleDefinitionVariant[ 'meta' ];
|
|
65
|
+
props: Props;
|
|
66
|
+
custom_css?: CustomCss | null;
|
|
67
|
+
mode?: 'merge' | 'replace';
|
|
68
|
+
} >
|
|
69
|
+
) {
|
|
70
|
+
const style = state.data[ payload.id ] ?? {
|
|
71
|
+
id: payload.id,
|
|
72
|
+
label: payload.id,
|
|
73
|
+
type: 'class' as const,
|
|
74
|
+
variants: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const variant = getVariantByMeta( style, payload.meta );
|
|
78
|
+
let customCss = ( 'custom_css' in payload ? payload.custom_css : variant?.custom_css ) ?? null;
|
|
79
|
+
customCss = customCss?.raw ? customCss : null;
|
|
80
|
+
|
|
81
|
+
if ( variant ) {
|
|
82
|
+
const payloadProps = JSON.parse( JSON.stringify( payload.props ) ) as Props;
|
|
83
|
+
const mode = payload.mode ?? 'merge';
|
|
84
|
+
|
|
85
|
+
if ( mode === 'replace' ) {
|
|
86
|
+
variant.props = payloadProps;
|
|
87
|
+
} else {
|
|
88
|
+
const variantProps = JSON.parse( JSON.stringify( variant.props ) ) as Props;
|
|
89
|
+
variant.props = { ...variantProps, ...payloadProps };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
variant.custom_css = customCss;
|
|
93
|
+
} else {
|
|
94
|
+
style.variants.push( {
|
|
95
|
+
meta: payload.meta,
|
|
96
|
+
props: payload.props,
|
|
97
|
+
custom_css: customCss,
|
|
98
|
+
} );
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
state.data[ payload.id ] = style;
|
|
102
|
+
state.isDirty = true;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
reset( state ) {
|
|
106
|
+
state.data = structuredClone( state.initialData );
|
|
107
|
+
state.isDirty = false;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
commit( state ) {
|
|
111
|
+
state.initialData = structuredClone( state.data );
|
|
112
|
+
state.isDirty = false;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
deleteTag( state, { payload }: PayloadAction< StyleDefinitionID > ) {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
117
|
+
delete state.data[ payload ];
|
|
118
|
+
state.isDirty = true;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
} );
|
|
122
|
+
|
|
123
|
+
export const selectData = createSelector(
|
|
124
|
+
( state: StateWithDefaultStyles ) => state.defaultStyles.data,
|
|
125
|
+
( data ) => data
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
export const selectIsDirty = createSelector(
|
|
129
|
+
( state: StateWithDefaultStyles ) => state.defaultStyles.isDirty,
|
|
130
|
+
( isDirty ) => isDirty
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
export const selectInitialData = createSelector(
|
|
134
|
+
( state: StateWithDefaultStyles ) => state.defaultStyles.initialData,
|
|
135
|
+
( initialData ) => initialData
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
export const selectTagStyle = createSelector(
|
|
139
|
+
[ selectData, ( _state: StateWithDefaultStyles, id: StyleDefinitionID ) => id ],
|
|
140
|
+
( data, id ) => data[ id ]
|
|
141
|
+
);
|