@elementor/editor-canvas 4.4.0-beta1 → 4.4.0-beta2

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
@@ -1122,7 +1122,7 @@ import * as React9 from "react";
1122
1122
  import {
1123
1123
  __privateUseListenTo as useListenTo5,
1124
1124
  commandEndEvent as commandEndEvent3,
1125
- getCanvasIframeDocument as getCanvasIframeDocument4
1125
+ getCanvasIframeDocument as getCanvasIframeDocument5
1126
1126
  } from "@elementor/editor-v1-adapters";
1127
1127
  import { Portal as Portal2 } from "@elementor/ui";
1128
1128
 
@@ -1239,6 +1239,25 @@ function signalizedProcess(signal, steps = []) {
1239
1239
  };
1240
1240
  }
1241
1241
 
1242
+ // src/hooks/use-pregenerated-links-handoff.ts
1243
+ import { useLayoutEffect } from "react";
1244
+ import { getCanvasIframeDocument as getCanvasIframeDocument4 } from "@elementor/editor-v1-adapters";
1245
+ function usePregeneratedLinksHandoff(canvasHead, renderedProviders) {
1246
+ useLayoutEffect(() => {
1247
+ if (!isCurrentCanvasHead(canvasHead)) {
1248
+ return;
1249
+ }
1250
+ Object.entries(renderedProviders).forEach(([providerKey, { provider, items }]) => {
1251
+ if (items.length > 0 && provider.isPregeneratedLink) {
1252
+ removeProviderPregeneratedLinks(providerKey, provider.isPregeneratedLink);
1253
+ }
1254
+ });
1255
+ }, [canvasHead, renderedProviders]);
1256
+ }
1257
+ function isCurrentCanvasHead(canvasHead) {
1258
+ return !!canvasHead && canvasHead === getCanvasIframeDocument4()?.head;
1259
+ }
1260
+
1242
1261
  // src/hooks/use-style-prop-resolver.ts
1243
1262
  import { useMemo as useMemo3 } from "react";
1244
1263
  import { getStylesSchema } from "@elementor/editor-styles";
@@ -1524,7 +1543,7 @@ function useStyleRenderer(resolve) {
1524
1543
  }
1525
1544
 
1526
1545
  // src/hooks/use-style-items.ts
1527
- function useStyleItems() {
1546
+ function useStyleItems(canvasHead) {
1528
1547
  const resolve = useStylePropResolver();
1529
1548
  const renderStyles = useStyleRenderer(resolve);
1530
1549
  const breakpoints = useBreakpoints();
@@ -1571,6 +1590,7 @@ function useStyleItems() {
1571
1590
  await Promise.all(promises);
1572
1591
  });
1573
1592
  });
