@elementor/editor-editing-panel 1.17.1 → 1.19.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/index.js +1014 -724
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +826 -536
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +12 -11
  7. package/src/components/control-label-with-adornments.tsx +13 -0
  8. package/src/components/css-classes/css-class-item.tsx +32 -10
  9. package/src/components/css-classes/css-class-menu.tsx +25 -9
  10. package/src/components/css-classes/css-class-selector.tsx +13 -7
  11. package/src/components/editing-panel-hooks.tsx +0 -2
  12. package/src/components/editing-panel.tsx +2 -2
  13. package/src/components/multi-combobox.tsx +9 -4
  14. package/src/components/style-sections/border-section/border-radius-field.tsx +6 -5
  15. package/src/components/style-sections/position-section/dimensions-field.tsx +2 -2
  16. package/src/components/style-sections/spacing-section/spacing-section.tsx +4 -4
  17. package/src/components/style-sections/typography-section/font-family-field.tsx +2 -46
  18. package/src/components/style-sections/typography-section/font-style-field.tsx +1 -1
  19. package/src/components/style-sections/typography-section/hooks/use-font-families.ts +52 -0
  20. package/src/components/style-sections/typography-section/text-decoration-field.tsx +40 -89
  21. package/src/components/style-tab.tsx +34 -33
  22. package/src/contexts/styles-inheritance-context.tsx +80 -0
  23. package/src/controls-registry/control.tsx +3 -1
  24. package/src/controls-registry/styles-field.tsx +14 -4
  25. package/src/dynamics/components/dynamic-selection.tsx +111 -74
  26. package/src/hooks/use-styles-fields.ts +3 -4
  27. package/src/hooks/use-styles-rerender.ts +10 -0
  28. package/src/init.ts +6 -0
  29. package/src/styles-inheritance/styles-inheritance-indicator.tsx +70 -0
  30. package/src/sync/types.ts +1 -1
  31. package/src/hooks/use-close-editor-panel.ts +0 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-editing-panel",
3
- "version": "1.17.1",
3
+ "version": "1.19.0",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,16 +39,17 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "0.18.2",
43
- "@elementor/editor-controls": "0.16.0",
44
- "@elementor/editor-elements": "0.6.2",
45
- "@elementor/editor-panels": "0.12.2",
46
- "@elementor/editor-props": "0.10.0",
47
- "@elementor/editor-responsive": "0.13.2",
48
- "@elementor/editor-styles": "0.6.2",
49
- "@elementor/editor-styles-repository": "0.7.5",
50
- "@elementor/editor-ui": "0.4.1",
51
- "@elementor/editor-v1-adapters": "0.10.2",
42
+ "@elementor/editor": "0.18.3",
43
+ "@elementor/editor-controls": "0.18.0",
44
+ "@elementor/editor-current-user": "0.3.0",
45
+ "@elementor/editor-elements": "0.6.4",
46
+ "@elementor/editor-panels": "0.13.0",
47
+ "@elementor/editor-props": "0.11.0",
48
+ "@elementor/editor-responsive": "0.13.3",
49
+ "@elementor/editor-styles": "0.6.3",
50
+ "@elementor/editor-styles-repository": "0.7.7",
51
+ "@elementor/editor-ui": "0.4.2",
52
+ "@elementor/editor-v1-adapters": "0.11.0",
52
53
  "@elementor/icons": "1.37.0",
53
54
  "@elementor/locations": "0.7.6",
54
55
  "@elementor/menus": "0.1.3",
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ import { type PropsWithChildren } from 'react';
3
+ import { ControlAdornments, ControlLabel } from '@elementor/editor-controls';
4
+ import { Stack } from '@elementor/ui';
5
+
6
+ export const ControlLabelWithAdornments = ( { children }: PropsWithChildren< object > ) => {
7
+ return (
8
+ <Stack direction="row" alignItems="center" justifyItems="start" gap={ 1 }>
9
+ <ControlLabel>{ children }</ControlLabel>
10
+ <ControlAdornments />
11
+ </Stack>
12
+ );
13
+ };
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { useState } from 'react';
2
+ import { type ReactElement, useState } from 'react';
3
3
  import { stylesRepository } from '@elementor/editor-styles-repository';
