@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.js +155 -124
- package/dist/index.mjs +50 -19
- package/package.json +20 -20
- package/src/components/__tests__/style-renderer.test.tsx +1 -0
- package/src/components/style-renderer.tsx +1 -1
- package/src/hooks/__tests__/use-style-items.test.ts +162 -1
- package/src/hooks/use-pregenerated-links-handoff.ts +33 -0
- package/src/hooks/use-style-items.ts +5 -7
- package/src/mcp/resources/__tests__/selected-element-resource.test.ts +106 -0
- package/src/mcp/resources/selected-element-resource.ts +11 -1
- package/src/mcp/tools/configure-element/__tests__/tool.test.ts +53 -1
- package/src/mcp/tools/configure-element/tool.ts +1 -1
- package/src/renderers/__tests__/create-dom-renderer.test.ts +24 -0
- package/src/renderers/create-dom-renderer.ts +10 -0
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
|
|
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"), () =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 } =
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
6254
|
+
getCanvasIframeDocument as getCanvasIframeDocument6
|
|
6224
6255
|
} from "@elementor/editor-v1-adapters";
|
|
6225
6256
|
function useCanvasDocument() {
|
|
6226
|
-
return useListenTo6(commandEndEvent8("editor/documents/attach-preview"), () =>
|
|
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-
|
|
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-
|
|
41
|
-
"@elementor/editor-controls": "4.4.0-
|
|
42
|
-
"@elementor/editor-documents": "4.4.0-
|
|
43
|
-
"@elementor/editor-elements": "4.4.0-
|
|
44
|
-
"@elementor/editor-interactions": "4.4.0-
|
|
45
|
-
"@elementor/editor-mcp": "4.4.0-
|
|
46
|
-
"@elementor/editor-notifications": "4.4.0-
|
|
47
|
-
"@elementor/editor-props": "4.4.0-
|
|
48
|
-
"@elementor/editor-responsive": "4.4.0-
|
|
49
|
-
"@elementor/editor-styles": "4.4.0-
|
|
50
|
-
"@elementor/editor-styles-repository": "4.4.0-
|
|
51
|
-
"@elementor/editor-ui": "4.4.0-
|
|
52
|
-
"@elementor/editor-v1-adapters": "4.4.0-
|
|
53
|
-
"@elementor/events": "4.4.0-
|
|
54
|
-
"@elementor/http-client": "4.4.0-
|
|
55
|
-
"@elementor/schema": "4.4.0-
|
|
56
|
-
"@elementor/twing": "4.4.0-
|
|
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-
|
|
59
|
-
"@elementor/wp-media": "4.4.0-
|
|
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 {
|
|
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
|
+
} );
|