@elementor/editor-elements 4.3.0-974 → 4.3.0-975

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.
@@ -0,0 +1,148 @@
1
+ import { type PropValue } from '@elementor/editor-props';
2
+
3
+ import { getContainer } from '../sync/get-container';
4
+ import { addModelToParent, removeModelFromParent } from '../sync/resolve-element';
5
+ import { resolveInsertIndex } from '../sync/resolve-insert-index';
6
+ import { type V1Element, type V1ElementConfig, type V1ElementData, type V1ElementModelProps } from '../sync/types';
7
+ import { createChildrenStash } from './stash';
8
+ import { type ChildDependencyRule, type ChildrenStash } from './types';
9
+ import { evaluateWhen } from './utils';
10
+
11
+ type SettingsChangeSource = {
12
+ get: ( key: string ) => unknown;
13
+ toJSON: () => Record< string, PropValue >;
14
+ on?: ( event: string, callback: ( ...args: unknown[] ) => void ) => void;
15
+ off?: ( event: string, callback: ( ...args: unknown[] ) => void ) => void;
16
+ };
17
+
18
+ type ReactiveModel = {
19
+ get: ( key: string ) => unknown;
20
+ };
21
+
22
+ type BindSettingsReconcileArgs = {
23
+ model: ReactiveModel;
24
+ elementConfig: V1ElementConfig | undefined;
25
+ };
26
+
27
+ type Detach = () => void;
28
+
29
+ export function bindSettingsReconcile( { model, elementConfig }: BindSettingsReconcileArgs ): Detach {
30
+ const stash = createChildrenStash();
31
+ const rules = elementConfig?.children_dependencies;
32
+
33
+ if ( ! rules?.length ) {
34
+ return () => {};
35
+ }
36
+
37
+ const settingsModel = model.get( 'settings' ) as SettingsChangeSource | undefined;
38
+ const elementId = model.get( 'id' ) as string | undefined;
39
+
40
+ if ( ! settingsModel?.on || ! settingsModel?.off || ! elementId ) {
41
+ return () => {};
42
+ }
43
+
44
+ const lastMet = new Map< string, boolean >();
45
+
46
+ rules.forEach( ( rule ) => {
47
+ lastMet.set( rule.child_type, evaluateWhen( rule.when, settingsModel.toJSON() ) );
48
+ } );
49
+
50
+ const onChange = () => {
51
+ const currentSettings = settingsModel.toJSON();
52
+
53
+ rules.forEach( ( rule ) => {
54
+ const previous = lastMet.get( rule.child_type ) ?? false;
55
+ const current = evaluateWhen( rule.when, currentSettings );
56
+
57
+ if ( previous === current ) {
58
+ return;
59
+ }
60
+
61
+ lastMet.set( rule.child_type, current );
62
+
63
+ if ( current ) {
64
+ attachChildFromRule( elementId, rule, stash );
65
+ } else {
66
+ detachChildFromRule( elementId, rule, stash );
67
+ }
68
+ } );
69
+ };
70
+
71
+ settingsModel.on( 'change', onChange );
72
+
73
+ return () => {
74
+ settingsModel.off?.( 'change', onChange );
75
+ stash.clearAllForElement( elementId );
76
+ };
77
+ }
78
+
79
+ function attachChildFromRule( parentId: string, rule: ChildDependencyRule, stash: ChildrenStash ): void {
80
+ const parent = getContainer( parentId ) ?? undefined;
81
+ const currentChildren = getDirectChildData( parent );
82
+ const alreadyPresent = currentChildren.some( ( child ) => child.elType === rule.child_type );
83
+
84
+ if ( alreadyPresent ) {
85
+ return;
86
+ }
87
+
88
+ const stashed = rule.stash ? stash.get( parentId, rule.child_type ) : undefined;
89
+ const modelData = stashed ?? rule.default_model ?? ( { elType: rule.child_type } as V1ElementData );
90
+ const insertAt = resolveInsertIndex( rule.position, currentChildren );
91
+
92
+ const attached = addModelToParent( parentId, modelData as unknown as V1ElementModelProps, { at: insertAt } );
93
+
94
+ if ( ! attached ) {
95
+ return;
96
+ }
97
+
98
+ if ( stashed ) {
99
+ stash.clear( parentId, rule.child_type );
100
+ }
101
+
102
+ requestNavigatorRefresh( parentId );
103
+ }
104
+
105
+ function detachChildFromRule( parentId: string, rule: ChildDependencyRule, stash: ChildrenStash ): void {
106
+ const parent = getContainer( parentId ) ?? undefined;
107
+ const child = parent?.children?.find( ( candidate ) => candidate.model.get( 'elType' ) === rule.child_type );
108
+
109
+ if ( ! child ) {
110
+ return;
111
+ }
112
+
113
+ const childSnapshot = child.model.toJSON() as unknown as V1ElementData;
114
+ const removed = removeModelFromParent( parentId, child.id );
115
+
116
+ if ( ! removed ) {
117
+ return;
118
+ }
119
+
120
+ if ( rule.stash ) {
121
+ stash.save( parentId, rule.child_type, childSnapshot );
122
+ }
123
+
124
+ requestNavigatorRefresh( parentId );
125
+ }
126
+
127
+ // Backbone `add`/`remove` on the parent elements collection is silent (see
128
+ // assets/dev/js/editor/document/component.js addModelToParent / removeModelFromParent),
129
+ // so the Marionette Navigator CompositeView never observes our mutation. The
130
+ // legacy navigator exposes an `elementor/navigator/refresh-children` custom
131
+ // window event (assets/dev/js/editor/regions/navigator/element.js) that
132
+ // re-renders the subtree for a given element id — dispatch it here so the
133
+ // Structure panel stays in sync with the model.
134
+ function requestNavigatorRefresh( parentId: string ): void {
135
+ if ( typeof window === 'undefined' || typeof window.dispatchEvent !== 'function' ) {
136
+ return;
137
+ }
138
+
139
+ window.dispatchEvent(
140
+ new CustomEvent( 'elementor/navigator/refresh-children', {
141
+ detail: { elementId: parentId },
142
+ } )
143
+ );
144
+ }
145
+
146
+ function getDirectChildData( parent: V1Element | undefined ): V1ElementData[] {
147
+ return ( parent?.children ?? [] ).map( ( child ) => child.model.toJSON() as unknown as V1ElementData );
148
+ }
@@ -0,0 +1,4 @@
1
+ export { bindSettingsReconcile } from './bind-settings-reconcile';
2
+ export { evaluateWhen } from './utils';
3
+ export { reconcileInitialChildren } from './reconcile-initial-children';
4
+ export type { ChildDependenciesConfig, ChildDependencyRule } from './types';
@@ -0,0 +1,60 @@
1
+ import { resolveInsertIndex } from '../sync/resolve-insert-index';
2
+ import { type V1ElementConfig, type V1ElementData, type V1ElementSettingsProps } from '../sync/types';
3
+ import { createChildrenStash } from './stash';
4
+ import { type ChildDependencyRule } from './types';
5
+ import { evaluateWhen } from './utils';
6
+
7
+ type ReconcileInitialChildrenArgs = {
8
+ elementId: string;
9
+ elementConfig: V1ElementConfig | undefined;
10
+ attributes: {
11
+ elements?: V1ElementData[];
12
+ settings?: V1ElementSettingsProps;
13
+ };
14
+ };
15
+
16
+ export function reconcileInitialChildren( {
17
+ elementId,
18
+ elementConfig,
19
+ attributes,
20
+ }: ReconcileInitialChildrenArgs ): void {
21
+ const stash = createChildrenStash();
22
+ const rules = elementConfig?.children_dependencies;
23
+
24
+ if ( ! rules?.length ) {
25
+ return;
26
+ }
27
+
28
+ const elements = [ ...( attributes.elements ?? [] ) ];
29
+ const settings = attributes.settings ?? {};
30
+
31
+ rules.forEach( ( rule: ChildDependencyRule ) => {
32
+ const isMet = evaluateWhen( rule.when, settings );
33
+ const existingIndex = elements.findIndex( ( element ) => element.elType === rule.child_type );
34
+ const isPresent = existingIndex >= 0;
35
+
36
+ if ( isMet && ! isPresent ) {
37
+ const stashed = rule.stash ? stash.get( elementId, rule.child_type ) : undefined;
38
+ const modelData = stashed ?? rule.default_model ?? ( { elType: rule.child_type } as V1ElementData );
39
+ const insertAt = resolveInsertIndex( rule.position, elements );
40
+
41
+ elements.splice( insertAt, 0, modelData );
42
+
43
+ if ( stashed ) {
44
+ stash.clear( elementId, rule.child_type );
45
+ }
46
+
47
+ return;
48
+ }
49
+
50
+ if ( ! isMet && isPresent ) {
51
+ const [ removed ] = elements.splice( existingIndex, 1 );
52
+
53
+ if ( rule.stash && removed ) {
54
+ stash.save( elementId, rule.child_type, removed );
55
+ }
56
+ }
57
+ } );
58
+
59
+ attributes.elements = elements;
60
+ }
@@ -0,0 +1,40 @@
1
+ import { getSessionStorageItem, removeSessionStorageItem, setSessionStorageItem } from '@elementor/session';
2
+
3
+ import { type V1ElementData } from '../sync/types';
4
+ import { type ChildrenStash } from './types';
5
+
6
+ const STASH_KEY_PREFIX = 'elementor/editor-state';
7
+ const STASH_KEY_SEGMENT = 'children-deps';
8
+
9
+ export function createChildrenStash(): ChildrenStash {
10
+ return {
11
+ get( elementId, childType ) {
12
+ return getSessionStorageItem< V1ElementData >( buildStashKey( elementId, childType ) );
13
+ },
14
+ save( elementId, childType, data ) {
15
+ setSessionStorageItem( buildStashKey( elementId, childType ), data );
16
+ },
17
+ clear( elementId, childType ) {
18
+ removeSessionStorageItem( buildStashKey( elementId, childType ) );
19
+ },
20
+ clearAllForElement( elementId ) {
21
+ const prefix = buildElementStashPrefix( elementId );
22
+
23
+ for ( let index = sessionStorage.length - 1; index >= 0; index-- ) {
24
+ const key = sessionStorage.key( index );
25
+
26
+ if ( key?.startsWith( prefix ) ) {
27
+ removeSessionStorageItem( key );
28
+ }
29
+ }
30
+ },
31
+ };
32
+ }
33
+
34
+ function buildStashKey( elementId: string, childType: string ): string {
35
+ return `${ STASH_KEY_PREFIX }/${ elementId }/${ STASH_KEY_SEGMENT }/${ childType }`;
36
+ }
37
+
38
+ function buildElementStashPrefix( elementId: string ): string {
39
+ return `${ STASH_KEY_PREFIX }/${ elementId }/${ STASH_KEY_SEGMENT }/`;
40
+ }
@@ -0,0 +1,20 @@
1
+ import { type Dependency } from '@elementor/editor-props';
2
+
3
+ import { type ElementPosition, type V1ElementData } from '../sync/types';
4
+
5
+ export type ChildDependencyRule = {
6
+ child_type: string;
7
+ when: Dependency;
8
+ position: ElementPosition;
9
+ stash: boolean;
10
+ default_model: V1ElementData | null;
11
+ };
12
+
13
+ export type ChildDependenciesConfig = ChildDependencyRule[];
14
+
15
+ export type ChildrenStash = {
16
+ get: ( elementId: string, childType: string ) => V1ElementData | undefined;
17
+ save: ( elementId: string, childType: string, data: V1ElementData ) => void;
18
+ clear: ( elementId: string, childType: string ) => void;
19
+ clearAllForElement: ( elementId: string ) => void;
20
+ };
@@ -0,0 +1,5 @@
1
+ import { type Dependency, isDependencyMet, type PropValue } from '@elementor/editor-props';
2
+
3
+ export function evaluateWhen( when: Dependency | undefined, settings: Record< string, PropValue > ): boolean {
4
+ return isDependencyMet( when, settings ).isMet;
5
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,15 @@
2
2
  export * from './types';
3
3
  export type * from './sync/types';
4
4
 
5
+ // children dependencies (schema-driven settings <-> children sync)
6
+ export {
7
+ bindSettingsReconcile,
8
+ type ChildDependenciesConfig,
9
+ type ChildDependencyRule,
10
+ evaluateWhen,
11
+ reconcileInitialChildren,
12
+ } from './children-dependencies';
13
+
5
14
  // hooks
6
15
  export { useElementChildren, type ElementChildren, type ElementModel } from './hooks/use-element-children';
7
16
  export { useElementEditorSettings } from './hooks/use-element-editor-settings';
@@ -46,6 +55,7 @@ export { moveElement, type MoveElementParams } from './sync/move-element';
46
55
  export { moveElements } from './sync/move-elements';
47
56
  export { removeElements } from './sync/remove-elements';
48
57
  export { replaceElement } from './sync/replace-element';
58
+ export { resolveInsertIndex } from './sync/resolve-insert-index';
49
59
  export { updateElementEditorSettings } from './sync/update-element-editor-settings';
50
60
  export { updateElementSettings, type UpdateElementSettingsArgs } from './sync/update-element-settings';
51
61
 
@@ -0,0 +1,34 @@
1
+ import { type ElementPosition, type V1ElementData } from './types';
2
+
3
+ export function resolveInsertIndex( position: ElementPosition, elements: V1ElementData[] ): number {
4
+ const lastIndex = elements.length;
5
+
6
+ switch ( position.kind ) {
7
+ case 'first':
8
+ return 0;
9
+
10
+ case 'last':
11
+ return lastIndex;
12
+
13
+ case 'index': {
14
+ const index = typeof position.value === 'number' ? position.value : lastIndex;
15
+
16
+ return Math.max( 0, Math.min( index, lastIndex ) );
17
+ }
18
+
19
+ case 'after_type': {
20
+ const anchor = elements.findIndex( ( element ) => element.elType === position.value );
21
+
22
+ return anchor >= 0 ? anchor + 1 : lastIndex;
23
+ }
24
+
25
+ case 'before_type': {
26
+ const anchor = elements.findIndex( ( element ) => element.elType === position.value );
27
+
28
+ return anchor >= 0 ? anchor : lastIndex;
29
+ }
30
+
31
+ default:
32
+ return lastIndex;
33
+ }
34
+ }
package/src/sync/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type PropsSchema, type PropValue, type SizePropValue } from '@elementor/editor-props';
2
2
  import { type ClassState, type StyleDefinition, type StyleDefinitionID } from '@elementor/editor-styles';