1593
+ usePregeneratedLinksHandoff(canvasHead, styleItems);
1574
1594
  const breakpointSorter = useMemo5(
1575
1595
  () => createBreakpointSorter(breakpoints.map((breakpoint) => breakpoint.id)),
1576
1596
  [breakpoints]
@@ -1608,9 +1628,6 @@ function createProviderSubscriber2({ provider, renderStyles, setStyleItems, getC
1608
1628
  const cache = getCache();
1609
1629
  const hasDiffInfo = current !== void 0 && previous !== void 0;
1610
1630
  const hasCache = cache.orderedIds.length > 0;
1611
- if (hasCache && provider.isPregeneratedLink) {
1612
- removeProviderPregeneratedLinks(provider.getKey(), provider.isPregeneratedLink);
1613
- }
1614
1631
  if (hasDiffInfo && hasCache) {
1615
1632
  return updateItems(cache, previous, current, signal);
1616
1633
  }
@@ -1712,7 +1729,7 @@ function rebuildCache(cache, allStyles, items) {
1712
1729
  // src/components/style-renderer.tsx
1713
1730
  function StyleRenderer() {
1714
1731
  const container = usePortalContainer2();
1715
- const styleItems = useStyleItems();
1732
+ const styleItems = useStyleItems(container);
1716
1733
  const linksAttrs = useDocumentsCssLinks();
1717
1734
  if (!container) {
1718
1735
  return null;
@@ -1720,7 +1737,7 @@ function StyleRenderer() {
1720
1737
  return /* @__PURE__ */ React9.createElement(Portal2, { container }, filterUniqueStyleDefinitions(styleItems).map((item) => /* @__PURE__ */ React9.createElement("style", { key: `${item.id}-${item.breakpoint}-${item.state ?? "normal"}` }, item.value)), linksAttrs.map((attrs) => /* @__PURE__ */ React9.createElement("link", { ...attrs, key: attrs.id })));
1721
1738
  }
1722
1739
  function usePortalContainer2() {
1723
- return useListenTo5(commandEndEvent3("editor/documents/attach-preview"), () => getCanvasIframeDocument4()?.head);
1740
+ return useListenTo5(commandEndEvent3("editor/documents/attach-preview"), () => getCanvasIframeDocument5()?.head);
1724
1741
  }
1725
1742
  function filterUniqueStyleDefinitions(styleItems) {
1726
1743
  const seen = /* @__PURE__ */ new Map();
@@ -2793,6 +2810,7 @@ function createDomRenderer() {
2793
2810
  const environment = createEnvironment(loader);
2794
2811
  environment.registerEscapingStrategy(escapeHtmlTag, "html_tag");
2795
2812
  environment.registerEscapingStrategy(escapeURL, "full_url");
2813
+ environment.registerEscapingStrategy(escapeJsIdentifier, "js_identifier");
2796
2814
  return {
2797
2815
  register: loader.setTemplate,
2798
2816
  render: environment.render
@@ -2806,6 +2824,12 @@ function escapeHtmlTag(value) {
2806
2824
  const normalizedTag = value?.toLowerCase?.() ?? "";
2807
2825
  return allowedTags.includes(normalizedTag) ? value : "div";
2808
2826
  }
2827
+ var JS_IDENTIFIER_PATTERN = /^[A-Za-z0-9_]+$/;
2828
+ function escapeJsIdentifier(value) {
2829
+ const id = String(value ?? "");
2830
+ const match = id.match(JS_IDENTIFIER_PATTERN);
2831
+ return match?.[0] === id ? id : "";
2832
+ }
2809
2833
  function escapeURL(value) {
2810
2834
  const allowedProtocols = ["http:", "https:", "mailto:", "tel:"];
2811
2835
  try {
@@ -3592,7 +3616,7 @@ var ReplacementBase = class {
3592
3616
 
3593
3617
  // src/legacy/replacements/inline-editing/canvas-inline-editor.tsx
3594
3618
  import * as React10 from "react";
3595
- import { useCallback as useCallback2, useEffect as useEffect12, useLayoutEffect, useState as useState8 } from "react";
3619
+ import { useCallback as useCallback2, useEffect as useEffect12, useLayoutEffect as useLayoutEffect2, useState as useState8 } from "react";
3596
3620
  import { InlineEditor, InlineEditorToolbar } from "@elementor/editor-controls";
3597
3621
  import { ThemeProvider } from "@elementor/editor-ui";
3598
3622
  import { Box as Box3 } from "@elementor/ui";
@@ -3803,7 +3827,7 @@ var InlineEditingToolbar = ({ anchor, editor, id }) => {
3803
3827
  whileElementsMounted: autoUpdate2,
3804
3828
  middleware: [horizontalShifterMiddleware, flip()]
3805
3829
  });
3806
- useLayoutEffect(() => {
3830
+ useLayoutEffect2(() => {
3807
3831
  refs.setReference(anchor);
3808
3832
  return () => refs.setReference(null);
3809
3833
  }, [anchor, refs]);
@@ -4312,7 +4336,7 @@ function initTabsModelExtensions() {
4312
4336
  }
4313
4337
 
4314
4338
  // src/mcp/canvas-mcp.ts
4315
- import { Schema as Schema3 } from "@elementor/editor-props";
4339
+ import { Schema as Schema4 } from "@elementor/editor-props";
4316
4340
 
4317
4341
  // src/mcp/resources/available-widgets-resource.ts
4318
4342
  import { httpService as httpService3 } from "@elementor/http-client";
@@ -4686,6 +4710,7 @@ var initGeneralContextResource = (reg) => {
4686
4710
 
4687
4711
  // src/mcp/resources/selected-element-resource.ts
4688
4712
  import { getContainer as getContainer3, getSelectedElements, getWidgetsCache as getWidgetsCache3 } from "@elementor/editor-elements";
4713
+ import { Schema } from "@elementor/editor-props";
4689
4714
  import {
4690
4715
  __privateListenTo as listenTo4,
4691
4716
  commandEndEvent as commandEndEvent7
@@ -4796,13 +4821,19 @@ function getElementProperties(container, widgetType) {
4796
4821
  if (!settings || typeof settings !== "object") {
4797
4822
  return null;
4798
4823
  }
4799
- const widgetConfig = widgetType ? getWidgetsCache3()?.[widgetType] : null;
4824
+ const widgetsCache = getWidgetsCache3();
4825
+ const widgetConfig = widgetType ? widgetsCache?.[widgetType] : null;
4800
4826
  const controls = widgetConfig?.controls;
4827
+ const schemaType = widgetType ?? container.model.get("elType") ?? null;
4828
+ const propsSchema = schemaType ? widgetsCache?.[schemaType]?.atomic_props_schema : void 0;
4801
4829
  const filtered = {};
4802
4830
  for (const [key, value] of Object.entries(settings)) {
4803
4831
  if (value === void 0 || value === null || value === "") {
4804
4832
  continue;
4805
4833
  }
4834
+ if (Schema.isPropSensitive(propsSchema?.[key])) {
4835
+ continue;
4836
+ }
4806
4837
  const controlDefault = controls?.[key]?.default;
4807
4838
  if (controlDefault !== void 0 && JSON.stringify(value) === JSON.stringify(controlDefault)) {
4808
4839
  continue;
@@ -4836,7 +4867,7 @@ function getElementDisplayName(container) {
4836
4867
  // src/mcp/tools/configure-element/tool.ts
4837
4868
  import { getContainer as getContainer4, getWidgetsCache as getWidgetsCache6 } from "@elementor/editor-elements";
4838
4869
  import { dispatchMcpStylesAppliedEvent } from "@elementor/editor-mcp";
4839
- import { Schema as Schema2 } from "@elementor/editor-props";
4870
+ import { Schema as Schema3 } from "@elementor/editor-props";
4840
4871
 
4841
4872
  // src/mcp/utils/do-update-element-property.ts
4842
4873
  import {
@@ -4846,7 +4877,7 @@ import {
4846
4877
  updateElementSettings,
4847
4878
  updateElementStyle
4848
4879
  } from "@elementor/editor-elements";
4849
- import { getPropSchemaFromCache as getPropSchemaFromCache2, Schema } from "@elementor/editor-props";
4880
+ import { getPropSchemaFromCache as getPropSchemaFromCache2, Schema as Schema2 } from "@elementor/editor-props";
4850
4881
  import { getStylesSchema as getStylesSchema2, getVariantByMeta } from "@elementor/editor-styles";
4851
4882
  import { __privateRunCommandSync as runCommandSync2 } from "@elementor/editor-v1-adapters";
4852
4883
 
@@ -4999,7 +5030,7 @@ var UnsupportedPropertyError = class extends Error {
4999
5030
  };
5000
5031
  function resolvePropValue(value, forceKey) {
5001
5032
  const Utils = window.elementorV2.editorVariables.Utils;
5002
- return Schema.adjustLlmPropValueSchema(value, {
5033
+ return Schema2.adjustLlmPropValueSchema(value, {
5003
5034
  forceKey,
5004
5035
  transformers: {
5005
5036
  ...Utils.globalVariablesLLMResolvers,
@@ -5106,7 +5137,7 @@ var doUpdateElementProperty = (params) => {
5106
5137
  }
5107
5138
  const propKey = elementPropSchema[propertyName].key;
5108
5139
  const value = resolvePropValue(propertyValue, propKey);
5109
- const { valid, jsonSchema } = Schema.validatePropValue(elementPropSchema[propertyName], propertyValue);
5140
+ const { valid, jsonSchema } = Schema2.validatePropValue(elementPropSchema[propertyName], propertyValue);
5110
5141
  if (!valid) {
5111
5142
  throw new Error(
5112
5143
  `Invalid PropValue for elementId: ${elementId}. PropKey: ${propKey}, PropValue: ${JSON.stringify(
@@ -5353,7 +5384,7 @@ var initConfigureElementTool = (reg) => {
5353
5384
  const toUpdate = Object.entries(propertiesToUpdate);
5354
5385
  const skippedProps = [];
5355
5386
  for (const [propertyName, propertyValue] of toUpdate) {
5356
- if (!Schema2.isPropKeyConfigurable(propertyName)) {
5387
+ if (!Schema3.isPropKeyConfigurable(propertyName, widgetData.atomic_props_schema[propertyName])) {
5357
5388
  throw new Error(`Not allowed to update ${propertyName}`);
5358
5389
  }
5359
5390
  try {
@@ -5614,7 +5645,7 @@ var initGetPageStructureTool = (reg) => {
5614
5645
 
5615
5646
  // src/mcp/canvas-mcp.ts
5616
5647
  var initCanvasMcp = (reg) => {
5617
- Schema3.setDynamicTagNamesResolver(getDynamicTagNamesByCategories);
5648
+ Schema4.setDynamicTagNamesResolver(getDynamicTagNamesByCategories);
5618
5649
  initWidgetsSchemaResource(reg);
5619
5650
  initAvailableWidgetsResource(reg);
5620
5651
  initDocumentStructureResource(reg);
@@ -6220,10 +6251,10 @@ function getRectClipPath(rect, viewport) {
6220
6251
  import {
6221
6252
  __privateUseListenTo as useListenTo6,
6222
6253
  commandEndEvent as commandEndEvent8,
6223
- getCanvasIframeDocument as getCanvasIframeDocument5
6254
+ getCanvasIframeDocument as getCanvasIframeDocument6
6224
6255
  } from "@elementor/editor-v1-adapters";
6225
6256
  function useCanvasDocument() {
6226
- return useListenTo6(commandEndEvent8("editor/documents/attach-preview"), () => getCanvasIframeDocument5());
6257
+ return useListenTo6(commandEndEvent8("editor/documents/attach-preview"), () => getCanvasIframeDocument6());
6227
6258
  }
6228
6259
 
6229
6260
  // src/hooks/use-escape-on-canvas.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.4.0-beta1",
4
+ "version": "4.4.0-beta2",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,26 +37,26 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "4.4.0-beta1",
41
- "@elementor/editor-controls": "4.4.0-beta1",
42
- "@elementor/editor-documents": "4.4.0-beta1",
43
- "@elementor/editor-elements": "4.4.0-beta1",
44
- "@elementor/editor-interactions": "4.4.0-beta1",
45
- "@elementor/editor-mcp": "4.4.0-beta1",
46
- "@elementor/editor-notifications": "4.4.0-beta1",
47
- "@elementor/editor-props": "4.4.0-beta1",
48
- "@elementor/editor-responsive": "4.4.0-beta1",
49
- "@elementor/editor-styles": "4.4.0-beta1",
50
- "@elementor/editor-styles-repository": "4.4.0-beta1",
51
- "@elementor/editor-ui": "4.4.0-beta1",
52
- "@elementor/editor-v1-adapters": "4.4.0-beta1",
53
- "@elementor/events": "4.4.0-beta1",
54
- "@elementor/http-client": "4.4.0-beta1",
55
- "@elementor/schema": "4.4.0-beta1",
56
- "@elementor/twing": "4.4.0-beta1",
40
+ "@elementor/editor": "4.4.0-beta2",
41
+ "@elementor/editor-controls": "4.4.0-beta2",
42
+ "@elementor/editor-documents": "4.4.0-beta2",
43
+ "@elementor/editor-elements": "4.4.0-beta2",
44
+ "@elementor/editor-interactions": "4.4.0-beta2",
45
+ "@elementor/editor-mcp": "4.4.0-beta2",
46
+ "@elementor/editor-notifications": "4.4.0-beta2",
47
+ "@elementor/editor-props": "4.4.0-beta2",
48
+ "@elementor/editor-responsive": "4.4.0-beta2",
49
+ "@elementor/editor-styles": "4.4.0-beta2",
50
+ "@elementor/editor-styles-repository": "4.4.0-beta2",
51
+ "@elementor/editor-ui": "4.4.0-beta2",
52
+ "@elementor/editor-v1-adapters": "4.4.0-beta2",
53
+ "@elementor/events": "4.4.0-beta2",
54
+ "@elementor/http-client": "4.4.0-beta2",
55
+ "@elementor/schema": "4.4.0-beta2",
56
+ "@elementor/twing": "4.4.0-beta2",
57
57
  "@elementor/ui": "1.41.4",
58
- "@elementor/utils": "4.4.0-beta1",
59
- "@elementor/wp-media": "4.4.0-beta1",
58
+ "@elementor/utils": "4.4.0-beta2",
59
+ "@elementor/wp-media": "4.4.0-beta2",
60
60
  "@floating-ui/react": "^0.27.5",
61
61
  "@wordpress/i18n": "^5.13.0",
62
62
  "dompurify": "^3.2.6"
@@ -64,6 +64,7 @@ describe( '<StyleRenderer />', () => {
64
64
  render( <StyleRenderer /> );
65
65
 
66
66
  // Assert.
67
+ expect( useStyleItems ).toHaveBeenCalledWith( mockContainer );
67
68
  // eslint-disable-next-line testing-library/no-test-id-queries
68
69
  expect( screen.getByTestId( 'portal' ) ).toMatchSnapshot();
69
70
  } );
@@ -13,7 +13,7 @@ import { type StyleItem } from '../renderers/create-styles-renderer';
13
13
  export function StyleRenderer() {
14
14
  const container = usePortalContainer();
15
15
 
16
- const styleItems = useStyleItems();
16
+ const styleItems = useStyleItems( container );
17
17
  const linksAttrs = useDocumentsCssLinks();
18
18
 
19
19
  if ( ! container ) {
@@ -1,12 +1,18 @@
1
1
  import { createMockStyleDefinition, createMockStyleDefinitionWithVariants, createMockStylesProvider } from 'test-utils';
2
2
  import { type StyleDefinition } from '@elementor/editor-styles';
3
3
  import { stylesRepository } from '@elementor/editor-styles-repository';
4
- import { registerDataHook } from '@elementor/editor-v1-adapters';
4
+ import { getCanvasIframeDocument, registerDataHook } from '@elementor/editor-v1-adapters';
5
5
  import { act, renderHook } from '@testing-library/react';
6
6
 
7
+ import { removeProviderPregeneratedLinks } from '../../utils/pregenerated-links-removal';
7
8
  import { useStyleItems } from '../use-style-items';
8
9
  import { useStyleRenderer } from '../use-style-renderer';
9
10
 
11
+ jest.mock( '../../utils/pregenerated-links-removal', () => ( {
12
+ removeProviderPregeneratedLinks: jest.fn(),
13
+ resetRemovedProviders: jest.fn(),
14
+ } ) );
15
+
10
16
  jest.mock( '@elementor/editor-styles-repository', () => ( {
11
17
  ...jest.requireActual( '@elementor/editor-styles-repository' ),
12
18
  stylesRepository: {
@@ -17,6 +23,7 @@ jest.mock( '@elementor/editor-styles-repository', () => ( {
17
23
  jest.mock( '@elementor/editor-v1-adapters', () => ( {
18
24
  ...jest.requireActual( '@elementor/editor-v1-adapters' ),
19
25
  registerDataHook: jest.fn(),
26
+ getCanvasIframeDocument: jest.fn(),
20
27
  } ) );
21
28
 
22
29
  jest.mock( '../use-style-prop-resolver', () => ( {
@@ -513,4 +520,158 @@ describe( 'useStyleItems', () => {
513
520
  expect( renderStylesMock.mock.calls[ 0 ][ 0 ].styles[ 0 ].id ).toBe( 'style2' );
514
521
  expect( result.current ).toHaveLength( 3 );
515
522
  } );
523
+
524
+ describe( 'pregenerated links removal', () => {
525
+ let attachPreviewCallback: () => Promise< void >;
526
+ let canvasDocument: Document;
527
+
528
+ beforeEach( () => {
529
+ jest.mocked( removeProviderPregeneratedLinks ).mockClear();
530
+
531
+ canvasDocument = document.implementation.createHTMLDocument();
532
+ jest.mocked( getCanvasIframeDocument ).mockReturnValue( canvasDocument );
533
+
534
+ jest.mocked( registerDataHook ).mockImplementation( ( position, command, callback ) => {
535
+ if ( command === 'editor/documents/attach-preview' && position === 'after' ) {
536
+ attachPreviewCallback = callback as never;
537
+ }
538
+
539
+ return null as never;
540
+ } );
541
+ } );
542
+
543
+ const createProviderWithPregeneratedLinks = ( key: string, styles: StyleDefinition[] ) => ( {
544
+ ...createMockStylesProvider( { key }, styles ),
545
+ isPregeneratedLink: jest.fn(),
546
+ } );
547
+
548
+ it( 'should not remove pregenerated links before the canvas head is available', async () => {
549
+ // Arrange.
550
+ const provider = createProviderWithPregeneratedLinks( 'global-classes', [
551
+ createMockStyleDefinition( { id: 'style1' } ),
552
+ ] );
553
+
554
+ jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ provider ] );
555
+
556
+ const { result } = renderHook( () => useStyleItems( null ) );
557
+
558
+ // Act.
559
+ await act( async () => {
560
+ await attachPreviewCallback?.();
561
+ } );
562
+
563
+ await act( async () => {
564
+ await attachPreviewCallback?.();
565
+ } );
566
+
567
+ // Assert.
568
+ expect( result.current ).toHaveLength( 1 );
569
+ expect( removeProviderPregeneratedLinks ).not.toHaveBeenCalled();
570
+ } );
571
+
572
+ it( 'should remove pregenerated links once rendered items are committed into the canvas head', async () => {
573
+ // Arrange.
574
+ const provider = createProviderWithPregeneratedLinks( 'global-classes', [
575
+ createMockStyleDefinition( { id: 'style1' } ),
576
+ ] );
577
+
578
+ jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ provider ] );
579
+
580
+ const { rerender } = renderHook( ( { head } ) => useStyleItems( head ), {
581
+ initialProps: { head: null as HTMLElement | null },
582
+ } );
583
+
584
+ await act( async () => {
585
+ await attachPreviewCallback?.();
586
+ } );
587
+
588
+ // Act.
589
+ rerender( { head: canvasDocument.head } );
590
+
591
+ // Assert.
592
+ expect( removeProviderPregeneratedLinks ).toHaveBeenCalledTimes( 1 );
593
+ expect( removeProviderPregeneratedLinks ).toHaveBeenCalledWith(
594
+ 'global-classes',
595
+ provider.isPregeneratedLink
596
+ );
597
+ } );
598
+
599
+ it( 'should not remove pregenerated links while the portal targets a stale document head', async () => {
600
+ // Arrange.
601
+ const provider = createProviderWithPregeneratedLinks( 'global-classes', [
602
+ createMockStyleDefinition( { id: 'style1' } ),
603
+ ] );
604
+
605
+ jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ provider ] );
606
+
607
+ const staleDocument = document.implementation.createHTMLDocument();
608
+
609
+ const { rerender } = renderHook( ( { head } ) => useStyleItems( head ), {
610
+ initialProps: { head: staleDocument.head },
611
+ } );
612
+
613
+ // Act.
614
+ await act( async () => {
615
+ await attachPreviewCallback?.();
616
+ } );
617
+
618
+ // Assert.
619
+ expect( removeProviderPregeneratedLinks ).not.toHaveBeenCalled();
620
+
621
+ // Act.
622
+ rerender( { head: canvasDocument.head } );
623
+
624
+ // Assert.
625
+ expect( removeProviderPregeneratedLinks ).toHaveBeenCalledTimes( 1 );
626
+ } );
627
+
628
+ it( 'should keep pregenerated links while the provider has no rendered items', async () => {
629
+ // Arrange.
630
+ const placeholderStyle: StyleDefinition = {
631
+ id: 'g-placeholder',
632
+ type: 'class',
633
+ label: 'placeholder',
634
+ variants: [],
635
+ };
636
+
637
+ const provider = createProviderWithPregeneratedLinks( 'global-classes', [ placeholderStyle ] );
638
+
639
+ jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ provider ] );
640
+
641
+ const { result } = renderHook( () => useStyleItems( canvasDocument.head ) );
642
+
643
+ // Act.
644
+ await act( async () => {
645
+ await attachPreviewCallback?.();
646
+ } );
647
+
648
+ await act( async () => {
649
+ await attachPreviewCallback?.();
650
+ } );
651
+
652
+ // Assert.
653
+ expect( result.current ).toEqual( [] );
654
+ expect( removeProviderPregeneratedLinks ).not.toHaveBeenCalled();
655
+ } );
656
+
657
+ it( 'should not remove links for providers without pregenerated links', async () => {
658
+ // Arrange.
659
+ const provider = createMockStylesProvider( { key: 'provider1' }, [
660
+ createMockStyleDefinition( { id: 'style1' } ),
661
+ ] );
662
+
663
+ jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ provider ] );
664
+
665
+ const { result } = renderHook( () => useStyleItems( canvasDocument.head ) );
666
+
667
+ // Act.
668
+ await act( async () => {
669
+ await attachPreviewCallback?.();
670
+ } );
671
+
672
+ // Assert.
673
+ expect( result.current ).toHaveLength( 1 );
674
+ expect( removeProviderPregeneratedLinks ).not.toHaveBeenCalled();
675
+ } );
676
+ } );
516
677
  } );
@@ -0,0 +1,33 @@
1
+ import { useLayoutEffect } from 'react';
2
+ import { type StylesProvider } from '@elementor/editor-styles-repository';
3
+ import { getCanvasIframeDocument } from '@elementor/editor-v1-adapters';
4
+
5
+ import { type StyleItem } from '../renderers/create-styles-renderer';
6
+ import { removeProviderPregeneratedLinks } from '../utils/pregenerated-links-removal';
7
+
8
+ type RenderedProviders = Record< string, { provider: StylesProvider; items: StyleItem[] } >;
9
+
10
+ // The pregenerated <link /> css can only be dropped once the client-side <style /> tags replacing it are committed
11
+ // into the canvas, otherwise the canvas is painted without any of the provider's styles.
12
+ export function usePregeneratedLinksHandoff(
13
+ canvasHead: HTMLElement | null | undefined,
14
+ renderedProviders: RenderedProviders
15
+ ) {
16
+ useLayoutEffect( () => {
17
+ if ( ! isCurrentCanvasHead( canvasHead ) ) {
18
+ return;
19
+ }
20
+
21
+ Object.entries( renderedProviders ).forEach( ( [ providerKey, { provider, items } ] ) => {
22
+ if ( items.length > 0 && provider.isPregeneratedLink ) {
23
+ removeProviderPregeneratedLinks( providerKey, provider.isPregeneratedLink );
24
+ }
25
+ } );
26
+ }, [ canvasHead, renderedProviders ] );
27
+ }
28
+
29
+ // The portal container is resolved before the preview document replaces the iframe's initial document,
30
+ // so a stale head would let the live canvas lose its pregenerated links while the styles render elsewhere.
31
+ function isCurrentCanvasHead( canvasHead?: HTMLElement | null ): canvasHead is HTMLElement {
32
+ return !! canvasHead && canvasHead === getCanvasIframeDocument()?.head;
33
+ }
@@ -6,9 +6,10 @@ import { registerDataHook } from '@elementor/editor-v1-adapters';
6
6
 
7
7
  import { type RendererStyleDefinition, type StyleItem, type StyleRenderer } from '../renderers/create-styles-renderer';
8
8
  import { abortPreviousRuns } from '../utils/abort-previous-runs';
9
- import { removeProviderPregeneratedLinks, resetRemovedProviders } from '../utils/pregenerated-links-removal';
9
+ import { resetRemovedProviders } from '../utils/pregenerated-links-removal';
10
10
  import { signalizedProcess } from '../utils/signalized-process';
11
11
  import { useOnMount } from './use-on-mount';
12
+ import { usePregeneratedLinksHandoff } from './use-pregenerated-links-handoff';
12
13
  import { useStylePropResolver } from './use-style-prop-resolver';
13
14
  import { useStyleRenderer } from './use-style-renderer';
14
15
 
@@ -28,7 +29,7 @@ type ProviderAndSubscriber = {
28
29
 
29
30
  type ProviderAndStyleItemsMap = Record< string, ProviderAndStyleItems >;
30
31
 
31
- export function useStyleItems() {
32
+ export function useStyleItems( canvasHead?: HTMLElement | null ) {
32
33
  const resolve = useStylePropResolver();
33
34
  const renderStyles = useStyleRenderer( resolve );
34
35
  const breakpoints = useBreakpoints();
@@ -88,6 +89,8 @@ export function useStyleItems() {
88
89
  } );
89
90
  } );
90
91
 
92
+ usePregeneratedLinksHandoff( canvasHead, styleItems );
93
+
91
94
  const breakpointSorter = useMemo(
92
95
  () => createBreakpointSorter( breakpoints.map( ( breakpoint ) => breakpoint.id ) ),
93
96
  [ breakpoints ]
@@ -158,11 +161,6 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems, getC
158
161
  const hasDiffInfo = current !== undefined && previous !== undefined;
159
162
  const hasCache = cache.orderedIds.length > 0;
160
163
 
161
- if ( hasCache && provider.isPregeneratedLink ) {
162
- // if styles were rendered already (i.e. hasCache = true), we can safely remove the pregenerated css rules imported via <link /> tags
163
- removeProviderPregeneratedLinks( provider.getKey(), provider.isPregeneratedLink );
164
- }
165
-
166
164
  if ( hasDiffInfo && hasCache ) {
167
165
  return updateItems( cache, previous, current, signal );
168
166
  }
@@ -0,0 +1,106 @@
1
+ import { getContainer, getSelectedElements, getWidgetsCache } from '@elementor/editor-elements';
2
+
3
+ import { initSelectedElementResource } from '../selected-element-resource';
4
+
5
+ jest.mock( '@elementor/editor-elements', () => ( {
6
+ getContainer: jest.fn(),
7
+ getSelectedElements: jest.fn(),
8
+ getWidgetsCache: jest.fn(),
9
+ } ) );
10
+
11
+ jest.mock( '@elementor/editor-v1-adapters', () => ( {
12
+ __privateListenTo: jest.fn(),
13
+ commandEndEvent: jest.fn(),
14
+ } ) );
15
+
16
+ type ResourceHandler = () => Promise< { contents: { text: string }[] } >;
17
+
18
+ const stringProp = ( meta: Record< string, unknown > = {} ) => ( {
19
+ kind: 'plain',
20
+ key: 'string',
21
+ settings: {},
22
+ meta,
23
+ } );
24
+
25
+ const givenSelectedContainer = ( modelProps: Record< string, unknown >, settings: Record< string, unknown > ) => {
26
+ jest.mocked( getSelectedElements ).mockReturnValue( [ { id: 'el-1' } ] as never );
27
+ jest.mocked( getContainer ).mockReturnValue( {
28
+ id: 'el-1',
29
+ type: modelProps.widgetType ? 'widget' : ( modelProps.elType as string ),
30
+ model: {
31
+ get: ( key: string ) => modelProps[ key ],
32
+ config: { atomic: true },
33
+ },
34
+ settings: {
35
+ toJSON: () => settings,
36
+ },
37
+ } as never );
38
+ };
39
+
40
+ const readProperties = async () => {
41
+ const resource = jest.fn();
42
+ initSelectedElementResource( { resource, sendResourceUpdated: jest.fn() } as never );
43
+
44
+ const handler = resource.mock.calls[ 0 ][ 3 ] as ResourceHandler;
45
+ const result = await handler();
46
+
47
+ return JSON.parse( result.contents[ 0 ].text ).properties;
48
+ };
49
+
50
+ describe( 'selected-element-resource', () => {
51
+ beforeEach( () => {
52
+ jest.clearAllMocks();
53
+ } );
54
+
55
+ it( 'omits sensitive props of an atomic widget', async () => {
56
+ // Arrange.
57
+ jest.mocked( getWidgetsCache ).mockReturnValue( {
58
+ 'e-heading': {
59
+ atomic_props_schema: {
60
+ tag: stringProp(),
61
+ api_key: stringProp( { sensitive: true } ),
62
+ },
63
+ },
64
+ } as never );
65
+
66
+ givenSelectedContainer(
67
+ { elType: 'widget', widgetType: 'e-heading' },
68
+ {
69
+ tag: { $$type: 'string', value: 'h2' },
70
+ api_key: { $$type: 'string', value: 'secret' },
71
+ }
72
+ );
73
+
74
+ // Act.
75
+ const properties = await readProperties();
76
+
77
+ // Assert.
78
+ expect( properties ).toEqual( { tag: { $$type: 'string', value: 'h2' } } );
79
+ } );
80
+
81
+ it( 'omits sensitive props of an atomic element that has no widgetType', async () => {
82
+ // Arrange.
83
+ jest.mocked( getWidgetsCache ).mockReturnValue( {
84
+ 'e-form': {
85
+ atomic_props_schema: {
86
+ 'form-name': stringProp(),
87
+ 'mailchimp-api-key': stringProp( { sensitive: true } ),
88
+ },
89
+ },
90
+ } as never );
91
+
92
+ givenSelectedContainer(
93
+ { elType: 'e-form' },
94
+ {
95
+ 'form-name': { $$type: 'string', value: 'Contact' },
96
+ 'mailchimp-api-key': { $$type: 'string', value: 'secret' },
97
+ }
98
+ );
99
+
100
+ // Act.
101
+ const properties = await readProperties();
102
+
103
+ // Assert.
104
+ expect( properties ).toEqual( { 'form-name': { $$type: 'string', value: 'Contact' } } );
105
+ } );
106
+ } );