@elementor/editor-controls 0.1.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 (36) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +4 -0
  3. package/dist/index.d.mts +148 -0
  4. package/dist/index.d.ts +148 -0
  5. package/dist/index.js +1346 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +1320 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +52 -0
  10. package/src/bound-prop-context.tsx +30 -0
  11. package/src/components/control-label.tsx +10 -0
  12. package/src/components/control-toggle-button-group.tsx +84 -0
  13. package/src/components/repeater.tsx +200 -0
  14. package/src/components/text-field-inner-selection.tsx +76 -0
  15. package/src/control-actions/control-actions-context.tsx +27 -0
  16. package/src/control-actions/control-actions.tsx +32 -0
  17. package/src/controls/background-overlay-repeater-control.tsx +119 -0
  18. package/src/controls/box-shadow-repeater-control.tsx +227 -0
  19. package/src/controls/color-control.tsx +32 -0
  20. package/src/controls/equal-unequal-sizes-control.tsx +231 -0
  21. package/src/controls/font-family-control.tsx +154 -0
  22. package/src/controls/image-control.tsx +64 -0
  23. package/src/controls/image-media-control.tsx +71 -0
  24. package/src/controls/linked-dimensions-control.tsx +140 -0
  25. package/src/controls/number-control.tsx +31 -0
  26. package/src/controls/select-control.tsx +31 -0
  27. package/src/controls/size-control.tsx +77 -0
  28. package/src/controls/stroke-control.tsx +106 -0
  29. package/src/controls/text-area-control.tsx +32 -0
  30. package/src/controls/text-control.tsx +18 -0
  31. package/src/controls/toggle-control.tsx +34 -0
  32. package/src/create-control-replacement.tsx +54 -0
  33. package/src/create-control.tsx +41 -0
  34. package/src/hooks/use-filtered-font-families.ts +38 -0
  35. package/src/hooks/use-sync-external-state.tsx +51 -0
  36. package/src/index.ts +31 -0
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+ import { type PropValue } from '@elementor/editor-props';
3
+ import { MenuItem, Select, type SelectChangeEvent } from '@elementor/ui';
4
+
5
+ import { useBoundProp } from '../bound-prop-context';
6
+ import ControlActions from '../control-actions/control-actions';
7
+ import { createControl } from '../create-control';
8
+
9
+ type Props< T > = {
10
+ options: Array< { label: string; value: T; disabled?: boolean } >;
11
+ };
12
+
13
+ export const SelectControl = createControl( < T extends PropValue >( { options }: Props< T > ) => {
14
+ const { value, setValue } = useBoundProp< T >();
15
+
16
+ const handleChange = ( event: SelectChangeEvent< T > ) => {
17
+ setValue( event.target.value as T );
18
+ };
19
+
20
+ return (
21
+ <ControlActions>
22
+ <Select displayEmpty size="tiny" value={ value ?? '' } onChange={ handleChange } fullWidth>
23
+ { options.map( ( { label, ...props } ) => (
24
+ <MenuItem key={ props.value } { ...props }>
25
+ { label }
26
+ </MenuItem>
27
+ ) ) }
28
+ </Select>
29
+ </ControlActions>
30
+ );
31
+ } );
@@ -0,0 +1,77 @@
1
+ import * as React from 'react';
2
+ import { type SizePropValue } from '@elementor/editor-props';
3
+ import { InputAdornment } from '@elementor/ui';
4
+
5
+ import { useBoundProp } from '../bound-prop-context';
6
+ import { SelectionEndAdornment, TextFieldInnerSelection } from '../components/text-field-inner-selection';
7
+ import ControlActions from '../control-actions/control-actions';
8
+ import { createControl } from '../create-control';
9
+ import { useSyncExternalState } from '../hooks/use-sync-external-state';
10
+
11
+ export type Unit = 'px' | '%' | 'em' | 'rem' | 'vw' | 'vh';
12
+
13
+ const defaultUnits: Unit[] = [ 'px', '%', 'em', 'rem', 'vw', 'vh' ];
14
+
15
+ const defaultUnit = 'px';
16
+ const defaultSize = NaN;
17
+
18
+ export type SizeControlProps = {
19
+ placeholder?: string;
20
+ startIcon?: React.ReactNode;
21
+ units?: Unit[];
22
+ };
23
+
24
+ export const SizeControl = createControl( ( { units = defaultUnits, placeholder, startIcon }: SizeControlProps ) => {
25
+ const { value, setValue } = useBoundProp< SizePropValue | undefined >();
26
+
27
+ const [ state, setState ] = useSyncExternalState< SizePropValue >( {
28
+ external: value,
29
+ setExternal: setValue,
30
+ persistWhen: ( controlValue ) => !! controlValue?.value?.size || controlValue?.value?.size === 0,
31
+ fallback: ( controlValue ) => ( {
32
+ $$type: 'size',
33
+ value: { unit: controlValue?.value?.unit || defaultUnit, size: defaultSize },
34
+ } ),
35
+ } );
36
+
37
+ const handleUnitChange = ( unit: Unit ) => {
38
+ setState( ( prev ) => ( {
39
+ ...prev,
40
+ value: {
41
+ ...prev.value,
42
+ unit,
43
+ },
44
+ } ) );
45
+ };
46
+
47
+ const handleSizeChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
48
+ const { value: size } = event.target;
49
+
50
+ setState( ( prev ) => ( {
51
+ ...prev,
52
+ value: {
53
+ ...prev.value,
54
+ size: size || size === '0' ? parseFloat( size ) : defaultSize,
55
+ },
56
+ } ) );
57
+ };
58
+
59
+ return (
60
+ <ControlActions>
61
+ <TextFieldInnerSelection
62
+ endAdornment={
63
+ <SelectionEndAdornment
64
+ options={ units }
65
+ onClick={ handleUnitChange }
66
+ value={ state.value.unit ?? defaultUnit }
67
+ />
68
+ }
69
+ placeholder={ placeholder }
70
+ startAdornment={ startIcon ?? <InputAdornment position="start">{ startIcon }</InputAdornment> }
71
+ type="number"
72
+ value={ Number.isNaN( state.value.size ) ? '' : state.value.size }
73
+ onChange={ handleSizeChange }
74
+ />
75
+ </ControlActions>
76
+ );
77
+ } );
@@ -0,0 +1,106 @@
1
+ import * as React from 'react';
2
+ import { type PropValue, type StrokePropValue, type TransformablePropValue } from '@elementor/editor-props';
3
+ import { Grid, Stack } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { BoundPropProvider, useBoundProp } from '../bound-prop-context';
7
+ import { ControlLabel } from '../components/control-label';
8
+ import { createControl } from '../create-control';
9
+ import { ColorControl } from './color-control';
10
+ import { SizeControl, type Unit } from './size-control';
11
+
12
+ type SetContextValue = ( v: PropValue ) => void;
13
+
14
+ const defaultStrokeControlValue: StrokePropValue = {
15
+ $$type: 'stroke',
16
+ value: {
17
+ color: {
18
+ $$type: 'color',
19
+ value: '#000000',
20
+ },
21
+ width: {
22
+ $$type: 'size',
23
+ value: {
24
+ unit: 'px',
25
+ size: NaN,
26
+ },
27
+ },
28
+ },
29
+ };
30
+
31
+ const units: Unit[] = [ 'px', 'em', 'rem' ];
32
+
33
+ export const StrokeControl = createControl( () => {
34
+ const { value, setValue } = useBoundProp< StrokePropValue >( defaultStrokeControlValue );
35
+
36
+ const setStrokeWidth = ( newValue: TransformablePropValue< 'size', { unit: Unit; size: number } > ) => {
37
+ const updatedValue = {
38
+ ...( value?.value ?? defaultStrokeControlValue.value ),
39
+ width: newValue,
40
+ };
41
+
42
+ setValue( {
43
+ $$type: 'stroke',
44
+ value: updatedValue,
45
+ } );
46
+ };
47
+
48
+ const setStrokeColor = ( newValue: TransformablePropValue< 'color', string > ) => {
49
+ const updatedValue = {
50
+ ...( value?.value ?? defaultStrokeControlValue.value ),
51
+ color: newValue,
52
+ };
53
+
54
+ setValue( {
55
+ $$type: 'stroke',
56
+ value: updatedValue,
57
+ } );
58
+ };
59
+
60
+ return (
61
+ <Stack gap={ 1.5 }>
62
+ <Control
63
+ bind="width"
64
+ label={ __( 'Stroke Width', 'elementor' ) }
65
+ value={ value?.value.width ?? defaultStrokeControlValue.value.width }
66
+ setValue={ setStrokeWidth }
67
+ >
68
+ <SizeControl units={ units } />
69
+ </Control>
70
+
71
+ <Control
72
+ bind="color"
73
+ label={ __( 'Stroke Color', 'elementor' ) }
74
+ value={ value?.value.color ?? defaultStrokeControlValue.value.color }
75
+ setValue={ setStrokeColor }
76
+ >
77
+ <ColorControl />
78
+ </Control>
79
+ </Stack>
80
+ );
81
+ } );
82
+
83
+ const Control = < T extends PropValue >( {
84
+ bind,
85
+ value,
86
+ setValue,
87
+ label,
88
+ children,
89
+ }: {
90
+ bind: string;
91
+ value: T;
92
+ setValue: ( v: T ) => void;
93
+ label: string;
94
+ children: React.ReactNode;
95
+ } ) => (
96
+ <BoundPropProvider bind={ bind } value={ value } setValue={ setValue as SetContextValue }>
97
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
98
+ <Grid item xs={ 6 }>
99
+ <ControlLabel>{ label }</ControlLabel>
100
+ </Grid>
101
+ <Grid item xs={ 6 }>
102
+ { children }
103
+ </Grid>
104
+ </Grid>
105
+ </BoundPropProvider>
106
+ );
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+ import { TextField } from '@elementor/ui';
3
+
4
+ import { useBoundProp } from '../bound-prop-context';
5
+ import ControlActions from '../control-actions/control-actions';
6
+ import { createControl } from '../create-control';
7
+
8
+ type Props = {
9
+ placeholder?: string;
10
+ };
11
+
12
+ export const TextAreaControl = createControl( ( { placeholder }: Props ) => {
13
+ const { value, setValue } = useBoundProp< string >();
14
+
15
+ const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
16
+ setValue( event.target.value );
17
+ };
18
+
19
+ return (
20
+ <ControlActions>
21
+ <TextField
22
+ size="tiny"
23
+ multiline
24
+ fullWidth
25
+ rows={ 5 }
26
+ value={ value }
27
+ onChange={ handleChange }
28
+ placeholder={ placeholder }
29
+ />
30
+ </ControlActions>
31
+ );
32
+ } );
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import { TextField } from '@elementor/ui';
3
+
4
+ import { useBoundProp } from '../bound-prop-context';
5
+ import ControlActions from '../control-actions/control-actions';
6
+ import { createControl } from '../create-control';
7
+
8
+ export const TextControl = createControl( ( { placeholder }: { placeholder?: string } ) => {
9
+ const { value, setValue } = useBoundProp< string >( '' );
10
+
11
+ const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => setValue( event.target.value );
12
+
13
+ return (
14
+ <ControlActions>
15
+ <TextField size="tiny" fullWidth value={ value } onChange={ handleChange } placeholder={ placeholder } />
16
+ </ControlActions>
17
+ );
18
+ } );
@@ -0,0 +1,34 @@
1
+ import * as React from 'react';
2
+ import { type PropValue } from '@elementor/editor-props';
3
+ import { type ToggleButtonProps } from '@elementor/ui';
4
+
5
+ import { useBoundProp } from '../bound-prop-context';
6
+ import { ControlToggleButtonGroup, type ToggleButtonGroupItem } from '../components/control-toggle-button-group';
7
+ import { createControl } from '../create-control';
8
+
9
+ type ToggleControlProps< T extends PropValue > = {
10
+ options: ToggleButtonGroupItem< T >[];
11
+ fullWidth?: boolean;
12
+ size?: ToggleButtonProps[ 'size' ];
13
+ };
14
+
15
+ export const ToggleControl = createControl(
16
+ < T extends PropValue >( { options, fullWidth = false, size = 'tiny' }: ToggleControlProps< T > ) => {
17
+ const { value, setValue } = useBoundProp< T | null >();
18
+
19
+ const handleToggle = ( option: T | null ) => {
20
+ setValue( option );
21
+ };
22
+
23
+ return (
24
+ <ControlToggleButtonGroup
25
+ items={ options }
26
+ value={ value || null }
27
+ onChange={ handleToggle }
28
+ exclusive={ true }
29
+ fullWidth={ fullWidth }
30
+ size={ size }
31
+ />
32
+ );
33
+ }
34
+ );
@@ -0,0 +1,54 @@
1
+ import * as React from 'react';
2
+ import { type ComponentType, createContext, useContext } from 'react';
3
+ import { type PropValue } from '@elementor/editor-props';
4
+
5
+ import { useBoundProp } from './bound-prop-context';
6
+
7
+ export type ReplaceWhenParams = {
8
+ value: PropValue;
9
+ };
10
+
11
+ export type CreateControlReplacement = {
12
+ component: ComponentType;
13
+ condition: ( { value }: ReplaceWhenParams ) => boolean;
14
+ };
15
+
16
+ const ControlReplacementContext = createContext< CreateControlReplacement | undefined >( undefined );
17
+
18
+ export const ControlReplacementProvider = ( {
19
+ component,
20
+ condition,
21
+ children,
22
+ }: React.PropsWithChildren< CreateControlReplacement > ) => {
23
+ return (
24
+ <ControlReplacementContext.Provider value={ { component, condition } }>
25
+ { children }
26
+ </ControlReplacementContext.Provider>
27
+ );
28
+ };
29
+ export const useControlReplacement = () => {
30
+ const { value } = useBoundProp();
31
+ const controlReplacement = useContext( ControlReplacementContext );
32
+
33
+ let shouldReplace = false;
34
+
35
+ try {
36
+ shouldReplace = !! controlReplacement?.condition( { value } ) && !! controlReplacement.component;
37
+ } catch {}
38
+
39
+ return shouldReplace ? controlReplacement?.component : undefined;
40
+ };
41
+
42
+ export const createControlReplacement = () => {
43
+ let controlReplacement: CreateControlReplacement;
44
+
45
+ function replaceControl( { component, condition }: CreateControlReplacement ) {
46
+ controlReplacement = { component, condition };
47
+ }
48
+
49
+ function getControlReplacement() {
50
+ return controlReplacement;
51
+ }
52
+
53
+ return { replaceControl, getControlReplacement };
54
+ };
@@ -0,0 +1,41 @@
1
+ import * as React from 'react';
2
+ import { type ComponentProps, type ComponentType } from 'react';
3
+ import { ErrorBoundary } from '@elementor/ui';
4
+
5
+ import { useControlReplacement } from './create-control-replacement';
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ type AnyComponentType = ComponentType< any >;
9
+
10
+ type Options = {
11
+ supportsReplacements?: boolean;
12
+ };
13
+
14
+ const brandSymbol = Symbol( 'control' );
15
+
16
+ export type ControlComponent< TComponent extends AnyComponentType = AnyComponentType > = TComponent & {
17
+ [ brandSymbol ]: true;
18
+ };
19
+
20
+ export function createControl< T extends AnyComponentType >(
21
+ Component: T,
22
+ { supportsReplacements = true }: Options = {}
23
+ ) {
24
+ return ( ( props: ComponentProps< T > ) => {
25
+ const ControlReplacement = useControlReplacement();
26
+
27
+ if ( ControlReplacement && supportsReplacements ) {
28
+ return (
29
+ <ErrorBoundary fallback={ null }>
30
+ <ControlReplacement { ...props } />
31
+ </ErrorBoundary>
32
+ );
33
+ }
34
+
35
+ return (
36
+ <ErrorBoundary fallback={ null }>
37
+ <Component { ...props } />
38
+ </ErrorBoundary>
39
+ );
40
+ } ) as ControlComponent< T >;
41
+ }
@@ -0,0 +1,38 @@
1
+ import { __ } from '@wordpress/i18n';
2
+
3
+ export type SupportedFonts = 'system' | 'googlefonts' | 'customfonts';
4
+
5
+ const supportedCategories: Record< SupportedFonts, string > = {
6
+ system: __( 'System', 'elementor' ),
7
+ googlefonts: __( 'Google Fonts', 'elementor' ),
8
+ customfonts: __( 'Custom Fonts', 'elementor' ),
9
+ };
10
+
11
+ export const useFilteredFontFamilies = ( fontFamilies: Record< string, SupportedFonts >, searchValue: string ) => {
12
+ const filteredFontFamilies = Object.entries( fontFamilies ).reduce< Map< string, string[] > >(
13
+ ( acc, [ font, category ] ) => {
14
+ const isMatch = font.toLowerCase().includes( searchValue.trim().toLowerCase() );
15
+
16
+ if ( ! isMatch ) {
17
+ return acc;
18
+ }
19
+
20
+ const categoryLabel = supportedCategories[ category as SupportedFonts ];
21
+
22
+ if ( categoryLabel ) {
23
+ const existingCategory = acc.get( categoryLabel );
24
+
25
+ if ( existingCategory ) {
26
+ existingCategory.push( font );
27
+ } else {
28
+ acc.set( categoryLabel, [ font ] );
29
+ }
30
+ }
31
+
32
+ return acc;
33
+ },
34
+ new Map()
35
+ );
36
+
37
+ return [ ...filteredFontFamilies ];
38
+ };
@@ -0,0 +1,51 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ type UseInternalStateOptions< TValue > = {
4
+ external: TValue | undefined;
5
+ setExternal: ( value: TValue | undefined ) => void;
6
+ persistWhen: ( value: TValue | undefined ) => boolean;
7
+ fallback: ( value: TValue | undefined ) => TValue;
8
+ };
9
+
10
+ export const useSyncExternalState = < TValue, >( {
11
+ external,
12
+ setExternal,
13
+ persistWhen,
14
+ fallback,
15
+ }: UseInternalStateOptions< TValue > ) => {
16
+ function toExternal( internalValue: TValue | undefined ) {
17
+ if ( persistWhen( internalValue ) ) {
18
+ return internalValue;
19
+ }
20
+
21
+ return undefined;
22
+ }
23
+
24
+ function toInternal( externalValue: TValue | undefined, internalValue: TValue | undefined ) {
25
+ if ( ! externalValue ) {
26
+ return fallback( internalValue );
27
+ }
28
+
29
+ return externalValue;
30
+ }
31
+
32
+ const [ internal, setInternal ] = useState< TValue >( toInternal( external, undefined ) );
33
+
34
+ useEffect( () => {
35
+ setInternal( ( prevInternal ) => toInternal( external, prevInternal ) );
36
+
37
+ // eslint-disable-next-line react-hooks/exhaustive-deps
38
+ }, [ external ] );
39
+
40
+ type SetterFunc = ( value: TValue ) => TValue;
41
+
42
+ const setInternalValue = ( setter: SetterFunc | TValue ) => {
43
+ const setterFn = ( typeof setter === 'function' ? setter : () => setter ) as SetterFunc;
44
+ const updated = setterFn( internal );
45
+
46
+ setInternal( updated );
47
+ setExternal( toExternal( updated ) );
48
+ };
49
+
50
+ return [ internal, setInternalValue ] as const;
51
+ };
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ // control types
2
+ export { ImageControl } from './controls/image-control';
3
+ export { TextControl } from './controls/text-control';
4
+ export { TextAreaControl } from './controls/text-area-control';
5
+ export { SizeControl } from './controls/size-control';
6
+ export { StrokeControl } from './controls/stroke-control';
7
+ export { BoxShadowRepeaterControl } from './controls/box-shadow-repeater-control';
8
+ export { BackgroundOverlayRepeaterControl } from './controls/background-overlay-repeater-control';
9
+ export { SelectControl } from './controls/select-control';
10
+ export { ColorControl } from './controls/color-control';
11
+ export { ToggleControl } from './controls/toggle-control';
12
+ export { NumberControl } from './controls/number-control';
13
+ export { EqualUnequalSizesControl } from './controls/equal-unequal-sizes-control';
14
+ export { LinkedDimensionsControl } from './controls/linked-dimensions-control';
15
+ export { FontFamilyControl } from './controls/font-family-control';
16
+
17
+ // components
18
+ export { ControlLabel } from './components/control-label';
19
+
20
+ // types
21
+ export type { ControlComponent } from './create-control';
22
+ export type { ToggleButtonGroupItem } from './components/control-toggle-button-group';
23
+ export type { EqualUnequalItems } from './controls/equal-unequal-sizes-control';
24
+
25
+ // providers
26
+ export { createControlReplacement, ControlReplacementProvider } from './create-control-replacement';
27
+ export { useBoundProp, BoundPropProvider } from './bound-prop-context';
28
+ export { ControlActionsProvider, useControlActions } from './control-actions/control-actions-context';
29
+
30
+ // hooks
31
+ export { useSyncExternalState } from './hooks/use-sync-external-state';