@elementor/editor-editing-panel 1.30.0 → 1.32.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 +37 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +966 -630
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +870 -527
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
- package/src/components/creatable-autocomplete/autocomplete-option-internal-properties.ts +21 -0
- package/src/components/creatable-autocomplete/creatable-autocomplete.tsx +175 -0
- package/src/components/creatable-autocomplete/index.ts +3 -0
- package/src/components/creatable-autocomplete/types.ts +38 -0
- package/src/components/creatable-autocomplete/use-autocomplete-change.ts +75 -0
- package/src/components/creatable-autocomplete/use-autocomplete-states.ts +42 -0
- package/src/components/creatable-autocomplete/use-create-option.ts +45 -0
- package/src/components/creatable-autocomplete/use-filter-options.ts +50 -0
- package/src/components/css-classes/css-class-item.tsx +2 -2
- package/src/components/css-classes/css-class-selector.tsx +44 -27
- package/src/components/editing-panel-tabs.tsx +19 -14
- package/src/components/style-sections/position-section/offset-field.tsx +22 -0
- package/src/components/style-sections/position-section/position-section.tsx +4 -0
- package/src/components/style-tab.tsx +26 -3
- package/src/contexts/scroll-context.tsx +60 -0
- package/src/hooks/use-normalized-inheritance-chain-items.tsx +68 -0
- package/src/index.ts +1 -0
- package/src/styles-inheritance/styles-inheritance-indicator.tsx +65 -19
- package/src/styles-inheritance/styles-inheritance-infotip.tsx +50 -0
- package/src/sync/get-experiments-config.ts +7 -0
- package/src/sync/types.ts +7 -0
- package/src/components/multi-combobox.tsx +0 -165
|
@@ -4,8 +4,9 @@ import { Divider, Stack, Tab, TabPanel, Tabs, useTabs } from '@elementor/ui';
|
|
|
4
4
|
import { __ } from '@wordpress/i18n';
|
|
5
5
|
|
|
6
6
|
import { useElement } from '../contexts/element-context';
|
|
7
|
+
import { ScrollProvider } from '../contexts/scroll-context';
|
|
7
8
|
import { SettingsTab } from './settings-tab';
|
|
8
|
-
import { StyleTab } from './style-tab';
|
|
9
|
+
import { stickyHeaderStyles, StyleTab } from './style-tab';
|
|
9
10
|
|
|
10
11
|
type TabValue = 'settings' | 'style';
|
|
11
12
|
|
|
@@ -18,19 +19,23 @@ export const EditingPanelTabs = () => {
|
|
|
18
19
|
// When switching between elements, the local states should be reset. We are using key to rerender the tabs.
|
|
19
20
|
// Reference: https://react.dev/learn/preserving-and-resetting-state#resetting-a-form-with-a-key
|
|
20
21
|
<Fragment key={ element.id }>
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
<ScrollProvider>
|
|
23
|
+
<Stack direction="column" sx={ { width: '100%' } }>
|
|
24
|
+
<Stack sx={ { ...stickyHeaderStyles, top: 0 } }>
|
|
25
|
+
<Tabs variant="fullWidth" size="small" sx={ { mt: 0.5 } } { ...getTabsProps() }>
|
|
26
|
+
<Tab label={ __( 'General', 'elementor' ) } { ...getTabProps( 'settings' ) } />
|
|
27
|
+
<Tab label={ __( 'Style', 'elementor' ) } { ...getTabProps( 'style' ) } />
|
|
28
|
+
</Tabs>
|
|
29
|
+
<Divider />
|
|
30
|
+
</Stack>
|
|
31
|
+
<TabPanel { ...getTabPanelProps( 'settings' ) } disablePadding>
|
|
32
|
+
<SettingsTab />
|
|
33
|
+
</TabPanel>
|
|
34
|
+
<TabPanel { ...getTabPanelProps( 'style' ) } disablePadding>
|
|
35
|
+
<StyleTab />
|
|
36
|
+
</TabPanel>
|
|
37
|
+
</Stack>
|
|
38
|
+
</ScrollProvider>
|
|
34
39
|
</Fragment>
|
|
35
40
|
);
|
|
36
41
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { SizeControl } from '@elementor/editor-controls';
|
|
3
|
+
import { Grid } from '@elementor/ui';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { StylesField } from '../../../controls-registry/styles-field';
|
|
7
|
+
import { ControlLabel } from '../../control-label';
|
|
8
|
+
|
|
9
|
+
export const OffsetField = () => {
|
|
10
|
+
return (
|
|
11
|
+
<StylesField bind="scroll-margin-top">
|
|
12
|
+
<Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
|
|
13
|
+
<Grid item xs={ 6 }>
|
|
14
|
+
<ControlLabel>{ __( 'Anchor offset', 'elementor' ) }</ControlLabel>
|
|
15
|
+
</Grid>
|
|
16
|
+
<Grid item xs={ 6 }>
|
|
17
|
+
<SizeControl units={ [ 'px', 'em', 'rem', 'vw', 'vh' ] } />
|
|
18
|
+
</Grid>
|
|
19
|
+
</Grid>
|
|
20
|
+
</StylesField>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -5,8 +5,10 @@ import { useSessionStorage } from '@elementor/session';
|
|
|
5
5
|
import { useStyle } from '../../../contexts/style-context';
|
|
6
6
|
import { useStylesField } from '../../../hooks/use-styles-field';
|
|
7
7
|
import { useStylesFields } from '../../../hooks/use-styles-fields';
|
|
8
|
+
import { PanelDivider } from '../../panel-divider';
|
|
8
9
|
import { SectionContent } from '../../section-content';
|
|
9
10
|
import { DimensionsField } from './dimensions-field';
|
|
11
|
+
import { OffsetField } from './offset-field';
|
|
10
12
|
import { PositionField } from './position-field';
|
|
11
13
|
import { ZIndexField } from './z-index-field';
|
|
12
14
|
|
|
@@ -66,6 +68,8 @@ export const PositionSection = () => {
|
|
|
66
68
|
<ZIndexField />
|
|
67
69
|
</>
|
|
68
70
|
) : null }
|
|
71
|
+
<PanelDivider />
|
|
72
|
+
<OffsetField />
|
|
69
73
|
</SectionContent>
|
|
70
74
|
);
|
|
71
75
|
};
|
|
@@ -4,11 +4,12 @@ import { CLASSES_PROP_KEY } from '@elementor/editor-props';
|
|
|
4
4
|
import { useActiveBreakpoint } from '@elementor/editor-responsive';
|
|
5
5
|
import { type StyleDefinitionID, type StyleDefinitionState } from '@elementor/editor-styles';
|
|
6
6
|
import { SessionStorageProvider } from '@elementor/session';
|
|
7
|
-
import { Divider } from '@elementor/ui';
|
|
7
|
+
import { Divider, Stack } from '@elementor/ui';
|
|
8
8
|
import { __ } from '@wordpress/i18n';
|
|
9
9
|
|
|
10
10
|
import { ClassesPropProvider } from '../contexts/classes-prop-context';
|
|
11
11
|
import { useElement } from '../contexts/element-context';
|
|
12
|
+
import { useScrollDirection } from '../contexts/scroll-context';
|
|
12
13
|
import { StyleProvider } from '../contexts/style-context';
|
|
13
14
|
import { StyleInheritanceProvider } from '../contexts/styles-inheritance-context';
|
|
14
15
|
import { useActiveStyleDefId } from '../hooks/use-active-style-def-id';
|
|
@@ -24,6 +25,16 @@ import { SizeSection } from './style-sections/size-section/size-section';
|
|
|
24
25
|
import { SpacingSection } from './style-sections/spacing-section/spacing-section';
|
|
25
26
|
import { TypographySection } from './style-sections/typography-section/typography-section';
|
|
26
27
|
|
|
28
|
+
const TABS_HEADER_HEIGHT = '37px';
|
|
29
|
+
|
|
30
|
+
export const stickyHeaderStyles = {
|
|
31
|
+
position: 'sticky',
|
|
32
|
+
zIndex: 1,
|
|
33
|
+
opacity: 1,
|
|
34
|
+
backgroundColor: 'background.default',
|
|
35
|
+
transition: 'top 300ms ease',
|
|
36
|
+
};
|
|
37
|
+
|
|
27
38
|
export const StyleTab = () => {
|
|
28
39
|
const currentClassesProp = useCurrentClassesProp();
|
|
29
40
|
const [ activeStyleDefId, setActiveStyleDefId ] = useActiveStyleDefId( currentClassesProp );
|
|
@@ -43,8 +54,10 @@ export const StyleTab = () => {
|
|
|
43
54
|
>
|
|
44
55
|
<SessionStorageProvider prefix={ activeStyleDefId ?? '' }>
|
|
45
56
|
<StyleInheritanceProvider>
|
|
46
|
-
<
|
|
47
|
-
|
|
57
|
+
<ClassesHeader>
|
|
58
|
+
<CssClassSelector />
|
|
59
|
+
<Divider />
|
|
60
|
+
</ClassesHeader>
|
|
48
61
|
<SectionsList>
|
|
49
62
|
<Section title={ __( 'Layout', 'elementor' ) }>
|
|
50
63
|
<LayoutSection />
|
|
@@ -78,6 +91,16 @@ export const StyleTab = () => {
|
|
|
78
91
|
);
|
|
79
92
|
};
|
|
80
93
|
|
|
94
|
+
function ClassesHeader( { children }: { children: React.ReactNode } ) {
|
|
95
|
+
const scrollDirection = useScrollDirection();
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Stack sx={ { ...stickyHeaderStyles, top: scrollDirection === 'up' ? TABS_HEADER_HEIGHT : 0 } }>
|
|
99
|
+
{ children }
|
|
100
|
+
</Stack>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
function useCurrentClassesProp(): string {
|
|
82
105
|
const { elementType } = useElement();
|
|
83
106
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { styled } from '@elementor/ui';
|
|
4
|
+
|
|
5
|
+
type ScrollDirection = 'up' | 'down';
|
|
6
|
+
|
|
7
|
+
type ScrollContextValue = {
|
|
8
|
+
direction: ScrollDirection;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const ScrollContext = createContext< ScrollContextValue | undefined >( undefined );
|
|
12
|
+
|
|
13
|
+
const ScrollPanel = styled( 'div' )`
|
|
14
|
+
height: 100%;
|
|
15
|
+
overflow-y: auto;
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const DEFAULT_SCROLL_DIRECTION: ScrollDirection = 'up';
|
|
19
|
+
|
|
20
|
+
export function ScrollProvider( { children }: { children: ReactNode } ) {
|
|
21
|
+
const [ direction, setDirection ] = useState< ScrollDirection >( DEFAULT_SCROLL_DIRECTION );
|
|
22
|
+
const ref = useRef< HTMLDivElement >( null );
|
|
23
|
+
const scrollPos = useRef< number >( 0 );
|
|
24
|
+
|
|
25
|
+
useEffect( () => {
|
|
26
|
+
const scrollElement = ref.current;
|
|
27
|
+
|
|
28
|
+
if ( ! scrollElement ) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleScroll = () => {
|
|
33
|
+
const { scrollTop } = scrollElement;
|
|
34
|
+
|
|
35
|
+
if ( scrollTop > scrollPos.current ) {
|
|
36
|
+
setDirection( 'down' );
|
|
37
|
+
} else if ( scrollTop < scrollPos.current ) {
|
|
38
|
+
setDirection( 'up' );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
scrollPos.current = scrollTop;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
scrollElement.addEventListener( 'scroll', handleScroll );
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
scrollElement.removeEventListener( 'scroll', handleScroll );
|
|
48
|
+
};
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<ScrollContext.Provider value={ { direction } }>
|
|
53
|
+
<ScrollPanel ref={ ref }>{ children }</ScrollPanel>
|
|
54
|
+
</ScrollContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useScrollDirection() {
|
|
59
|
+
return useContext( ScrollContext )?.direction ?? DEFAULT_SCROLL_DIRECTION;
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { type PropsResolver } from '@elementor/editor-canvas';
|
|
3
|
+
import { type PropKey } from '@elementor/editor-props';
|
|
4
|
+
|
|
5
|
+
import { type NormalizedItem } from '../styles-inheritance/styles-inheritance-infotip';
|
|
6
|
+
import { type SnapshotPropValue } from '../styles-inheritance/types';
|
|
7
|
+
|
|
8
|
+
const MAXIMUM_ITEMS = 2;
|
|
9
|
+
|
|
10
|
+
export const useNormalizedInheritanceChainItems = (
|
|
11
|
+
inheritanceChain: SnapshotPropValue[],
|
|
12
|
+
bind: PropKey,
|
|
13
|
+
resolve: PropsResolver
|
|
14
|
+
) => {
|
|
15
|
+
const [ items, setItems ] = useState< NormalizedItem[] >( [] );
|
|
16
|
+
|
|
17
|
+
useEffect( () => {
|
|
18
|
+
( async () => {
|
|
19
|
+
const normalizedItems = await Promise.all(
|
|
20
|
+
inheritanceChain
|
|
21
|
+
.filter( ( item ) => item.style?.label )
|
|
22
|
+
.map( ( item, index ) => normalizeInheritanceItem( item, index, bind, resolve ) )
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const validItems = normalizedItems.filter( ( item ) => item.value !== '' ).slice( 0, MAXIMUM_ITEMS );
|
|
26
|
+
|
|
27
|
+
setItems( validItems );
|
|
28
|
+
} )();
|
|
29
|
+
}, [ inheritanceChain, bind, resolve ] );
|
|
30
|
+
|
|
31
|
+
return items;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const normalizeInheritanceItem = async (
|
|
35
|
+
item: SnapshotPropValue,
|
|
36
|
+
index: number,
|
|
37
|
+
bind: PropKey,
|
|
38
|
+
resolve: PropsResolver
|
|
39
|
+
): Promise< NormalizedItem > => {
|
|
40
|
+
const state = item.variant?.meta?.state || '';
|
|
41
|
+
const label = item.style?.label || '';
|
|
42
|
+
const displayLabel = state ? `${ label }:${ state }` : label;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
id: item.style?.id ? item.style?.id + state : index,
|
|
46
|
+
breakpoint: item.variant?.meta?.breakpoint,
|
|
47
|
+
displayLabel,
|
|
48
|
+
value: await getTransformedValue( item, bind, resolve ),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const getTransformedValue = async (
|
|
53
|
+
item: SnapshotPropValue,
|
|
54
|
+
bind: PropKey,
|
|
55
|
+
resolve: PropsResolver
|
|
56
|
+
): Promise< string > => {
|
|
57
|
+
try {
|
|
58
|
+
const result = await resolve( {
|
|
59
|
+
props: {
|
|
60
|
+
[ bind ]: item.value,
|
|
61
|
+
},
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
return Object.values( result ).join( ' ' );
|
|
65
|
+
} catch {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { PopoverActionProps } from './popover-action';
|
|
|
3
3
|
export { registerControlReplacement } from './control-replacement';
|
|
4
4
|
export { injectIntoClassSelectorActions } from './components/css-classes/css-class-selector';
|
|
5
5
|
export { usePanelActions, usePanelStatus } from './panel';
|
|
6
|
+
export { type ValidationResult, type ValidationEvent } from './components/creatable-autocomplete';
|
|
6
7
|
export { controlActionsMenu } from './controls-actions';
|
|
7
8
|
|
|
8
9
|
export { init } from './init';
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { useBoundProp } from '@elementor/editor-controls';
|
|
3
4
|
import { ELEMENTS_BASE_STYLES_PROVIDER_KEY, isElementsStylesProvider } from '@elementor/editor-styles-repository';
|
|
5
|
+
import { IconButton, Infotip } from '@elementor/ui';
|
|
4
6
|
import { __ } from '@wordpress/i18n';
|
|
5
7
|
|
|
6
8
|
import { StyleIndicator } from '../components/style-indicator';
|
|
7
9
|
import { useStyle } from '../contexts/style-context';
|
|
8
10
|
import { useStylesInheritanceField } from '../contexts/styles-inheritance-context';
|
|
11
|
+
import { getExperimentsConfig } from '../sync/get-experiments-config';
|
|
12
|
+
import { StyleIndicatorInfotip } from './styles-inheritance-infotip';
|
|
9
13
|
|
|
10
14
|
export const StylesInheritanceIndicator = () => {
|
|
15
|
+
const [ open, setOpen ] = useState( false );
|
|
16
|
+
|
|
11
17
|
const { value, path } = useBoundProp();
|
|
12
18
|
const { id: currentStyleId, provider: currentStyleProvider, meta: currentStyleMeta } = useStyle();
|
|
13
19
|
|
|
@@ -27,27 +33,67 @@ export const StylesInheritanceIndicator = () => {
|
|
|
27
33
|
|
|
28
34
|
const { breakpoint, state } = variant.meta;
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
style.id === currentStyleId &&
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const isFinalValue: boolean =
|
|
37
|
+
style.id === currentStyleId && breakpoint === currentStyleMeta.breakpoint && state === currentStyleMeta.state;
|
|
38
|
+
|
|
39
|
+
const hasValue: boolean = value !== null && value !== undefined;
|
|
40
|
+
|
|
41
|
+
const label: string = getLabel( { isFinalValue, hasValue } );
|
|
42
|
+
const variantType = getVariant( { isFinalValue, hasValue, currentStyleProvider } );
|
|
43
|
+
|
|
44
|
+
const { e_indications_popover: eIndicationsPopover } = getExperimentsConfig();
|
|
45
|
+
|
|
46
|
+
if ( ! eIndicationsPopover ) {
|
|
47
|
+
return <StyleIndicator variant={ variantType } aria-label={ label } />;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const toggleOpen = () => setOpen( ( prev ) => ! prev );
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Infotip
|
|
54
|
+
placement="top"
|
|
55
|
+
content={ <StyleIndicatorInfotip inheritanceChain={ inheritanceChain } bind={ bind } /> }
|
|
56
|
+
open={ open }
|
|
57
|
+
onClose={ () => setOpen( false ) }
|
|
58
|
+
trigger="manual"
|
|
59
|
+
>
|
|
60
|
+
<IconButton onClick={ toggleOpen } aria-label={ label }>
|
|
61
|
+
<StyleIndicator variant={ variantType } />
|
|
62
|
+
</IconButton>
|
|
63
|
+
</Infotip>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getLabel = ( { isFinalValue, hasValue }: { isFinalValue: boolean; hasValue: boolean } ) => {
|
|
68
|
+
if ( isFinalValue ) {
|
|
69
|
+
return __( 'This is the final value', 'elementor' );
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if ( hasValue ) {
|
|
73
|
+
return __( 'This value is overridden by another style', 'elementor' );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return __( 'This has value from another style', 'elementor' );
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const getVariant = ( {
|
|
80
|
+
isFinalValue,
|
|
81
|
+
hasValue,
|
|
82
|
+
currentStyleProvider,
|
|
83
|
+
}: {
|
|
84
|
+
isFinalValue: boolean;
|
|
85
|
+
hasValue: boolean;
|
|
86
|
+
currentStyleProvider: object | null;
|
|
87
|
+
} ): 'local' | 'global' | 'overridden' | undefined => {
|
|
88
|
+
if ( isFinalValue ) {
|
|
89
|
+
return isElementsStylesProvider( ( currentStyleProvider as { getKey: () => string } )?.getKey?.() )
|
|
90
|
+
? 'local'
|
|
91
|
+
: 'global';
|
|
41
92
|
}
|
|
42
93
|
|
|
43
|
-
if (
|
|
44
|
-
return
|
|
45
|
-
<StyleIndicator
|
|
46
|
-
aria-label={ __( 'This value is overridden by another style', 'elementor' ) }
|
|
47
|
-
variant="overridden"
|
|
48
|
-
/>
|
|
49
|
-
);
|
|
94
|
+
if ( hasValue ) {
|
|
95
|
+
return 'overridden';
|
|
50
96
|
}
|
|
51
97
|
|
|
52
|
-
return
|
|
98
|
+
return undefined;
|
|
53
99
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { createPropsResolver, type PropsResolver, styleTransformersRegistry } from '@elementor/editor-canvas';
|
|
4
|
+
import { type PropKey } from '@elementor/editor-props';
|
|
5
|
+
import { getStylesSchema, type StyleDefinitionVariant } from '@elementor/editor-styles';
|
|
6
|
+
import { Card, CardContent, List, ListItem, ListItemText } from '@elementor/ui';
|
|
7
|
+
|
|
8
|
+
import { useNormalizedInheritanceChainItems } from '../hooks/use-normalized-inheritance-chain-items';
|
|
9
|
+
import { type SnapshotPropValue } from './types';
|
|
10
|
+
|
|
11
|
+
type StyleIndicatorInfotipProps = {
|
|
12
|
+
inheritanceChain: SnapshotPropValue[];
|
|
13
|
+
bind: PropKey;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type NormalizedItem = {
|
|
17
|
+
id: string | number;
|
|
18
|
+
breakpoint?: StyleDefinitionVariant[ 'meta' ][ 'breakpoint' ];
|
|
19
|
+
displayLabel: string;
|
|
20
|
+
value: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const StyleIndicatorInfotip = ( { inheritanceChain, bind }: StyleIndicatorInfotipProps ) => {
|
|
24
|
+
const resolve = useMemo< PropsResolver >( () => {
|
|
25
|
+
const stylesSchema = getStylesSchema();
|
|
26
|
+
|
|
27
|
+
return createPropsResolver( {
|
|
28
|
+
transformers: styleTransformersRegistry,
|
|
29
|
+
schema: { [ bind ]: stylesSchema[ bind ] },
|
|
30
|
+
} );
|
|
31
|
+
}, [ bind ] );
|
|
32
|
+
|
|
33
|
+
const items = useNormalizedInheritanceChainItems( inheritanceChain, bind, resolve );
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Card elevation={ 0 } sx={ { maxWidth: 320 } }>
|
|
37
|
+
<CardContent sx={ { p: 1.5, pb: 2.5 } }>
|
|
38
|
+
<List>
|
|
39
|
+
{ items.map( ( item ) => (
|
|
40
|
+
<ListItem key={ item.id }>
|
|
41
|
+
<ListItemText
|
|
42
|
+
primary={ `${ item.breakpoint } | ${ item.displayLabel }. ${ item.value }` }
|
|
43
|
+
/>
|
|
44
|
+
</ListItem>
|
|
45
|
+
) ) }
|
|
46
|
+
</List>
|
|
47
|
+
</CardContent>
|
|
48
|
+
</Card>
|
|
49
|
+
);
|
|
50
|
+
};
|
package/src/sync/types.ts
CHANGED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Autocomplete, type AutocompleteProps, createFilterOptions, TextField, type Theme } from '@elementor/ui';
|
|
4
|
-
|
|
5
|
-
export type Option = {
|
|
6
|
-
label: string;
|
|
7
|
-
value: string | null;
|
|
8
|
-
fixed?: boolean;
|
|
9
|
-
group?: string;
|
|
10
|
-
key?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type Action< TOption extends Option > = {
|
|
14
|
-
label: ( value: string ) => string;
|
|
15
|
-
apply: ( value: string ) => void | Promise< void >;
|
|
16
|
-
condition: ( options: TOption[], value: string ) => boolean;
|
|
17
|
-
group?: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type ActionAsOption< TOption extends Option > = TOption & {
|
|
21
|
-
apply: Action< TOption >[ 'apply' ];
|
|
22
|
-
condition: Action< TOption >[ 'condition' ];
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type Props< TOption extends Option > = Omit<
|
|
26
|
-
AutocompleteProps< TOption, true, true, true >,
|
|
27
|
-
'renderInput' | 'onSelect'
|
|
28
|
-
> & {
|
|
29
|
-
actions?: Action< TOption >[];
|
|
30
|
-
selected: TOption[];
|
|
31
|
-
options: TOption[];
|
|
32
|
-
onSelect?: ( value: TOption[] ) => void;
|
|
33
|
-
placeholder?: string;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export function MultiCombobox< TOption extends Option >( {
|
|
37
|
-
actions = [],
|
|
38
|
-
selected,
|
|
39
|
-
options,
|
|
40
|
-
onSelect,
|
|
41
|
-
placeholder,
|
|
42
|
-
...props
|
|
43
|
-
}: Props< TOption > ) {
|
|
44
|
-
const filter = useFilterOptions< TOption >();
|
|
45
|
-
const { run, loading } = useActionRunner< TOption >();
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<Autocomplete
|
|
49
|
-
{ ...props }
|
|
50
|
-
freeSolo
|
|
51
|
-
multiple
|
|
52
|
-
clearOnBlur
|
|
53
|
-
selectOnFocus
|
|
54
|
-
disableClearable
|
|
55
|
-
handleHomeEndKeys
|
|
56
|
-
disabled={ loading }
|
|
57
|
-
value={ selected }
|
|
58
|
-
options={ options }
|
|
59
|
-
renderInput={ ( params ) => (
|
|
60
|
-
<TextField
|
|
61
|
-
{ ...params }
|
|
62
|
-
placeholder={ placeholder }
|
|
63
|
-
sx={ ( theme: Theme ) => ( {
|
|
64
|
-
'.MuiAutocomplete-inputRoot.MuiInputBase-adornedStart': {
|
|
65
|
-
paddingLeft: theme.spacing( 0.25 ),
|
|
66
|
-
paddingRight: theme.spacing( 0.25 ),
|
|
67
|
-
},
|
|
68
|
-
} ) }
|
|
69
|
-
/>
|
|
70
|
-
) }
|
|
71
|
-
onChange={ ( _, selectedOrInputValue, reason ) => {
|
|
72
|
-
const inputValue = selectedOrInputValue.find( ( option ) => typeof option === 'string' );
|
|
73
|
-
const optionsAndActions = selectedOrInputValue.filter( ( option ) => typeof option !== 'string' );
|
|
74
|
-
|
|
75
|
-
// Handles user input when Enter is pressed
|
|
76
|
-
if ( reason === 'createOption' ) {
|
|
77
|
-
const [ firstAction ] = filterActions( actions, { options, inputValue: inputValue ?? '' } );
|
|
78
|
-
|
|
79
|
-
if ( firstAction?.value ) {
|
|
80
|
-
return run( firstAction.apply, firstAction.value );
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Handles the user's action selection when triggered.
|
|
85
|
-
const action = optionsAndActions.find( ( value ) => isAction( value ) );
|
|
86
|
-
|
|
87
|
-
if ( reason === 'selectOption' && action?.value ) {
|
|
88
|
-
return run( action.apply, action.value );
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Every other case, we update the selected values.
|
|
92
|
-
const fixedValues = options.filter( ( option ) => !! option.fixed );
|
|
93
|
-
|
|
94
|
-
onSelect?.( [ ...new Set( [ ...optionsAndActions, ...fixedValues ] ) ] );
|
|
95
|
-
} }
|
|
96
|
-
getOptionLabel={ ( option ) => ( typeof option === 'string' ? option : option.label ) }
|
|
97
|
-
getOptionKey={ ( option ) => {
|
|
98
|
-
if ( typeof option === 'string' ) {
|
|
99
|
-
return option;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return option.key ?? option.value ?? option.label;
|
|
103
|
-
} }
|
|
104
|
-
filterOptions={ ( optionList, params ) => {
|
|
105
|
-
const selectedValues = selected.map( ( option ) => option.value );
|
|
106
|
-
|
|
107
|
-
return [
|
|
108
|
-
...filterActions( actions, { options: optionList, inputValue: params.inputValue } ),
|
|
109
|
-
...filter(
|
|
110
|
-
optionList.filter( ( option ) => ! selectedValues.includes( option.value ) ),
|
|
111
|
-
params
|
|
112
|
-
),
|
|
113
|
-
];
|
|
114
|
-
} }
|
|
115
|
-
groupBy={ ( option ) => option.group ?? '' }
|
|
116
|
-
renderOption={ ( optionProps, { label, group } ) => (
|
|
117
|
-
<li { ...optionProps } style={ { display: 'block', textOverflow: 'ellipsis' } } data-group={ group }>
|
|
118
|
-
{ label }
|
|
119
|
-
</li>
|
|
120
|
-
) }
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function useFilterOptions< TOption extends Option >() {
|
|
126
|
-
return useState( () => createFilterOptions< TOption >() )[ 0 ];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function useActionRunner< TOption extends Option >() {
|
|
130
|
-
const [ loading, setLoading ] = useState( false );
|
|
131
|
-
|
|
132
|
-
const run = async ( apply: Action< TOption >[ 'apply' ], value: string ) => {
|
|
133
|
-
setLoading( true );
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
await apply( value );
|
|
137
|
-
} catch {
|
|
138
|
-
// TODO: Do something with the error.
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
setLoading( false );
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return { run, loading };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function filterActions< TOption extends Option >(
|
|
148
|
-
actions: Action< TOption >[],
|
|
149
|
-
{ options, inputValue }: { options: TOption[]; inputValue: string }
|
|
150
|
-
) {
|
|
151
|
-
return actions
|
|
152
|
-
.filter( ( action ) => action.condition( options, inputValue ) )
|
|
153
|
-
.map( ( action, index ) => ( {
|
|
154
|
-
label: action.label( inputValue ),
|
|
155
|
-
value: inputValue,
|
|
156
|
-
group: action.group,
|
|
157
|
-
apply: action.apply,
|
|
158
|
-
condition: action.condition,
|
|
159
|
-
key: index.toString(),
|
|
160
|
-
} ) ) as ActionAsOption< TOption >[];
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function isAction< TOption extends Option >( option: TOption ): option is ActionAsOption< TOption > {
|
|
164
|
-
return 'apply' in option && 'condition' in option;
|
|
165
|
-
}
|