@elementor/editor-editing-panel 4.3.0-1048 → 4.3.0-1049
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/dist/index.js +626 -288
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +544 -199
- package/dist/index.mjs.map +1 -1
- package/package.json +24 -24
- package/src/controls-registry/element-controls/accordion-items-control/accordion-items-control.tsx +141 -0
- package/src/controls-registry/element-controls/accordion-items-control/use-actions.ts +235 -0
- package/src/controls-registry/element-controls/accordion-items-control/use-show-icon-write-through.ts +135 -0
- package/src/controls-registry/element-controls/registry.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-editing-panel",
|
|
3
|
-
"version": "4.3.0-
|
|
3
|
+
"version": "4.3.0-1049",
|
|
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-
|
|
43
|
-
"@elementor/editor-canvas": "4.3.0-
|
|
44
|
-
"@elementor/editor-controls": "4.3.0-
|
|
45
|
-
"@elementor/editor-documents": "4.3.0-
|
|
46
|
-
"@elementor/editor-elements": "4.3.0-
|
|
47
|
-
"@elementor/editor-interactions": "4.3.0-
|
|
48
|
-
"@elementor/editor-notifications": "4.3.0-
|
|
49
|
-
"@elementor/editor-panels": "4.3.0-
|
|
50
|
-
"@elementor/editor-props": "4.3.0-
|
|
51
|
-
"@elementor/editor-responsive": "4.3.0-
|
|
52
|
-
"@elementor/editor-styles": "4.3.0-
|
|
53
|
-
"@elementor/editor-styles-repository": "4.3.0-
|
|
54
|
-
"@elementor/editor-ui": "4.3.0-
|
|
55
|
-
"@elementor/editor-v1-adapters": "4.3.0-
|
|
56
|
-
"@elementor/http-client": "4.3.0-
|
|
42
|
+
"@elementor/editor": "4.3.0-1049",
|
|
43
|
+
"@elementor/editor-canvas": "4.3.0-1049",
|
|
44
|
+
"@elementor/editor-controls": "4.3.0-1049",
|
|
45
|
+
"@elementor/editor-documents": "4.3.0-1049",
|
|
46
|
+
"@elementor/editor-elements": "4.3.0-1049",
|
|
47
|
+
"@elementor/editor-interactions": "4.3.0-1049",
|
|
48
|
+
"@elementor/editor-notifications": "4.3.0-1049",
|
|
49
|
+
"@elementor/editor-panels": "4.3.0-1049",
|
|
50
|
+
"@elementor/editor-props": "4.3.0-1049",
|
|
51
|
+
"@elementor/editor-responsive": "4.3.0-1049",
|
|
52
|
+
"@elementor/editor-styles": "4.3.0-1049",
|
|
53
|
+
"@elementor/editor-styles-repository": "4.3.0-1049",
|
|
54
|
+
"@elementor/editor-ui": "4.3.0-1049",
|
|
55
|
+
"@elementor/editor-v1-adapters": "4.3.0-1049",
|
|
56
|
+
"@elementor/http-client": "4.3.0-1049",
|
|
57
57
|
"@elementor/icons": "~1.75.1",
|
|
58
|
-
"@elementor/editor-variables": "4.3.0-
|
|
59
|
-
"@elementor/locations": "4.3.0-
|
|
60
|
-
"@elementor/menus": "4.3.0-
|
|
61
|
-
"@elementor/query": "4.3.0-
|
|
62
|
-
"@elementor/schema": "4.3.0-
|
|
63
|
-
"@elementor/session": "4.3.0-
|
|
58
|
+
"@elementor/editor-variables": "4.3.0-1049",
|
|
59
|
+
"@elementor/locations": "4.3.0-1049",
|
|
60
|
+
"@elementor/menus": "4.3.0-1049",
|
|
61
|
+
"@elementor/query": "4.3.0-1049",
|
|
62
|
+
"@elementor/schema": "4.3.0-1049",
|
|
63
|
+
"@elementor/session": "4.3.0-1049",
|
|
64
64
|
"@elementor/ui": "1.37.5",
|
|
65
|
-
"@elementor/utils": "4.3.0-
|
|
66
|
-
"@elementor/wp-media": "4.3.0-
|
|
65
|
+
"@elementor/utils": "4.3.0-1049",
|
|
66
|
+
"@elementor/wp-media": "4.3.0-1049",
|
|
67
67
|
"@wordpress/i18n": "^5.13.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
package/src/controls-registry/element-controls/accordion-items-control/accordion-items-control.tsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
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 { ACCORDION_ELEMENT_TYPE, ACCORDION_ITEM_ELEMENT_TYPE, type AccordionItem, useActions } from './use-actions';
|
|
10
|
+
import { useShowIconWriteThrough } from './use-show-icon-write-through';
|
|
11
|
+
|
|
12
|
+
export const AccordionItemsControl = ( { label }: { label: string } ) => {
|
|
13
|
+
const { element, settings } = useElement();
|
|
14
|
+
const { addItem, duplicateItem, moveItem, removeItem } = useActions();
|
|
15
|
+
|
|
16
|
+
// Items are direct children of the root, so the root itself is the parent to read them from —
|
|
17
|
+
// there is no menu / content-area indirection as in Tabs.
|
|
18
|
+
const { [ ACCORDION_ITEM_ELEMENT_TYPE ]: items } = useElementChildren(
|
|
19
|
+
element.id,
|
|
20
|
+
{ [ ACCORDION_ELEMENT_TYPE ]: ACCORDION_ITEM_ELEMENT_TYPE },
|
|
21
|
+
{ includeSelfAsParent: true }
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// The root's `show_icon` (rendered elsewhere in this same "Content" section via the standard
|
|
25
|
+
// `Switch_Control` -> `SettingsField` pipeline). Read here, alongside the repeater, purely to
|
|
26
|
+
// drive the write-through to every head - see `useShowIconWriteThrough` for why this can't live
|
|
27
|
+
// inside the generic switch control itself.
|
|
28
|
+
const showIcon = booleanPropTypeUtil.extract( settings.show_icon ) ?? true;
|
|
29
|
+
|
|
30
|
+
useShowIconWriteThrough( element.id, showIcon );
|
|
31
|
+
|
|
32
|
+
const repeaterValues: RepeaterItem< AccordionItem >[] = items.map( ( item, index ) => {
|
|
33
|
+
return {
|
|
34
|
+
id: item.id,
|
|
35
|
+
title: item.editorSettings?.title,
|
|
36
|
+
index,
|
|
37
|
+
};
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
const setValue = (
|
|
41
|
+
_newValues: RepeaterItem< AccordionItem >[],
|
|
42
|
+
_options: CreateOptions,
|
|
43
|
+
meta?: SetRepeaterValuesMeta< RepeaterItem< AccordionItem > >
|
|
44
|
+
) => {
|
|
45
|
+
if ( meta?.action?.type === 'add' ) {
|
|
46
|
+
return addItem( {
|
|
47
|
+
accordionId: element.id,
|
|
48
|
+
// The new item's number comes from the titles already in use, not from the item count -
|
|
49
|
+
// see `getNextItemNumber`.
|
|
50
|
+
existingTitles: repeaterValues.map( ( { title } ) => title ),
|
|
51
|
+
items: meta.action.payload,
|
|
52
|
+
showIcon,
|
|
53
|
+
} );
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if ( meta?.action?.type === 'remove' ) {
|
|
57
|
+
return removeItem( { items: meta.action.payload } );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ( meta?.action?.type === 'duplicate' ) {
|
|
61
|
+
return duplicateItem( { items: meta.action.payload } );
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if ( meta?.action?.type === 'reorder' ) {
|
|
65
|
+
const { from, to } = meta.action.payload;
|
|
66
|
+
|
|
67
|
+
return moveItem( {
|
|
68
|
+
accordionId: element.id,
|
|
69
|
+
movedElementId: items[ from ].id,
|
|
70
|
+
toIndex: to,
|
|
71
|
+
} );
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Repeater
|
|
77
|
+
showToggle={ false }
|
|
78
|
+
values={ repeaterValues }
|
|
79
|
+
setValues={ setValue }
|
|
80
|
+
showRemove={ repeaterValues.length > 1 }
|
|
81
|
+
label={ label }
|
|
82
|
+
// Accordion items are element children, not a prop on the root, so this repeater has no
|
|
83
|
+
// prop of its own to bind to - and the default adornments include the component
|
|
84
|
+
// "expose property" indicator, which reads whatever prop key is in context. Wrapping the
|
|
85
|
+
// repeater in a `SettingsField` just to give that indicator a context (the way Tabs does
|
|
86
|
+
// with `default-active-tab`) made exposing "Accordion Items" write the overridable wrapper
|
|
87
|
+
// into `default_state`, so the Default State toggle - the real owner of that key - turned
|
|
88
|
+
// exposed at the same time. Nothing here is exposable, hence no adornments at all.
|
|
89
|
+
adornment={ () => null }
|
|
90
|
+
itemSettings={ {
|
|
91
|
+
getId: ( { item } ) => item.id,
|
|
92
|
+
initialValues: { id: '', title: __( 'Accordion Item', 'elementor' ) },
|
|
93
|
+
Label: ItemLabel,
|
|
94
|
+
Content: ItemContent,
|
|
95
|
+
Icon: () => null,
|
|
96
|
+
} }
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const ItemLabel = ( { value }: { value: AccordionItem } ) => {
|
|
102
|
+
return (
|
|
103
|
+
<Stack sx={ { minHeight: 20 } } direction="row" alignItems="center" gap={ 1.5 }>
|
|
104
|
+
<span>{ value?.title }</span>
|
|
105
|
+
</Stack>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const ItemContent = ( { value }: { value: AccordionItem } ) => {
|
|
110
|
+
if ( ! value.id ) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Stack p={ 2 } gap={ 1.5 }>
|
|
116
|
+
<ItemNameControl elementId={ value.id } />
|
|
117
|
+
</Stack>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const ItemNameControl = ( { elementId }: { elementId: string } ) => {
|
|
122
|
+
const editorSettings = useElementEditorSettings( elementId );
|
|
123
|
+
|
|
124
|
+
const label = editorSettings?.title ?? '';
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Stack gap={ 1 }>
|
|
128
|
+
<ControlFormLabel>{ __( 'Name', 'elementor' ) }</ControlFormLabel>
|
|
129
|
+
<TextField
|
|
130
|
+
size="tiny"
|
|
131
|
+
value={ label }
|
|
132
|
+
onChange={ ( { target }: React.ChangeEvent< HTMLInputElement > ) => {
|
|
133
|
+
updateElementEditorSettings( {
|
|
134
|
+
elementId,
|
|
135
|
+
settings: { title: target.value },
|
|
136
|
+
} );
|
|
137
|
+
} }
|
|
138
|
+
/>
|
|
139
|
+
</Stack>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { type ItemsActionPayload } from '@elementor/editor-controls';
|
|
2
|
+
import {
|
|
3
|
+
type CreateElementParams,
|
|
4
|
+
createElements,
|
|
5
|
+
duplicateElements,
|
|
6
|
+
generateElementId,
|
|
7
|
+
getContainer,
|
|
8
|
+
moveElements,
|
|
9
|
+
removeElements,
|
|
10
|
+
type V1ElementData,
|
|
11
|
+
} from '@elementor/editor-elements';
|
|
12
|
+
import { booleanPropTypeUtil, escapedHtmlPropTypeUtil } from '@elementor/editor-props';
|
|
13
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
14
|
+
|
|
15
|
+
export type AccordionItem = {
|
|
16
|
+
id: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const ACCORDION_ELEMENT_TYPE = 'e-accordion';
|
|
21
|
+
export const ACCORDION_ITEM_ELEMENT_TYPE = 'e-accordion-item';
|
|
22
|
+
|
|
23
|
+
const ACCORDION_ITEM_HEADER_ELEMENT_TYPE = 'e-accordion-item-header';
|
|
24
|
+
const ACCORDION_ITEM_TITLE_ELEMENT_TYPE = 'e-accordion-item-title';
|
|
25
|
+
const ACCORDION_ITEM_ICON_ELEMENT_TYPE = 'e-accordion-item-icon';
|
|
26
|
+
const ACCORDION_ITEM_CONTENT_ELEMENT_TYPE = 'e-accordion-item-content';
|
|
27
|
+
const PARAGRAPH_WIDGET_TYPE = 'e-paragraph';
|
|
28
|
+
|
|
29
|
+
const getItemTitle = ( position: number ) =>
|
|
30
|
+
/* translators: %d: Accordion item position. */
|
|
31
|
+
sprintf( __( 'Accordion Item %d', 'elementor' ), position );
|
|
32
|
+
|
|
33
|
+
const TRAILING_NUMBER = /(\d+)\s*$/;
|
|
34
|
+
|
|
35
|
+
// Picks the number for a newly added item from the titles that exist right now, never from how many
|
|
36
|
+
// items there are.
|
|
37
|
+
//
|
|
38
|
+
// The Repeater's add action reports `index = items.length`, i.e. the current visible count. Deriving
|
|
39
|
+
// the number from that collides after a delete: remove "Accordion Item 1" from the default pair and
|
|
40
|
+
// the count drops to 1, so the next add would be numbered 2 — a duplicate of the surviving
|
|
41
|
+
// "Accordion Item 2". The number is not just a Structure-panel label, it is baked into the item's
|
|
42
|
+
// rendered paragraph, so the canvas would show two identical headers.
|
|
43
|
+
//
|
|
44
|
+
// Taking `max( trailing number of each existing title ) + 1` keeps the surviving item in view when
|
|
45
|
+
// the number is chosen, which is what fixes that case. The final loop then guarantees the property
|
|
46
|
+
// we actually care about — the new title differs from every title currently present — even when a
|
|
47
|
+
// user has renamed items by hand. It cannot know about numbers used by items that were already
|
|
48
|
+
// deleted, so a delete-the-highest-then-add sequence can still reuse a number; that reuse is never a
|
|
49
|
+
// visible duplicate, which is the failure that matters.
|
|
50
|
+
const getNextItemNumber = ( existingTitles: ( string | undefined )[] ) => {
|
|
51
|
+
const taken = new Set( existingTitles.filter( ( title ): title is string => Boolean( title ) ) );
|
|
52
|
+
|
|
53
|
+
const highest = existingTitles.reduce( ( max: number, title ) => {
|
|
54
|
+
const [ , trailingNumber ] = title?.match( TRAILING_NUMBER ) ?? [];
|
|
55
|
+
const parsed = trailingNumber ? Number( trailingNumber ) : 0;
|
|
56
|
+
|
|
57
|
+
return Number.isFinite( parsed ) && parsed > max ? parsed : max;
|
|
58
|
+
}, 0 );
|
|
59
|
+
|
|
60
|
+
let next = highest + 1;
|
|
61
|
+
|
|
62
|
+
while ( taken.has( getItemTitle( next ) ) ) {
|
|
63
|
+
next += 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return next;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Builds one `e-accordion-item` subtree, mirroring `Atomic_Accordion::build_default_item()`.
|
|
70
|
+
//
|
|
71
|
+
// Each level of an atomic element's default children is hydrated independently
|
|
72
|
+
// (`AtomicElementBaseModel.onElementCreate()`), so the title slot's own defaults cannot know which
|
|
73
|
+
// item they belong to and would render an unnumbered "Accordion Item". Spelling the
|
|
74
|
+
// item -> header -> title -> paragraph chain out here is what lets the numbered text reach the
|
|
75
|
+
// rendered paragraph, exactly as the default two-item tree does. The icon and content branches need
|
|
76
|
+
// no per-index content, so they keep hydrating their own defaults.
|
|
77
|
+
//
|
|
78
|
+
// `V1ElementData` is the plain-object form of a model tree: nested `elements` are objects that
|
|
79
|
+
// Backbone turns into models on `initialize`, and ids have to be generated here because only the
|
|
80
|
+
// outermost model gets one from `document/elements/create`.
|
|
81
|
+
const buildItemModel = ( position: number, showIcon: boolean ): V1ElementData => {
|
|
82
|
+
const numberedTitle = getItemTitle( position );
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
elType: ACCORDION_ITEM_ELEMENT_TYPE,
|
|
86
|
+
id: generateElementId(),
|
|
87
|
+
editor_settings: { title: numberedTitle, initial_position: position },
|
|
88
|
+
elements: [
|
|
89
|
+
{
|
|
90
|
+
elType: ACCORDION_ITEM_HEADER_ELEMENT_TYPE,
|
|
91
|
+
id: generateElementId(),
|
|
92
|
+
editor_settings: { title: __( 'Header', 'elementor' ) },
|
|
93
|
+
// Seeded from the root's *current* `show_icon` value, not the schema default: if a user
|
|
94
|
+
// has already turned Show Icon off, a newly added item must start with its icon hidden
|
|
95
|
+
// too, not re-show one just because the item itself is brand new. See the comment on
|
|
96
|
+
// the mirrored prop in `Atomic_Accordion_Item_Header` for why this duplication exists.
|
|
97
|
+
settings: { show_icon: booleanPropTypeUtil.create( showIcon ) },
|
|
98
|
+
elements: [
|
|
99
|
+
{
|
|
100
|
+
elType: ACCORDION_ITEM_TITLE_ELEMENT_TYPE,
|
|
101
|
+
id: generateElementId(),
|
|
102
|
+
editor_settings: { title: __( 'Title', 'elementor' ) },
|
|
103
|
+
elements: [
|
|
104
|
+
{
|
|
105
|
+
elType: 'widget',
|
|
106
|
+
widgetType: PARAGRAPH_WIDGET_TYPE,
|
|
107
|
+
id: generateElementId(),
|
|
108
|
+
// A leaf still needs an explicit empty `elements`: `ElementModel.initialize()` only
|
|
109
|
+
// turns the attribute into a collection when it is defined, and v1 code walking a
|
|
110
|
+
// subtree (`deselectRecursive()` on delete) calls `.forEach()` on it unguarded.
|
|
111
|
+
elements: [],
|
|
112
|
+
settings: {
|
|
113
|
+
paragraph: escapedHtmlPropTypeUtil.create( numberedTitle ),
|
|
114
|
+
tag: { $$type: 'string', value: 'span' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
elType: ACCORDION_ITEM_ICON_ELEMENT_TYPE,
|
|
121
|
+
id: generateElementId(),
|
|
122
|
+
editor_settings: { title: __( 'Icon', 'elementor' ) },
|
|
123
|
+
elements: [],
|
|
124
|
+
hydrateDefaultChildren: true,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
elType: ACCORDION_ITEM_CONTENT_ELEMENT_TYPE,
|
|
130
|
+
id: generateElementId(),
|
|
131
|
+
editor_settings: { title: __( 'Content', 'elementor' ) },
|
|
132
|
+
elements: [],
|
|
133
|
+
hydrateDefaultChildren: true,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const useActions = () => {
|
|
140
|
+
const addItem = ( {
|
|
141
|
+
accordionId,
|
|
142
|
+
existingTitles,
|
|
143
|
+
items,
|
|
144
|
+
showIcon,
|
|
145
|
+
}: {
|
|
146
|
+
accordionId: string;
|
|
147
|
+
existingTitles: ( string | undefined )[];
|
|
148
|
+
items: ItemsActionPayload< AccordionItem >;
|
|
149
|
+
// The root's *current* `show_icon` value, so the new item's header starts in sync with it
|
|
150
|
+
// instead of the schema default - see the comment on `buildItemModel`.
|
|
151
|
+
showIcon: boolean;
|
|
152
|
+
} ) => {
|
|
153
|
+
const accordion = getContainer( accordionId );
|
|
154
|
+
|
|
155
|
+
if ( ! accordion ) {
|
|
156
|
+
throw new Error( 'Accordion container not found' );
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Grows as we go, so adding several items in one action can't hand out the same number twice.
|
|
160
|
+
const titles = [ ...existingTitles ];
|
|
161
|
+
|
|
162
|
+
items.forEach( () => {
|
|
163
|
+
const position = getNextItemNumber( titles );
|
|
164
|
+
|
|
165
|
+
createElements( {
|
|
166
|
+
title: __( 'Accordion', 'elementor' ),
|
|
167
|
+
elements: [
|
|
168
|
+
{
|
|
169
|
+
container: accordion,
|
|
170
|
+
// `buildItemModel()` returns `V1ElementData`, whose `elements` field is plain nested
|
|
171
|
+
// data (`V1ElementData[]`) - correct for a tree we're constructing to send as a
|
|
172
|
+
// creation payload. `CreateElementParams['model']` is derived from
|
|
173
|
+
// `V1ElementModelProps`, whose `elements` field is typed as `V1Model<...>[]` - live
|
|
174
|
+
// Backbone-model wrappers with `get`/`set`/`toJSON`, the shape for reading an
|
|
175
|
+
// *existing* element out of the document, not for describing one to create. A single
|
|
176
|
+
// cast is enough (the two types overlap enough for TS to allow it directly); no
|
|
177
|
+
// `unknown` escape hatch needed.
|
|
178
|
+
model: buildItemModel( position, showIcon ) as CreateElementParams[ 'model' ],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
titles.push( getItemTitle( position ) );
|
|
184
|
+
} );
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const removeItem = ( { items }: { items: ItemsActionPayload< AccordionItem > } ) => {
|
|
188
|
+
removeElements( {
|
|
189
|
+
title: __( 'Accordion', 'elementor' ),
|
|
190
|
+
elementIds: items.map( ( { item } ) => item.id ),
|
|
191
|
+
} );
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const duplicateItem = ( { items }: { items: ItemsActionPayload< AccordionItem > } ) => {
|
|
195
|
+
duplicateElements( {
|
|
196
|
+
title: __( 'Duplicate Accordion Item', 'elementor' ),
|
|
197
|
+
elementIds: items.map( ( { item } ) => item.id ),
|
|
198
|
+
} );
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const moveItem = ( {
|
|
202
|
+
accordionId,
|
|
203
|
+
movedElementId,
|
|
204
|
+
toIndex,
|
|
205
|
+
}: {
|
|
206
|
+
accordionId: string;
|
|
207
|
+
movedElementId: string;
|
|
208
|
+
toIndex: number;
|
|
209
|
+
} ) => {
|
|
210
|
+
const accordion = getContainer( accordionId );
|
|
211
|
+
const movedElement = getContainer( movedElementId );
|
|
212
|
+
|
|
213
|
+
if ( ! accordion || ! movedElement ) {
|
|
214
|
+
throw new Error( 'Accordion item or container not found' );
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
moveElements( {
|
|
218
|
+
title: __( 'Reorder Accordion Items', 'elementor' ),
|
|
219
|
+
moves: [
|
|
220
|
+
{
|
|
221
|
+
element: movedElement,
|
|
222
|
+
targetContainer: accordion,
|
|
223
|
+
options: { at: toIndex },
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
} );
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
addItem,
|
|
231
|
+
removeItem,
|
|
232
|
+
duplicateItem,
|
|
233
|
+
moveItem,
|
|
234
|
+
};
|
|
235
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { getContainer, getElementSettings, updateElementSettings, type V1Element } 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 { ACCORDION_ITEM_ELEMENT_TYPE } from './use-actions';
|
|
9
|
+
|
|
10
|
+
const ACCORDION_ITEM_HEADER_ELEMENT_TYPE = 'e-accordion-item-header';
|
|
11
|
+
|
|
12
|
+
type CascadeShowIconPayload = {
|
|
13
|
+
accordionId: string;
|
|
14
|
+
showIcon: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// A header's `show_icon` is either an explicit boolean prop value (`booleanPropTypeUtil.create`'s
|
|
18
|
+
// return shape) or `null`/absent when it was never set - see `undo()` below for why both must
|
|
19
|
+
// round-trip correctly.
|
|
20
|
+
type CascadeShowIconResult = {
|
|
21
|
+
previous: Record< string, ReturnType< typeof booleanPropTypeUtil.create > | null | undefined >;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// `show_icon` is global on the root (`Atomic_Accordion::define_props_schema()`); every
|
|
25
|
+
// `e-accordion-item-header` carries a *mirrored* `show_icon` prop of its own purely because the
|
|
26
|
+
// children-dependencies reconciler evaluates a rule against the declaring element's own settings
|
|
27
|
+
// and can only attach/detach that element's own direct children (see the comment on both props in
|
|
28
|
+
// PHP). This function is the write-through: it pushes the root's current value onto every header's
|
|
29
|
+
// mirrored prop, in one undo-able history transaction, so a single undo restores every header at
|
|
30
|
+
// once. It does not touch the icon child itself - each header's own `Child_Dependency` (bound to that
|
|
31
|
+
// header's settings `change` event) reacts to the prop flip and attaches/detaches/restashes the icon
|
|
32
|
+
// on its own, exactly as it would if the header's `show_icon` had been changed directly.
|
|
33
|
+
export const cascadeShowIconToHeaders = undoable< CascadeShowIconPayload, CascadeShowIconResult, undefined >(
|
|
34
|
+
{
|
|
35
|
+
do: ( { accordionId, showIcon } ) => {
|
|
36
|
+
const headerIds = getAccordionHeaderIds( accordionId );
|
|
37
|
+
|
|
38
|
+
const previous = Object.fromEntries(
|
|
39
|
+
headerIds.map(
|
|
40
|
+
( headerId ) =>
|
|
41
|
+
[
|
|
42
|
+
headerId,
|
|
43
|
+
getElementSettings< ReturnType< typeof booleanPropTypeUtil.create > >( headerId, [
|
|
44
|
+
'show_icon',
|
|
45
|
+
] ).show_icon,
|
|
46
|
+
] as const
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
headerIds.forEach( ( headerId ) => {
|
|
51
|
+
updateElementSettings( {
|
|
52
|
+
id: headerId,
|
|
53
|
+
props: { show_icon: booleanPropTypeUtil.create( showIcon ) },
|
|
54
|
+
withHistory: false,
|
|
55
|
+
} );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
return { previous };
|
|
59
|
+
},
|
|
60
|
+
undo: ( _payload, { previous } ) => {
|
|
61
|
+
// Write back whatever `previousValue` actually was, including `null`/`undefined` (a header
|
|
62
|
+
// whose `show_icon` was never explicitly set before the cascade). Skipping on falsy here
|
|
63
|
+
// used to only ever skip that unset case - explicit `false` is a *prop object*
|
|
64
|
+
// (`{ $$type: 'boolean', value: false }`), never itself falsy - so the guard silently ate
|
|
65
|
+
// the common case (the default two-item tree's headers start with `show_icon` unset) instead
|
|
66
|
+
// of the one it looked like it was guarding against. An explicit `null` evaluates the same
|
|
67
|
+
// as "unset" in the `Child_Dependency` reconciler (`extractValue()` -> `?.value` is
|
|
68
|
+
// `undefined` either way), so this restores the icon exactly as it was.
|
|
69
|
+
Object.entries( previous ).forEach( ( [ headerId, previousValue ] ) => {
|
|
70
|
+
updateElementSettings( {
|
|
71
|
+
id: headerId,
|
|
72
|
+
props: { show_icon: previousValue ?? null },
|
|
73
|
+
withHistory: false,
|
|
74
|
+
} );
|
|
75
|
+
} );
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: __( 'Accordion', 'elementor' ),
|
|
80
|
+
subtitle: __( 'Show Icon', 'elementor' ),
|
|
81
|
+
// `undoable()`'s debounce only delays when the *history entry* is pushed onto the undo stack -
|
|
82
|
+
// the settings write itself (`do()`) still runs synchronously on every call. The root's own
|
|
83
|
+
// `show_icon` change goes through `SettingsField` -> `useUndoableUpdateElementProp`
|
|
84
|
+
// (`settings-field.tsx`), which debounces its history push by `HISTORY_DEBOUNCE_WAIT` (800ms).
|
|
85
|
+
// Without matching that here, this cascade's history entry (pushed immediately) would land on
|
|
86
|
+
// the undo stack *before* the root's (pushed 800ms later), so the first Undo after a toggle
|
|
87
|
+
// would revert the root switch alone while every header stayed on its new value - the switch and
|
|
88
|
+
// the icons would visibly disagree until a second Undo. Matching the debounce window fixes the
|
|
89
|
+
// stack order to be deterministic (root's own entry, then this one) regardless of how quickly a
|
|
90
|
+
// user hits Undo. It does not make the two changes one atomic transaction - see the comment on
|
|
91
|
+
// `useShowIconWriteThrough` for why that would require forking the shared `Switch_Control`.
|
|
92
|
+
debounce: { wait: HISTORY_DEBOUNCE_WAIT },
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
function getAccordionHeaderIds( accordionId: string ): string[] {
|
|
97
|
+
const accordion = getContainer( accordionId );
|
|
98
|
+
|
|
99
|
+
const itemContainers = ( accordion?.children ?? [] ).filter(
|
|
100
|
+
( child ) => child.model.get( 'elType' ) === ACCORDION_ITEM_ELEMENT_TYPE
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const headerContainers = itemContainers
|
|
104
|
+
.map(
|
|
105
|
+
( item ) =>
|
|
106
|
+
item.children?.find( ( child ) => child.model.get( 'elType' ) === ACCORDION_ITEM_HEADER_ELEMENT_TYPE )
|
|
107
|
+
)
|
|
108
|
+
.filter( ( header ): header is V1Element => Boolean( header ) );
|
|
109
|
+
|
|
110
|
+
return headerContainers.map( ( header ) => header.id );
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Watches the root's *current* `show_icon` value (read reactively from the panel's element
|
|
114
|
+
// settings) and cascades it to every header on change. Skips the very first render for a given
|
|
115
|
+
// element so mounting the panel on an already-toggled-off accordion doesn't re-fire a no-op
|
|
116
|
+
// cascade. Deliberately lives alongside the repeater control rather than inside the generic
|
|
117
|
+
// `Switch_Control` -> `SettingsField` pipeline: that pipeline is shared by every atomic element's
|
|
118
|
+
// switch controls, so it is not a safe place to special-case one element's cross-element write-
|
|
119
|
+
// through.
|
|
120
|
+
export function useShowIconWriteThrough( accordionId: string, showIcon: boolean ): void {
|
|
121
|
+
const previousRef = useRef< { accordionId: string; showIcon: boolean } | null >( null );
|
|
122
|
+
|
|
123
|
+
useEffect( () => {
|
|
124
|
+
const previous = previousRef.current;
|
|
125
|
+
previousRef.current = { accordionId, showIcon };
|
|
126
|
+
|
|
127
|
+
// No baseline yet, or the accordion identity changed (e.g. a different element got selected
|
|
128
|
+
// into the same mounted control) - just record the new baseline, never cascade.
|
|
129
|
+
if ( ! previous || previous.accordionId !== accordionId || previous.showIcon === showIcon ) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
cascadeShowIconToHeaders( { accordionId, showIcon } );
|
|
134
|
+
}, [ accordionId, showIcon ] );
|
|
135
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ControlComponent } from '@elementor/editor-controls';
|
|
2
2
|
|
|
3
3
|
import { type ControlRegistry, controlsRegistry } from '../controls-registry';
|
|
4
|
+
import { AccordionItemsControl } from './accordion-items-control/accordion-items-control';
|
|
4
5
|
import { TabsControl } from './tabs-control/tabs-control';
|
|
5
6
|
|
|
6
7
|
const controlTypes = {
|
|
7
8
|
tabs: { component: TabsControl as ControlComponent, layout: 'full' },
|
|
9
|
+
'accordion-items': { component: AccordionItemsControl as ControlComponent, layout: 'full' },
|
|
8
10
|
} as const satisfies ControlRegistry;
|
|
9
11
|
|
|
10
12
|
export const registerElementControls = () => {
|