@elementor/editor-canvas 4.0.0-551 → 4.0.0-564

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.
@@ -6,9 +6,9 @@ import { canBeTemplated, type TemplatedElementConfig } from './create-templated-
6
6
  import {
7
7
  createAfterRender,
8
8
  createBeforeRender,
9
+ createTwigRenderState,
10
+ renderChildrenWithOptimization,
9
11
  renderTwigTemplate,
10
- setupTwigRenderer,
11
- type TwigRenderContext,
12
12
  } from './twig-rendering-utils';
13
13
  import { type ElementType, type ElementView, type LegacyWindow } from './types';
14
14
 
@@ -91,11 +91,7 @@ export function createNestedTemplatedElementView( {
91
91
  }: CreateNestedTemplatedElementViewOptions ): typeof ElementView {
92
92
  const legacyWindow = window as unknown as LegacyWindow;
93
93
 
94
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer( {
95
- type,
96
- renderer,
97
- element,
98
- } );
94
+ const renderState = createTwigRenderState( { renderer, element } );
99
95
 
100
96
  const AtomicElementBaseView = legacyWindow.elementor.modules.elements.views.createAtomicElementBase( type );
101
97
  const parentRenderChildren = AtomicElementBaseView.prototype._renderChildren;
@@ -110,6 +106,10 @@ export function createNestedTemplatedElementView( {
110
106
  return 'twig';
111
107
  },
112
108
 
109
+ invalidateRenderCache() {
110
+ renderState.cacheState.invalidate();
111
+ },
112
+
113
113
  render() {
114
114
  this._abortController?.abort();
115
115
  this._abortController = new AbortController();
@@ -117,7 +117,6 @@ export function createNestedTemplatedElementView( {
117
117
  const process = signalizedProcess( this._abortController.signal )
118
118
  .then( () => this._beforeRender() )
119
119
  .then( () => this._renderTemplate() )
120
- // Dispatch the render event after the template is ready
121
120
  .then( () => this._onTemplateReady() )
122
121
  .then( () => this._renderChildren() )
123
122
  .then( () => this._afterRender() );
@@ -152,14 +151,13 @@ export function createNestedTemplatedElementView( {
152
151
 
153
152
  await renderTwigTemplate( {
154
153
  view: this,
155
- signal: this._abortController?.signal as AbortSignal,
156
- resolveProps,
157
- templateKey,
158
- baseStylesDictionary,
159
- type,
160
- renderer,
161
- buildContext: ( context: TwigRenderContext ) => ( {
162
- ...context,
154
+ signal: this._abortController?.signal,
155
+ renderState,
156
+ buildContext: ( resolvedSettings ) => ( {
157
+ id: model.get( 'id' ),
158
+ type,
159
+ settings: resolvedSettings,
160
+ base_styles: element.base_styles_dictionary,
163
161
  editor_attributes: buildEditorAttributes( model ),
164
162
  editor_classes: buildEditorClasses( model ),
165
163
  } ),
@@ -199,18 +197,12 @@ export function createNestedTemplatedElementView( {
199
197
  },
200
198
 
201
199
  async _renderChildren() {
202
- parentRenderChildren.call( this );
203
-
204
- const renderPromises: Promise< void >[] = [];
205
-
206
- this.children.each( ( childView: ElementView ) => {
207
- if ( childView._currentRenderPromise ) {
208
- renderPromises.push( childView._currentRenderPromise );
209
- }
200
+ await renderChildrenWithOptimization( {
201
+ children: this.children,
202
+ domUpdateWasSkipped: renderState.cacheState.domUpdateWasSkipped,
203
+ renderChildren: () => parentRenderChildren.call( this ),
210
204
  } );
211
205
 
212
- await Promise.all( renderPromises );
213
-
214
206
  this._removeChildrenPlaceholder();
215
207
  },
216
208
 
@@ -6,7 +6,9 @@ import { createElementViewClassDeclaration } from './create-element-type';
6
6
  import {
7
7
  createAfterRender,
8
8
  createBeforeRender,
9
- setupTwigRenderer,
9
+ createTwigRenderState,
10
+ renderChildrenWithOptimization,
11
+ renderTwigTemplate,
10
12
  type TwigViewInterface,
11
13
  } from './twig-rendering-utils';
12
14
  import {
@@ -67,17 +69,10 @@ export function createTemplatedElementView( {
67
69
  }: CreateTemplatedElementTypeOptions ): typeof ElementView {
68
70
  const BaseView = createElementViewClassDeclaration();
69
71
 
70
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer( {
71
- type,
72
- renderer,
73
- element,
74
- } );
72
+ const renderState = createTwigRenderState( { renderer, element } );
75
73
 
76
74
  return class extends BaseView {
77
- #abortController: AbortController | null = null;
78
- #childrenRenderPromises: Promise< void >[] = [];
79
- #lastResolvedSettingsHash: string | null = null;
80
- #domUpdateWasSkipped = false;
75
+ _abortController: AbortController | null = null;
81
76
 
82
77
  getTemplateType() {
83
78
  return 'twig';
@@ -100,14 +95,14 @@ export function createTemplatedElementView( {
100
95
  }
101
96
 
102
97
  invalidateRenderCache() {
103
- this.#lastResolvedSettingsHash = null;
98
+ renderState.cacheState.invalidate();
104
99
  }
105
100
 
106
101
  render() {
107
- this.#abortController?.abort();
108
- this.#abortController = new AbortController();
102
+ this._abortController?.abort();
103
+ this._abortController = new AbortController();
109
104
 
110
- const process = signalizedProcess( this.#abortController.signal )
105
+ const process = signalizedProcess( this._abortController.signal )
111
106
  .then( () => this._beforeRender() )
112
107
  .then( () => this._renderTemplate() )
113
108
  .then( () => this._renderChildren() )
@@ -119,92 +114,27 @@ export function createTemplatedElementView( {
119
114
  }
120
115
 
121
116
  async _renderChildren() {
122
- this.#childrenRenderPromises = [];
123
-
124
- // Optimize rendering by reusing existing child views instead of recreating them.
125
- if ( this.#shouldReuseChildren() ) {
126
- this.#rerenderExistingChildren();
127
- } else {
128
- super._renderChildren();
129
- }
130
-
131
- this.#collectChildrenRenderPromises();
132
- await this._waitForChildrenToComplete();
133
- }
134
-
135
- #shouldReuseChildren() {
136
- return this.#domUpdateWasSkipped && this.children?.length > 0;
137
- }
138
-
139
- #rerenderExistingChildren() {
140
- this.children?.each( ( childView: ElementView ) => {
141
- childView.render();
142
- } );
143
- }
144
-
145
- #collectChildrenRenderPromises() {
146
- this.children?.each( ( childView: ElementView ) => {
147
- if ( childView._currentRenderPromise ) {
148
- this.#childrenRenderPromises.push( childView._currentRenderPromise );
149
- }
117
+ await renderChildrenWithOptimization( {
118
+ children: this.children,
119
+ domUpdateWasSkipped: renderState.cacheState.domUpdateWasSkipped,
120
+ renderChildren: () => super._renderChildren(),
150
121
  } );
151
122
  }
152
123
 
153
- async _waitForChildrenToComplete() {
154
- if ( this.#childrenRenderPromises.length > 0 ) {
155
- await Promise.all( this.#childrenRenderPromises );
156
- }
157
- }
158
-
159
124
  async _renderTemplate() {
160
- this.triggerMethod( 'before:render:template' );
161
-
162
- const process = signalizedProcess( this.#abortController?.signal as AbortSignal )
163
- .then( ( _, signal ) => {
164
- const settings = this.model.get( 'settings' ).toJSON();
165
- return resolveProps( {
166
- props: settings,
167
- signal,
168
- renderContext: this.getResolverRenderContext(),
169
- } );
170
- } )
171
- .then( ( settings ) => {
172
- return this.afterSettingsResolve( settings );
173
- } )
174
- .then( async ( settings ) => {
175
- const settingsHash = JSON.stringify( settings );
176
- const settingsChanged = settingsHash !== this.#lastResolvedSettingsHash;
177
-
178
- if ( ! settingsChanged && this.isRendered ) {
179
- this.#domUpdateWasSkipped = true;
180
- return null;
181
- }
182
- this.#domUpdateWasSkipped = false;
183
-
184
- this.#lastResolvedSettingsHash = settingsHash;
185
-
186
- const context = {
187
- id: this.model.get( 'id' ),
188
- type,
189
- settings,
190
- base_styles: baseStylesDictionary,
191
- };
192
-
193
- return renderer.render( templateKey, context );
194
- } )
195
- .then( ( html ) => {
196
- if ( html === null ) {
197
- return;
198
- }
199
-
200
- this.$el.html( html );
201
- } );
202
-
203
- await process.execute();
204
-
205
- this.bindUIElements();
206
-
207
- this.triggerMethod( 'render:template' );
125
+ await renderTwigTemplate( {
126
+ view: this,
127
+ signal: this._abortController?.signal,
128
+ renderState,
129
+ buildContext: ( resolvedSettings ) => ( {
130
+ id: this.model.get( 'id' ),
131
+ type,
132
+ settings: resolvedSettings,
133
+ base_styles: element.base_styles_dictionary,
134
+ } ),
135
+ attachContent: ( html: string ) => this.$el.html( html ),
136
+ afterSettingsResolve: ( settings ) => this.afterSettingsResolve( settings ),
137
+ } );
208
138
  }
209
139
 
210
140
  afterSettingsResolve( settings: { [ key: string ]: unknown } ) {
@@ -13,10 +13,10 @@ const createUnionPropType = ( keys: string[] ): PropType =>
13
13
  } ) as PropType;
14
14
 
15
15
  describe( 'isInlineEditingAllowed', () => {
16
- it( 'should allow inline editing for html prop values', () => {
16
+ it( 'should allow inline editing for html-v2 prop values', () => {
17
17
  expect(
18
18
  isInlineEditingAllowed( {
19
- rawValue: { $$type: 'html', value: 'Hello' },
19
+ rawValue: { $$type: 'html-v2', value: { content: 'Hello', children: [] } },
20
20
  propTypeFromSchema: null,
21
21
  } )
22
22
  ).toBe( true );
@@ -40,11 +40,11 @@ describe( 'isInlineEditingAllowed', () => {
40
40
  ).toBe( false );
41
41
  } );
42
42
 
43
- it( 'should allow when value is unset but schema key is html', () => {
43
+ it( 'should allow when value is unset but schema key is html-v2', () => {
44
44
  expect(
45
45
  isInlineEditingAllowed( {
46
46
  rawValue: undefined,
47
- propTypeFromSchema: createPlainPropType( 'html' ),
47
+ propTypeFromSchema: createPlainPropType( 'html-v2' ),
48
48
  } )
49
49
  ).toBe( true );
50
50
  } );
@@ -67,16 +67,16 @@ describe( 'isInlineEditingAllowed', () => {
67
67
  ).toBe( false );
68
68
  } );
69
69
 
70
- it( 'should allow when value is unset and union schema includes html', () => {
70
+ it( 'should allow when value is unset and union schema includes html-v2', () => {
71
71
  expect(
72
72
  isInlineEditingAllowed( {
73
73
  rawValue: undefined,
74
- propTypeFromSchema: createUnionPropType( [ 'dynamic', 'html' ] ),
74
+ propTypeFromSchema: createUnionPropType( [ 'dynamic', 'html-v2' ] ),
75
75
  } )
76
76
  ).toBe( true );
77
77
  } );
78
78
 
79
- it( 'should disallow when value is unset and union schema does not include html/string', () => {
79
+ it( 'should disallow when value is unset and union schema does not include html-v2/string', () => {
80
80
  expect(
81
81
  isInlineEditingAllowed( {
82
82
  rawValue: undefined,
@@ -56,9 +56,6 @@ export const CanvasInlineEditor = ( {
56
56
  <InlineEditingOverlay expectedTag={ expectedTag } rootElement={ rootElement } id={ id } />
57
57
  <style>
58
58
  { `
59
- .${ EDITOR_WRAPPER_SELECTOR }, .${ EDITOR_WRAPPER_SELECTOR } > * {
60
- height: 100%;
61
- }
62
59
  .ProseMirror > * {
63
60
  height: 100%;
64
61
  }
@@ -1,7 +1,13 @@
1
1
  import * as React from 'react';
2
2
  import { createRoot, type Root } from 'react-dom/client';
3
3
  import { getContainer, getElementLabel, getElementType } from '@elementor/editor-elements';
4
- import { htmlPropTypeUtil, type PropType, type PropValue, stringPropTypeUtil } from '@elementor/editor-props';
4
+ import {
5
+ htmlV2PropTypeUtil,
6
+ parseHtmlChildren,
7
+ type PropType,
8
+ type PropValue,
9
+ stringPropTypeUtil,
10
+ } from '@elementor/editor-props';
5
11
  import { __privateRunCommandSync as runCommandSync, getCurrentEditMode, undoable } from '@elementor/editor-v1-adapters';
6
12
  import { __ } from '@wordpress/i18n';
7
13
 
@@ -122,12 +128,18 @@ export default class InlineEditingReplacement extends ReplacementBase {
122
128
  getExtractedContentValue() {
123
129
  const propValue = this.getInlineEditablePropValue();
124
130
 
125
- return htmlPropTypeUtil.extract( propValue ) ?? '';
131
+ return htmlV2PropTypeUtil.extract( propValue )?.content ?? '';
126
132
  }
127
133
 
128
134
  setContentValue( value: string | null ) {
129
135
  const settingKey = this.getInlineEditablePropertyName();
130
- const valueToSave = htmlPropTypeUtil.create( value || '' );
136
+ const html = value || '';
137
+ const parsed = parseHtmlChildren( html );
138
+
139
+ const valueToSave = htmlV2PropTypeUtil.create( {
140
+ content: parsed.content || null,
141
+ children: parsed.children,
142
+ } );
131
143
 
132
144
  undoable(
133
145
  {
@@ -162,12 +174,12 @@ export default class InlineEditingReplacement extends ReplacementBase {
162
174
  }
163
175
 
164
176
  if ( propType.kind === 'union' ) {
165
- if ( propType.prop_types[ htmlPropTypeUtil.key ] ) {
166
- return htmlPropTypeUtil.key;
167
- }
177
+ const textKeys = [ htmlV2PropTypeUtil.key, stringPropTypeUtil.key ];
168
178
 
169
- if ( propType.prop_types[ stringPropTypeUtil.key ] ) {
170
- return stringPropTypeUtil.key;
179
+ for ( const key of textKeys ) {
180
+ if ( propType.prop_types[ key ] ) {
181
+ return key;
182
+ }
171
183
  }
172
184
 
173
185
  return null;
@@ -1,4 +1,4 @@
1
- import { htmlPropTypeUtil, type PropType, stringPropTypeUtil } from '@elementor/editor-props';
1
+ import { htmlV2PropTypeUtil, type PropType, stringPropTypeUtil } from '@elementor/editor-props';
2
2
 
3
3
  type InlineEditingEligibilityArgs = {
4
4
  rawValue: unknown;
@@ -9,8 +9,10 @@ const hasKey = ( propType: PropType ): propType is PropType & { key: unknown } =
9
9
  return 'key' in propType;
10
10
  };
11
11
 
12
+ const TEXT_PROP_TYPE_KEYS = new Set( [ htmlV2PropTypeUtil.key, stringPropTypeUtil.key ] );
13
+
12
14
  const isCoreTextPropTypeKey = ( key: unknown ): boolean => {
13
- return key === htmlPropTypeUtil.key || key === stringPropTypeUtil.key;
15
+ return ( TEXT_PROP_TYPE_KEYS as Set< unknown > ).has( key );
14
16
  };
15
17
 
16
18
  const isAllowedBySchema = ( propTypeFromSchema: PropType | null ): boolean => {
@@ -26,9 +28,7 @@ const isAllowedBySchema = ( propTypeFromSchema: PropType | null ): boolean => {
26
28
  return false;
27
29
  }
28
30
 
29
- return Boolean(
30
- propTypeFromSchema.prop_types[ htmlPropTypeUtil.key ] || propTypeFromSchema.prop_types[ stringPropTypeUtil.key ]
31
- );
31
+ return [ ...TEXT_PROP_TYPE_KEYS ].some( ( key ) => propTypeFromSchema.prop_types[ key ] );
32
32
  };
33
33
 
34
34
  export const isInlineEditingAllowed = ( { rawValue, propTypeFromSchema }: InlineEditingEligibilityArgs ): boolean => {
@@ -36,5 +36,5 @@ export const isInlineEditingAllowed = ( { rawValue, propTypeFromSchema }: Inline
36
36
  return isAllowedBySchema( propTypeFromSchema );
37
37
  }
38
38
 
39
- return htmlPropTypeUtil.isValid( rawValue ) || stringPropTypeUtil.isValid( rawValue );
39
+ return htmlV2PropTypeUtil.isValid( rawValue ) || stringPropTypeUtil.isValid( rawValue );
40
40
  };
@@ -1,4 +1,4 @@
1
- import { htmlPropTypeUtil } from '@elementor/editor-props';
1
+ import { htmlV2PropTypeUtil } from '@elementor/editor-props';
2
2
 
3
3
  import { type ModelExtensions } from './create-nested-templated-element-type';
4
4
  import { registerModelExtensions } from './init-legacy-views';
@@ -23,7 +23,7 @@ const tabModelExtensions: ModelExtensions = {
23
23
  ...paragraphElement,
24
24
  settings: {
25
25
  ...paragraphElement.settings,
26
- paragraph: htmlPropTypeUtil.create( `Tab ${ position }` ),
26
+ paragraph: htmlV2PropTypeUtil.create( { content: `Tab ${ position }`, children: [] } ),
27
27
  },
28
28
  };
29
29
 
@@ -5,42 +5,26 @@ import { createPropsResolver, type PropsResolver } from '../renderers/create-pro
5
5
  import { settingsTransformersRegistry } from '../settings-transformers-registry';
6
6
  import { type ElementView, type RenderContext } from './types';
7
7
 
8
- export type TwigElementConfig = Required<
8
+ type TwigElementConfig = Required<
9
9
  Pick< V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary' >
10
10
  >;
11
11
 
12
- export type TwigRenderContext = {
13
- id: string;
14
- type: string;
15
- settings: Record< string, unknown >;
16
- base_styles: Record< string, unknown >;
17
- [ key: string ]: unknown;
12
+ type TwigRenderState = {
13
+ templateKey: string;
14
+ resolveProps: PropsResolver;
15
+ renderer: DomRenderer;
16
+ cacheState: RenderCacheState;
18
17
  };
19
18
 
20
- export type SetupTwigRendererOptions = {
21
- type: string;
19
+ type CreateTwigRenderStateOptions = {
22
20
  renderer: DomRenderer;
23
21
  element: TwigElementConfig;
24
22
  };
25
23
 
26
- export type SetupTwigRendererResult = {
27
- templateKey: string;
28
- baseStylesDictionary: Record< string, unknown >;
29
- resolveProps: PropsResolver;
30
- };
31
-
32
- export function canBeTwigTemplated( element: Partial< TwigElementConfig > ): element is TwigElementConfig {
33
- return !! (
34
- element.atomic_props_schema &&
35
- element.twig_templates &&
36
- element.twig_main_template &&
37
- element.base_styles_dictionary
38
- );
39
- }
40
-
41
- export function setupTwigRenderer( { renderer, element }: SetupTwigRendererOptions ): SetupTwigRendererResult {
24
+ export function createTwigRenderState( { renderer, element }: CreateTwigRenderStateOptions ): TwigRenderState {
42
25
  const templateKey = element.twig_main_template;
43
- const baseStylesDictionary = element.base_styles_dictionary;
26
+
27
+ const cacheState = createRenderCacheState();
44
28
 
45
29
  Object.entries( element.twig_templates ).forEach( ( [ key, template ] ) => {
46
30
  renderer.register( key, template );
@@ -51,7 +35,7 @@ export function setupTwigRenderer( { renderer, element }: SetupTwigRendererOptio
51
35
  schema: element.atomic_props_schema,
52
36
  } );
53
37
 
54
- return { templateKey, baseStylesDictionary, resolveProps };
38
+ return { templateKey, resolveProps, renderer, cacheState };
55
39
  }
56
40
 
57
41
  export interface TwigViewInterface extends Omit< ElementView, 'getResolverRenderContext' > {
@@ -72,60 +56,87 @@ export function createAfterRender< TView extends TwigViewInterface >( view: TVie
72
56
  view.triggerMethod( 'render', view );
73
57
  }
74
58
 
59
+ type RenderCacheState = {
60
+ lastResolvedSettingsHash: string | null;
61
+ domUpdateWasSkipped: boolean;
62
+ invalidate: () => void;
63
+ };
64
+
65
+ export function createRenderCacheState(): RenderCacheState {
66
+ return {
67
+ lastResolvedSettingsHash: null,
68
+ domUpdateWasSkipped: false,
69
+ invalidate() {
70
+ this.lastResolvedSettingsHash = null;
71
+ this.domUpdateWasSkipped = false;
72
+ },
73
+ };
74
+ }
75
+
76
+ type TwigRenderContext = {
77
+ id: string;
78
+ type: string;
79
+ settings: Record< string, unknown >;
80
+ base_styles: Record< string, unknown >;
81
+ [ key: string ]: unknown;
82
+ };
83
+
75
84
  export type RenderTwigTemplateOptions< TView extends TwigViewInterface > = {
76
85
  view: TView;
77
- signal: AbortSignal;
78
- resolveProps: PropsResolver;
79
- templateKey: string;
80
- baseStylesDictionary: Record< string, unknown >;
81
- type: string;
82
- renderer: DomRenderer;
83
- buildContext?: ( context: TwigRenderContext ) => TwigRenderContext;
86
+ signal?: AbortSignal;
87
+ renderState: TwigRenderState;
88
+ buildContext: ( resolvedSettings: Record< string, unknown > ) => TwigRenderContext;
84
89
  attachContent: ( html: string ) => void;
90
+ afterSettingsResolve?: ( settings: Record< string, unknown > ) => Record< string, unknown >;
85
91
  };
86
92
 
87
93
  export async function renderTwigTemplate< TView extends TwigViewInterface >( {
88
94
  view,
89
95
  signal,
90
- resolveProps,
91
- templateKey,
92
- baseStylesDictionary,
93
- type,
94
- renderer,
96
+ renderState,
95
97
  buildContext,
96
98
  attachContent,
99
+ afterSettingsResolve,
97
100
  }: RenderTwigTemplateOptions< TView > ): Promise< void > {
98
101
  view.triggerMethod( 'before:render:template' );
102
+ const { resolveProps, cacheState, renderer, templateKey } = renderState;
99
103
 
100
- if ( signal.aborted ) {
104
+ if ( signal?.aborted ) {
101
105
  return;
102
106
  }
103
107
 
104
108
  const settings = view.model.get( 'settings' ).toJSON();
105
- const resolvedSettings = await resolveProps( {
109
+ let resolvedSettings = await resolveProps( {
106
110
  props: settings,
107
111
  signal,
108
112
  renderContext: view.getResolverRenderContext?.(),
109
113
  } );
110
114
 
111
- if ( signal.aborted ) {
115
+ if ( signal?.aborted ) {
112
116
  return;
113
117
  }
114
118
 
115
- let context: TwigRenderContext = {
116
- id: view.model.get( 'id' ),
117
- type,
118
- settings: resolvedSettings,
119
- base_styles: baseStylesDictionary,
120
- };
119
+ if ( afterSettingsResolve ) {
120
+ resolvedSettings = afterSettingsResolve( resolvedSettings );
121
+ }
121
122
 
122
- if ( buildContext ) {
123
- context = buildContext( context );
123
+ const settingsHash = JSON.stringify( resolvedSettings );
124
+ const settingsChanged = settingsHash !== cacheState.lastResolvedSettingsHash;
125
+
126
+ if ( ! settingsChanged && view.isRendered ) {
127
+ cacheState.domUpdateWasSkipped = true;
128
+ view.bindUIElements();
129
+ view.triggerMethod( 'render:template' );
130
+ return;
124
131
  }
125
132
 
133
+ cacheState.domUpdateWasSkipped = false;
134
+ cacheState.lastResolvedSettingsHash = settingsHash;
135
+
136
+ const context = buildContext( resolvedSettings );
126
137
  const html = await renderer.render( templateKey, context );
127
138
 
128
- if ( signal.aborted ) {
139
+ if ( signal?.aborted ) {
129
140
  return;
130
141
  }
131
142
 
@@ -134,3 +145,51 @@ export async function renderTwigTemplate< TView extends TwigViewInterface >( {
134
145
  view.bindUIElements();
135
146
  view.triggerMethod( 'render:template' );
136
147
  }
148
+
149
+ export type ChildrenCollection = ElementView[ 'children' ];
150
+
151
+ export function collectChildrenRenderPromises( children: ChildrenCollection | undefined ): Promise< void >[] {
152
+ const promises: Promise< void >[] = [];
153
+
154
+ children?.each( ( childView: ElementView ) => {
155
+ if ( childView._currentRenderPromise ) {
156
+ promises.push( childView._currentRenderPromise );
157
+ }
158
+ } );
159
+
160
+ return promises;
161
+ }
162
+
163
+ type RenderChildrenOptions = {
164
+ children: ChildrenCollection | undefined;
165
+ domUpdateWasSkipped: boolean;
166
+ renderChildren: () => void;
167
+ };
168
+
169
+ export async function renderChildrenWithOptimization( {
170
+ children,
171
+ domUpdateWasSkipped,
172
+ renderChildren,
173
+ }: RenderChildrenOptions ): Promise< void > {
174
+ // Safe side: when children is empty, fall back to original renderChildren function to handle emptyView.
175
+ const shouldReuseChildren = domUpdateWasSkipped && !! children?.length;
176
+
177
+ if ( shouldReuseChildren ) {
178
+ rerenderExistingChildViews( children );
179
+ } else {
180
+ renderChildren();
181
+ }
182
+
183
+ const promises = collectChildrenRenderPromises( children );
184
+ await waitForChildrenToComplete( promises );
185
+ }
186
+
187
+ function rerenderExistingChildViews( children: ChildrenCollection | undefined ) {
188
+ children?.each( ( childView ) => childView.render() );
189
+ }
190
+
191
+ async function waitForChildrenToComplete( promises: Promise< void >[] ): Promise< void > {
192
+ if ( promises.length > 0 ) {
193
+ await Promise.all( promises );
194
+ }
195
+ }
@@ -74,6 +74,8 @@ export declare class ElementView {
74
74
 
75
75
  model: BackboneModel< ElementModel >;
76
76
 
77
+ _abortController: AbortController | null;
78
+
77
79
  collection: BackboneCollection< ElementModel >;
78
80
 
79
81
  children: {
@@ -35,5 +35,8 @@ export const outputSchema = {
35
35
  'The built XML structure as a string. Must use this XML after completion of building the composition, it contains real IDs.'
36
36
  )
37
37
  .optional(),
38
- llm_instructions: z.string().describe( 'Instructions what to do next, Important to follow these instructions!' ),
38
+ llm_instructions: z
39
+ .string()
40
+ .describe( 'Instructions what to do next, Important to follow these instructions!' )
41
+ .optional(),
39
42
  };
@@ -1,4 +1,3 @@
1
- /* eslint-disable testing-library/render-result-naming-convention */
2
1
  import { createDomRenderer } from '../create-dom-renderer';
3
2
 
4
3
  describe( 'createDomRenderer', () => {
@@ -1,4 +1,3 @@
1
- /* eslint-disable testing-library/render-result-naming-convention */
2
1
  import type { BreakpointsMap } from '@elementor/editor-responsive';
3
2
  import { encodeString } from '@elementor/utils';
4
3
 
@@ -0,0 +1,10 @@
1
+ import { createTransformer } from '../create-transformer';
2
+
3
+ type HtmlV2Value = {
4
+ content: string | null;
5
+ children: Record< string, unknown >;
6
+ };
7
+
8
+ export const htmlV2Transformer = createTransformer( ( value: HtmlV2Value ) => {
9
+ return value?.content ?? '';
10
+ } );