@elementor/editor-canvas 3.35.0-434 → 3.35.0-436

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.d.mts CHANGED
@@ -100,6 +100,7 @@ declare class ElementView {
100
100
  getRenderContext(): NamespacedRenderContext | undefined;
101
101
  getResolverRenderContext(): RenderContext | undefined;
102
102
  getNamespaceKey(): string;
103
+ invalidateRenderCache(): void;
103
104
  }
104
105
  type JQueryElement = {
105
106
  find: (selector: string) => JQueryElement;
package/dist/index.d.ts CHANGED
@@ -100,6 +100,7 @@ declare class ElementView {
100
100
  getRenderContext(): NamespacedRenderContext | undefined;
101
101
  getResolverRenderContext(): RenderContext | undefined;
102
102
  getNamespaceKey(): string;
103
+ invalidateRenderCache(): void;
103
104
  }
104
105
  type JQueryElement = {
105
106
  find: (selector: string) => JQueryElement;
package/dist/index.js CHANGED
@@ -1605,6 +1605,8 @@ function createTemplatedElementView({
1605
1605
  return class extends BaseView {
1606
1606
  #abortController = null;
1607
1607
  #childrenRenderPromises = [];
1608
+ #lastResolvedSettingsHash = null;
1609
+ #domUpdateWasSkipped = false;
1608
1610
  getTemplateType() {
1609
1611
  return "twig";
1610
1612
  }
@@ -1620,8 +1622,9 @@ function createTemplatedElementView({
1620
1622
  getResolverRenderContext() {
1621
1623
  return this._parent?.getResolverRenderContext?.();
1622
1624
  }
1623
- // Override `render` function to support async `_renderTemplate`
1624
- // Note that `_renderChildren` asynchronity is still NOT supported, so only the parent element rendering can be async
1625
+ invalidateRenderCache() {
1626
+ this.#lastResolvedSettingsHash = null;
1627
+ }
1625
1628
  render() {
1626
1629
  this.#abortController?.abort();
1627
1630
  this.#abortController = new AbortController();
@@ -1630,21 +1633,35 @@ function createTemplatedElementView({
1630
1633
  return this._currentRenderPromise;
1631
1634
  }
1632
1635
  async _renderChildren() {
1633
- super._renderChildren();
1634
1636
  this.#childrenRenderPromises = [];
1637
+ if (this.#shouldReuseChildren()) {
1638
+ this.#rerenderExistingChildren();
1639
+ } else {
1640
+ super._renderChildren();
1641
+ }
1642
+ this.#collectChildrenRenderPromises();
1643
+ await this._waitForChildrenToComplete();
1644
+ }
1645
+ #shouldReuseChildren() {
1646
+ return this.#domUpdateWasSkipped && this.children?.length > 0;
1647
+ }
1648
+ #rerenderExistingChildren() {
1649
+ this.children?.each((childView) => {
1650
+ childView.render();
1651
+ });
1652
+ }
1653
+ #collectChildrenRenderPromises() {
1635
1654
  this.children?.each((childView) => {
1636
1655
  if (childView._currentRenderPromise) {
1637
1656
  this.#childrenRenderPromises.push(childView._currentRenderPromise);
1638
1657
  }
1639
1658
  });
1640
- await this._waitForChildrenToComplete();
1641
1659
  }
1642
1660
  async _waitForChildrenToComplete() {
1643
1661
  if (this.#childrenRenderPromises.length > 0) {
1644
1662
  await Promise.all(this.#childrenRenderPromises);
1645
1663
  }
1646
1664
  }
1647
- // Overriding Marionette original `_renderTemplate` method to inject our renderer.
1648
1665
  async _renderTemplate() {
1649
1666
  this.triggerMethod("before:render:template");
1650
1667
  const process = signalizedProcess(this.#abortController?.signal).then((_, signal) => {
@@ -1657,6 +1674,14 @@ function createTemplatedElementView({
1657
1674
  }).then((settings) => {
1658
1675
  return this.afterSettingsResolve(settings);
1659
1676
  }).then(async (settings) => {
1677
+ const settingsHash = JSON.stringify(settings);
1678
+ const settingsChanged = settingsHash !== this.#lastResolvedSettingsHash;
1679
+ if (!settingsChanged && this.isRendered) {
1680
+ this.#domUpdateWasSkipped = true;
1681
+ return null;
1682
+ }
1683
+ this.#domUpdateWasSkipped = false;
1684
+ this.#lastResolvedSettingsHash = settingsHash;
1660
1685
  const context = {
1661
1686
  id: this.model.get("id"),
1662
1687
  type,
@@ -1664,7 +1689,12 @@ function createTemplatedElementView({
1664
1689
  base_styles: baseStylesDictionary
1665
1690
  };
1666
1691
  return renderer.render(templateKey, context);
1667
- }).then((html) => this.$el.html(html));
1692
+ }).then((html) => {
1693
+ if (html === null) {
1694
+ return;
1695
+ }
1696
+ this.$el.html(html);
1697
+ });
1668
1698
  await process.execute();
1669
1699
  this.bindUIElements();
1670
1700
  this.triggerMethod("render:template");
@@ -1980,10 +2010,11 @@ var createViewWithReplacements = (options) => {
1980
2010
  element: this.el,
1981
2011
  type: this?.model?.get("widgetType") ?? this.container?.model?.get("elType") ?? null,
1982
2012
  id: this?.model?.get("id") ?? null,
1983
- refreshView: this.render.bind(this)
2013
+ refreshView: this.refreshView.bind(this)
1984
2014
  };
1985
2015
  }
1986
2016
  refreshView() {
2017
+ this.invalidateRenderCache?.();
1987
2018
  this.render();
1988
2019
  }
1989
2020
  renderOnChange() {
package/dist/index.mjs CHANGED
@@ -1564,6 +1564,8 @@ function createTemplatedElementView({
1564
1564
  return class extends BaseView {
1565
1565
  #abortController = null;
1566
1566
  #childrenRenderPromises = [];
1567
+ #lastResolvedSettingsHash = null;
1568
+ #domUpdateWasSkipped = false;
1567
1569
  getTemplateType() {
1568
1570
  return "twig";
1569
1571
  }
@@ -1579,8 +1581,9 @@ function createTemplatedElementView({
1579
1581
  getResolverRenderContext() {
1580
1582
  return this._parent?.getResolverRenderContext?.();
1581
1583
  }
1582
- // Override `render` function to support async `_renderTemplate`
1583
- // Note that `_renderChildren` asynchronity is still NOT supported, so only the parent element rendering can be async
1584
+ invalidateRenderCache() {
1585
+ this.#lastResolvedSettingsHash = null;
1586
+ }
1584
1587
  render() {
1585
1588
  this.#abortController?.abort();
1586
1589
  this.#abortController = new AbortController();
@@ -1589,21 +1592,35 @@ function createTemplatedElementView({
1589
1592
  return this._currentRenderPromise;
1590
1593
  }
1591
1594
  async _renderChildren() {
1592
- super._renderChildren();
1593
1595
  this.#childrenRenderPromises = [];
1596
+ if (this.#shouldReuseChildren()) {
1597
+ this.#rerenderExistingChildren();
1598
+ } else {
1599
+ super._renderChildren();
1600
+ }
1601
+ this.#collectChildrenRenderPromises();
1602
+ await this._waitForChildrenToComplete();
1603
+ }
1604
+ #shouldReuseChildren() {
1605
+ return this.#domUpdateWasSkipped && this.children?.length > 0;
1606
+ }
1607
+ #rerenderExistingChildren() {
1608
+ this.children?.each((childView) => {
1609
+ childView.render();
1610
+ });
1611
+ }
1612
+ #collectChildrenRenderPromises() {
1594
1613
  this.children?.each((childView) => {
1595
1614
  if (childView._currentRenderPromise) {
1596
1615
  this.#childrenRenderPromises.push(childView._currentRenderPromise);
1597
1616
  }
1598
1617
  });
1599
- await this._waitForChildrenToComplete();
1600
1618
  }
1601
1619
  async _waitForChildrenToComplete() {
1602
1620
  if (this.#childrenRenderPromises.length > 0) {
1603
1621
  await Promise.all(this.#childrenRenderPromises);
1604
1622
  }
1605
1623
  }
1606
- // Overriding Marionette original `_renderTemplate` method to inject our renderer.
1607
1624
  async _renderTemplate() {
1608
1625
  this.triggerMethod("before:render:template");
1609
1626
  const process = signalizedProcess(this.#abortController?.signal).then((_, signal) => {
@@ -1616,6 +1633,14 @@ function createTemplatedElementView({
1616
1633
  }).then((settings) => {
1617
1634
  return this.afterSettingsResolve(settings);
1618
1635
  }).then(async (settings) => {
1636
+ const settingsHash = JSON.stringify(settings);
1637
+ const settingsChanged = settingsHash !== this.#lastResolvedSettingsHash;
1638
+ if (!settingsChanged && this.isRendered) {
1639
+ this.#domUpdateWasSkipped = true;
1640
+ return null;
1641
+ }
1642
+ this.#domUpdateWasSkipped = false;
1643
+ this.#lastResolvedSettingsHash = settingsHash;
1619
1644
  const context = {
1620
1645
  id: this.model.get("id"),
1621
1646
  type,
@@ -1623,7 +1648,12 @@ function createTemplatedElementView({
1623
1648
  base_styles: baseStylesDictionary
1624
1649
  };
1625
1650
  return renderer.render(templateKey, context);
1626
- }).then((html) => this.$el.html(html));
1651
+ }).then((html) => {
1652
+ if (html === null) {
1653
+ return;
1654
+ }
1655
+ this.$el.html(html);
1656
+ });
1627
1657
  await process.execute();
1628
1658
  this.bindUIElements();
1629
1659
  this.triggerMethod("render:template");
@@ -1942,10 +1972,11 @@ var createViewWithReplacements = (options) => {
1942
1972
  element: this.el,
1943
1973
  type: this?.model?.get("widgetType") ?? this.container?.model?.get("elType") ?? null,
1944
1974
  id: this?.model?.get("id") ?? null,
1945
- refreshView: this.render.bind(this)
1975
+ refreshView: this.refreshView.bind(this)
1946
1976
  };
1947
1977
  }
1948
1978
  refreshView() {
1979
+ this.invalidateRenderCache?.();
1949
1980
  this.render();
1950
1981
  }
1951
1982
  renderOnChange() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "3.35.0-434",
4
+ "version": "3.35.0-436",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,24 +37,24 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "3.35.0-434",
41
- "@elementor/editor-controls": "3.35.0-434",
42
- "@elementor/editor-documents": "3.35.0-434",
43
- "@elementor/editor-elements": "3.35.0-434",
44
- "@elementor/editor-interactions": "3.35.0-434",
45
- "@elementor/editor-mcp": "3.35.0-434",
46
- "@elementor/editor-notifications": "3.35.0-434",
47
- "@elementor/editor-props": "3.35.0-434",
48
- "@elementor/editor-responsive": "3.35.0-434",
49
- "@elementor/editor-styles": "3.35.0-434",
50
- "@elementor/editor-styles-repository": "3.35.0-434",
51
- "@elementor/editor-ui": "3.35.0-434",
52
- "@elementor/editor-v1-adapters": "3.35.0-434",
53
- "@elementor/schema": "3.35.0-434",
54
- "@elementor/twing": "3.35.0-434",
40
+ "@elementor/editor": "3.35.0-436",
41
+ "@elementor/editor-controls": "3.35.0-436",
42
+ "@elementor/editor-documents": "3.35.0-436",
43
+ "@elementor/editor-elements": "3.35.0-436",
44
+ "@elementor/editor-interactions": "3.35.0-436",
45
+ "@elementor/editor-mcp": "3.35.0-436",
46
+ "@elementor/editor-notifications": "3.35.0-436",
47
+ "@elementor/editor-props": "3.35.0-436",
48
+ "@elementor/editor-responsive": "3.35.0-436",
49
+ "@elementor/editor-styles": "3.35.0-436",
50
+ "@elementor/editor-styles-repository": "3.35.0-436",
51
+ "@elementor/editor-ui": "3.35.0-436",
52
+ "@elementor/editor-v1-adapters": "3.35.0-436",
53
+ "@elementor/schema": "3.35.0-436",
54
+ "@elementor/twing": "3.35.0-436",
55
55
  "@elementor/ui": "1.36.17",
56
- "@elementor/utils": "3.35.0-434",
57
- "@elementor/wp-media": "3.35.0-434",
56
+ "@elementor/utils": "3.35.0-436",
57
+ "@elementor/wp-media": "3.35.0-436",
58
58
  "@floating-ui/react": "^0.27.5",
59
59
  "@wordpress/i18n": "^5.13.0"
60
60
  },
@@ -1,12 +1,17 @@
1
1
  import { mockLegacyElementor } from 'test-utils';
2
2
 
3
- import { createTemplatedElementType } from '../create-templated-element-type';
3
+ import {
4
+ canBeTemplated,
5
+ createTemplatedElementType,
6
+ createTemplatedElementView,
7
+ } from '../create-templated-element-type';
4
8
 
5
9
  const MOCK_ELEMENT_TYPE = 'test-element';
10
+ const MOCK_HTML = '<div>Element</div>';
6
11
 
7
12
  const createMockRenderer = () => ( {
8
13
  register: jest.fn(),
9
- render: jest.fn( () => Promise.resolve( '<div>Element</div>' ) ),
14
+ render: jest.fn( () => Promise.resolve( MOCK_HTML ) ),
10
15
  } );
11
16
 
12
17
  const createMockElementConfig = () => ( {
@@ -54,3 +59,134 @@ describe( 'createTemplatedElementType', () => {
54
59
  expect( viewClass1 ).toBe( viewClass2 );
55
60
  } );
56
61
  } );
62
+
63
+ describe( 'createTemplatedElementView', () => {
64
+ beforeEach( () => {
65
+ mockLegacyElementor();
66
+ } );
67
+
68
+ describe( 'class structure', () => {
69
+ it( 'should return twig as template type', () => {
70
+ // Arrange
71
+ const ViewClass = createTemplatedElementView( {
72
+ type: MOCK_ELEMENT_TYPE,
73
+ renderer: createMockRenderer(),
74
+ element: createMockElementConfig(),
75
+ } );
76
+
77
+ // Assert
78
+ expect( ViewClass.prototype.getTemplateType() ).toBe( 'twig' );
79
+ } );
80
+
81
+ it( 'should return element type as namespace key', () => {
82
+ // Arrange
83
+ const ViewClass = createTemplatedElementView( {
84
+ type: MOCK_ELEMENT_TYPE,
85
+ renderer: createMockRenderer(),
86
+ element: createMockElementConfig(),
87
+ } );
88
+
89
+ // Assert
90
+ expect( ViewClass.prototype.getNamespaceKey() ).toBe( MOCK_ELEMENT_TYPE );
91
+ } );
92
+ } );
93
+
94
+ describe( 'template registration', () => {
95
+ it( 'should register templates with the renderer', () => {
96
+ // Arrange
97
+ const utils = createMockRenderer();
98
+ const elementConfig = {
99
+ ...createMockElementConfig(),
100
+ twig_templates: {
101
+ template1: '<div>Template 1</div>',
102
+ template2: '<div>Template 2</div>',
103
+ },
104
+ };
105
+
106
+ // Act
107
+ createTemplatedElementView( {
108
+ type: MOCK_ELEMENT_TYPE,
109
+ renderer: utils,
110
+ element: elementConfig,
111
+ } );
112
+
113
+ // Assert
114
+ expect( utils.register ).toHaveBeenCalledTimes( 2 );
115
+ expect( utils.register ).toHaveBeenCalledWith( 'template1', '<div>Template 1</div>' );
116
+ expect( utils.register ).toHaveBeenCalledWith( 'template2', '<div>Template 2</div>' );
117
+ } );
118
+ } );
119
+ } );
120
+
121
+ describe( 'canBeTemplated', () => {
122
+ it( 'should return true when all required properties are present', () => {
123
+ // Arrange
124
+ const element = createMockElementConfig();
125
+
126
+ // Act
127
+ const result = canBeTemplated( element );
128
+
129
+ // Assert
130
+ expect( result ).toBe( true );
131
+ } );
132
+
133
+ it( 'should return false when atomic_props_schema is missing', () => {
134
+ // Arrange
135
+ const element = {
136
+ twig_templates: {},
137
+ twig_main_template: 'main',
138
+ base_styles_dictionary: {},
139
+ };
140
+
141
+ // Act
142
+ const result = canBeTemplated( element );
143
+
144
+ // Assert
145
+ expect( result ).toBe( false );
146
+ } );
147
+
148
+ it( 'should return false when twig_templates is missing', () => {
149
+ // Arrange
150
+ const element = {
151
+ twig_main_template: 'main',
152
+ atomic_props_schema: {},
153
+ base_styles_dictionary: {},
154
+ };
155
+
156
+ // Act
157
+ const result = canBeTemplated( element );
158
+
159
+ // Assert
160
+ expect( result ).toBe( false );
161
+ } );
162
+
163
+ it( 'should return false when twig_main_template is missing', () => {
164
+ // Arrange
165
+ const element = {
166
+ twig_templates: {},
167
+ atomic_props_schema: {},
168
+ base_styles_dictionary: {},
169
+ };
170
+
171
+ // Act
172
+ const result = canBeTemplated( element );
173
+
174
+ // Assert
175
+ expect( result ).toBe( false );
176
+ } );
177
+
178
+ it( 'should return false when base_styles_dictionary is missing', () => {
179
+ // Arrange
180
+ const element = {
181
+ twig_templates: {},
182
+ twig_main_template: 'main',
183
+ atomic_props_schema: {},
184
+ };
185
+
186
+ // Act
187
+ const result = canBeTemplated( element );
188
+
189
+ // Assert
190
+ expect( result ).toBe( false );
191
+ } );
192
+ } );
@@ -79,6 +79,8 @@ export function createTemplatedElementView( {
79
79
  return class extends BaseView {
80
80
  #abortController: AbortController | null = null;
81
81
  #childrenRenderPromises: Promise< void >[] = [];
82
+ #lastResolvedSettingsHash: string | null = null;
83
+ #domUpdateWasSkipped = false;
82
84
 
83
85
  getTemplateType() {
84
86
  return 'twig';
@@ -100,8 +102,10 @@ export function createTemplatedElementView( {
100
102
  return this._parent?.getResolverRenderContext?.();
101
103
  }
102
104
 
103
- // Override `render` function to support async `_renderTemplate`
104
- // Note that `_renderChildren` asynchronity is still NOT supported, so only the parent element rendering can be async
105
+ invalidateRenderCache() {
106
+ this.#lastResolvedSettingsHash = null;
107
+ }
108
+
105
109
  render() {
106
110
  this.#abortController?.abort();
107
111
  this.#abortController = new AbortController();
@@ -118,17 +122,35 @@ export function createTemplatedElementView( {
118
122
  }
119
123
 
120
124
  async _renderChildren() {
121
- super._renderChildren();
122
-
123
125
  this.#childrenRenderPromises = [];
124
126
 
127
+ // Optimize rendering by reusing existing child views instead of recreating them.
128
+ if ( this.#shouldReuseChildren() ) {
129
+ this.#rerenderExistingChildren();
130
+ } else {
131
+ super._renderChildren();
132
+ }
133
+
134
+ this.#collectChildrenRenderPromises();
135
+ await this._waitForChildrenToComplete();
136
+ }
137
+
138
+ #shouldReuseChildren() {
139
+ return this.#domUpdateWasSkipped && this.children?.length > 0;
140
+ }
141
+
142
+ #rerenderExistingChildren() {
143
+ this.children?.each( ( childView: ElementView ) => {
144
+ childView.render();
145
+ } );
146
+ }
147
+
148
+ #collectChildrenRenderPromises() {
125
149
  this.children?.each( ( childView: ElementView ) => {
126
150
  if ( childView._currentRenderPromise ) {
127
151
  this.#childrenRenderPromises.push( childView._currentRenderPromise );
128
152
  }
129
153
  } );
130
-
131
- await this._waitForChildrenToComplete();
132
154
  }
133
155
 
134
156
  async _waitForChildrenToComplete() {
@@ -137,14 +159,12 @@ export function createTemplatedElementView( {
137
159
  }
138
160
  }
139
161
 
140
- // Overriding Marionette original `_renderTemplate` method to inject our renderer.
141
162
  async _renderTemplate() {
142
163
  this.triggerMethod( 'before:render:template' );
143
164
 
144
165
  const process = signalizedProcess( this.#abortController?.signal as AbortSignal )
145
166
  .then( ( _, signal ) => {
146
167
  const settings = this.model.get( 'settings' ).toJSON();
147
-
148
168
  return resolveProps( {
149
169
  props: settings,
150
170
  signal,
@@ -155,7 +175,17 @@ export function createTemplatedElementView( {
155
175
  return this.afterSettingsResolve( settings );
156
176
  } )
157
177
  .then( async ( settings ) => {
158
- // Same as the Backend.
178
+ const settingsHash = JSON.stringify( settings );
179
+ const settingsChanged = settingsHash !== this.#lastResolvedSettingsHash;
180
+
181
+ if ( ! settingsChanged && this.isRendered ) {
182
+ this.#domUpdateWasSkipped = true;
183
+ return null;
184
+ }
185
+ this.#domUpdateWasSkipped = false;
186
+
187
+ this.#lastResolvedSettingsHash = settingsHash;
188
+
159
189
  const context = {
160
190
  id: this.model.get( 'id' ),
161
191
  type,
@@ -165,7 +195,13 @@ export function createTemplatedElementView( {
165
195
 
166
196
  return renderer.render( templateKey, context );
167
197
  } )
168
- .then( ( html ) => this.$el.html( html ) );
198
+ .then( ( html ) => {
199
+ if ( html === null ) {
200
+ return;
201
+ }
202
+
203
+ this.$el.html( html );
204
+ } );
169
205
 
170
206
  await process.execute();
171
207
 
@@ -45,11 +45,12 @@ export const createViewWithReplacements = ( options: CreateTemplatedElementTypeO
45
45
  element: this.el,
46
46
  type: this?.model?.get( 'widgetType' ) ?? this.container?.model?.get( 'elType' ) ?? null,
47
47
  id: this?.model?.get( 'id' ) ?? null,
48
- refreshView: this.render.bind( this ),
48
+ refreshView: this.refreshView.bind( this ),
49
49
  };
50
50
  }
51
51
 
52
52
  refreshView() {
53
+ this.invalidateRenderCache?.();
53
54
  this.render();
54
55
  }
55
56
 
@@ -121,6 +121,8 @@ export declare class ElementView {
121
121
  getResolverRenderContext(): RenderContext | undefined;
122
122
 
123
123
  getNamespaceKey(): string;
124
+
125
+ invalidateRenderCache(): void;
124
126
  }
125
127
 
126
128
  type JQueryElement = {