@elementor/editor-editing-panel 0.14.2 → 0.15.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 +18 -0
  2. package/dist/index.d.mts +29 -1
  3. package/dist/index.d.ts +29 -1
  4. package/dist/index.js +327 -263
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +309 -259
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +4 -4
  9. package/src/components/editing-panel.tsx +1 -1
  10. package/src/components/settings-tab.tsx +6 -17
  11. package/src/components/style-sections/size-section.tsx +5 -6
  12. package/src/components/style-sections/typography-section/font-size-control.tsx +16 -0
  13. package/src/components/style-sections/typography-section/font-weight-control.tsx +24 -0
  14. package/src/{controls/control-types → components/style-sections/typography-section}/text-style-control.tsx +16 -14
  15. package/src/components/style-sections/typography-section/typography-section.tsx +20 -0
  16. package/src/components/style-tab.tsx +1 -1
  17. package/src/contexts/element-context.tsx +5 -3
  18. package/src/controls/components/control-container.tsx +18 -0
  19. package/src/controls/control-replacement.ts +26 -0
  20. package/src/controls/control-types/image-control.tsx +3 -18
  21. package/src/controls/control-types/size-control.tsx +4 -2
  22. package/src/controls/control-types/text-area-control.tsx +1 -1
  23. package/src/controls/control.tsx +50 -0
  24. package/src/controls/{get-control-by-type.ts → controls-registry.tsx} +13 -9
  25. package/src/controls/settings-control.tsx +8 -21
  26. package/src/hooks/use-dynamic-tags-config.ts +35 -0
  27. package/src/hooks/use-element-type.ts +5 -0
  28. package/src/index.ts +3 -0
  29. package/src/sync/get-atomic-dynamic-tags.ts +14 -0
  30. package/src/sync/get-elementor-config.ts +7 -0
  31. package/src/sync/types.ts +15 -1
  32. package/src/types.ts +14 -0
  33. package/LICENSE +0 -674
  34. package/src/components/style-sections/typography-section.tsx +0 -15
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import { Stack, StackProps, styled } from '@elementor/ui';
3
+
4
+ const StyledStack = styled( Stack )( ( { theme, gap, direction } ) => ( {
5
+ '> :only-child': {
6
+ width: '100%',
7
+ },
8
+ '&:where( :has( > :nth-child( 2 ):last-child ) ) > :where( * )': {
9
+ width: direction === 'column' ? '100%' : `calc( 50% - ${ theme.spacing( gap / 2 ) })`,
10
+ },
11
+ '&:where( :has( > :nth-child( 3 ):last-child ) ) > :where( * )': {
12
+ width: direction === 'column' ? '100%' : `calc( 33.3333% - ${ theme.spacing( gap * 2 ) } / 3)`,
13
+ },
14
+ } ) );
15
+
16
+ export const ControlContainer = ( props: StackProps ) => (
17
+ <StyledStack gap={ 1 } direction="row" alignItems="center" justifyContent="space-between" { ...props } />
18
+ );
@@ -0,0 +1,26 @@
1
+ import { PropValue } from '../types';
2
+
3
+ type ReplaceWhenParams = {
4
+ value: PropValue;
5
+ };
6
+
7
+ type ControlReplacement = {
8
+ component: React.ComponentType;
9
+ condition: ( { value }: ReplaceWhenParams ) => boolean;
10
+ };
11
+
12
+ let controlReplacement: ControlReplacement | undefined;
13
+
14
+ export const replaceControl = ( { component, condition }: ControlReplacement ) => {
15
+ controlReplacement = { component, condition };
16
+ };
17
+
18
+ export const getControlReplacement = ( { value }: ReplaceWhenParams ) => {
19
+ let shouldReplace = false;
20
+
21
+ try {
22
+ shouldReplace = !! controlReplacement?.condition( { value } );
23
+ } catch {}
24
+
25
+ return shouldReplace ? controlReplacement?.component : undefined;
26
+ };
@@ -18,25 +18,10 @@ type Image = {
18
18
  };
19
19
  };
20
20
 
