@elementor/editor-editing-panel 1.1.0 → 1.3.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 (34) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/index.js +868 -444
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +851 -403
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +15 -14
  7. package/src/components/css-class-selector.tsx +131 -0
  8. package/src/components/multi-combobox/multi-combobox.tsx +34 -32
  9. package/src/components/multi-combobox/types.ts +2 -0
  10. package/src/components/multi-combobox/use-combobox-actions.ts +4 -4
  11. package/src/components/style-sections/border-section/border-radius-field.tsx +4 -4
  12. package/src/components/style-sections/border-section/border-width-field.tsx +4 -4
  13. package/src/components/style-sections/layout-section/align-items-field.tsx +72 -0
  14. package/src/components/style-sections/layout-section/align-self-child-field.tsx +72 -0
  15. package/src/components/style-sections/layout-section/flex-direction-field.tsx +64 -0
  16. package/src/components/style-sections/layout-section/flex-order-field.tsx +120 -0
  17. package/src/components/style-sections/layout-section/flex-size-field.tsx +164 -0
  18. package/src/components/style-sections/layout-section/justify-content-field.tsx +62 -62
  19. package/src/components/style-sections/layout-section/layout-section.tsx +27 -3
  20. package/src/components/style-sections/layout-section/utils/rotated-icon.tsx +52 -0
  21. package/src/components/style-sections/layout-section/wrap-field.tsx +52 -0
  22. package/src/components/style-sections/position-section/position-section.tsx +3 -3
  23. package/src/components/style-sections/typography-section/line-height-field.tsx +21 -0
  24. package/src/components/style-sections/typography-section/text-stroke-field.tsx +41 -6
  25. package/src/components/style-sections/typography-section/text-style-field.tsx +31 -8
  26. package/src/components/style-sections/typography-section/typography-section.tsx +3 -1
  27. package/src/components/style-tab.tsx +2 -2
  28. package/src/controls-registry/controls-registry.tsx +4 -0
  29. package/src/dynamics/components/dynamic-selection-control.tsx +8 -5
  30. package/src/dynamics/components/dynamic-selection.tsx +10 -8
  31. package/src/dynamics/dynamic-control.tsx +9 -11
  32. package/src/dynamics/utils.ts +20 -3
  33. package/src/components/css-class-selector-section.tsx +0 -76
  34. package/src/components/style-sections/layout-section/utils/rotate-flex-icon.ts +0 -12
@@ -1,16 +1,51 @@
1
1
  import * as React from 'react';
2
- import { ControlLabel, StrokeControl } from '@elementor/editor-controls';
2
+ import { StrokeControl } from '@elementor/editor-controls';
3
3
  import { __ } from '@wordpress/i18n';
4
4
 
5
5
  import { StylesField } from '../../../controls-registry/styles-field';
6
- import { CollapsibleField } from '../../collapsible-field';
6
+ import { useStylesField } from '../../../hooks/use-styles-field';
7
+ import { AddOrRemoveContent } from '../../add-or-remove-content';
8
+
9
+ const initTextStroke = {
10
+ $$type: 'stroke',
11
+ value: {
12
+ color: {
13
+ $$type: 'color',
14
+ value: '#000000',
15
+ },
16
+ width: {
17
+ $$type: 'size',
18
+ value: {
19
+ unit: 'px',
20
+ size: 1,
21
+ },
22
+ },
23
+ },
24
+ };
7
25
 
8
26
  export const TextStrokeField = () => {
27
+ const [ textStroke, setTextStroke ] = useStylesField( '-webkit-text-stroke' );
28
+
29
+ const addTextStroke = () => {
30
+ setTextStroke( initTextStroke );
31
+ };
32
+
33
+ const removeTextStroke = () => {
34
+ setTextStroke( null );
35
+ };
36
+
37
+ const hasTextStroke = Boolean( textStroke );
38
+
9
39
  return (
10
- <StylesField bind="-webkit-text-stroke">
11
- <CollapsibleField label={ <ControlLabel>{ __( 'Text Stroke', 'elementor' ) }</ControlLabel> }>
40
+ <AddOrRemoveContent
41
+ label={ __( 'Text Stroke', 'elementor' ) }
42
+ isAdded={ hasTextStroke }
43
+ onAdd={ addTextStroke }
44
+ onRemove={ removeTextStroke }
45
+ >
46
+ <StylesField bind={ '-webkit-text-stroke' }>
12
47
  <StrokeControl />
13
- </CollapsibleField>
14
- </StylesField>
48
+ </StylesField>
49
+ </AddOrRemoveContent>
15
50
  );
16
51
  };
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { ControlLabel } from '@elementor/editor-controls';
3
+ import { type StringPropValue } from '@elementor/editor-props';
3
4
  import { ItalicIcon, StrikethroughIcon, UnderlineIcon } from '@elementor/icons';
