@elementor/editor-editing-panel 4.3.0-1060 → 4.3.0-1062

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-1060",
3
+ "version": "4.3.0-1062",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,31 +39,31 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "4.3.0-1060",
43
- "@elementor/editor-canvas": "4.3.0-1060",
44
- "@elementor/editor-controls": "4.3.0-1060",
45
- "@elementor/editor-documents": "4.3.0-1060",
46
- "@elementor/editor-elements": "4.3.0-1060",
47
- "@elementor/editor-interactions": "4.3.0-1060",
48
- "@elementor/editor-notifications": "4.3.0-1060",
49
- "@elementor/editor-panels": "4.3.0-1060",
50
- "@elementor/editor-props": "4.3.0-1060",
51
- "@elementor/editor-responsive": "4.3.0-1060",
52
- "@elementor/editor-styles": "4.3.0-1060",
53
- "@elementor/editor-styles-repository": "4.3.0-1060",
54
- "@elementor/editor-ui": "4.3.0-1060",
55
- "@elementor/editor-v1-adapters": "4.3.0-1060",
56
- "@elementor/http-client": "4.3.0-1060",
42
+ "@elementor/editor": "4.3.0-1062",
43
+ "@elementor/editor-canvas": "4.3.0-1062",
44
+ "@elementor/editor-controls": "4.3.0-1062",
45
+ "@elementor/editor-documents": "4.3.0-1062",
46
+ "@elementor/editor-elements": "4.3.0-1062",
47
+ "@elementor/editor-interactions": "4.3.0-1062",
48
+ "@elementor/editor-notifications": "4.3.0-1062",
49
+ "@elementor/editor-panels": "4.3.0-1062",
50
+ "@elementor/editor-props": "4.3.0-1062",
51
+ "@elementor/editor-responsive": "4.3.0-1062",
52
+ "@elementor/editor-styles": "4.3.0-1062",
53
+ "@elementor/editor-styles-repository": "4.3.0-1062",
54
+ "@elementor/editor-ui": "4.3.0-1062",
55
+ "@elementor/editor-v1-adapters": "4.3.0-1062",
56
+ "@elementor/http-client": "4.3.0-1062",
57
57
  "@elementor/icons": "~1.75.1",
58
- "@elementor/editor-variables": "4.3.0-1060",
59
- "@elementor/locations": "4.3.0-1060",
60
- "@elementor/menus": "4.3.0-1060",
61
- "@elementor/query": "4.3.0-1060",
62
- "@elementor/schema": "4.3.0-1060",
63
- "@elementor/session": "4.3.0-1060",
58
+ "@elementor/editor-variables": "4.3.0-1062",
59
+ "@elementor/locations": "4.3.0-1062",
60
+ "@elementor/menus": "4.3.0-1062",
61
+ "@elementor/query": "4.3.0-1062",
62
+ "@elementor/schema": "4.3.0-1062",
63
+ "@elementor/session": "4.3.0-1062",
64
64
  "@elementor/ui": "1.37.5",
65
- "@elementor/utils": "4.3.0-1060",
66
- "@elementor/wp-media": "4.3.0-1060",
65
+ "@elementor/utils": "4.3.0-1062",
66
+ "@elementor/wp-media": "4.3.0-1062",
67
67
  "@wordpress/i18n": "^5.13.0"
68
68
  },