3
3
 
4
+ import { type ChildDependencyRule } from '../children-dependencies/types';
4
5
  import { type ControlItem, type PseudoState } from '../types';
5
6
 
6
7
  export type ExtendedWindow = Window & {
@@ -168,6 +169,13 @@ export type V1ElementData = Omit< V1ElementModelProps, 'elements' > & {
168
169
  elements?: V1ElementData[];
169
170
  };
170
171
 
172
+ export type ElementPositionKind = 'last' | 'first' | 'index' | 'after_type' | 'before_type';
173
+
174
+ export type ElementPosition = {
175
+ kind: ElementPositionKind;
176
+ value: number | string | null;
177
+ };
178
+
171
179
  export type V1ElementEditorSettingsProps = {
172
180
  title?: string;
173
181
  initial_position?: number;
@@ -197,6 +205,7 @@ export type V1ElementConfig< T = object, TChild = unknown > = {
197
205
  show_in_panel?: boolean;
198
206
  allowed_child_types?: string[];
199
207
  default_children?: Array< Record< string, TChild > >;
208
+ children_dependencies?: ChildDependencyRule[];
200
209
  meta?: { [ key: string ]: string | number | boolean | null | NonNullable< V1ElementConfig[ 'meta' ] > };
201
210
  } & T;
202
211
 
@@ -205,4 +214,6 @@ type V1Model< T > = {
205
214
  set: < K extends keyof T >( key: K, value: T[ K ] ) => void;
206
215
  toJSON: ( options?: { remove?: string[] } ) => T;
207
216
  trigger?: ( event: string, ...args: unknown[] ) => void;
217
+ on?: ( event: string, callback: ( ...args: unknown[] ) => void, context?: unknown ) => void;
218
+ off?: ( event: string, callback?: ( ...args: unknown[] ) => void, context?: unknown ) => void;
208
219
  };