4
5
  import { Grid, ToggleButton as ToggleButtonBase, ToggleButtonGroup, type ToggleButtonProps } from '@elementor/ui';
5
6
  import { __ } from '@wordpress/i18n';
@@ -9,10 +10,32 @@ import { useStylesField } from '../../../hooks/use-styles-field';
9
10
  const buttonSize = 'tiny';
10
11
 
11
12
  export const TextStyleField = () => {
12
- const [ fontStyle, setFontStyle ] = useStylesField< string | null >( 'font-style' );
13
- const [ textDecoration, setTextDecoration ] = useStylesField< string | null >( 'text-decoration' );
13
+ const [ fontStyle, setFontStyle ] = useStylesField< StringPropValue | null >( 'font-style' );
14
+ const [ textDecoration, setTextDecoration ] = useStylesField< StringPropValue | null >( 'text-decoration' );
14
15
 
15
- const formats = [ fontStyle, ...( textDecoration || '' ).split( ' ' ) ];
16
+ const formats = [ fontStyle?.value, ...( textDecoration?.value || '' ).split( ' ' ) ];
17
+
18
+ const handleSetFontStyle = ( newValue: string | null ) => {
19
+ if ( newValue === null ) {
20
+ return setFontStyle( null );
21
+ }
22
+
23
+ setFontStyle( {
24
+ $$type: 'string',
25
+ value: newValue,
26
+ } );
27
+ };
28
+
29
+ const handleSetTextDecoration = ( newValue: string | null ) => {
30
+ if ( newValue === null ) {
31
+ return setTextDecoration( null );
32
+ }
33
+
34
+ setTextDecoration( {
35
+ $$type: 'string',
36
+ value: newValue,
37
+ } );
38
+ };
16
39
 
17
40
  return (
18
41
  <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
@@ -23,7 +46,7 @@ export const TextStyleField = () => {
23
46
  <ToggleButtonGroup value={ formats }>
24
47
  <ToggleButton
25
48
  value="italic"
26
- onChange={ ( v ) => setFontStyle( fontStyle === v ? null : v ) }
49
+ onChange={ ( v ) => handleSetFontStyle( fontStyle?.value === v ? null : v ) }
27
50
  aria-label="italic"
28
51
  sx={ { marginLeft: 'auto' } }
29
52
  >
@@ -31,16 +54,16 @@ export const TextStyleField = () => {
31
54
  </ToggleButton>
32
55
  <ShorthandControl
33
56
  value="line-through"
34
- currentValues={ textDecoration || '' }
35
- updateValues={ setTextDecoration }
57
+ currentValues={ textDecoration?.value || '' }
58
+ updateValues={ handleSetTextDecoration }
36
59
  aria-label="line-through"
37
60
  >
38
61
  <StrikethroughIcon fontSize={ buttonSize } />
39
62
  </ShorthandControl>
40
63
  <ShorthandControl
41
64
  value="underline"
42
- currentValues={ textDecoration || '' }
43
- updateValues={ setTextDecoration }
65
+ currentValues={ textDecoration?.value || '' }
66
+ updateValues={ handleSetTextDecoration }
44
67
  aria-label="underline"
45
68
  >
46
69
  <UnderlineIcon fontSize={ buttonSize } />
@@ -6,6 +6,7 @@ import { FontFamilyField } from './font-family-field';
6
6
  import { FontSizeField } from './font-size-field';
7
7
  import { FontWeightField } from './font-weight-field';
8
8
  import { LetterSpacingField } from './letter-spacing-field';
9
+ import { LineHeightField } from './line-height-field';
9
10
  import { TextAlignmentField } from './text-alignment-field';
10
11
  import { TextColorField } from './text-color-field';
11
12
  import { TextDirectionField } from './text-direction-field';
@@ -21,13 +22,14 @@ export const TypographySection = () => {
21
22
  <FontWeightField />
22
23
  <FontSizeField />
23
24
  <Divider />
25
+ <TextAlignmentField />
24
26
  <TextColorField />
25
27
  <CollapsibleContent>
26
28
  <Stack gap={ 1.5 } sx={ { pt: 1.5 } }>
29
+ <LineHeightField />
27
30
  <LetterSpacingField />
28
31
  <WordSpacingField />
29
32
  <Divider />
30
- <TextAlignmentField />
31
33
  <TextStyleField />
32
34
  <TransformField />
33
35
  <TextDirectionField />
@@ -10,7 +10,7 @@ import { __ } from '@wordpress/i18n';
10
10
  import { ClassesPropProvider } from '../contexts/classes-prop-context';
11
11
  import { useElement } from '../contexts/element-context';
12
12
  import { StyleProvider } from '../contexts/style-context';
13
- import { CssClassSelectorSection } from './css-class-selector-section';
13
+ import { CssClassSelector } from './css-class-selector';
14
14
  import { Section } from './section';
15
15
  import { SectionsList } from './sections-list';
16
16
  import { BackgroundSection } from './style-sections/background-section/background-section';
@@ -32,7 +32,7 @@ export const StyleTab = () => {
32
32
  return (
33
33
  <ClassesPropProvider prop={ currentClassesProp }>
34
34
  <StyleProvider meta={ { breakpoint, state: null } } id={ activeStyleDefId } setId={ setActiveStyleDefId }>
35
- <CssClassSelectorSection />
35
+ <CssClassSelector />
36
36
  <Divider />
37
37
  <SectionsList>
38
38
  <Section title={ __( 'Layout', 'elementor' ) }>
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  type ControlComponent,
3
3
  ImageControl,
4
+ LinkControl,
4
5
  SelectControl,
5
6
  SizeControl,
6
7
  TextAreaControl,
7
8
  TextControl,
9
+ UrlControl,
8
10
  } from '@elementor/editor-controls';
9
11
 
10
12
  export type ControlLayout = 'full' | 'two-columns';
@@ -17,6 +19,8 @@ const controlTypes = {
17
19
  textarea: { component: TextAreaControl, layout: 'full' },
18
20
  size: { component: SizeControl, layout: 'two-columns' },
19
21
  select: { component: SelectControl, layout: 'two-columns' },
22
+ link: { component: LinkControl, layout: 'full' },
23
+ url: { component: UrlControl, layout: 'full' },
20
24
  } as const satisfies ControlRegistry;
21
25
 
22
26
  export type ControlType = keyof typeof controlTypes;
@@ -27,15 +27,17 @@ import { type ControlType, getControlByType } from '../../controls-registry/cont
27
27
  import { usePropValueHistory } from '../../hooks/use-prop-value-history';
28
28
  import { DynamicControl } from '../dynamic-control';
29
29
  import { useDynamicTag } from '../hooks/use-dynamic-tag';
30
- import { type DynamicPropValue, type DynamicTag } from '../types';
30
+ import { type DynamicTag } from '../types';
31
+ import { dynamicPropTypeUtil } from '../utils';
31
32
  import { DynamicSelection } from './dynamic-selection';
32
33
 
33
34
  const SIZE = 'tiny';
34
35
 
35
36
  export const DynamicSelectionControl = () => {
36
- const { bind, value, setValue } = useBoundProp< DynamicPropValue | null >();
37
+ const { setValue: setAnyValue } = useBoundProp();
38
+ const { bind, value } = useBoundProp( dynamicPropTypeUtil );
37
39
  const { getPropValue: getPropValueFromHistory } = usePropValueHistory();
38
- const { name: tagName = '' } = value?.value || {};
40
+ const { name: tagName = '' } = value;
39
41
 
40
42
  const selectionPopoverId = useId();
41
43
  const selectionPopoverState = usePopupState( { variant: 'popover', popupId: selectionPopoverId } );
@@ -43,8 +45,9 @@ export const DynamicSelectionControl = () => {
43
45
  const dynamicTag = useDynamicTag( bind, tagName );
44
46
 
45
47
  const removeDynamicTag = () => {
46
- const propValue = getPropValueFromHistory< DynamicPropValue >( bind );
47
- setValue( propValue ?? null );
48
+ const propValue = getPropValueFromHistory( bind );
49
+
50
+ setAnyValue( propValue ?? null );
48
51
  };
49
52
 
50
53
  if ( ! dynamicTag ) {
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { Fragment, useState } from 'react';
3
3
  import { useBoundProp } from '@elementor/editor-controls';
4
- import { type PropKey, type PropValue } from '@elementor/editor-props';
4
+ import { type PropKey } from '@elementor/editor-props';
5
5
  import { PhotoIcon, SearchIcon } from '@elementor/icons';
6
6
  import {
7
7
  Box,
@@ -20,8 +20,7 @@ import { __ } from '@wordpress/i18n';
20
20
  import { usePropValueHistory } from '../../hooks/use-prop-value-history';
21
21
  import { usePropDynamicTags } from '../hooks/use-prop-dynamic-tags';
22
22
  import { getAtomicDynamicTags } from '../sync/get-atomic-dynamic-tags';
23
- import { type DynamicPropValue } from '../types';
24
- import { isDynamicPropValue } from '../utils';
23
+ import { dynamicPropTypeUtil } from '../utils';
25
24
 
26
25
  type Option = {
27
26
  label: string;
@@ -39,10 +38,13 @@ export type DynamicSelectionProps = {
39
38
  export const DynamicSelection = ( { onSelect }: DynamicSelectionProps ) => {
40
39
  const [ searchValue, setSearchValue ] = useState( '' );
41
40
  const { groups: dynamicGroups } = getAtomicDynamicTags() || {};
42
- const { bind, value: currentValue, setValue } = useBoundProp< DynamicPropValue | PropValue >();
41
+
42
+ const { value: anyValue } = useBoundProp();
43
+ const { bind, value: dynamicvalue, setValue } = useBoundProp( dynamicPropTypeUtil );
44
+
43
45
  const { setPropValue: updatePropValueHistory } = usePropValueHistory();
44
46
 
45
- const isCurrentValueDynamic = isDynamicPropValue( currentValue );
47
+ const isCurrentValueDynamic = !! dynamicvalue;
46
48
 
47
49
  const options = useFilteredOptions( bind, searchValue );
48
50
 
@@ -52,10 +54,10 @@ export const DynamicSelection = ( { onSelect }: DynamicSelectionProps ) => {
52
54
 
53
55
  const handleSetDynamicTag = ( value: string ) => {
54
56
  if ( ! isCurrentValueDynamic ) {
55
- updatePropValueHistory( bind, currentValue );
57
+ updatePropValueHistory( bind, anyValue );
56
58
  }
57
59
 
58
- setValue( { $$type: 'dynamic', value: { name: value, settings: {} } } );
60
+ setValue( { name: value, settings: {} } );
59
61
 
60
62
  onSelect?.();
61
63
  };
@@ -88,7 +90,7 @@ export const DynamicSelection = ( { onSelect }: DynamicSelectionProps ) => {
88
90
  { dynamicGroups?.[ category ]?.title || category }
89
91
  </ListSubheader>
90
92
  { items.map( ( { value, label: tagLabel } ) => {
91
- const isSelected = isCurrentValueDynamic && value === currentValue?.value?.name;
93
+ const isSelected = isCurrentValueDynamic && value === dynamicvalue?.name;
92
94
 
93
95
  return (
94
96
  <MenuItem
@@ -1,17 +1,17 @@
1
1
  import * as React from 'react';
2
2
  import { BoundPropProvider, useBoundProp } from '@elementor/editor-controls';
3
- import { type PropKey, type PropValue } from '@elementor/editor-props';
3
+ import { isTransformable, type PropKey, type PropValue } from '@elementor/editor-props';
4
4
 
5
5
  import { useDynamicTag } from './hooks/use-dynamic-tag';
6
- import { type DynamicPropValue } from './types';
6
+ import { dynamicPropTypeUtil } from './utils';
7
7
 
8
8
  export type DynamicControlProps = React.PropsWithChildren< {
9
9
  bind: PropKey;
10
10
  } >;
11
11
 
12
12
  export const DynamicControl = ( { bind, children }: DynamicControlProps ) => {
13
- const { value, setValue, bind: propName } = useBoundProp< DynamicPropValue >();
14
- const { name = '', settings } = value?.value ?? {};
13
+ const { value, setValue, bind: propName } = useBoundProp( dynamicPropTypeUtil );
14
+ const { name = '', settings } = value ?? {};
15
15
 
16
16
  const dynamicTag = useDynamicTag( propName, name );
17
17
 
@@ -24,13 +24,11 @@ export const DynamicControl = ( { bind, children }: DynamicControlProps ) => {
24
24
 
25
25
  const setDynamicValue = ( newValue: PropValue ) => {
26
26
  setValue( {
27
- $$type: 'dynamic',
28
- value: {
29
- name,
30
- settings: {
31
- ...settings,
32
- [ bind ]: newValue,
33
- },
27
+ name,
28
+ settings: {
29
+ ...settings,
30
+ // The value inside the dynamic is not a transformable value, so we need to store the whole object.
31
+ [ bind ]: isTransformable( newValue ) ? newValue.value : newValue,
34
32
  },
35
33
  } );
36
34
  };
@@ -1,8 +1,15 @@
1
- import { isTransformable, type PropType, type PropValue, type TransformablePropType } from '@elementor/editor-props';
1
+ import {
2
+ createPropUtils,
3
+ isTransformable,
4
+ type PropType,
5
+ type PropValue,
6
+ type TransformablePropType,
7
+ } from '@elementor/editor-props';
8
+ import { z } from '@elementor/schema';
2
9
 
3
- import { type DynamicPropType, type DynamicPropValue } from './types';
10
+ import { type DynamicPropType } from './types';
4
11
 
5
- const DYNAMIC_PROP_TYPE_KEY = 'dynamic';
12
+ export const DYNAMIC_PROP_TYPE_KEY = 'dynamic';
6
13
 
7
14
  export const isDynamicPropType = ( prop: TransformablePropType ): prop is DynamicPropType =>
8
15
  prop.key === DYNAMIC_PROP_TYPE_KEY;
@@ -20,3 +27,13 @@ export const isDynamicPropValue = ( prop: PropValue ): prop is DynamicPropValue
20
27
  export const supportsDynamic = ( propType: PropType ): boolean => {
21
28
  return !! getDynamicPropType( propType );
22
29
  };
30
+
31
+ export const dynamicPropTypeUtil = createPropUtils(
32
+ DYNAMIC_PROP_TYPE_KEY,
33
+ z.strictObject( {
34
+ name: z.string(),
35
+ settings: z.record( z.any() ).optional(),
36
+ } )
37
+ );
38
+
39
+ export type DynamicPropValue = z.infer< typeof dynamicPropTypeUtil.schema >;
@@ -1,76 +0,0 @@
1
- import * as React from 'react';
2
- import { useElementSetting, useElementStyles } from '@elementor/editor-elements';
3
- import { type ClassesPropValue } from '@elementor/editor-props';
4
- import { Chip, Stack, Typography } from '@elementor/ui';
5
- import { __ } from '@wordpress/i18n';
6
-
7
- import { useClassesProp } from '../contexts/classes-prop-context';
8
- import { useElement } from '../contexts/element-context';
9
- import { useStyle } from '../contexts/style-context';
10
- import { MultiCombobox, type Option } from './multi-combobox';
11
-
12
- const ID = 'elementor-css-class-selector';
13
- const TAGS_LIMIT = 8;
14
-
15
- export function CssClassSelectorSection() {
16
- const options = useOptions();
17
-
18
- const { id: activeId, setId: setActiveId } = useStyle();
19
- const appliedIds = useAppliedClassesIds();
20
-
21
- const applied = options.filter( ( option ) => appliedIds.includes( option.value ) );
22
- const active = options.find( ( option ) => option.value === activeId ) || null;
23
-
24
- return (
25
- <Stack gap={ 1 } p={ 2 }>
26
- <Typography component="label" variant="caption" htmlFor={ ID }>
27
- { __( 'CSS Classes', 'elementor' ) }
28
- </Typography>
29
- <MultiCombobox
30
- id={ ID }
31
- size="tiny"
32
- options={ options }
33
- selected={ applied }
34
- limitTags={ TAGS_LIMIT }
35
- optionsLabel={ __( 'Global CSS Classes', 'elementor' ) }
36
- renderTags={ ( tagValue, getTagProps ) =>
37
- tagValue.map( ( option, index ) => {
38
- const chipProps = getTagProps( { index } );
39
-
40
- return (
41
- <Chip
42
- { ...chipProps }
43
- key={ chipProps.key }
44
- size="small"
45
- label={ option.label }
46
- variant={ option.value === active?.value ? 'filled' : 'standard' }
47
- color={ option.color ?? 'default' }
48
- onClick={ () => setActiveId( option.value ) }
49
- onDelete={ null }
50
- />
51
- );
52
- } )
53
- }
54
- />
55
- </Stack>
56
- );
57
- }
58
-
59
- function useAppliedClassesIds() {
60
- const { element } = useElement();
61
- const currentClassesProp = useClassesProp();
62
-
63
- return useElementSetting< ClassesPropValue >( element.id, currentClassesProp )?.value || [];
64
- }
65
-
66
- function useOptions() {
67
- const { element } = useElement();
68
-
69
- const styleDefs = useElementStyles( element.id );
70
-
71
- return Object.values( styleDefs ).map< Option >( ( styleDef ) => ( {
72
- label: styleDef.label,
73
- value: styleDef.id,
74
- color: 'primary',
75
- } ) );
76
- }
@@ -1,12 +0,0 @@
1
- export function rotateFlexIcon( direction: string | null = 'row', initValue: number ) {
2
- const rotationIndexMap: Record< string, number > = {
3
- row: 0,
4
- column: 1,
5
- 'row-reverse': 2,
6
- 'column-reverse': 3,
7
- };
8
-
9
- const rotationIndex = initValue + ( rotationIndexMap[ direction || 'row' ] ?? 0 );
10
-
11
- return `rotate(calc(90deg * ${ rotationIndex }))`;
12
- }