21
- // TODO: Use schema to get default image.
22
- const defaultState: Image = {
23
- $$type: 'image',
24
- value: {
25
- url: '/wp-content/plugins/elementor/assets/images/placeholder.png',
26
- },
27
- };
28
-
29
21
  export const ImageControl = () => {
30
- const { value, setValue } = useControl< Image >( defaultState );
22
+ const { value, setValue } = useControl< Image >();
31
23
  const { data: attachment } = useWpMediaAttachment( value?.value?.attachmentId );
32
-
33
- const getImageSrc = () => {
34
- if ( attachment?.url ) {
35
- return attachment.url;
36
- }
37
-
38
- return value?.value?.url ?? defaultState.value.url;
39
- };
24
+ const src = attachment?.url ?? value?.value?.url;
40
25
 
41
26
  const { open } = useWpMediaFrame( {
42
27
  types: [ 'image' ],
@@ -54,7 +39,7 @@ export const ImageControl = () => {
54
39
 
55
40
  return (
56
41
  <Card variant="outlined">
57
- <CardMedia image={ getImageSrc() } sx={ { height: 150 } } />
42
+ <CardMedia image={ src } sx={ { height: 150 } } />
58
43
  <CardOverlay>
59
44
  <Button
60
45
  color="inherit"
@@ -5,15 +5,17 @@ import { useControl } from '../control-context';
5
5
  import { useSyncExternalState } from '../hooks/use-sync-external-state';
6
6
 
7
7
  export type SizeControlProps = {
8
- units: Unit[];
8
+ units?: Unit[];
9
9
  placeholder?: string;
10
10
  };
11
11
 
12
12
  export type Unit = 'px' | '%' | 'em' | 'rem' | 'vw';
13
13
 
14
+ const defaultUnits: Unit[] = [ 'px', '%', 'em', 'rem', 'vw' ];
15
+
14
16
  export type SizeControlValue = TransformablePropValue< { unit: Unit; size: number } >;
15
17
 
16
- export const SizeControl = ( { units, placeholder }: SizeControlProps ) => {
18
+ export const SizeControl = ( { units = defaultUnits, placeholder }: SizeControlProps ) => {
17
19
  const { value, setValue } = useControl< SizeControlValue >();
18
20
 
19
21
  const [ state, setState ] = useSyncExternalState< SizeControlValue >( {
@@ -7,7 +7,7 @@ type Props = {
7
7
  };
8
8
 
9
9
  export const TextAreaControl = ( { placeholder }: Props ) => {
10
- const { value, setValue } = useControl< string >( '' );
10
+ const { value, setValue } = useControl< string >();
11
11
 
12
12
  const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
13
13
  setValue( event.target.value );
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import type { ComponentProps } from 'react';
3
+ import { getControlReplacement } from './control-replacement';
4
+ import { useControl } from './control-context';
5
+ import { createError } from '@elementor/utils';
6
+ import { ControlType, ControlTypes, getControlByType } from './controls-registry';
7
+
8
+ export type ControlTypeErrorContext = {
9
+ type: string;
10
+ };
11
+
12
+ const ControlTypeError = createError< ControlTypeErrorContext >( {
13
+ code: 'CONTROL_TYPE_NOT_FOUND',
14
+ message: `Control type not found.`,
15
+ } );
16
+
17
+ type IsRequired< T, K extends keyof T > = object extends Pick< T, K > ? false : true;
18
+
19
+ type AnyPropertyRequired< T > = {
20
+ [ K in keyof T ]: IsRequired< T, K >;
21
+ }[ keyof T ] extends true
22
+ ? true
23
+ : false;
24
+
25
+ type ControlProps< T extends ControlType > = AnyPropertyRequired< ComponentProps< ControlTypes[ T ] > > extends true
26
+ ? {
27
+ props: ComponentProps< ControlTypes[ T ] >;
28
+ type: T;
29
+ }
30
+ : {
31
+ props?: ComponentProps< ControlTypes[ T ] >;
32
+ type: T;
33
+ };
34
+
35
+ export const Control = < T extends ControlType >( { props, type }: ControlProps< T > ) => {
36
+ const { value } = useControl();
37
+
38
+ const ControlByType = getControlByType( type );
39
+
40
+ if ( ! ControlByType ) {
41
+ throw new ControlTypeError( {
42
+ context: { type },
43
+ } );
44
+ }
45
+
46
+ const ControlComponent = getControlReplacement( { value } ) || ControlByType;
47
+
48
+ // @ts-expect-error ControlComponent props are inferred from the type (T).
49
+ return <ControlComponent { ...props } />;
50
+ };
@@ -1,15 +1,19 @@
1
- import { SelectControl } from './control-types/select-control';
2
- import { TextAreaControl } from './control-types/text-area-control';
3
- import { TextControl } from './control-types/text-control';
4
1
  import { ImageControl } from './control-types/image-control';
2
+ import { TextControl } from './control-types/text-control';
3
+ import { TextAreaControl } from './control-types/text-area-control';
4
+ import { SizeControl } from './control-types/size-control';
5
+ import { SelectControl } from './control-types/select-control';
5
6
 
6
- const controlTypes = {
7
+ export const controlTypes = {
7
8
  image: ImageControl,
8
- select: SelectControl,
9
9
  text: TextControl,
10
10
  textarea: TextAreaControl,
11
- };
11
+ size: SizeControl,
12
+ select: SelectControl,
13
+ } as const;
14
+
15
+ export type ControlTypes = typeof controlTypes;
16
+
17
+ export type ControlType = keyof ControlTypes;
12
18
 
13
- export const getControlByType = ( type: string ) => {
14
- return controlTypes[ type as keyof typeof controlTypes ] ?? null;
15
- };
19
+ export const getControlByType = ( type: ControlType ) => controlTypes[ type ];
@@ -1,11 +1,11 @@
1
1
  import * as React from 'react';
2
- import { ControlContext } from '../controls/control-context';
3
- import { Stack, styled } from '@elementor/ui';
2
+ import { ControlContext } from './control-context';
4
3
  import { PropKey, PropValue } from '../types';
5
4
  import { useElementContext } from '../contexts/element-context';
6
5
  import { useWidgetSettings } from '../hooks/use-widget-settings';
7
6
  import { updateSettings } from '../sync/update-settings';
8
7
  import { ControlLabel } from '../components/control-label';
8
+ import { ControlContainer } from './components/control-container';
9
9
 
10
10
  type Props = {
11
11
  bind: PropKey;
@@ -13,8 +13,11 @@ type Props = {
13
13
  };
14
14
 
15
15
  export const SettingsControlProvider = ( { bind, children }: Props ) => {
16
- const { element } = useElementContext();
17
- const value = useWidgetSettings( { id: element.id, bind } );
16
+ const { element, elementType } = useElementContext();
17
+
18
+ const defaultValue = elementType.propsSchema[ bind ]?.default;
19
+ const settingsValue = useWidgetSettings( { id: element.id, bind } );
20
+ const value = settingsValue ?? defaultValue ?? null;
18
21
 
19
22
  const setValue = ( newValue: PropValue ) => {
20
23
  updateSettings( {
@@ -30,26 +33,10 @@ export const SettingsControlProvider = ( { bind, children }: Props ) => {
30
33
 
31
34
  const SettingsControl = ( { children, bind }: Props ) => (
32
35
  <SettingsControlProvider bind={ bind }>
33
- <StyledStack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap">
34
- { children }
35
- </StyledStack>
36
+ <ControlContainer flexWrap="wrap">{ children }</ControlContainer>
36
37
  </SettingsControlProvider>
37
38
  );
38
39
 
39
- const StyledStack = styled( Stack )( ( { theme } ) => {
40
- const gap = theme.spacing( 1 );
41
-
42
- return {
43
- gap,
44
- '& > *': {
45
- width: `calc(50% - ${ gap } / 2)`,
46
- },
47
- '& > label': {
48
- flexShrink: 0,
49
- },
50
- };
51
- } );
52
-
53
40
  // TODO: When we start using useControl inside the label component, we should create a new component for it,
54
41
  // and keep ControlLabel as a simple label component without context.
55
42
  SettingsControl.Label = ControlLabel;
@@ -0,0 +1,35 @@
1
+ import { useMemo } from 'react';
2
+ import { useElementContext } from '../contexts/element-context';
3
+ import { PropKey } from '../types';
4
+ import { getAtomicDynamicTags } from '../sync/get-atomic-dynamic-tags';
5
+
6
+ export const useDynamicTagsConfig = ( propName: PropKey ) => {
7
+ let categories: string[] = [];
8
+
9
+ const { elementType } = useElementContext();
10
+
11
+ const propSchema = elementType.propsSchema?.[ propName ];
12
+
13
+ if ( propSchema ) {
14
+ const dynamicTags = getAtomicDynamicTags();
15
+
16
+ categories = dynamicTags?.propTypesToDynamic?.[ propSchema.type ] || [];
17
+ }
18
+
19
+ // eslint-disable-next-line react-hooks/exhaustive-deps
20
+ return useMemo( () => getDynamicTagsByCategories( categories ), [ categories.join() ] );
21
+ };
22
+
23
+ const getDynamicTagsByCategories = ( categories: string[] ) => {
24
+ const dynamicTags = getAtomicDynamicTags();
25
+
26
+ if ( ! categories.length || ! dynamicTags?.tags ) {
27
+ return [];
28
+ }
29
+
30
+ const _categories = new Set( categories );
31
+
32
+ return Object.values( dynamicTags.tags ).filter( ( dynamicTag ) =>
33
+ dynamicTag.categories.some( ( category ) => _categories.has( category ) )
34
+ );
35
+ };
@@ -17,9 +17,14 @@ export default function useElementType( type?: string ) {
17
17
  return null;
18
18
  }
19
19
 
20
+ if ( ! elementType?.atomic_props_schema ) {
21
+ return null;
22
+ }
23
+
20
24
  return {
21
25
  key: type,
22
26
  controls: elementType.atomic_controls,
27
+ propsSchema: elementType.atomic_props_schema,
23
28
  title: elementType.title,
24
29
  };
25
30
  },
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export { replaceControl } from './controls/control-replacement';
2
+ export { useControl } from './controls/control-context';
3
+
1
4
  import init from './init';
2
5
 
3
6
  init();
@@ -0,0 +1,14 @@
1
+ import { getElementorConfig } from './get-elementor-config';
2
+
3
+ export const getAtomicDynamicTags = () => {
4
+ const { atomicDynamicTags } = getElementorConfig();
5
+
6
+ if ( ! atomicDynamicTags ) {
7
+ return null;
8
+ }
9
+
10
+ return {
11
+ tags: atomicDynamicTags.tags,
12
+ propTypesToDynamic: atomicDynamicTags.prop_types_to_dynamic,
13
+ };
14
+ };
@@ -0,0 +1,7 @@
1
+ import { ExtendedWindow } from './types';
2
+
3
+ export const getElementorConfig = () => {
4
+ const extendedWindow = window as unknown as ExtendedWindow;
5
+
6
+ return extendedWindow.elementor?.config ?? {};
7
+ };
package/src/sync/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ControlItem, PropValue } from '../types';
1
+ import { ControlItem, PropsSchema, PropValue } from '../types';
2
2
  import { StyleDefinition, StyleDefinitionID } from '@elementor/editor-style';
3
3
 
4
4
  export type ExtendedWindow = Window & {
@@ -10,14 +10,28 @@ export type ExtendedWindow = Window & {
10
10
  string,
11
11
  {
12
12
  atomic_controls?: ControlItem[];
13
+ atomic_props_schema?: PropsSchema;
13
14
  controls: object;
14
15
  title: string;
15
16
  }
16
17
  >;
17
18
  getContainer?: ( id: string ) => V1Element;
19
+ config?: {
20
+ atomicDynamicTags?: {
21
+ tags: DynamicTags;
22
+ prop_types_to_dynamic: Record< string, string[] >;
23
+ };
24
+ };
18
25
  };
19
26
  };
20
27
 
28
+ export type DynamicTags = Record< DynamicTag[ 'name' ], DynamicTag >;
29
+
30
+ export type DynamicTag = {
31
+ name: string;
32
+ categories: string[];
33
+ };
34
+
21
35
  export type V1Element = {
22
36
  model: V1Model< V1ElementModelProps >;
23
37
  settings?: V1Model< V1ElementSettingsProps >;
package/src/types.ts CHANGED
@@ -8,6 +8,7 @@ export type Element = {
8
8
  export type ElementType = {
9
9
  key: string;
10
10
  controls: ControlItem[];
11
+ propsSchema: PropsSchema;
11
12
  title: string;
12
13
  };
13
14
 
@@ -33,6 +34,19 @@ export type Control = {
33
34
 
34
35
  export type ControlItem = ControlsSection | Control;
35
36
 
37
+ export type Constraint = {
38
+ type: string;
39
+ value: unknown;
40
+ };
41
+
42
+ export type PropDefinition = {
43
+ type: string;
44
+ default: PropValue;
45
+ constraints?: Constraint[];
46
+ };
47
+
48
+ export type PropsSchema = Record< Control[ 'value' ][ 'bind' ], PropDefinition >;
49
+
36
50
  type MaybeArray< T > = T | T[];
37
51
 
38
52
  export type TransformablePropValue< T = unknown > = {