@elementor/editor-editing-panel 4.3.0-beta2 → 4.3.0-beta3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-editing-panel",
3
- "version": "4.3.0-beta2",
3
+ "version": "4.3.0-beta3",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,31 +39,32 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "4.3.0-beta2",
43
- "@elementor/editor-canvas": "4.3.0-beta2",
44
- "@elementor/editor-controls": "4.3.0-beta2",
45
- "@elementor/editor-documents": "4.3.0-beta2",
46
- "@elementor/editor-elements": "4.3.0-beta2",
47
- "@elementor/editor-interactions": "4.3.0-beta2",
48
- "@elementor/editor-notifications": "4.3.0-beta2",
49
- "@elementor/editor-panels": "4.3.0-beta2",
50
- "@elementor/editor-props": "4.3.0-beta2",
51
- "@elementor/editor-responsive": "4.3.0-beta2",
52
- "@elementor/editor-styles": "4.3.0-beta2",
53
- "@elementor/editor-styles-repository": "4.3.0-beta2",
54
- "@elementor/editor-ui": "4.3.0-beta2",
55
- "@elementor/editor-v1-adapters": "4.3.0-beta2",
56
- "@elementor/http-client": "4.3.0-beta2",
42
+ "@elementor/editor": "4.3.0-beta3",
43
+ "@elementor/editor-canvas": "4.3.0-beta3",
44
+ "@elementor/editor-controls": "4.3.0-beta3",
45
+ "@elementor/editor-current-user": "4.3.0-beta3",
46
+ "@elementor/editor-documents": "4.3.0-beta3",
47
+ "@elementor/editor-elements": "4.3.0-beta3",
48
+ "@elementor/editor-interactions": "4.3.0-beta3",
49
+ "@elementor/editor-notifications": "4.3.0-beta3",
50
+ "@elementor/editor-panels": "4.3.0-beta3",
51
+ "@elementor/editor-props": "4.3.0-beta3",
52
+ "@elementor/editor-responsive": "4.3.0-beta3",
53
+ "@elementor/editor-styles": "4.3.0-beta3",
54
+ "@elementor/editor-styles-repository": "4.3.0-beta3",
55
+ "@elementor/editor-ui": "4.3.0-beta3",
56
+ "@elementor/editor-v1-adapters": "4.3.0-beta3",
57
+ "@elementor/http-client": "4.3.0-beta3",
57
58
  "@elementor/icons": "~1.75.1",
58
- "@elementor/editor-variables": "4.3.0-beta2",
59
- "@elementor/locations": "4.3.0-beta2",
60
- "@elementor/menus": "4.3.0-beta2",
61
- "@elementor/query": "4.3.0-beta2",
62
- "@elementor/schema": "4.3.0-beta2",
63
- "@elementor/session": "4.3.0-beta2",
59
+ "@elementor/editor-variables": "4.3.0-beta3",
60
+ "@elementor/locations": "4.3.0-beta3",
61
+ "@elementor/menus": "4.3.0-beta3",
62
+ "@elementor/query": "4.3.0-beta3",
63
+ "@elementor/schema": "4.3.0-beta3",
64
+ "@elementor/session": "4.3.0-beta3",
64
65
  "@elementor/ui": "1.37.5",
65
- "@elementor/utils": "4.3.0-beta2",
66
- "@elementor/wp-media": "4.3.0-beta2",
66
+ "@elementor/utils": "4.3.0-beta3",
67
+ "@elementor/wp-media": "4.3.0-beta3",
67
68
  "@wordpress/i18n": "^5.13.0"
68
69
  },