69
69
  "peerDependencies": {
@@ -0,0 +1,129 @@
1
+ import { type ItemsActionPayload } from '@elementor/editor-controls';
2
+ import {
3
+ createElements,
4
+ duplicateElements,
5
+ getContainer,
6
+ moveElements,
7
+ removeElements,
8
+ } from '@elementor/editor-elements';
9
+ import { booleanPropTypeUtil } from '@elementor/editor-props';
10
+ import { __ } from '@wordpress/i18n';
11
+
12
+ export type ListItem = {
13
+ id: string;
14
+ title?: string;
15
+ };
16
+
17
+ export const LIST_ITEM_ELEMENT_TYPE = 'e-list-item';
18
+
19
+ const TRAILING_NUMBER = /(\d+)\s*$/;
20
+
21
+ const getItemTitle = ( position: number ) => `Item ${ position }`;
22
+
23
+ const getNextItemNumber = ( existingTitles: ( string | undefined )[] ) => {
24
+ const taken = new Set( existingTitles.filter( ( title ): title is string => Boolean( title ) ) );
25
+
26
+ const highest = existingTitles.reduce( ( max: number, title ) => {
27
+ const [ , trailingNumber ] = title?.match( TRAILING_NUMBER ) ?? [];
28
+ const parsed = trailingNumber ? Number( trailingNumber ) : 0;
29
+
30
+ return Number.isFinite( parsed ) && parsed > max ? parsed : max;
31
+ }, 0 );
32
+
33
+ let next = highest + 1;
34
+
35
+ while ( taken.has( getItemTitle( next ) ) ) {
36
+ next += 1;
37
+ }
38
+
39
+ return next;
40
+ };
41
+
42
+ export const duplicateItem = ( { items }: { items: ItemsActionPayload< ListItem > } ) => {
43
+ duplicateElements( {
44
+ elementIds: items.map( ( { item } ) => item.id ),
45
+ title: __( 'Duplicate List Item', 'elementor' ),
46
+ } );
47
+ };
48
+
49
+ export const moveItem = ( {
50
+ toIndex,
51
+ listContainerId,
52
+ movedElementId,
53
+ }: {
54
+ toIndex: number;
55
+ listContainerId: string;
56
+ movedElementId: string;
57
+ } ) => {
58
+ const movedElement = getContainer( movedElementId );
59
+ const listContainer = getContainer( listContainerId );
60
+
61
+ if ( ! movedElement || ! listContainer ) {
62
+ throw new Error( 'List item or list container not found' );
63
+ }
64
+
65
+ moveElements( {
66
+ title: __( 'Reorder List Items', 'elementor' ),
67
+ moves: [
68
+ {
69
+ element: movedElement,
70
+ targetContainer: listContainer,
71
+ options: { at: toIndex },
72
+ },
73
+ ],
74
+ } );
75
+ };
76
+
77
+ export const removeItem = ( { items }: { items: ItemsActionPayload< ListItem > } ) => {
78
+ removeElements( {
79
+ title: __( 'List Items', 'elementor' ),
80
+ elementIds: items.map( ( { item } ) => item.id ),
81
+ } );
82
+ };
83
+
84
+ export const addItem = ( {
85
+ existingTitles,
86
+ listContainerId,
87
+ items,
88
+ showMarkers,
89
+ }: {
90
+ existingTitles: ( string | undefined )[];
91
+ listContainerId: string;
92
+ items: ItemsActionPayload< ListItem >;
93
+ showMarkers: boolean;
94
+ } ) => {
95
+ const listContainer = getContainer( listContainerId );
96
+
97
+ if ( ! listContainer ) {
98
+ throw new Error( 'List container not found' );
99
+ }
100
+
101
+ const titles = [ ...existingTitles ];
102
+
103
+ items.forEach( ( { index } ) => {
104
+ const position = getNextItemNumber( titles );
105
+
106
+ createElements( {
107
+ title: __( 'List Items', 'elementor' ),
108
+ elements: [
109
+ {
110
+ container: listContainer,
111
+ model: {
112
+ elType: LIST_ITEM_ELEMENT_TYPE,
113
+ settings: {
114
+ show_markers: booleanPropTypeUtil.create( showMarkers ),
115
+ },
116
+ hydrateDefaultChildren: true,
117
+ editor_settings: {
118
+ title: getItemTitle( position ),
119
+ initial_position: position,
120
+ },
121
+ },
122
+ options: { at: index },
123
+ },
124
+ ],
125
+ } );
126
+
127
+ titles.push( getItemTitle( position ) );
128
+ } );
129
+ };
@@ -0,0 +1,152 @@
1
+ import * as React from 'react';
2
+ import { ControlFormLabel, Repeater, type RepeaterItem, type SetRepeaterValuesMeta } from '@elementor/editor-controls';
3
+ import { updateElementEditorSettings, useElementChildren, useElementEditorSettings } from '@elementor/editor-elements';
4
+ import { booleanPropTypeUtil, type CreateOptions } from '@elementor/editor-props';
5
+ import { Stack, TextField } from '@elementor/ui';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { useElement } from '../../../contexts/element-context';
9
+ import { addItem, duplicateItem, LIST_ITEM_ELEMENT_TYPE, type ListItem, moveItem, removeItem } from './list-actions';
10
+ import { useShowMarkersWriteThrough } from './use-show-markers-write-through';
11
+
12
+ const LIST_ELEMENT_TYPE = 'e-list';
13
+
14
+ const getEffectiveListItemLabel = ( label: string | undefined, fallbackLabel: string ) => {
15
+ return label?.trim() ? label : fallbackLabel;
16
+ };
17
+
18
+ const getDefaultListItemLabel = ( index: number ) => {
19
+ return `Item ${ index + 1 }`;
20
+ };
21
+
22
+ export const ListItemsControl = ( { label }: { label: string } ) => {
23
+ return <ListItemsControlContent label={ label } />;
24
+ };
25
+
26
+ const ListItemsControlContent = ( { label }: { label: string } ) => {
27
+ const { element, settings } = useElement();
28
+ const { [ LIST_ITEM_ELEMENT_TYPE ]: listItems } = useElementChildren(
29
+ element.id,
30
+ { [ LIST_ELEMENT_TYPE ]: LIST_ITEM_ELEMENT_TYPE },
31
+ { includeSelfAsParent: true }
32
+ );
33
+ // The root owns `show_markers`, but each child item needs a mirrored copy for its own
34
+ // `Child_Dependency`, so the repeater hosts the write-through that keeps those values undoable.
35
+ const showMarkers = booleanPropTypeUtil.extract( settings.show_markers ) ?? true;
36
+
37
+ useShowMarkersWriteThrough( element.id, showMarkers );
38
+
39
+ const repeaterValues: RepeaterItem< ListItem >[] = listItems.map( ( item, index ) => ( {
40
+ id: item.id,
41
+ title: item.editorSettings?.title ?? getDefaultListItemLabel( index ),
42
+ index,
43
+ } ) );
44
+
45
+ const setValue = (
46
+ _newValues: RepeaterItem< ListItem >[],
47
+ _options: CreateOptions,
48
+ meta?: SetRepeaterValuesMeta< RepeaterItem< ListItem > >
49
+ ) => {
50
+ if ( meta?.action?.type === 'add' ) {
51
+ return addItem( {
52
+ existingTitles: repeaterValues.map( ( { title } ) => title ),
53
+ listContainerId: element.id,
54
+ items: meta.action.payload,
55
+ showMarkers,
56
+ } );
57
+ }
58
+
59
+ if ( meta?.action?.type === 'remove' ) {
60
+ return removeItem( { items: meta.action.payload } );
61
+ }
62
+
63
+ if ( meta?.action?.type === 'duplicate' ) {
64
+ return duplicateItem( { items: meta.action.payload } );
65
+ }
66
+
67
+ if ( meta?.action?.type === 'reorder' ) {
68
+ const { from, to } = meta.action.payload;
69
+
70
+ return moveItem( {
71
+ toIndex: to,
72
+ listContainerId: element.id,
73
+ movedElementId: listItems[ from ].id,
74
+ } );
75
+ }
76
+ };
77
+
78
+ return (
79
+ <Repeater
80
+ showToggle={ false }
81
+ values={ repeaterValues }
82
+ setValues={ setValue }
83
+ showRemove={ repeaterValues.length > 1 }
84
+ label={ label }
85
+ // List items are child elements, not a prop on the root, so there is nothing meaningful
86
+ // to expose or bind here. Using an unrelated prop key just to satisfy adornment context
87
+ // makes the panel act as if that real prop belongs to this repeater.
88
+ adornment={ () => null }
89
+ itemSettings={ {
90
+ getId: ( { item } ) => item.id,
91
+ initialValues: { id: '', title: 'Item' },
92
+ Label: ItemLabel,
93
+ Content: ItemContent,
94
+ Icon: () => null,
95
+ } }
96
+ />
97
+ );
98
+ };
99
+
100
+ const ItemLabel = ( { value, index }: { value: ListItem; index: number } ) => {
101
+ const fallbackLabel = value.title ?? getDefaultListItemLabel( index );
102
+
103
+ return (
104
+ <Stack sx={ { minHeight: 20 } } direction="row" alignItems="center" gap={ 1.5 }>
105
+ { value.id ? (
106
+ <ListItemRepeaterLabel elementId={ value.id } fallbackLabel={ fallbackLabel } />
107
+ ) : (
108
+ <span>{ fallbackLabel }</span>
109
+ ) }
110
+ </Stack>
111
+ );
112
+ };
113
+
114
+ const ItemContent = ( { value }: { value: ListItem } ) => {
115
+ if ( ! value.id ) {
116
+ return null;
117
+ }
118
+
119
+ return (
120
+ <Stack p={ 2 } gap={ 1.5 }>
121
+ <ListItemLabelControl elementId={ value.id } fallbackLabel={ value.title ?? '' } />
122
+ </Stack>
123
+ );
124
+ };
125
+
126
+ const ListItemRepeaterLabel = ( { elementId, fallbackLabel }: { elementId: string; fallbackLabel: string } ) => {
127
+ const editorSettings = useElementEditorSettings( elementId );
128
+ const label = getEffectiveListItemLabel( editorSettings?.title, fallbackLabel );
129
+
130
+ return <span>{ label }</span>;
131
+ };
132
+
133
+ const ListItemLabelControl = ( { elementId, fallbackLabel }: { elementId: string; fallbackLabel: string } ) => {
134
+ const editorSettings = useElementEditorSettings( elementId );
135
+ const label = getEffectiveListItemLabel( editorSettings?.title, fallbackLabel );
136
+
137
+ return (
138
+ <Stack gap={ 1 }>
139
+ <ControlFormLabel>{ __( 'Item name', 'elementor' ) }</ControlFormLabel>
140
+ <TextField
141
+ size="tiny"
142
+ value={ label }
143
+ onChange={ ( { target }: React.ChangeEvent< HTMLInputElement > ) => {
144
+ updateElementEditorSettings( {
145
+ elementId,
146
+ settings: { title: target.value },
147
+ } );
148
+ } }
149
+ />
150
+ </Stack>
151
+ );
152
+ };
@@ -0,0 +1,89 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { getContainer, getElementSettings, updateElementSettings } from '@elementor/editor-elements';
3
+ import { booleanPropTypeUtil } from '@elementor/editor-props';
4
+ import { undoable } from '@elementor/editor-v1-adapters';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { HISTORY_DEBOUNCE_WAIT } from '../../../hooks/use-styles-fields';
8
+ import { LIST_ITEM_ELEMENT_TYPE } from './list-actions';
9
+
10
+ type CascadeShowMarkersPayload = {
11
+ listId: string;
12
+ showMarkers: boolean;
13
+ };
14
+
15
+ type CascadeShowMarkersResult = {
16
+ previous: Record< string, ReturnType< typeof booleanPropTypeUtil.create > | null | undefined >;
17
+ };
18
+
19
+ // `show_markers` lives on the root list, but each `e-list-item` also carries a mirrored copy so its
20
+ // own `Child_Dependency` can attach/detach the marker child. This write-through keeps those mirrored
21
+ // props in sync through a debounced undoable history entry so Undo restores the switch and all items.
22
+ export const cascadeShowMarkersToItems = undoable< CascadeShowMarkersPayload, CascadeShowMarkersResult, undefined >(
23
+ {
24
+ do: ( { listId, showMarkers } ) => {
25
+ const itemIds = getListItemIds( listId );
26
+
27
+ const previous = Object.fromEntries(
28
+ itemIds.map(
29
+ ( itemId ) =>
30
+ [
31
+ itemId,
32
+ getElementSettings< ReturnType< typeof booleanPropTypeUtil.create > >( itemId, [
33
+ 'show_markers',
34
+ ] ).show_markers,
35
+ ] as const
36
+ )
37
+ );
38
+
39
+ itemIds.forEach( ( itemId ) => {
40
+ updateElementSettings( {
41
+ id: itemId,
42
+ props: { show_markers: booleanPropTypeUtil.create( showMarkers ) },
43
+ withHistory: false,
44
+ } );
45
+ } );
46
+
47
+ return { previous };
48
+ },
49
+ undo: ( _payload, { previous } ) => {
50
+ Object.entries( previous ).forEach( ( [ itemId, previousValue ] ) => {
51
+ updateElementSettings( {
52
+ id: itemId,
53
+ props: { show_markers: previousValue ?? null },
54
+ withHistory: false,
55
+ } );
56
+ } );
57
+ },
58
+ },
59
+ {
60
+ title: __( 'List', 'elementor' ),
61
+ subtitle: __( 'Show Markers', 'elementor' ),
62
+ debounce: { wait: HISTORY_DEBOUNCE_WAIT },
63
+ }
64
+ );
65
+
66
+ function getListItemIds( listId: string ): string[] {
67
+ const list = getContainer( listId );
68
+
69
+ const itemContainers = ( list?.children ?? [] ).filter(
70
+ ( child ) => child.model.get( 'elType' ) === LIST_ITEM_ELEMENT_TYPE
71
+ );
72
+
73
+ return itemContainers.map( ( item ): string => item.id );
74
+ }
75
+
76
+ export function useShowMarkersWriteThrough( listId: string, showMarkers: boolean ): void {
77
+ const previousRef = useRef< { listId: string; showMarkers: boolean } | null >( null );
78
+
79
+ useEffect( () => {
80
+ const previous = previousRef.current;
81
+ previousRef.current = { listId, showMarkers };
82
+
83
+ if ( ! previous || previous.listId !== listId || previous.showMarkers === showMarkers ) {
84
+ return;
85
+ }
86
+
87
+ cascadeShowMarkersToItems( { listId, showMarkers } );
88
+ }, [ listId, showMarkers ] );
89
+ }
@@ -2,9 +2,11 @@ import { type ControlComponent } from '@elementor/editor-controls';
2
2
 
3
3
  import { type ControlRegistry, controlsRegistry } from '../controls-registry';
4
4
  import { AccordionItemsControl } from './accordion-items-control/accordion-items-control';
5
+ import { ListItemsControl } from './list-items-control/list-items-control';
5
6
  import { TabsControl } from './tabs-control/tabs-control';
6
7
 
7
8
  const controlTypes = {
9
+ 'list-items': { component: ListItemsControl as ControlComponent, layout: 'full' },
8
10
  tabs: { component: TabsControl as ControlComponent, layout: 'full' },
9
11
  'accordion-items': { component: AccordionItemsControl as ControlComponent, layout: 'full' },
10
12
  } as const satisfies ControlRegistry;