@elementor/editor-canvas 4.0.0 → 4.0.1

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
@@ -910,22 +910,30 @@ function useStyleItems() {
910
910
  const [styleItems, setStyleItems] = useState3({});
911
911
  const styleItemsCacheRef = useRef2(/* @__PURE__ */ new Map());
912
912
  const providerAndSubscribers = useMemo4(() => {
913
- return stylesRepository2.getProviders().map((provider) => {
914
- const providerKey = provider.getKey();
913
+ const createEmptyCache = () => {
914
+ return { orderedIds: [], itemsById: /* @__PURE__ */ new Map() };
915
+ };
916
+ const getCache = (provider) => {
917
+ const providerKey = safeGetKey(provider);
918
+ if (!providerKey) {
919
+ return createEmptyCache();
920
+ }
915
921
  if (!styleItemsCacheRef.current.has(providerKey)) {
916
- styleItemsCacheRef.current.set(providerKey, { orderedIds: [], itemsById: /* @__PURE__ */ new Map() });
922
+ styleItemsCacheRef.current.set(providerKey, createEmptyCache());
917
923
  }
918
- const cache = styleItemsCacheRef.current.get(providerKey);
919
- return {
924
+ return styleItemsCacheRef.current.get(providerKey);
925
+ };
926
+ return stylesRepository2.getProviders().map(
927
+ (provider) => ({
920
928
  provider,
921
929
  subscriber: createProviderSubscriber2({
922
930
  provider,
923
931
  renderStyles,
924
932
  setStyleItems,
925
- cache
933
+ getCache: () => getCache(provider)
926
934
  })
927
- };
928
- });
935
+ })
936
+ );
929
937
  }, [renderStyles]);
930
938
  useEffect6(() => {
931
939
  const unsubscribes = providerAndSubscribers.map(
@@ -965,15 +973,23 @@ function stateSorter({ state: stateA }, { state: stateB }) {
965
973
  function createBreakpointSorter(breakpointsOrder) {
966
974
  return ({ breakpoint: breakpointA }, { breakpoint: breakpointB }) => breakpointsOrder.indexOf(breakpointA) - breakpointsOrder.indexOf(breakpointB);
967
975
  }
968
- function createProviderSubscriber2({ provider, renderStyles, setStyleItems, cache }) {
976
+ function safeGetKey(provider) {
977
+ try {
978
+ return provider.getKey();
979
+ } catch {
980
+ return null;
981
+ }
982
+ }
983
+ function createProviderSubscriber2({ provider, renderStyles, setStyleItems, getCache }) {
969
984
  return abortPreviousRuns(
970
985
  (abortController, previous, current) => signalizedProcess(abortController.signal).then((_, signal) => {
986
+ const cache = getCache();
971
987
  const hasDiffInfo = current !== void 0 && previous !== void 0;
972
988
  const hasCache = cache.orderedIds.length > 0;
973
989
  if (hasDiffInfo && hasCache) {
974
- return updateItems(previous, current, signal);
990
+ return updateItems(cache, previous, current, signal);
975
991
  }
976
- return createItems(signal);
992
+ return createItems(cache, signal);
977
993
  }).then((items) => {
978
994
  setStyleItems((prev) => ({
979
995
  ...prev,
@@ -981,7 +997,7 @@ function createProviderSubscriber2({ provider, renderStyles, setStyleItems, cach
981
997
  }));
982
998
  }).execute()
983
999
  );
984
- async function updateItems(previous, current, signal) {
1000
+ async function updateItems(cache, previous, current, signal) {
985
1001
  const changedIds = getChangedStyleIds(previous, current);
986
1002
  cache.orderedIds = provider.actions.all().map((style) => style.id).reverse();
987
1003
  if (changedIds.length > 0) {
@@ -996,7 +1012,7 @@ function createProviderSubscriber2({ provider, renderStyles, setStyleItems, cach
996
1012
  }
997
1013
  return getOrderedItems(cache);
998
1014
  }
999
- async function createItems(signal) {
1015
+ async function createItems(cache, signal) {
1000
1016
  const allStyles = provider.actions.all();
1001
1017
  const styles = [...allStyles].reverse().map((style) => {
1002
1018
  return {
@@ -1391,14 +1407,74 @@ var plainTransformer = createTransformer((value) => {
1391
1407
  return value;
1392
1408
  });
1393
1409
 
1394
- // src/transformers/shared/video-src-transformer.ts
1410
+ // src/transformers/shared/svg-src-transformer.ts
1411
+ import DOMPurify from "dompurify";
1395
1412
  import { getMediaAttachment as getMediaAttachment2 } from "@elementor/wp-media";
1413
+ var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
1414
+ function processSvgContent(svgText) {
1415
+ const sanitized = DOMPurify.sanitize(svgText, {
1416
+ USE_PROFILES: { svg: true, svgFilters: true }
1417
+ });
1418
+ const parser = new DOMParser();
1419
+ const doc = parser.parseFromString(sanitized, "image/svg+xml");
1420
+ const svgElement = doc.querySelector("svg");
1421
+ if (!svgElement) {
1422
+ return null;
1423
+ }
1424
+ svgElement.setAttribute("fill", "currentColor");
1425
+ const existingStyle = svgElement.getAttribute("style") ?? "";
1426
+ const trimmed = existingStyle.trim();
1427
+ const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
1428
+ svgElement.setAttribute("style", merged);
1429
+ return svgElement.outerHTML;
1430
+ }
1431
+ async function fetchSvgContent(url, signal) {
1432
+ try {
1433
+ const response = await fetch(url, { signal });
1434
+ if (!response.ok) {
1435
+ return null;
1436
+ }
1437
+ const contentType = response.headers.get("content-type") ?? "";
1438
+ const isSvg = contentType.includes("svg") || contentType.includes("xml") || url.endsWith(".svg");
1439
+ if (!isSvg) {
1440
+ return null;
1441
+ }
1442
+ return await response.text();
1443
+ } catch {
1444
+ return null;
1445
+ }
1446
+ }
1447
+ function resolveSvgSrcId(id) {
1448
+ if (typeof id !== "number" || id <= 0) {
1449
+ return null;
1450
+ }
1451
+ return id;
1452
+ }
1453
+ var svgSrcTransformer = createTransformer(async (value, { signal }) => {
1454
+ const id = resolveSvgSrcId(value.id);
1455
+ const urlFromValue = typeof value.url === "string" ? value.url : null;
1456
+ let url = urlFromValue;
1457
+ if (id && !urlFromValue) {
1458
+ const attachment = await getMediaAttachment2({ id });
1459
+ url = attachment?.url ?? null;
1460
+ }
1461
+ const resolvedUrl = typeof url === "string" ? url : null;
1462
+ if (!resolvedUrl) {
1463
+ return { html: null, url: null };
1464
+ }
1465
+ const svgText = await fetchSvgContent(resolvedUrl, signal);
1466
+ const html = svgText ? processSvgContent(svgText) : null;
1467
+ return { html, url: resolvedUrl };
1468
+ });
1469
+
1470
+ // src/transformers/shared/video-src-transformer.ts
1471
+ import { getMediaAttachment as getMediaAttachment3 } from "@elementor/wp-media";
1396
1472
  var videoSrcTransformer = createTransformer(async (value) => {
1397
1473
  const { id, url } = value;
1398
1474
  if (!id) {
1399
1475
  return { id: null, url };
1400
1476
  }
1401
- const attachment = await getMediaAttachment2({ id });
1477
+ const attachment = await getMediaAttachment3({ id });
1402
1478
  return {
1403
1479
  id,
1404
1480
  url: attachment?.url ?? url
@@ -1407,7 +1483,7 @@ var videoSrcTransformer = createTransformer(async (value) => {
1407
1483
 
1408
1484
  // src/init-settings-transformers.ts
1409
1485
  function initSettingsTransformers() {
1410
- settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("video-src", videoSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).register("html-v3", htmlV3Transformer).registerFallback(plainTransformer);
1486
+ settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("svg-src", svgSrcTransformer).register("video-src", videoSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).register("html-v3", htmlV3Transformer).registerFallback(plainTransformer);
1411
1487
  }
1412
1488
 
1413
1489
  // src/transformers/styles/background-color-overlay-transformer.ts
@@ -2287,9 +2363,11 @@ function createNestedTemplatedElementView({
2287
2363
  });
2288
2364
  }
2289
2365
 
2366
+ // src/legacy/replacements/manager.ts
2367
+ import { createRoot } from "react-dom/client";
2368
+
2290
2369
  // src/legacy/replacements/inline-editing/inline-editing-elements.tsx
2291
2370
  import * as React6 from "react";
2292
- import { createRoot } from "react-dom/client";
2293
2371
  import { getContainer, getElementLabel, getElementType as getElementType2 } from "@elementor/editor-elements";
2294
2372
  import {
2295
2373
  htmlV3PropTypeUtil as htmlV3PropTypeUtil2,
@@ -2312,6 +2390,8 @@ var ReplacementBase = class {
2312
2390
  type;
2313
2391
  id;
2314
2392
  refreshView;
2393
+ reactRoot;
2394
+ reactContainer;
2315
2395
  constructor(settings) {
2316
2396
  this.getSetting = settings.getSetting;
2317
2397
  this.setSetting = settings.setSetting;
@@ -2319,6 +2399,8 @@ var ReplacementBase = class {
2319
2399
  this.type = settings.type;
2320
2400
  this.id = settings.id;
2321
2401
  this.refreshView = settings.refreshView;
2402
+ this.reactRoot = settings.reactRoot;
2403
+ this.reactContainer = settings.reactContainer;
2322
2404
  }
2323
2405
  static getTypes() {
2324
2406
  return null;
@@ -2339,7 +2421,7 @@ var ReplacementBase = class {
2339
2421
 
2340
2422
  // src/legacy/replacements/inline-editing/canvas-inline-editor.tsx
2341
2423
  import * as React5 from "react";
2342
- import { useEffect as useEffect8, useLayoutEffect, useState as useState5 } from "react";
2424
+ import { useCallback as useCallback2, useEffect as useEffect8, useLayoutEffect, useState as useState5 } from "react";
2343
2425
  import { InlineEditor, InlineEditorToolbar } from "@elementor/editor-controls";
2344
2426
  import { Box as Box2, ThemeProvider } from "@elementor/ui";
2345
2427
  import { autoUpdate as autoUpdate2, flip, FloatingPortal as FloatingPortal2, useFloating as useFloating2 } from "@floating-ui/react";
@@ -2369,7 +2451,8 @@ var INLINE_EDITING_PROPERTY_PER_TYPE = {
2369
2451
  "e-button": "text",
2370
2452
  "e-form-label": "text",
2371
2453
  "e-heading": "title",
2372
- "e-paragraph": "paragraph"
2454
+ "e-paragraph": "paragraph",
2455
+ "e-form-submit-button": "text"
2373
2456
  };
2374
2457
  var getInlineEditorElement = (elementWrapper, expectedTag) => {
2375
2458
  return !expectedTag ? null : elementWrapper.querySelector(expectedTag);
@@ -2387,6 +2470,11 @@ var useOnClickOutsideIframe = (handleUnmount) => {
2387
2470
  };
2388
2471
  var useRenderToolbar = (ownerDocument, id) => {
2389
2472
  const [anchor, setAnchor] = useState4(null);
2473
+ useEffect7(() => {
2474
+ if (!anchor) {
2475
+ removeToolbarAnchor(ownerDocument, id);
2476
+ }
2477
+ }, [anchor, ownerDocument, id]);
2390
2478
  const onSelectionEnd = (view) => {
2391
2479
  const hasSelection = !view.state.selection.empty;
2392
2480
  removeToolbarAnchor(ownerDocument, id);
@@ -2396,7 +2484,10 @@ var useRenderToolbar = (ownerDocument, id) => {
2396
2484
  setAnchor(null);
2397
2485
  }
2398
2486
  };
2399
- return { onSelectionEnd, anchor };
2487
+ const clearAnchor = useCallback(() => {
2488
+ setAnchor(null);
2489
+ }, []);
2490
+ return { onSelectionEnd, anchor, clearAnchor };
2400
2491
  };
2401
2492
  var createAnchorBasedOnSelection = (ownerDocument, id) => {
2402
2493
  const frameWindow = ownerDocument.defaultView;
@@ -2463,46 +2554,59 @@ var horizontalShifterMiddleware = {
2463
2554
  };
2464
2555
 
2465
2556
  // src/legacy/replacements/inline-editing/canvas-inline-editor.tsx
2466
- var EDITOR_WRAPPER_SELECTOR = "inline-editor-wrapper";
2467
2557
  var CanvasInlineEditor = ({
2468
2558
  elementClasses,
2469
2559
  initialValue,
2470
2560
  expectedTag,
2471
2561
  rootElement,
2562
+ contentElement,
2472
2563
  id,
2473
2564
  setValue,
2474
- ...props
2565
+ requestDestroy
2475
2566
  }) => {
2567
+ const [active, setActive] = useState5(true);
2476
2568
  const [editor, setEditor] = useState5(null);
2477
- const { onSelectionEnd, anchor: toolbarAnchor } = useRenderToolbar(rootElement.ownerDocument, id);
2478
- const onBlur = () => {
2479
- removeToolbarAnchor(rootElement.ownerDocument, id);
2480
- props.onBlur();
2481
- };
2482
- useOnClickOutsideIframe(onBlur);
2483
- return /* @__PURE__ */ React5.createElement(ThemeProvider, null, /* @__PURE__ */ React5.createElement(InlineEditingOverlay, { expectedTag, rootElement, id }), /* @__PURE__ */ React5.createElement("style", null, `
2484
- .ProseMirror > * {
2485
- height: 100%;
2486
- }
2487
- .${EDITOR_WRAPPER_SELECTOR} .ProseMirror > button[contenteditable="true"] {
2488
- height: auto;
2489
- cursor: text;
2490
- }
2491
- `), /* @__PURE__ */ React5.createElement(
2569
+ const { onSelectionEnd, anchor: toolbarAnchor, clearAnchor } = useRenderToolbar(rootElement.ownerDocument, id);
2570
+ useEffect8(() => {
2571
+ if (!active) {
2572
+ clearAnchor();
2573
+ requestDestroy();
2574
+ }
2575
+ }, [active, clearAnchor, requestDestroy]);
2576
+ const dismiss = useCallback2(() => {
2577
+ setEditor(null);
2578
+ setActive(false);
2579
+ }, []);
2580
+ useOnClickOutsideIframe(dismiss);
2581
+ useEffect8(() => {
2582
+ const ownerDocument = contentElement.ownerDocument;
2583
+ const handleClickAway = (event) => {
2584
+ if (contentElement.contains(event.target)) {
2585
+ return;
2586
+ }
2587
+ dismiss();
2588
+ };
2589
+ ownerDocument.addEventListener("mousedown", handleClickAway);
2590
+ return () => ownerDocument.removeEventListener("mousedown", handleClickAway);
2591
+ }, [contentElement, dismiss]);
2592
+ if (!active) {
2593
+ return null;
2594
+ }
2595
+ return /* @__PURE__ */ React5.createElement(ThemeProvider, null, /* @__PURE__ */ React5.createElement(InlineEditingOverlay, { expectedTag, rootElement, id }), /* @__PURE__ */ React5.createElement(
2492
2596
  InlineEditor,
2493
2597
  {
2494
2598
  onEditorCreate: setEditor,
2599
+ mountElement: contentElement,
2495
2600
  editorProps: {
2496
2601
  attributes: {
2497
- style: "outline: none;overflow-wrap: normal;height:100%"
2602
+ style: "outline: none; display: inherit; justify-content: inherit; align-items: inherit; flex-direction: inherit; text-align: inherit;"
2498
2603
  }
2499
2604
  },
2500
2605
  elementClasses,
2501
2606
  value: initialValue,
2502
2607
  setValue,
2503
- onBlur,
2608
+ onBlur: dismiss,
2504
2609
  autofocus: true,
2505
- expectedTag,
2506
2610
  onSelectionEnd
2507
2611
  }
2508
2612
  ), toolbarAnchor && editor && /* @__PURE__ */ React5.createElement(InlineEditingToolbar, { anchor: toolbarAnchor, editor, id }));
@@ -2531,7 +2635,18 @@ var InlineEditingToolbar = ({ anchor, editor, id }) => {
2531
2635
  refs.setReference(anchor);
2532
2636
  return () => refs.setReference(null);
2533
2637
  }, [anchor, refs]);
2534
- return /* @__PURE__ */ React5.createElement(FloatingPortal2, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React5.createElement(Box2, { ref: refs.setFloating, role: "presentation", style: { ...floatingStyles, pointerEvents: "none" } }, /* @__PURE__ */ React5.createElement(InlineEditorToolbar, { editor, elementId: id })));
2638
+ return /* @__PURE__ */ React5.createElement(FloatingPortal2, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React5.createElement(
2639
+ Box2,
2640
+ {
2641
+ ref: refs.setFloating,
2642
+ role: "presentation",
2643
+ style: {
2644
+ ...floatingStyles,
2645
+ pointerEvents: "none"
2646
+ }
2647
+ },
2648
+ /* @__PURE__ */ React5.createElement(InlineEditorToolbar, { editor, elementId: id })
2649
+ ));
2535
2650
  };
2536
2651
 
2537
2652
  // src/legacy/replacements/inline-editing/inline-editing-eligibility.ts
@@ -2565,8 +2680,8 @@ var isInlineEditingAllowed = ({ rawValue, propTypeFromSchema }) => {
2565
2680
  // src/legacy/replacements/inline-editing/inline-editing-elements.tsx
2566
2681
  var HISTORY_DEBOUNCE_WAIT = 800;
2567
2682
  var InlineEditingReplacement = class extends ReplacementBase {
2568
- inlineEditorRoot = null;
2569
2683
  handlerAttached = false;
2684
+ editing = false;
2570
2685
  getReplacementKey() {
2571
2686
  return "inline-editing";
2572
2687
  }
@@ -2574,7 +2689,7 @@ var InlineEditingReplacement = class extends ReplacementBase {
2574
2689
  return Object.keys(INLINE_EDITING_PROPERTY_PER_TYPE);
2575
2690
  }
2576
2691
  isEditingModeActive() {
2577
- return !!this.inlineEditorRoot;
2692
+ return this.editing;
2578
2693
  }
2579
2694
  shouldRenderReplacement() {
2580
2695
  return this.isInlineEditingEligible() && getCurrentEditMode() === "edit";
@@ -2617,8 +2732,8 @@ var InlineEditingReplacement = class extends ReplacementBase {
2617
2732
  resetInlineEditorRoot() {
2618
2733
  this.element.removeEventListener("click", this.handleRenderInlineEditor);
2619
2734
  this.handlerAttached = false;
2620
- this.inlineEditorRoot?.unmount?.();
2621
- this.inlineEditorRoot = null;
2735
+ this.reactRoot.render(null);
2736
+ this.editing = false;
2622
2737
  }
2623
2738
  unmountInlineEditor() {
2624
2739
  this.resetInlineEditorRoot();
@@ -2729,12 +2844,16 @@ var InlineEditingReplacement = class extends ReplacementBase {
2729
2844
  if (this.isEditingModeActive()) {
2730
2845
  this.resetInlineEditorRoot();
2731
2846
  }
2732
- const elementClasses = this.element.children?.[0]?.classList.toString() ?? "";
2847
+ const contentElement = this.element.children?.[0];
2848
+ if (!contentElement) {
2849
+ return;
2850
+ }
2851
+ const elementClasses = contentElement.classList.toString();
2733
2852
  const propValue = this.getExtractedContentValue();
2734
2853
  const expectedTag = this.getExpectedTag();
2735
- this.element.innerHTML = "";
2736
- this.inlineEditorRoot = createRoot(this.element);
2737
- this.inlineEditorRoot.render(
2854
+ contentElement.innerHTML = "";
2855
+ this.editing = true;
2856
+ this.reactRoot.render(
2738
2857
  /* @__PURE__ */ React6.createElement(
2739
2858
  CanvasInlineEditor,
2740
2859
  {
@@ -2742,9 +2861,10 @@ var InlineEditingReplacement = class extends ReplacementBase {
2742
2861
  initialValue: propValue,
2743
2862
  expectedTag,
2744
2863
  rootElement: this.element,
2864
+ contentElement,
2745
2865
  id: this.id,
2746
2866
  setValue: this.setContentValue.bind(this),
2747
- onBlur: this.unmountInlineEditor.bind(this)
2867
+ requestDestroy: this.unmountInlineEditor.bind(this)
2748
2868
  }
2749
2869
  )
2750
2870
  );
@@ -2773,16 +2893,24 @@ var createViewWithReplacements = (options) => {
2773
2893
  return class extends TemplatedView {
2774
2894
  #replacement = null;
2775
2895
  #config;
2896
+ #reactContainer;
2897
+ #reactRoot;
2776
2898
  constructor(...args) {
2777
2899
  super(...args);
2778
2900
  const settings = this.model.get("settings");
2901
+ this.#reactContainer = this.el.ownerDocument.createElement("div");
2902
+ this.#reactContainer.style.display = "none";
2903
+ this.el.ownerDocument.body.appendChild(this.#reactContainer);
2904
+ this.#reactRoot = createRoot(this.#reactContainer);
2779
2905
  this.#config = {
2780
2906
  getSetting: settings.get.bind(settings),
2781
2907
  setSetting: settings.set.bind(settings),
2782
2908
  element: this.el,
2783
2909
  type: this?.model?.get("widgetType") ?? this.container?.model?.get("elType") ?? null,
2784
2910
  id: this?.model?.get("id") ?? null,
2785
- refreshView: this.refreshView.bind(this)
2911
+ refreshView: this.refreshView.bind(this),
2912
+ reactRoot: this.#reactRoot,
2913
+ reactContainer: this.#reactContainer
2786
2914
  };
2787
2915
  }
2788
2916
  refreshView() {
@@ -2803,6 +2931,8 @@ var createViewWithReplacements = (options) => {
2803
2931
  }
2804
2932
  onDestroy() {
2805
2933
  this.#triggerAltMethod("onDestroy");
2934
+ this.#reactRoot.unmount();
2935
+ this.#reactContainer.remove();
2806
2936
  }
2807
2937
  _afterRender() {
2808
2938
  this.#triggerAltMethod("_afterRender");
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",
4
+ "version": "4.0.1",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,24 +37,25 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "4.0.0",
41
- "@elementor/editor-controls": "4.0.0",
42
- "@elementor/editor-documents": "4.0.0",
43
- "@elementor/editor-elements": "4.0.0",
44
- "@elementor/editor-interactions": "4.0.0",
45
- "@elementor/editor-mcp": "4.0.0",
46
- "@elementor/editor-notifications": "4.0.0",
47
- "@elementor/editor-props": "4.0.0",
48
- "@elementor/editor-responsive": "4.0.0",
49
- "@elementor/editor-styles": "4.0.0",
50
- "@elementor/editor-styles-repository": "4.0.0",
51
- "@elementor/editor-ui": "4.0.0",
52
- "@elementor/editor-v1-adapters": "4.0.0",
53
- "@elementor/schema": "4.0.0",
54
- "@elementor/twing": "4.0.0",
40
+ "@elementor/editor": "4.0.1",
41
+ "dompurify": "^3.2.6",
42
+ "@elementor/editor-controls": "4.0.1",
43
+ "@elementor/editor-documents": "4.0.1",
44
+ "@elementor/editor-elements": "4.0.1",
45
+ "@elementor/editor-interactions": "4.0.1",
46
+ "@elementor/editor-mcp": "4.0.1",
47
+ "@elementor/editor-notifications": "4.0.1",
48
+ "@elementor/editor-props": "4.0.1",
49
+ "@elementor/editor-responsive": "4.0.1",
50
+ "@elementor/editor-styles": "4.0.1",
51
+ "@elementor/editor-styles-repository": "4.0.1",
52
+ "@elementor/editor-ui": "4.0.1",
53
+ "@elementor/editor-v1-adapters": "4.0.1",
54
+ "@elementor/schema": "4.0.1",
55
+ "@elementor/twing": "4.0.1",
55
56
  "@elementor/ui": "1.36.17",
56
- "@elementor/utils": "4.0.0",
57
- "@elementor/wp-media": "4.0.0",
57
+ "@elementor/utils": "4.0.1",
58
+ "@elementor/wp-media": "4.0.1",
58
59
  "@floating-ui/react": "^0.27.5",
59
60
  "@wordpress/i18n": "^5.13.0"
60
61
  },
@@ -13,6 +13,10 @@ jest.mock( '@elementor/editor-v1-adapters', () => ( {
13
13
  commandEndEvent: jest.fn(),
14
14
  } ) );
15
15
 
16
+ jest.mock( '@elementor/editor-documents', () => ( {
17
+ getCurrentDocument: jest.fn().mockReturnValue( { id: 1 } ),
18
+ } ) );
19
+
16
20
  jest.mock( '@elementor/ui', () => ( {
17
21
  Portal: jest.fn( ( { children } ) => <div data-testid="portal">{ children }</div> ),
18
22
  } ) );
@@ -340,6 +340,74 @@ describe( 'useStyleItems', () => {
340
340
  expect( breakpointOrderAfterUpdate ).toEqual( [ 'desktop', 'tablet', 'mobile' ] );
341
341
  } );
342
342
 
343
+ it( 'should recover and render styles when a provider key becomes available after initial failure', async () => {
344
+ // Arrange.
345
+ let shouldThrow = true;
346
+
347
+ const dynamicKey: () => string = () => {
348
+ if ( shouldThrow ) {
349
+ throw new Error( 'Document not ready' );
350
+ }
351
+
352
+ return 'late-provider';
353
+ };
354
+
355
+ const failingThenSucceedingProvider = createMockStylesProvider(
356
+ {
357
+ key: dynamicKey,
358
+ priority: 2,
359
+ },
360
+ [ createMockStyleDefinition( { id: 'late-style1' } ), createMockStyleDefinition( { id: 'late-style2' } ) ]
361
+ );
362
+
363
+ const stableProvider = createMockStylesProvider(
364
+ {
365
+ key: 'stable-provider',
366
+ priority: 1,
367
+ },
368
+ [
369
+ createMockStyleDefinition( { id: 'stable-style1' } ),
370
+ createMockStyleDefinition( { id: 'stable-style2' } ),
371
+ ]
372
+ );
373
+
374
+ jest.mocked( stylesRepository ).getProviders.mockReturnValue( [
375
+ failingThenSucceedingProvider,
376
+ stableProvider,
377
+ ] );
378
+
379
+ let attachPreviewCallback: () => Promise< void >;
380
+
381
+ jest.mocked( registerDataHook ).mockImplementation( ( position, command, callback ) => {
382
+ if ( command === 'editor/documents/attach-preview' && position === 'after' ) {
383
+ attachPreviewCallback = callback as never;
384
+ }
385
+
386
+ return null as never;
387
+ } );
388
+
389
+ // Act.
390
+ const { result } = renderHook( () => useStyleItems() );
391
+
392
+ // Assert - hook should not crash, should return empty initially.
393
+ expect( result.current ).toEqual( [] );
394
+
395
+ // Act - simulate document becoming ready, then trigger attach-preview.
396
+ shouldThrow = false;
397
+
398
+ await act( async () => {
399
+ await attachPreviewCallback?.();
400
+ } );
401
+
402
+ // Assert - both providers' styles should render in correct priority order.
403
+ expect( result.current ).toEqual( [
404
+ { id: 'stable-style2', breakpoint: 'desktop' },
405
+ { id: 'stable-style1', breakpoint: 'desktop' },
406
+ { id: 'late-style2', breakpoint: 'desktop' },
407
+ { id: 'late-style1', breakpoint: 'desktop' },
408
+ ] );
409
+ } );
410
+
343
411
  it( 'should only re-render changed styles on differential update', async () => {
344
412
  // Arrange.
345
413
  const renderStylesMock = jest.fn().mockImplementation( ( { styles } ) =>
@@ -36,25 +36,35 @@ export function useStyleItems() {
36
36
  const styleItemsCacheRef = useRef< Map< string, StyleItemsCache > >( new Map() );
37
37
 
38
38
  const providerAndSubscribers = useMemo( () => {
39
- return stylesRepository.getProviders().map( ( provider ): ProviderAndSubscriber => {
40
- const providerKey = provider.getKey();
39
+ const createEmptyCache = () => {
40
+ return { orderedIds: [], itemsById: new Map() };
41
+ };
42
+
43
+ const getCache = ( provider: StylesProvider ): StyleItemsCache => {
44
+ const providerKey = safeGetKey( provider );
45
+
46
+ if ( ! providerKey ) {
47
+ return createEmptyCache();
48
+ }
41
49
 
42
50
  if ( ! styleItemsCacheRef.current.has( providerKey ) ) {
43
- styleItemsCacheRef.current.set( providerKey, { orderedIds: [], itemsById: new Map() } );
51
+ styleItemsCacheRef.current.set( providerKey, createEmptyCache() );
44
52
  }
45
53
 
46
- const cache = styleItemsCacheRef.current.get( providerKey ) as StyleItemsCache;
54
+ return styleItemsCacheRef.current.get( providerKey ) as StyleItemsCache;
55
+ };
47
56
 
48
- return {
57
+ return stylesRepository.getProviders().map(
58
+ ( provider ): ProviderAndSubscriber => ( {
49
59
  provider,
50
60
  subscriber: createProviderSubscriber( {
51
61
  provider,
52
62
  renderStyles,
53
63
  setStyleItems,
54
- cache,
64
+ getCache: () => getCache( provider ),
55
65
  } ),
56
- };
57
- } );
66
+ } )
67
+ );
58
68
  }, [ renderStyles ] );
59
69
 
60
70
  useEffect( () => {
@@ -122,25 +132,34 @@ function createBreakpointSorter( breakpointsOrder: BreakpointId[] ) {
122
132
  breakpointsOrder.indexOf( breakpointB as BreakpointId );
123
133
  }
124
134
 
135
+ function safeGetKey( provider: StylesProvider ): string | null {
136
+ try {
137
+ return provider.getKey();
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
125
143
  type CreateProviderSubscriberArgs = {
126
144
  provider: StylesProvider;
127
145
  renderStyles: StyleRenderer;
128
146
  setStyleItems: Dispatch< SetStateAction< ProviderAndStyleItemsMap > >;
129
- cache: StyleItemsCache;
147
+ getCache: () => StyleItemsCache;
130
148
  };
131
149
 
132
- function createProviderSubscriber( { provider, renderStyles, setStyleItems, cache }: CreateProviderSubscriberArgs ) {
150
+ function createProviderSubscriber( { provider, renderStyles, setStyleItems, getCache }: CreateProviderSubscriberArgs ) {
133
151
  return abortPreviousRuns( ( abortController, previous?: StylesCollection, current?: StylesCollection ) =>
134
152
  signalizedProcess( abortController.signal )
135
153
  .then( ( _, signal ) => {
154
+ const cache = getCache();
136
155
  const hasDiffInfo = current !== undefined && previous !== undefined;
137
156
  const hasCache = cache.orderedIds.length > 0;
138
157
 
139
158
  if ( hasDiffInfo && hasCache ) {
140
- return updateItems( previous, current, signal );
159
+ return updateItems( cache, previous, current, signal );
141
160
  }
142
161
 
143
- return createItems( signal );
162
+ return createItems( cache, signal );
144
163
  } )
145
164
  .then( ( items ) => {
146
165
  setStyleItems( ( prev ) => ( {
@@ -151,7 +170,12 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems, cach
151
170
  .execute()
152
171
  );
153
172
 
154
- async function updateItems( previous: StylesCollection, current: StylesCollection, signal: AbortSignal ) {
173
+ async function updateItems(
174
+ cache: StyleItemsCache,
175
+ previous: StylesCollection,
176
+ current: StylesCollection,
177
+ signal: AbortSignal
178
+ ) {
155
179
  const changedIds = getChangedStyleIds( previous, current );
156
180
 
157
181
  cache.orderedIds = provider.actions
@@ -178,7 +202,7 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems, cach
178
202
  return getOrderedItems( cache );
179
203
  }
180
204
 
181
- async function createItems( signal: AbortSignal ) {
205
+ async function createItems( cache: StyleItemsCache, signal: AbortSignal ) {
182
206
  const allStyles = provider.actions.all();
183
207
 
184
208
  const styles = [ ...allStyles ].reverse().map( ( style ) => {