69
70
  "peerDependencies": {
@@ -0,0 +1,51 @@
1
+ import * as React from 'react';
2
+ import { type PropsWithChildren, useState } from 'react';
3
+ import { InfoCircleFilledIcon } from '@elementor/icons';
4
+ import { Alert, AlertTitle, Box, Infotip, Link } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ const LEARN_MORE_URL = 'https://go.elementor.com/content-only-access-infotip';
8
+
9
+ const INFOTIP_WIDTH = 300;
10
+
11
+ export const ContentOnlyInfotip = ( { children }: PropsWithChildren ) => {
12
+ const [ isOpen, setIsOpen ] = useState( false );
13
+
14
+ return (
15
+ <Infotip
16
+ open={ isOpen }
17
+ placement="bottom"
18
+ color="secondary"
19
+ content={
20
+ <Alert color="secondary" icon={ <InfoCircleFilledIcon /> } size="small">
21
+ <AlertTitle>{ __( 'Content-only access', 'elementor' ) }</AlertTitle>
22
+ <Box component="span">
23
+ { __( 'Your Site Admin has limited this role to content editing.', 'elementor' ) }{ ' ' }
24
+ <Link href={ LEARN_MORE_URL } target="_blank" rel="noreferrer" color="info.main">
25
+ { __( 'Learn More', 'elementor' ) }
26
+ </Link>
27
+ </Box>
28
+ </Alert>
29
+ }
30
+ slotProps={ {
31
+ popper: {
32
+ sx: { width: INFOTIP_WIDTH },
33
+ // The popper is portaled, so it needs its own hover handlers to stay open while the pointer
34
+ // travels from the tab to the "Learn More" link.
35
+ onMouseEnter: () => setIsOpen( true ),
36
+ onMouseLeave: () => setIsOpen( false ),
37
+ },
38
+ } }
39
+ >
40
+ { /* A disabled MUI tab sets pointer-events: none, so the trigger has to opt back in to receive hover. */ }
41
+ <Box
42
+ component="span"
43
+ sx={ { pointerEvents: 'auto' } }
44
+ onMouseEnter={ () => setIsOpen( true ) }
45
+ onMouseLeave={ () => setIsOpen( false ) }
46
+ >
47
+ { children }
48
+ </Box>
49
+ </Infotip>
50
+ );
51
+ };
@@ -1,5 +1,6 @@
1
1
  import { Fragment } from 'react';
2
2
  import * as React from 'react';
3
+ import { useHasContentOnlyAccess } from '@elementor/editor-current-user';
3
4
  import { getWidgetsCache } from '@elementor/editor-elements';
4
5
  import { Divider, Stack, Tab, TabPanel, Tabs, useTabs } from '@elementor/ui';
5
6
  import { __ } from '@wordpress/i18n';
@@ -8,12 +9,17 @@ import { useElement } from '../contexts/element-context';
8
9
  import { ScrollProvider } from '../contexts/scroll-context';
9
10
  import { useDefaultPanelSettings } from '../hooks/use-default-panel-settings';
10
11
  import { useStateByElement } from '../hooks/use-state-by-element';
12
+ import { ContentOnlyInfotip } from './content-only-infotip';
11
13
  import { InteractionsTab } from './interactions-tab';
12
14
  import { SettingsTab } from './settings-tab';
13
15
  import { stickyHeaderStyles, StyleTab } from './style-tab';
14
16
 
15
17
  type TabValue = 'settings' | 'style' | 'interactions';
16
18
 
19
+ const CONTENT_ONLY_TAB: TabValue = 'settings';
20
+
21
+ const DESIGN_TABS: TabValue[] = [ 'style', 'interactions' ];
22
+
17
23
  export const EditingPanelTabs = () => {
18
24
  const { element } = useElement();
19
25
 
@@ -31,10 +37,24 @@ const PanelTabContent = () => {
31
37
  const editorDefaults = useDefaultPanelSettings();
32
38
  const defaultComponentTab = editorDefaults.defaultTab as TabValue;
33
39
  const isPromotedElement = !! getWidgetsCache()?.[ element.type ]?.meta?.is_pro_promotion;
40
+ const hasContentOnlyAccess = useHasContentOnlyAccess();
41
+
42
+ // A promoted element has no General tab, and its Style tab only renders an upsell, so restricting it would leave the panel empty.
43
+ const areDesignTabsRestricted = hasContentOnlyAccess && ! isPromotedElement;
34
44
 
35
45
  const [ storedTab, setCurrentTab ] = useStateByElement< TabValue >( 'tab', defaultComponentTab );
36
- const currentTab = isPromotedElement && storedTab === 'settings' ? 'style' : storedTab;
46
+ const currentTab = resolveCurrentTab( { storedTab, isPromotedElement, areDesignTabsRestricted } );
37
47
  const { getTabProps, getTabPanelProps, getTabsProps } = useTabs< TabValue >( currentTab );
48
+
49
+ const getDesignTabProps = ( tab: TabValue ) => ( {
50
+ ...getTabProps( tab ),
51
+ disabled: areDesignTabsRestricted,
52
+ sx: areDesignTabsRestricted ? { pointerEvents: 'auto' } : undefined,
53
+ } );
54
+
55
+ const withRestrictionInfotip = ( label: string ) =>
56
+ areDesignTabsRestricted ? <ContentOnlyInfotip>{ label }</ContentOnlyInfotip> : label;
57
+
38
58
  return (
39
59
  <ScrollProvider>
40
60
  <Stack direction="column" sx={ { width: '100%' } }>
@@ -45,6 +65,10 @@ const PanelTabContent = () => {
45
65
  sx={ { mt: 0.5 } }
46
66
  { ...getTabsProps() }
47
67
  onChange={ ( _: unknown, newValue: TabValue ) => {
68
+ if ( areDesignTabsRestricted && DESIGN_TABS.includes( newValue ) ) {
69
+ return;
70
+ }
71
+
48
72
  getTabsProps().onChange( _, newValue );
49
73
  setCurrentTab( newValue );
50
74
  } }
@@ -52,8 +76,14 @@ const PanelTabContent = () => {
52
76
  { ! isPromotedElement && (
53
77
  <Tab label={ __( 'General', 'elementor' ) } { ...getTabProps( 'settings' ) } />
54
78
  ) }
55
- <Tab label={ __( 'Style', 'elementor' ) } { ...getTabProps( 'style' ) } />
56
- <Tab label={ __( 'Interactions', 'elementor' ) } { ...getTabProps( 'interactions' ) } />
79
+ <Tab
80
+ label={ withRestrictionInfotip( __( 'Style', 'elementor' ) ) }
81
+ { ...getDesignTabProps( 'style' ) }
82
+ />
83
+ <Tab
84
+ label={ withRestrictionInfotip( __( 'Interactions', 'elementor' ) ) }
85
+ { ...getDesignTabProps( 'interactions' ) }
86
+ />
57
87
  </Tabs>
58
88
  <Divider />
59
89
  </Stack>
@@ -62,13 +92,37 @@ const PanelTabContent = () => {
62
92
  <SettingsTab />
63
93
  </TabPanel>
64
94
  ) }
65
- <TabPanel { ...getTabPanelProps( 'style' ) } disablePadding>
66
- <StyleTab />
67
- </TabPanel>
68
- <TabPanel { ...getTabPanelProps( 'interactions' ) } disablePadding>
69
- <InteractionsTab />
70
- </TabPanel>
95
+ { ! areDesignTabsRestricted && (
96
+ <>
97
+ <TabPanel { ...getTabPanelProps( 'style' ) } disablePadding>
98
+ <StyleTab />
99
+ </TabPanel>
100
+ <TabPanel { ...getTabPanelProps( 'interactions' ) } disablePadding>
101
+ <InteractionsTab />
102
+ </TabPanel>
103
+ </>
104
+ ) }
71
105
  </Stack>
72
106
  </ScrollProvider>
73
107
  );
74
108
  };
109
+
110
+ const resolveCurrentTab = ( {
111
+ storedTab,
112
+ isPromotedElement,
113
+ areDesignTabsRestricted,
114
+ }: {
115
+ storedTab: TabValue;
116
+ isPromotedElement: boolean;
117
+ areDesignTabsRestricted: boolean;
118
+ } ): TabValue => {
119
+ if ( areDesignTabsRestricted && DESIGN_TABS.includes( storedTab ) ) {
120
+ return CONTENT_ONLY_TAB;
121
+ }
122
+
123
+ if ( isPromotedElement && storedTab === 'settings' ) {
124
+ return 'style';
125
+ }
126
+
127
+ return storedTab;
128
+ };
@@ -10,6 +10,7 @@ import {
10
10
  InlineEditingControl,
11
11
  KeyValueControl,
12
12
  LinkControl,
13
+ NoticeControl,
13
14
  NumberControl,
14
15
  QueryChipsControl,
15
16
  QueryControl,
@@ -97,6 +98,7 @@ const controlTypes = {
97
98
  propTypeUtil: timeRangePropTypeUtil,
98
99
  },
99
100
  'attachment-type': { component: AttachmentTypeControl, layout: 'custom', propTypeUtil: stringPropTypeUtil },
101
+ notice: { component: NoticeControl, layout: 'full', propTypeUtil: stringPropTypeUtil },
100
102
  } as const satisfies ControlRegistry;
101
103
 
102
104
  export type ControlType = keyof typeof controlTypes;
@@ -101,11 +101,22 @@ export const AccordionItemsControl = ( { label }: { label: string } ) => {
101
101
  const ItemLabel = ( { value }: { value: AccordionItem } ) => {
102
102
  return (
103
103
  <Stack sx={ { minHeight: 20 } } direction="row" alignItems="center" gap={ 1.5 }>
104
- <span>{ value?.title }</span>
104
+ { value.id ? (
105
+ <AccordionItemRepeaterLabel elementId={ value.id } fallbackTitle={ value.title } />
106
+ ) : (
107
+ <span>{ value?.title }</span>
108
+ ) }
105
109
  </Stack>
106
110
  );
107
111
  };
108
112
 
113
+ const AccordionItemRepeaterLabel = ( { elementId, fallbackTitle }: { elementId: string; fallbackTitle?: string } ) => {
114
+ const editorSettings = useElementEditorSettings( elementId );
115
+ const label = editorSettings?.title ?? fallbackTitle ?? '';
116
+
117
+ return <span>{ label }</span>;
118
+ };
119
+
109
120
  const ItemContent = ( { value }: { value: AccordionItem } ) => {
110
121
  if ( ! value.id ) {
111
122
  return null;
@@ -21,7 +21,6 @@ export const LIST_ITEM_ELEMENT_TYPE = 'e-list-item';
21
21
  const LIST_ITEM_MARKER_ELEMENT_TYPE = 'e-list-item-marker';
22
22
  const LIST_ITEM_CONTENT_ELEMENT_TYPE = 'e-list-item-content';
23
23
  const PARAGRAPH_WIDGET_TYPE = 'e-paragraph';
24
- const SVG_WIDGET_TYPE = 'e-svg';
25
24
 
26
25
  const TRAILING_NUMBER = /(\d+)\s*$/;
27
26
 
@@ -69,9 +68,13 @@ const buildItemModel = ( position: number, showMarkers: boolean ): V1ElementData
69
68
  elements: [
70
69
  {
71
70
  elType: 'widget',
72
- widgetType: SVG_WIDGET_TYPE,
71
+ widgetType: PARAGRAPH_WIDGET_TYPE,
73
72
  id: generateElementId(),
74
73
  elements: [],
74
+ settings: {
75
+ tag: { $$type: 'string', value: 'span' },
76
+ paragraph: escapedHtmlPropTypeUtil.create( '&bull;' ),
77
+ },
75
78
  },
76
79
  ],
77
80
  },
@@ -109,16 +109,25 @@ export const TabsControlContent = ( { label }: { label: string } ) => {
109
109
  };
110
110
 
111
111
  const ItemLabel = ( { value, index }: { value: TabItem; index: number } ) => {
112
- const elementTitle = value?.title;
113
-
114
112
  return (
115
113
  <Stack sx={ { minHeight: 20 } } direction="row" alignItems="center" gap={ 1.5 }>
116
- <span>{ elementTitle }</span>
114
+ { value.id ? (
115
+ <TabRepeaterLabel elementId={ value.id } fallbackTitle={ value.title } />
116
+ ) : (
117
+ <span>{ value?.title }</span>
118
+ ) }
117
119
  <ItemDefaultTab index={ index } />
118
120
  </Stack>
119
121
  );
120
122
  };
121
123
 
124
+ const TabRepeaterLabel = ( { elementId, fallbackTitle }: { elementId: string; fallbackTitle?: string } ) => {
125
+ const editorSettings = useElementEditorSettings( elementId );
126
+ const label = editorSettings?.title ?? fallbackTitle ?? '';
127
+
128
+ return <span>{ label }</span>;
129
+ };
130
+
122
131
  const ItemDefaultTab = ( { index }: { index: number } ) => {
123
132
  const { value: defaultItem } = useBoundProp( numberPropTypeUtil );
124
133
 
@@ -1,13 +1,21 @@
1
1
  import * as React from 'react';
2
2
  import { useMemo } from 'react';
3
- import { PropKeyProvider, PropProvider, type SetValueMeta } from '@elementor/editor-controls';
3
+ import { PropKeyProvider, PropProvider, type SetValueMeta, useBoundProp } from '@elementor/editor-controls';
4
4
  import { setDocumentModifiedStatus } from '@elementor/editor-documents';
5
5
  import { type ElementID, getElementLabel, getElementSettings, updateElementSettings } from '@elementor/editor-elements';
6
- import { type CreateOptions, type PropKey, type Props } from '@elementor/editor-props';
6
+ import {
7
+ type CreateOptions,
8
+ type PropKey,
9
+ type Props,
10
+ responsiveFallbackChain,
11
+ responsivePropTypeUtil,
12
+ } from '@elementor/editor-props';
13
+ import { type BreakpointId, useActiveBreakpoint, useBreakpoints } from '@elementor/editor-responsive';
7
14
  import { undoable } from '@elementor/editor-v1-adapters';
8
15
  import { __ } from '@wordpress/i18n';
9
16
 
10
17
  import { useElement } from '../contexts/element-context';
18
+ import { isResponsivePropType } from '../utils/is-responsive-prop-type';
11
19
  import {
12
20
  extractDependencyEffect,
13
21
  extractOrderedDependencies,
@@ -24,6 +32,7 @@ type SettingsFieldProps = {
24
32
  };
25
33
 
26
34
  const HISTORY_DEBOUNCE_WAIT = 800;
35
+ const DESKTOP_BREAKPOINT: BreakpointId = 'desktop';
27
36
 
28
37
  export const SettingsField = ( { bind, children, propDisplayName }: SettingsFieldProps ) => {
29
38
  const {
@@ -62,7 +71,36 @@ export const SettingsField = ( { bind, children, propDisplayName }: SettingsFiel
62
71
 
63
72
  return (
64
73
  <PropProvider propType={ propType } value={ value } setValue={ setValue } isDisabled={ isDisabled }>
65
- <PropKeyProvider bind={ bind }>{ children }</PropKeyProvider>
74
+ <PropKeyProvider bind={ bind }>
75
+ { isResponsivePropType( propsSchema[ bind ] ) ? (
76
+ <ResponsiveBinding>{ children }</ResponsiveBinding>
77
+ ) : (
78
+ children
79
+ ) }
80
+ </PropKeyProvider>
81
+ </PropProvider>
82
+ );
83
+ };
84
+
85
+ const ResponsiveBinding = ( { children }: { children: React.ReactNode } ) => {
86
+ const { value, setValue, propType, disabled } = useBoundProp( responsivePropTypeUtil );
87
+ const breakpoint = useActiveBreakpoint() ?? DESKTOP_BREAKPOINT;
88
+ const activeBreakpoints = ( useBreakpoints() ?? [] ).map( ( { id } ) => id );
89
+
90
+ const inherited = responsiveFallbackChain( breakpoint )
91
+ .filter( ( key ) => key !== breakpoint && activeBreakpoints.includes( key as BreakpointId ) )
92
+ .map( ( key ) => value?.[ key ] )
93
+ .find( ( entry ) => entry !== null && entry !== undefined );
94
+
95
+ return (
96
+ <PropProvider
97
+ propType={ propType }
98
+ value={ value }
99
+ setValue={ setValue }
100
+ placeholder={ { [ breakpoint ]: inherited } }
101
+ isDisabled={ () => disabled }
102
+ >
103
+ <PropKeyProvider bind={ breakpoint }>{ children }</PropKeyProvider>
66
104
  </PropProvider>
67
105
  );
68
106
  };
@@ -0,0 +1,19 @@
1
+ import { type PropType, type UnionPropType } from '@elementor/editor-props';
2
+
3
+ export function isResponsivePropType( propType: PropType | undefined ): boolean {
4
+ if ( ! propType ) {
5
+ return false;
6
+ }
7
+
8
+ if ( 'key' in propType && propType.key === 'responsive' ) {
9
+ return true;
10
+ }
11
+
12
+ if ( propType.kind === 'union' ) {
13
+ return Object.values( ( propType as UnionPropType ).prop_types ).some( ( inner ) =>
14
+ isResponsivePropType( inner )
15
+ );
16
+ }
17
+
18
+ return false;
19
+ }