4
4
  import { EditableField, EllipsisWithTooltip, useEditable } from '@elementor/editor-ui';
5
5
  import { DotsVerticalIcon } from '@elementor/icons';
@@ -19,13 +19,14 @@ import { useStyle } from '../../contexts/style-context';
19
19
  import { CssClassMenu } from './css-class-menu';
20
20
 
21
21
  type CssClassItemProps = {
22
- id: string;
22
+ id: string | null;
23
23
  label: string;
24
24
  provider: string;
25
25
  isActive: boolean;
26
26
  color: ChipOwnProps[ 'color' ];
27
+ icon: ReactElement | null;
27
28
  chipProps: ReturnType< AutocompleteRenderGetTagProps >;
28
- onClickActive: ( id: string ) => void;
29
+ onClickActive: ( id: string | null ) => void;
29
30
  renameLabel: ( newLabel: string ) => void;
30
31
  validateLabel?: ( newLabel: string ) => string | undefined | null;
31
32
  };
@@ -38,11 +39,12 @@ export function CssClassItem( {
38
39
  provider,
39
40
  isActive,
40
41
  color: colorProp,
42
+ icon,
41
43
  chipProps,
42
44
  onClickActive,
43
45
  renameLabel,
44
46
  }: CssClassItemProps ) {
45
- const { meta } = useStyle();
47
+ const { meta, setMetaState } = useStyle();
46
48
  const popupState = usePopupState( { variant: 'popover' } );
47
49
  const [ chipRef, setChipRef ] = useState< HTMLElement | null >( null );
48
50
  const { onDelete, ...chipGroupProps } = chipProps;
@@ -64,6 +66,8 @@ export function CssClassItem( {
64
66
  const providerActions = stylesRepository.getProviderByKey( provider )?.actions;
65
67
  const allowRename = Boolean( providerActions?.update );
66
68
 
69
+ const isShowingState = isActive && meta.state;
70
+
67
71
  return (
68
72
  <>
69
73
  <UnstableChipGroup ref={ setChipRef } { ...chipGroupProps } aria-label={ `Edit ${ label }` } role="group">
@@ -76,17 +80,26 @@ export function CssClassItem( {
76
80
  <EllipsisWithTooltip maxWidth="10ch" title={ label } as="div" />
77
81
  )
78
82
  }
79
- variant={ isActive && ! meta.state ? 'filled' : 'standard' }
83
+ variant={ isActive && ! meta.state && ! isEditing ? 'filled' : 'standard' }
84
+ shape="rounded"
85
+ icon={ icon }
80
86
  color={ color }
81
87
  onClick={ () => {
82
- if ( isActive && allowRename ) {
88
+ if ( isShowingState ) {
89
+ setMetaState( null );
90
+ return;
91
+ }
92
+
93
+ if ( allowRename && isActive ) {
83
94
  openEditMode();
95
+ return;
84
96
  }
85
97
 
86
98
  onClickActive( id );
87
99
  } }
88
100
  aria-pressed={ isActive }
89
101
  sx={ {
102
+ cursor: isActive && allowRename && ! isShowingState ? 'text' : 'pointer',
90
103
  '&.Mui-focusVisible': {
91
104
  boxShadow: 'none !important',
92
105
  },
@@ -94,17 +107,26 @@ export function CssClassItem( {
94
107
  />
95
108
  { ! isEditing && (
96
109
  <Chip
110
+ icon={ isShowingState ? undefined : <DotsVerticalIcon fontSize="tiny" /> }
97
111
  size={ CHIP_SIZE }
98
112
  label={
99
- <Stack direction="row" gap={ 0.5 } alignItems="center">
100
- { isActive && meta.state && <Typography variant="inherit">{ meta.state }</Typography> }
101
- <DotsVerticalIcon fontSize="inherit" />
102
- </Stack>
113
+ isShowingState ? (
114
+ <Stack direction="row" gap={ 0.5 } alignItems="center">
115
+ <Typography variant="inherit">{ meta.state }</Typography>
116
+ <DotsVerticalIcon fontSize="tiny" />
117
+ </Stack>
118
+ ) : undefined
103
119
  }
104
120
  variant="filled"
121
+ shape="rounded"
105
122
  color={ color }
106
123
  { ...bindTrigger( popupState ) }
107
124
  aria-label={ __( 'Open CSS Class Menu', 'elementor' ) }
125
+ sx={ {
126
+ paddingRight: 0,
127
+ ...( ! isShowingState ? { paddingLeft: 0 } : {} ),
128
+ '.MuiChip-label': isShowingState ? { paddingRight: 0 } : { padding: 0 },
129
+ } }
108
130
  />
109
131
  ) }
110
132
  </UnstableChipGroup>
@@ -10,7 +10,7 @@ import { useUnapplyClass } from '../../hooks/use-unapply-class';
10
10
  const STATES: NonNullable< StyleDefinitionState >[] = [ 'hover', 'focus', 'active' ];
11
11
 
12
12
  type CssClassMenuProps = {
13
- styleId: string;
13
+ styleId: string | null;
14
14
  provider: string;
15
15
  popupState: PopupState;
16
16
  handleRename: () => void;
@@ -42,8 +42,11 @@ export function CssClassMenu( { styleId, provider, popupState, handleRename, anc
42
42
  <ListSubheader sx={ { typography: 'caption', color: 'text.secondary', pb: 0.5, pt: 1 } }>
43
43
  { __( 'Pseudo classes', 'elementor' ) }
44
44
  </ListSubheader>
45
+ <StateMenuItem key="normal" state={ null } styleId={ styleId } closeMenu={ popupState.close } />
45
46
  { STATES.map( ( state ) => {
46
- return <StateMenuItem key={ state } state={ state } styleId={ styleId } />;
47
+ return (
48
+ <StateMenuItem key={ state } state={ state } styleId={ styleId } closeMenu={ popupState.close } />
49
+ );
47
50
  } ) }
48
51
  </Menu>
49
52
  );
@@ -56,10 +59,14 @@ function getMenuItemsByProvider( {
56
59
  closeMenu,
57
60
  }: {
58
61
  provider: string;
59
- styleId: string;
62
+ styleId: string | null;
60
63
  handleRename: () => void;
61
64
  closeMenu: () => void;
62
65
  } ) {
66
+ if ( ! styleId ) {
67
+ return [];
68
+ }
69
+
63
70
  const providerInstance = stylesRepository.getProviderByKey( provider );
64
71
  const providerActions = providerInstance?.actions;
65
72
 
@@ -67,7 +74,7 @@ function getMenuItemsByProvider( {
67
74
 
68
75
  const actions = [
69
76
  canUpdate && <RenameClassMenuItem key="rename-class" handleRename={ handleRename } closeMenu={ closeMenu } />,
70
- canDelete && <UnapplyClassMenuItem key="unapply-class" styleId={ styleId } />,
77
+ canDelete && <UnapplyClassMenuItem key="unapply-class" styleId={ styleId } closeMenu={ closeMenu } />,
71
78
  ].filter( Boolean );
72
79
 
73
80
  if ( actions.length ) {
@@ -87,10 +94,11 @@ function getMenuItemsByProvider( {
87
94
 
88
95
  type StateMenuItemProps = {
89
96
  state: StyleDefinitionState;
90
- styleId: string;
97
+ styleId: string | null;
98
+ closeMenu: () => void;
91
99
  };
92
100
 
93
- function StateMenuItem( { state, styleId, ...props }: StateMenuItemProps ) {
101
+ function StateMenuItem( { state, styleId, closeMenu, ...props }: StateMenuItemProps ) {
94
102
  const { id: activeId, setId: setActiveId, setMetaState: setActiveMetaState, meta } = useStyle();
95
103
  const { state: activeState } = meta;
96
104
 
@@ -108,18 +116,26 @@ function StateMenuItem( { state, styleId, ...props }: StateMenuItemProps ) {
108
116
  }
109
117
 
110
118
  setActiveMetaState( state );
119
+
120
+ closeMenu();
111
121
  } }
112
122
  >
113
- { state }
123
+ { state ? state : 'Normal' }
114
124
  </StyledMenuItem>
115
125
  );
116
126
  }
117
127
 
118
- function UnapplyClassMenuItem( { styleId, ...props }: { styleId: string } ) {
128
+ function UnapplyClassMenuItem( { styleId, closeMenu, ...props }: { styleId: string; closeMenu: () => void } ) {
119
129
  const unapplyClass = useUnapplyClass( styleId );
120
130
 
121
131
  return (
122
- <StyledMenuItem { ...props } onClick={ unapplyClass }>
132
+ <StyledMenuItem
133
+ { ...props }
134
+ onClick={ () => {
135
+ unapplyClass();
136
+ closeMenu();
137
+ } }
138
+ >
123
139
  { __( 'Remove', 'elementor' ) }
124
140
  </StyledMenuItem>
125
141
  );
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { type ReactElement } from 'react';
2
3
  import { getElementSetting, updateElementSettings, useElementSetting } from '@elementor/editor-elements';
3
4
  import { classesPropTypeUtil, type ClassesPropValue } from '@elementor/editor-props';
4
5
  import { type StyleDefinitionID } from '@elementor/editor-styles';
@@ -10,6 +11,7 @@ import {
10
11
  useCreateActionsByProvider,
11
12
  useProviders,
12
13
  } from '@elementor/editor-styles-repository';
14
+ import { MapPinIcon } from '@elementor/icons';
13
15
  import { createLocation } from '@elementor/locations';
14
16
  import { Chip, Stack, Typography } from '@elementor/ui';
15
17
  import { __ } from '@wordpress/i18n';
@@ -21,18 +23,20 @@ import { type Action, MultiCombobox, type Option } from '../multi-combobox';
21
23
  import { CssClassItem } from './css-class-item';
22
24
 
23
25
  const ID = 'elementor-css-class-selector';
24
- const TAGS_LIMIT = 8;
26
+ const TAGS_LIMIT = 50;
25
27
 
26
28
  type StyleDefOption = Option & {
27
29
  color: 'primary' | 'global';
30
+ icon: ReactElement | null;
28
31
  provider: string;
29
32
  };
30
33
 
31
34
  const EMPTY_OPTION = {
32
35
  label: __( 'local', 'elementor' ),
33
- value: '',
36
+ value: null,
34
37
  fixed: true,
35
38
  color: 'primary',
39
+ icon: <MapPinIcon />,
36
40
  provider: ELEMENTS_STYLES_PROVIDER_KEY,
37
41
  } satisfies StyleDefOption;
38
42
 
@@ -81,9 +85,11 @@ export function CssClassSelector() {
81
85
  values.map( ( value, index ) => {
82
86
  const chipProps = getTagProps( { index } );
83
87
  const isActive = value.value === active?.value;
84
- const isElementsProvider = value.provider === ELEMENTS_STYLES_PROVIDER_KEY;
85
88
 
86
89
  const renameLabel = ( newLabel: string ) => {
90
+ if ( ! value.value ) {
91
+ throw new Error( `Cannot rename a class without style id` );
92
+ }
87
93
  return updateClassByProvider( value.provider, { label: newLabel, id: value.value } );
88
94
  };
89
95
 
@@ -95,10 +101,9 @@ export function CssClassSelector() {
95
101
  id={ value.value }
96
102
  isActive={ isActive }
97
103
  color={ isActive && value.color ? value.color : 'default' }
104
+ icon={ value.icon }
98
105
  chipProps={ chipProps }
99
- // There is only a single local style, which might not exist, so setting it to
100
- // `null` will either return the actual style or the fallback one.
101
- onClickActive={ () => setActiveId( isElementsProvider ? null : value.value ) }
106
+ onClickActive={ () => setActiveId( value.value ) }
102
107
  renameLabel={ renameLabel }
103
108
  />
104
109
  );
@@ -141,6 +146,7 @@ function useOptions() {
141
146
  value: styleDef.id,
142
147
  fixed: isElements,
143
148
  color: isElements ? 'primary' : 'global',
149
+ icon: isElements ? <MapPinIcon /> : null,
144
150
  provider: provider.key,
145
151
  group: provider.labels?.plural,
146
152
  };
@@ -178,7 +184,7 @@ function useCreateActions( {
178
184
  }
179
185
 
180
186
  function useAppliedOptions( options: StyleDefOption[], appliedIds: StyleDefinitionID[] ) {
181
- const applied = options.filter( ( option ) => appliedIds.includes( option.value ) );
187
+ const applied = options.filter( ( option ) => option.value && appliedIds.includes( option.value ) );
182
188
 
183
189
  const hasElementsProviderStyleApplied = applied.some(
184
190
  ( option ) => option.provider === ELEMENTS_STYLES_PROVIDER_KEY
@@ -1,9 +1,7 @@
1
- import { useCloseEditorPanel } from '../hooks/use-close-editor-panel';
2
1
  import { useOpenEditorPanel } from '../hooks/use-open-editor-panel';
3
2
 
4
3
  export const EditingPanelHooks = () => {
5
4
  useOpenEditorPanel();
6
- useCloseEditorPanel();
7
5
 
8
6
  return null;
9
7
  };
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
  import { ControlActionsProvider, ControlReplacementProvider } from '@elementor/editor-controls';
3
3
  import { useSelectedElement } from '@elementor/editor-elements';
4
4
  import { Panel, PanelBody, PanelHeader, PanelHeaderTitle } from '@elementor/editor-panels';
5
- import { RocketIcon } from '@elementor/icons';
5
+ import { AtomIcon } from '@elementor/icons';
6
6
  import { SessionStorageProvider } from '@elementor/session';
7
7
  import { ErrorBoundary } from '@elementor/ui';
8
8
  import { __ } from '@wordpress/i18n';
@@ -33,7 +33,7 @@ export const EditingPanel = () => {
33
33
  <Panel>
34
34
  <PanelHeader>
35
35
  <PanelHeaderTitle>{ panelTitle }</PanelHeaderTitle>
36
- <RocketIcon fontSize="small" sx={ { color: 'text.disabled' } } />
36
+ <AtomIcon fontSize="small" sx={ { color: 'text.tertiary' } } />
37
37
  </PanelHeader>
38
38
  <PanelBody>
39
39
  <ControlActionsProvider items={ menuItems }>
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  export type Option = {
14
14
  label: string;
15
- value: string;
15
+ value: string | null;
16
16
  fixed?: boolean;
17
17
  group?: string;
18
18
  key?: string;
@@ -72,7 +72,7 @@ export function MultiCombobox< TOption extends Option >( {
72
72
  if ( reason === 'createOption' ) {
73
73
  const [ firstAction ] = filterActions( actions, { options, inputValue: inputValue ?? '' } );
74
74
 
75
- if ( firstAction ) {
75
+ if ( firstAction.value ) {
76
76
  return run( firstAction.apply, firstAction.value );
77
77
  }
78
78
  }
@@ -80,7 +80,7 @@ export function MultiCombobox< TOption extends Option >( {
80
80
  // Handles the user's action selection when triggered.
81
81
  const action = optionsAndActions.find( ( value ) => isAction( value ) );
82
82
 
83
- if ( reason === 'selectOption' && action ) {
83
+ if ( reason === 'selectOption' && action?.value ) {
84
84
  return run( action.apply, action.value );
85
85
  }
86
86
 
@@ -95,7 +95,7 @@ export function MultiCombobox< TOption extends Option >( {
95
95
  return option;
96
96
  }
97
97
 
98
- return option.key ?? option.value;
98
+ return option.key ?? option.value ?? option.label;
99
99
  } }
100
100
  filterOptions={ ( optionList, params ) => {
101
101
  const selectedValues = selected.map( ( option ) => option.value );
@@ -109,6 +109,11 @@ export function MultiCombobox< TOption extends Option >( {
109
109
  ];
110
110
  } }
111
111
  groupBy={ ( option ) => option.group ?? '' }
112
+ renderOption={ ( optionProps, { label } ) => (
113
+ <li { ...optionProps } style={ { display: 'block', textOverflow: 'ellipsis' } }>
114
+ { label }
115
+ </li>
116
+ ) }
112
117
  />
113
118
  );
114
119
  }
@@ -28,6 +28,7 @@ const getEndStartLabel = ( isSiteRtl: boolean ) =>
28
28
  isSiteRtl ? __( 'Bottom right', 'elementor' ) : __( 'Bottom left', 'elementor' );
29
29
  const getEndEndLabel = ( isSiteRtl: boolean ) =>
30
30
  isSiteRtl ? __( 'Bottom left', 'elementor' ) : __( 'Bottom right', 'elementor' );
31
+
31
32
  const getCorners = ( isSiteRtl: boolean ): EqualUnequalItems => [
32
33
  {
33
34
  label: getStartStartLabel( isSiteRtl ),
@@ -39,16 +40,16 @@ const getCorners = ( isSiteRtl: boolean ): EqualUnequalItems => [
39
40
  icon: <RotatedIcon icon={ StartEndIcon } size="tiny" />,
40
41
  bind: 'start-end',
41
42
  },
42
- {
43
- label: getEndEndLabel( isSiteRtl ),
44
- icon: <RotatedIcon icon={ EndEndIcon } size="tiny" />,
45
- bind: 'end-end',
46
- },
47
43
  {
48
44
  label: getEndStartLabel( isSiteRtl ),
49
45
  icon: <RotatedIcon icon={ EndStartIcon } size="tiny" />,
50
46
  bind: 'end-start',
51
47
  },
48
+ {
49
+ label: getEndEndLabel( isSiteRtl ),
50
+ icon: <RotatedIcon icon={ EndEndIcon } size="tiny" />,
51
+ bind: 'end-end',
52
+ },
52
53
  ];
53
54
 
54
55
  export const BorderRadiusField = () => {
@@ -33,11 +33,11 @@ export const DimensionsField = () => {
33
33
  <>
34
34
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
35
35
  <DimensionField side="inset-block-start" label={ __( 'Top', 'elementor' ) } />
36
- <DimensionField side="inset-inline-start" label={ getInlineStartLabel( isSiteRtl ) } />
36
+ <DimensionField side="inset-inline-end" label={ getInlineEndLabel( isSiteRtl ) } />
37
37
  </Stack>
38
38
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
39
39
  <DimensionField side="inset-block-end" label={ __( 'Bottom', 'elementor' ) } />
40
- <DimensionField side="inset-inline-end" label={ getInlineEndLabel( isSiteRtl ) } />
40
+ <DimensionField side="inset-inline-start" label={ getInlineStartLabel( isSiteRtl ) } />
41
41
  </Stack>
42
42
  </>
43
43
  );
@@ -12,10 +12,6 @@ export const SpacingSection = () => {
12
12
 
13
13
  return (
14
14
  <SectionContent>
15
- <StylesField bind={ 'padding' }>
16
- <LinkedDimensionsControl label={ __( 'Padding', 'elementor' ) } isSiteRtl={ isSiteRtl } />
17
- </StylesField>
18
- <PanelDivider />
19
15
  <StylesField bind={ 'margin' }>
20
16
  <LinkedDimensionsControl
21
17
  label={ __( 'Margin', 'elementor' ) }
@@ -23,6 +19,10 @@ export const SpacingSection = () => {
23
19
  extendedValues={ [ 'auto' ] }
24
20
  />
25
21
  </StylesField>
22
+ <PanelDivider />
23
+ <StylesField bind={ 'padding' }>
24
+ <LinkedDimensionsControl label={ __( 'Padding', 'elementor' ) } isSiteRtl={ isSiteRtl } />
25
+ </StylesField>
26
26
  </SectionContent>
27
27
  );
28
28
  };
@@ -1,22 +1,15 @@
1
1
  import * as React from 'react';
2
- import { useMemo } from 'react';
3
2
  import { ControlLabel, FontFamilyControl } from '@elementor/editor-controls';
4
3
  import { Grid } from '@elementor/ui';
5
4
  import { __ } from '@wordpress/i18n';
6
5
 
7
6
  import { StylesField } from '../../../controls-registry/styles-field';
8
- import { getElementorConfig } from '../../../sync/get-elementor-config';
9
-
10
- const supportedCategories: Record< string, string > = {
11
- system: __( 'System', 'elementor' ),
12
- custom: __( 'Custom Fonts', 'elementor' ),
13
- googlefonts: __( 'Google Fonts', 'elementor' ),
14
- };
7
+ import { useFontFamilies } from './hooks/use-font-families';
15
8
 
16
9
  export const FontFamilyField = () => {
17
10
  const fontFamilies = useFontFamilies();
18
11
 
19
- if ( Object.keys( fontFamilies ).length === 0 ) {
12
+ if ( fontFamilies.length === 0 ) {
20
13
  return null;
21
14
  }
22
15
 
@@ -33,40 +26,3 @@ export const FontFamilyField = () => {
33
26
  </StylesField>
34
27
  );
35
28
  };
36
-
37
- const getFontFamilies = () => {
38
- const { controls } = getElementorConfig();
39
-
40
- const options = controls?.font?.options;
41
-
42
- if ( ! options ) {
43
- return null;
44
- }
45
-
46
- return options;
47
- };
48
-
49
- const useFontFamilies = () => {
50
- const fontFamilies = getFontFamilies();
51
-
52
- return useMemo(
53
- () =>
54
- Object.entries( fontFamilies || {} ).reduce< Record< string, string[] > >( ( acc, [ font, category ] ) => {
55
- const categoryLabel = supportedCategories[ category as keyof typeof supportedCategories ];
56
- const existingCategory = acc[ categoryLabel ];
57
-
58
- if ( ! categoryLabel ) {
59
- return acc;
60
- }
61
-
62
- if ( ! existingCategory ) {
63
- acc[ categoryLabel ] = [];
64
- }
65
-
66
- acc[ categoryLabel ].push( font );
67
-
68
- return acc;
69
- }, {} ),
70
- [ fontFamilies ]
71
- );
72
- };
@@ -26,7 +26,7 @@ export const FontStyleField = () => (
26
26
  <StylesField bind="font-style">
27
27
  <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
28
28
  <Grid item xs={ 6 }>
29
- <ControlLabel>{ __( 'Font Style', 'elementor' ) }</ControlLabel>
29
+ <ControlLabel>{ __( 'Font style', 'elementor' ) }</ControlLabel>
30
30
  </Grid>
31
31
  <Grid item xs={ 6 } display="flex" justifyContent="end">
32
32
  <ToggleControl options={ options } />
@@ -0,0 +1,52 @@
1
+ import { useMemo } from 'react';
2
+ import { type FontCategory } from '@elementor/editor-controls';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { getElementorConfig } from '../../../../sync/get-elementor-config';
6
+
7
+ const supportedCategories: Record< string, string > = {
8
+ system: __( 'System', 'elementor' ),
9
+ custom: __( 'Custom Fonts', 'elementor' ),
10
+ googlefonts: __( 'Google Fonts', 'elementor' ),
11
+ };
12
+
13
+ const getFontFamilies = () => {
14
+ const { controls } = getElementorConfig();
15
+
16
+ const options = controls?.font?.options;
17
+
18
+ if ( ! options ) {
19
+ return null;
20
+ }
21
+
22
+ return options;
23
+ };
24
+
25
+ export const useFontFamilies = () => {
26
+ const fontFamilies = getFontFamilies();
27
+
28
+ return useMemo( () => {
29
+ const categoriesOrder = [ 'system', 'custom', 'googlefonts' ];
30
+
31
+ return Object.entries( fontFamilies || {} )
32
+ .reduce< FontCategory[] >( ( acc, [ font, category ] ) => {
33
+ if ( ! supportedCategories[ category ] ) {
34
+ return acc;
35
+ }
36
+
37
+ const categoryIndex = categoriesOrder.indexOf( category );
38
+
39
+ if ( ! acc[ categoryIndex ] ) {
40
+ acc[ categoryIndex ] = {
41
+ label: supportedCategories[ category ],
42
+ fonts: [],
43
+ };
44
+ }
45
+
46
+ acc[ categoryIndex ].fonts.push( font );
47
+
48
+ return acc;
49
+ }, [] )
50
+ .filter( Boolean );
51
+ }, [ fontFamilies ] );
52
+ };