@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.
package/dist/index.mjs CHANGED
@@ -1204,6 +1204,11 @@ var dateTimeTransformer = createTransformer((values) => {
1204
1204
  }).join(" ");
1205
1205
  });
1206
1206
 
1207
+ // src/transformers/settings/html-v2-transformer.ts
1208
+ var htmlV2Transformer = createTransformer((value) => {
1209
+ return value?.content ?? "";
1210
+ });
1211
+
1207
1212
  // src/transformers/settings/link-transformer.ts
1208
1213
  var linkTransformer = createTransformer(({ destination, isTargetBlank, tag }) => {
1209
1214
  return {
@@ -1258,7 +1263,7 @@ var plainTransformer = createTransformer((value) => {
1258
1263
 
1259
1264
  // src/init-settings-transformers.ts
1260
1265
  function initSettingsTransformers() {
1261
- settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).registerFallback(plainTransformer);
1266
+ settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).registerFallback(plainTransformer);
1262
1267
  }
1263
1268
 
1264
1269
  // src/transformers/styles/background-color-overlay-transformer.ts
@@ -1723,9 +1728,9 @@ function createElementViewClassDeclaration() {
1723
1728
  import { ELEMENT_STYLE_CHANGE_EVENT } from "@elementor/editor-elements";
1724
1729
 
1725
1730
  // src/legacy/twig-rendering-utils.ts
1726
- function setupTwigRenderer({ renderer, element }) {
1731
+ function createTwigRenderState({ renderer, element }) {
1727
1732
  const templateKey = element.twig_main_template;
1728
- const baseStylesDictionary = element.base_styles_dictionary;
1733
+ const cacheState = createRenderCacheState();
1729
1734
  Object.entries(element.twig_templates).forEach(([key, template]) => {
1730
1735
  renderer.register(key, template);
1731
1736
  });
@@ -1733,7 +1738,7 @@ function setupTwigRenderer({ renderer, element }) {
1733
1738
  transformers: settingsTransformersRegistry,
1734
1739
  schema: element.atomic_props_schema
1735
1740
  });
1736
- return { templateKey, baseStylesDictionary, resolveProps };
1741
+ return { templateKey, resolveProps, renderer, cacheState };
1737
1742
  }
1738
1743
  function createBeforeRender(view) {
1739
1744
  view._ensureViewIsIntact();
@@ -1746,47 +1751,91 @@ function createAfterRender(view) {
1746
1751
  view.isRendered = true;
1747
1752
  view.triggerMethod("render", view);
1748
1753
  }
1754
+ function createRenderCacheState() {
1755
+ return {
1756
+ lastResolvedSettingsHash: null,
1757
+ domUpdateWasSkipped: false,
1758
+ invalidate() {
1759
+ this.lastResolvedSettingsHash = null;
1760
+ this.domUpdateWasSkipped = false;
1761
+ }
1762
+ };
1763
+ }
1749
1764
  async function renderTwigTemplate({
1750
1765
  view,
1751
1766
  signal,
1752
- resolveProps,
1753
- templateKey,
1754
- baseStylesDictionary,
1755
- type,
1756
- renderer,
1767
+ renderState,
1757
1768
  buildContext,
1758
- attachContent
1769
+ attachContent,
1770
+ afterSettingsResolve
1759
1771
  }) {
1760
1772
  view.triggerMethod("before:render:template");
1761
- if (signal.aborted) {
1773
+ const { resolveProps, cacheState, renderer, templateKey } = renderState;
1774
+ if (signal?.aborted) {
1762
1775
  return;
1763
1776
  }
1764
1777
  const settings = view.model.get("settings").toJSON();
1765
- const resolvedSettings = await resolveProps({
1778
+ let resolvedSettings = await resolveProps({
1766
1779
  props: settings,
1767
1780
  signal,
1768
1781
  renderContext: view.getResolverRenderContext?.()
1769
1782
  });
1770
- if (signal.aborted) {
1783
+ if (signal?.aborted) {
1771
1784
  return;
1772
1785
  }
1773
- let context = {
1774
- id: view.model.get("id"),
1775
- type,
1776
- settings: resolvedSettings,
1777
- base_styles: baseStylesDictionary
1778
- };
1779
- if (buildContext) {
1780
- context = buildContext(context);
1786
+ if (afterSettingsResolve) {
1787
+ resolvedSettings = afterSettingsResolve(resolvedSettings);
1788
+ }
1789
+ const settingsHash = JSON.stringify(resolvedSettings);
1790
+ const settingsChanged = settingsHash !== cacheState.lastResolvedSettingsHash;
1791
+ if (!settingsChanged && view.isRendered) {
1792
+ cacheState.domUpdateWasSkipped = true;
1793
+ view.bindUIElements();
1794
+ view.triggerMethod("render:template");
1795
+ return;
1781
1796
  }
1797
+ cacheState.domUpdateWasSkipped = false;
1798
+ cacheState.lastResolvedSettingsHash = settingsHash;
1799
+ const context = buildContext(resolvedSettings);
1782
1800
  const html = await renderer.render(templateKey, context);
1783
- if (signal.aborted) {
1801
+ if (signal?.aborted) {
1784
1802
  return;
1785
1803
  }
1786
1804
  attachContent(html);
1787
1805
  view.bindUIElements();
1788
1806
  view.triggerMethod("render:template");
1789
1807
  }
1808
+ function collectChildrenRenderPromises(children) {
1809
+ const promises = [];
1810
+ children?.each((childView) => {
1811
+ if (childView._currentRenderPromise) {
1812
+ promises.push(childView._currentRenderPromise);
1813
+ }
1814
+ });
1815
+ return promises;
1816
+ }
1817
+ async function renderChildrenWithOptimization({
1818
+ children,
1819
+ domUpdateWasSkipped,
1820
+ renderChildren
1821
+ }) {
1822
+ const shouldReuseChildren = domUpdateWasSkipped && !!children?.length;
1823
+ if (shouldReuseChildren) {
1824
+ rerenderExistingChildViews(children);
1825
+ } else {
1826
+ renderChildren();
1827
+ }
1828
+ const promises = collectChildrenRenderPromises(children);
1829
+ await waitForChildrenToComplete(promises);
1830
+ }
1831
+ function rerenderExistingChildViews(children) {
1832
+ children?.each((childView) => childView.render());
1833
+ }
1834
+ async function waitForChildrenToComplete(promises) {
1835
+ if (promises.length > 0) {
1836
+ await Promise.all(promises);
1837
+ }
1838
+ }
1790
1839
 
1791
1840
  // src/legacy/create-templated-element-type.ts
1792
1841
  function canBeTemplated(element) {
@@ -1798,16 +1847,9 @@ function createTemplatedElementView({
1798
1847
  element
1799
1848
  }) {
1800
1849
  const BaseView = createElementViewClassDeclaration();
1801
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer({
1802
- type,
1803
- renderer,
1804
- element
1805
- });
1850
+ const renderState = createTwigRenderState({ renderer, element });
1806
1851
  return class extends BaseView {
1807
- #abortController = null;
1808
- #childrenRenderPromises = [];
1809
- #lastResolvedSettingsHash = null;
1810
- #domUpdateWasSkipped = false;
1852
+ _abortController = null;
1811
1853
  getTemplateType() {
1812
1854
  return "twig";
1813
1855
  }
@@ -1824,81 +1866,36 @@ function createTemplatedElementView({
1824
1866
  return this._parent?.getResolverRenderContext?.();
1825
1867
  }
1826
1868
  invalidateRenderCache() {
1827
- this.#lastResolvedSettingsHash = null;
1869
+ renderState.cacheState.invalidate();
1828
1870
  }
1829
1871
  render() {
1830
- this.#abortController?.abort();
1831
- this.#abortController = new AbortController();
1832
- const process = signalizedProcess(this.#abortController.signal).then(() => this._beforeRender()).then(() => this._renderTemplate()).then(() => this._renderChildren()).then(() => this._afterRender());
1872
+ this._abortController?.abort();
1873
+ this._abortController = new AbortController();
1874
+ const process = signalizedProcess(this._abortController.signal).then(() => this._beforeRender()).then(() => this._renderTemplate()).then(() => this._renderChildren()).then(() => this._afterRender());
1833
1875
  this._currentRenderPromise = process.execute();
1834
1876
  return this._currentRenderPromise;
1835
1877
  }
1836
1878
  async _renderChildren() {
1837
- this.#childrenRenderPromises = [];
1838
- if (this.#shouldReuseChildren()) {
1839
- this.#rerenderExistingChildren();
1840
- } else {
1841
- super._renderChildren();
1842
- }
1843
- this.#collectChildrenRenderPromises();
1844
- await this._waitForChildrenToComplete();
1845
- }
1846
- #shouldReuseChildren() {
1847
- return this.#domUpdateWasSkipped && this.children?.length > 0;
1848
- }
1849
- #rerenderExistingChildren() {
1850
- this.children?.each((childView) => {
1851
- childView.render();
1879
+ await renderChildrenWithOptimization({
1880
+ children: this.children,
1881
+ domUpdateWasSkipped: renderState.cacheState.domUpdateWasSkipped,
1882
+ renderChildren: () => super._renderChildren()
1852
1883
  });
1853
1884
  }
1854
- #collectChildrenRenderPromises() {
1855
- this.children?.each((childView) => {
1856
- if (childView._currentRenderPromise) {
1857
- this.#childrenRenderPromises.push(childView._currentRenderPromise);
1858
- }
1859
- });
1860
- }
1861
- async _waitForChildrenToComplete() {
1862
- if (this.#childrenRenderPromises.length > 0) {
1863
- await Promise.all(this.#childrenRenderPromises);
1864
- }
1865
- }
1866
1885
  async _renderTemplate() {
1867
- this.triggerMethod("before:render:template");
1868
- const process = signalizedProcess(this.#abortController?.signal).then((_, signal) => {
1869
- const settings = this.model.get("settings").toJSON();
1870
- return resolveProps({
1871
- props: settings,
1872
- signal,
1873
- renderContext: this.getResolverRenderContext()
1874
- });
1875
- }).then((settings) => {
1876
- return this.afterSettingsResolve(settings);
1877
- }).then(async (settings) => {
1878
- const settingsHash = JSON.stringify(settings);
1879
- const settingsChanged = settingsHash !== this.#lastResolvedSettingsHash;
1880
- if (!settingsChanged && this.isRendered) {
1881
- this.#domUpdateWasSkipped = true;
1882
- return null;
1883
- }
1884
- this.#domUpdateWasSkipped = false;
1885
- this.#lastResolvedSettingsHash = settingsHash;
1886
- const context = {
1886
+ await renderTwigTemplate({
1887
+ view: this,
1888
+ signal: this._abortController?.signal,
1889
+ renderState,
1890
+ buildContext: (resolvedSettings) => ({
1887
1891
  id: this.model.get("id"),
1888
1892
  type,
1889
- settings,
1890
- base_styles: baseStylesDictionary
1891
- };
1892
- return renderer.render(templateKey, context);
1893
- }).then((html) => {
1894
- if (html === null) {
1895
- return;
1896
- }
1897
- this.$el.html(html);
1893
+ settings: resolvedSettings,
1894
+ base_styles: element.base_styles_dictionary
1895
+ }),
1896
+ attachContent: (html) => this.$el.html(html),
1897
+ afterSettingsResolve: (settings) => this.afterSettingsResolve(settings)
1898
1898
  });
1899
- await process.execute();
1900
- this.bindUIElements();
1901
- this.triggerMethod("render:template");
1902
1899
  }
1903
1900
  afterSettingsResolve(settings) {
1904
1901
  return settings;
@@ -1969,11 +1966,7 @@ function createNestedTemplatedElementView({
1969
1966
  element
1970
1967
  }) {
1971
1968
  const legacyWindow = window;
1972
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer({
1973
- type,
1974
- renderer,
1975
- element
1976
- });
1969
+ const renderState = createTwigRenderState({ renderer, element });
1977
1970
  const AtomicElementBaseView = legacyWindow.elementor.modules.elements.views.createAtomicElementBase(type);
1978
1971
  const parentRenderChildren = AtomicElementBaseView.prototype._renderChildren;
1979
1972
  const parentOpenEditingPanel = AtomicElementBaseView.prototype._openEditingPanel;
@@ -1983,6 +1976,9 @@ function createNestedTemplatedElementView({
1983
1976
  getTemplateType() {
1984
1977
  return "twig";
1985
1978
  },
1979
+ invalidateRenderCache() {
1980
+ renderState.cacheState.invalidate();
1981
+ },
1986
1982
  render() {
1987
1983
  this._abortController?.abort();
1988
1984
  this._abortController = new AbortController();
@@ -2010,13 +2006,12 @@ function createNestedTemplatedElementView({
2010
2006
  await renderTwigTemplate({
2011
2007
  view: this,
2012
2008
  signal: this._abortController?.signal,
2013
- resolveProps,
2014
- templateKey,
2015
- baseStylesDictionary,
2016
- type,
2017
- renderer,
2018
- buildContext: (context) => ({
2019
- ...context,
2009
+ renderState,
2010
+ buildContext: (resolvedSettings) => ({
2011
+ id: model.get("id"),
2012
+ type,
2013
+ settings: resolvedSettings,
2014
+ base_styles: element.base_styles_dictionary,
2020
2015
  editor_attributes: buildEditorAttributes(model),
2021
2016
  editor_classes: buildEditorClasses(model)
2022
2017
  }),
@@ -2046,14 +2041,11 @@ function createNestedTemplatedElementView({
2046
2041
  oldEl.innerHTML = overlayHTML + newEl.innerHTML;
2047
2042
  },
2048
2043
  async _renderChildren() {
2049
- parentRenderChildren.call(this);
2050
- const renderPromises = [];
2051
- this.children.each((childView) => {
2052
- if (childView._currentRenderPromise) {
2053
- renderPromises.push(childView._currentRenderPromise);
2054
- }
2044
+ await renderChildrenWithOptimization({
2045
+ children: this.children,
2046
+ domUpdateWasSkipped: renderState.cacheState.domUpdateWasSkipped,
2047
+ renderChildren: () => parentRenderChildren.call(this)
2055
2048
  });
2056
- await Promise.all(renderPromises);
2057
2049
  this._removeChildrenPlaceholder();
2058
2050
  },
2059
2051
  _removeChildrenPlaceholder() {
@@ -2136,7 +2128,11 @@ function createNestedTemplatedElementView({
2136
2128
  import * as React6 from "react";
2137
2129
  import { createRoot } from "react-dom/client";
2138
2130
  import { getContainer, getElementLabel, getElementType as getElementType2 } from "@elementor/editor-elements";
2139
- import { htmlPropTypeUtil as htmlPropTypeUtil2, stringPropTypeUtil as stringPropTypeUtil2 } from "@elementor/editor-props";
2131
+ import {
2132
+ htmlV2PropTypeUtil as htmlV2PropTypeUtil2,
2133
+ parseHtmlChildren,
2134
+ stringPropTypeUtil as stringPropTypeUtil2
2135
+ } from "@elementor/editor-props";
2140
2136
  import { __privateRunCommandSync as runCommandSync, getCurrentEditMode, undoable } from "@elementor/editor-v1-adapters";
2141
2137
  import { __ as __3 } from "@wordpress/i18n";
2142
2138
 
@@ -2259,9 +2255,6 @@ var CanvasInlineEditor = ({
2259
2255
  };
2260
2256
  useOnClickOutsideIframe(onBlur);
2261
2257
  return /* @__PURE__ */ React5.createElement(ThemeProvider, null, /* @__PURE__ */ React5.createElement(InlineEditingOverlay, { expectedTag, rootElement, id }), /* @__PURE__ */ React5.createElement("style", null, `
2262
- .${EDITOR_WRAPPER_SELECTOR}, .${EDITOR_WRAPPER_SELECTOR} > * {
2263
- height: 100%;
2264
- }
2265
2258
  .ProseMirror > * {
2266
2259
  height: 100%;
2267
2260
  }
@@ -2385,12 +2378,13 @@ var useOnClickOutsideIframe = (handleUnmount) => {
2385
2378
  };
2386
2379
 
2387
2380
  // src/legacy/replacements/inline-editing/inline-editing-eligibility.ts
2388
- import { htmlPropTypeUtil, stringPropTypeUtil } from "@elementor/editor-props";
2381
+ import { htmlV2PropTypeUtil, stringPropTypeUtil } from "@elementor/editor-props";
2389
2382
  var hasKey = (propType) => {
2390
2383
  return "key" in propType;
2391
2384
  };
2385
+ var TEXT_PROP_TYPE_KEYS = /* @__PURE__ */ new Set([htmlV2PropTypeUtil.key, stringPropTypeUtil.key]);
2392
2386
  var isCoreTextPropTypeKey = (key) => {
2393
- return key === htmlPropTypeUtil.key || key === stringPropTypeUtil.key;
2387
+ return TEXT_PROP_TYPE_KEYS.has(key);
2394
2388
  };
2395
2389
  var isAllowedBySchema = (propTypeFromSchema) => {
2396
2390
  if (!propTypeFromSchema) {
@@ -2402,15 +2396,13 @@ var isAllowedBySchema = (propTypeFromSchema) => {
2402
2396
  if (propTypeFromSchema.kind !== "union") {
2403
2397
  return false;
2404
2398
  }
2405
- return Boolean(
2406
- propTypeFromSchema.prop_types[htmlPropTypeUtil.key] || propTypeFromSchema.prop_types[stringPropTypeUtil.key]
2407
- );
2399
+ return [...TEXT_PROP_TYPE_KEYS].some((key) => propTypeFromSchema.prop_types[key]);
2408
2400
  };
2409
2401
  var isInlineEditingAllowed = ({ rawValue, propTypeFromSchema }) => {
2410
2402
  if (rawValue === null || rawValue === void 0) {
2411
2403
  return isAllowedBySchema(propTypeFromSchema);
2412
2404
  }
2413
- return htmlPropTypeUtil.isValid(rawValue) || stringPropTypeUtil.isValid(rawValue);
2405
+ return htmlV2PropTypeUtil.isValid(rawValue) || stringPropTypeUtil.isValid(rawValue);
2414
2406
  };
2415
2407
 
2416
2408
  // src/legacy/replacements/inline-editing/inline-editing-elements.tsx
@@ -2495,11 +2487,16 @@ var InlineEditingReplacement = class extends ReplacementBase {
2495
2487
  }
2496
2488
  getExtractedContentValue() {
2497
2489
  const propValue = this.getInlineEditablePropValue();
2498
- return htmlPropTypeUtil2.extract(propValue) ?? "";
2490
+ return htmlV2PropTypeUtil2.extract(propValue)?.content ?? "";
2499
2491
  }
2500
2492
  setContentValue(value) {
2501
2493
  const settingKey = this.getInlineEditablePropertyName();
2502
- const valueToSave = htmlPropTypeUtil2.create(value || "");
2494
+ const html = value || "";
2495
+ const parsed = parseHtmlChildren(html);
2496
+ const valueToSave = htmlV2PropTypeUtil2.create({
2497
+ content: parsed.content || null,
2498
+ children: parsed.children
2499
+ });
2503
2500
  undoable(
2504
2501
  {
2505
2502
  do: () => {
@@ -2528,11 +2525,11 @@ var InlineEditingReplacement = class extends ReplacementBase {
2528
2525
  return null;
2529
2526
  }
2530
2527
  if (propType.kind === "union") {
2531
- if (propType.prop_types[htmlPropTypeUtil2.key]) {
2532
- return htmlPropTypeUtil2.key;
2533
- }
2534
- if (propType.prop_types[stringPropTypeUtil2.key]) {
2535
- return stringPropTypeUtil2.key;
2528
+ const textKeys = [htmlV2PropTypeUtil2.key, stringPropTypeUtil2.key];
2529
+ for (const key of textKeys) {
2530
+ if (propType.prop_types[key]) {
2531
+ return key;
2532
+ }
2536
2533
  }
2537
2534
  return null;
2538
2535
  }
@@ -2753,7 +2750,7 @@ function createNestedTemplatedType(type, renderer, element) {
2753
2750
  }
2754
2751
 
2755
2752
  // src/legacy/tabs-model-extensions.ts
2756
- import { htmlPropTypeUtil as htmlPropTypeUtil3 } from "@elementor/editor-props";
2753
+ import { htmlV2PropTypeUtil as htmlV2PropTypeUtil3 } from "@elementor/editor-props";
2757
2754
  var tabModelExtensions = {
2758
2755
  modifyDefaultChildren(elements) {
2759
2756
  if (!Array.isArray(elements) || elements.length === 0) {
@@ -2769,7 +2766,7 @@ var tabModelExtensions = {
2769
2766
  ...paragraphElement,
2770
2767
  settings: {
2771
2768
  ...paragraphElement.settings,
2772
- paragraph: htmlPropTypeUtil3.create(`Tab ${position}`)
2769
+ paragraph: htmlV2PropTypeUtil3.create({ content: `Tab ${position}`, children: [] })
2773
2770
  }
2774
2771
  };
2775
2772
  return [updatedParagraph, ...elements.slice(1)];
@@ -3443,7 +3440,7 @@ var outputSchema = {
3443
3440
  xmlStructure: z.string().describe(
3444
3441
  "The built XML structure as a string. Must use this XML after completion of building the composition, it contains real IDs."
3445
3442
  ).optional(),
3446
- llm_instructions: z.string().describe("Instructions what to do next, Important to follow these instructions!")
3443
+ llm_instructions: z.string().describe("Instructions what to do next, Important to follow these instructions!").optional()
3447
3444
  };
3448
3445
 
3449
3446
  // src/mcp/tools/build-composition/tool.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "4.0.0-551",
4
+ "version": "4.0.0-564",
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": "4.0.0-551",
41
- "@elementor/editor-controls": "4.0.0-551",
42
- "@elementor/editor-documents": "4.0.0-551",
43
- "@elementor/editor-elements": "4.0.0-551",
44
- "@elementor/editor-interactions": "4.0.0-551",
45
- "@elementor/editor-mcp": "4.0.0-551",
46
- "@elementor/editor-notifications": "4.0.0-551",
47
- "@elementor/editor-props": "4.0.0-551",
48
- "@elementor/editor-responsive": "4.0.0-551",
49
- "@elementor/editor-styles": "4.0.0-551",
50
- "@elementor/editor-styles-repository": "4.0.0-551",
51
- "@elementor/editor-ui": "4.0.0-551",
52
- "@elementor/editor-v1-adapters": "4.0.0-551",
53
- "@elementor/schema": "4.0.0-551",
54
- "@elementor/twing": "4.0.0-551",
40
+ "@elementor/editor": "4.0.0-564",
41
+ "@elementor/editor-controls": "4.0.0-564",
42
+ "@elementor/editor-documents": "4.0.0-564",
43
+ "@elementor/editor-elements": "4.0.0-564",
44
+ "@elementor/editor-interactions": "4.0.0-564",
45
+ "@elementor/editor-mcp": "4.0.0-564",
46
+ "@elementor/editor-notifications": "4.0.0-564",
47
+ "@elementor/editor-props": "4.0.0-564",
48
+ "@elementor/editor-responsive": "4.0.0-564",
49
+ "@elementor/editor-styles": "4.0.0-564",
50
+ "@elementor/editor-styles-repository": "4.0.0-564",
51
+ "@elementor/editor-ui": "4.0.0-564",
52
+ "@elementor/editor-v1-adapters": "4.0.0-564",
53
+ "@elementor/schema": "4.0.0-564",
54
+ "@elementor/twing": "4.0.0-564",
55
55
  "@elementor/ui": "1.36.17",
56
- "@elementor/utils": "4.0.0-551",
57
- "@elementor/wp-media": "4.0.0-551",
56
+ "@elementor/utils": "4.0.0-564",
57
+ "@elementor/wp-media": "4.0.0-564",
58
58
  "@floating-ui/react": "^0.27.5",
59
59
  "@wordpress/i18n": "^5.13.0"
60
60
  },
@@ -2,6 +2,7 @@ import { settingsTransformersRegistry } from './settings-transformers-registry';
2
2
  import { attributesTransformer } from './transformers/settings/attributes-transformer';
3
3
  import { createClassesTransformer } from './transformers/settings/classes-transformer';
4
4
  import { dateTimeTransformer } from './transformers/settings/date-time-transformer';
5
+ import { htmlV2Transformer } from './transformers/settings/html-v2-transformer';
5
6
  import { linkTransformer } from './transformers/settings/link-transformer';
6
7
  import { queryTransformer } from './transformers/settings/query-transformer';
7
8
  import { imageSrcTransformer } from './transformers/shared/image-src-transformer';
@@ -17,5 +18,6 @@ export function initSettingsTransformers() {
17
18
  .register( 'image-src', imageSrcTransformer )
18
19
  .register( 'attributes', attributesTransformer )
19
20
  .register( 'date-time', dateTimeTransformer )
21
+ .register( 'html-v2', htmlV2Transformer )
20
22
  .registerFallback( plainTransformer );
21
23
  }
@@ -0,0 +1,115 @@
1
+ import {
2
+ type ChildrenCollection,
3
+ collectChildrenRenderPromises,
4
+ createRenderCacheState,
5
+ renderChildrenWithOptimization,
6
+ } from '../twig-rendering-utils';
7
+
8
+ describe( 'twig-rendering-utils', () => {
9
+ describe( 'createRenderCacheState', () => {
10
+ it( 'should initialize with empty state and reset on invalidate', () => {
11
+ // Act
12
+ const state = createRenderCacheState();
13
+
14
+ // Assert
15
+ expect( state.lastResolvedSettingsHash ).toBeNull();
16
+ expect( state.domUpdateWasSkipped ).toBe( false );
17
+
18
+ // Arrange
19
+ state.lastResolvedSettingsHash = 'test-hash';
20
+ state.domUpdateWasSkipped = true;
21
+
22
+ // Act
23
+ state.invalidate();
24
+
25
+ // Assert
26
+ expect( state.lastResolvedSettingsHash ).toBeNull();
27
+ expect( state.domUpdateWasSkipped ).toBe( false );
28
+ } );
29
+ } );
30
+
31
+ describe( 'collectChildrenRenderPromises', () => {
32
+ it( 'should return empty array when children is undefined', () => {
33
+ expect( collectChildrenRenderPromises( undefined ) ).toEqual( [] );
34
+ } );
35
+
36
+ it( 'should collect only existing render promises from children', () => {
37
+ // Arrange
38
+ const promise1 = Promise.resolve();
39
+ const promise2 = Promise.resolve();
40
+
41
+ const children = createMockChildren( [
42
+ { _currentRenderPromise: promise1 },
43
+ { _currentRenderPromise: undefined },
44
+ { _currentRenderPromise: promise2 },
45
+ ] );
46
+
47
+ // Act
48
+ const result = collectChildrenRenderPromises( children );
49
+
50
+ // Assert
51
+ expect( result ).toEqual( [ promise1, promise2 ] );
52
+ } );
53
+ } );
54
+
55
+ describe( 'renderChildrenWithOptimization', () => {
56
+ it( 'should render new children when DOM was not skipped', () => {
57
+ // Arrange
58
+ const renderChildren = jest.fn();
59
+
60
+ // Act
61
+ renderChildrenWithOptimization( {
62
+ children: createMockChildren( [] ),
63
+ domUpdateWasSkipped: false,
64
+ renderChildren,
65
+ } );
66
+
67
+ // Assert
68
+ expect( renderChildren ).toHaveBeenCalled();
69
+ } );
70
+
71
+ it( 'should rerender existing children when DOM update was skipped', () => {
72
+ // Arrange
73
+ const renderChildren = jest.fn();
74
+ const childView1 = { render: jest.fn() };
75
+ const childView2 = { render: jest.fn() };
76
+
77
+ // Act
78
+ renderChildrenWithOptimization( {
79
+ children: createMockChildren( [ childView1, childView2 ] ),
80
+ domUpdateWasSkipped: true,
81
+ renderChildren,
82
+ } );
83
+
84
+ // Assert
85
+ expect( renderChildren ).not.toHaveBeenCalled();
86
+ expect( childView1.render ).toHaveBeenCalled();
87
+ expect( childView2.render ).toHaveBeenCalled();
88
+ } );
89
+
90
+ it( 'should safely fall back to renderChildren when skipped but children are empty to handle emptyView', () => {
91
+ // Arrange
92
+ const renderChildren = jest.fn();
93
+
94
+ // Act
95
+ renderChildrenWithOptimization( {
96
+ children: undefined,
97
+ domUpdateWasSkipped: true,
98
+ renderChildren,
99
+ } );
100
+
101
+ // Assert
102
+ expect( renderChildren ).toHaveBeenCalled();
103
+ } );
104
+ } );
105
+ } );
106
+
107
+ function createMockChildren( items: Record< string, unknown >[] ): ChildrenCollection {
108
+ return {
109
+ length: items.length,
110
+ findByIndex: jest.fn(),
111
+ each: jest.fn( ( callback ) => {
112
+ items.forEach( ( item ) => callback( item ) );
113
+ } ),
114
+ } as unknown as ChildrenCollection;
115
+ }