@elementor/editor-canvas 4.1.0-737 → 4.1.0-739
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 +79 -7
- package/dist/index.mjs +79 -7
- package/package.json +19 -18
- package/src/form-structure/__tests__/form-structure-utils.test.ts +70 -0
- package/src/form-structure/enforce-form-ancestor-commands.ts +8 -2
- package/src/form-structure/utils.ts +18 -2
- package/src/init-settings-transformers.ts +2 -0
- package/src/transformers/shared/__tests__/svg-src-transformer.test.ts +184 -0
- package/src/transformers/shared/svg-src-transformer.ts +87 -0
package/dist/index.js
CHANGED
|
@@ -1187,6 +1187,9 @@ function getArgsElementType(args) {
|
|
|
1187
1187
|
function getElementType(element) {
|
|
1188
1188
|
return element?.model.get("widgetType") || element?.model.get("elType");
|
|
1189
1189
|
}
|
|
1190
|
+
function getClipboardElementType(element) {
|
|
1191
|
+
return element?.widgetType || element?.elType;
|
|
1192
|
+
}
|
|
1190
1193
|
function isElementWithinFormSelector(element) {
|
|
1191
1194
|
return !!element?.view?.el?.closest('form,[data-element_type="e-form"]');
|
|
1192
1195
|
}
|
|
@@ -1204,7 +1207,7 @@ function hasElementTypes(element, types) {
|
|
|
1204
1207
|
}
|
|
1205
1208
|
function hasClipboardElementType(elements, type) {
|
|
1206
1209
|
return elements.some((element) => {
|
|
1207
|
-
const elementType = element
|
|
1210
|
+
const elementType = getClipboardElementType(element);
|
|
1208
1211
|
if (elementType === type) {
|
|
1209
1212
|
return true;
|
|
1210
1213
|
}
|
|
@@ -1213,13 +1216,22 @@ function hasClipboardElementType(elements, type) {
|
|
|
1213
1216
|
}
|
|
1214
1217
|
function hasClipboardElementTypes(elements, types) {
|
|
1215
1218
|
return elements.some((element) => {
|
|
1216
|
-
const elementType = element
|
|
1219
|
+
const elementType = getClipboardElementType(element);
|
|
1217
1220
|
if (elementType && types.has(elementType)) {
|
|
1218
1221
|
return true;
|
|
1219
1222
|
}
|
|
1220
1223
|
return element.elements ? hasClipboardElementTypes(element.elements, types) : false;
|
|
1221
1224
|
});
|
|
1222
1225
|
}
|
|
1226
|
+
function movedContainersIncludeAtomicFormRoot(containers) {
|
|
1227
|
+
return containers.some((container) => getElementType(container) === FORM_ELEMENT_TYPE);
|
|
1228
|
+
}
|
|
1229
|
+
function clipboardRootsAreAtomicForms(elements) {
|
|
1230
|
+
if (!elements.length) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
return elements.every((el) => getClipboardElementType(el) === FORM_ELEMENT_TYPE);
|
|
1234
|
+
}
|
|
1223
1235
|
|
|
1224
1236
|
// src/form-structure/enforce-form-ancestor-commands.ts
|
|
1225
1237
|
var FORM_FIELDS_OUTSIDE_ALERT = {
|
|
@@ -1257,7 +1269,7 @@ function blockFormFieldMove(args) {
|
|
|
1257
1269
|
const hasFormFieldElement = containers.some(
|
|
1258
1270
|
(container) => container ? hasElementTypes(container, FORM_FIELD_ELEMENT_TYPES) : false
|
|
1259
1271
|
);
|
|
1260
|
-
if (hasFormFieldElement && !isWithinForm(target)) {
|
|
1272
|
+
if (hasFormFieldElement && !isWithinForm(target) && !movedContainersIncludeAtomicFormRoot(containers)) {
|
|
1261
1273
|
handleBlockedFormField();
|
|
1262
1274
|
return true;
|
|
1263
1275
|
}
|
|
@@ -1273,7 +1285,7 @@ function blockFormFieldPaste(args) {
|
|
|
1273
1285
|
return false;
|
|
1274
1286
|
}
|
|
1275
1287
|
const hasFormFieldElement = hasClipboardElementTypes(data.clipboard.elements, FORM_FIELD_ELEMENT_TYPES);
|
|
1276
|
-
if (hasFormFieldElement && !isWithinForm(args.container)) {
|
|
1288
|
+
if (hasFormFieldElement && !isWithinForm(args.container) && !clipboardRootsAreAtomicForms(data.clipboard.elements)) {
|
|
1277
1289
|
handleBlockedFormField();
|
|
1278
1290
|
return true;
|
|
1279
1291
|
}
|
|
@@ -1453,14 +1465,74 @@ var plainTransformer = createTransformer((value) => {
|
|
|
1453
1465
|
return value;
|
|
1454
1466
|
});
|
|
1455
1467
|
|
|
1456
|
-
// src/transformers/shared/
|
|
1468
|
+
// src/transformers/shared/svg-src-transformer.ts
|
|
1469
|
+
var import_dompurify = __toESM(require("dompurify"));
|
|
1457
1470
|
var import_wp_media2 = require("@elementor/wp-media");
|
|
1471
|
+
var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
|
|
1472
|
+
function processSvgContent(svgText) {
|
|
1473
|
+
const sanitized = import_dompurify.default.sanitize(svgText, {
|
|
1474
|
+
USE_PROFILES: { svg: true, svgFilters: true }
|
|
1475
|
+
});
|
|
1476
|
+
const parser = new DOMParser();
|
|
1477
|
+
const doc = parser.parseFromString(sanitized, "image/svg+xml");
|
|
1478
|
+
const svgElement = doc.querySelector("svg");
|
|
1479
|
+
if (!svgElement) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
svgElement.setAttribute("fill", "currentColor");
|
|
1483
|
+
const existingStyle = svgElement.getAttribute("style") ?? "";
|
|
1484
|
+
const trimmed = existingStyle.trim();
|
|
1485
|
+
const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
|
|
1486
|
+
svgElement.setAttribute("style", merged);
|
|
1487
|
+
return svgElement.outerHTML;
|
|
1488
|
+
}
|
|
1489
|
+
async function fetchSvgContent(url, signal) {
|
|
1490
|
+
try {
|
|
1491
|
+
const response = await fetch(url, { signal });
|
|
1492
|
+
if (!response.ok) {
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1496
|
+
const isSvg = contentType.includes("svg") || contentType.includes("xml") || url.endsWith(".svg");
|
|
1497
|
+
if (!isSvg) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
return await response.text();
|
|
1501
|
+
} catch {
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
function resolveSvgSrcId(id) {
|
|
1506
|
+
if (typeof id !== "number" || id <= 0) {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
return id;
|
|
1510
|
+
}
|
|
1511
|
+
var svgSrcTransformer = createTransformer(async (value, { signal }) => {
|
|
1512
|
+
const id = resolveSvgSrcId(value.id);
|
|
1513
|
+
const urlFromValue = typeof value.url === "string" ? value.url : null;
|
|
1514
|
+
let url = urlFromValue;
|
|
1515
|
+
if (id && !urlFromValue) {
|
|
1516
|
+
const attachment = await (0, import_wp_media2.getMediaAttachment)({ id });
|
|
1517
|
+
url = attachment?.url ?? null;
|
|
1518
|
+
}
|
|
1519
|
+
const resolvedUrl = typeof url === "string" ? url : null;
|
|
1520
|
+
if (!resolvedUrl) {
|
|
1521
|
+
return { html: null, url: null };
|
|
1522
|
+
}
|
|
1523
|
+
const svgText = await fetchSvgContent(resolvedUrl, signal);
|
|
1524
|
+
const html = svgText ? processSvgContent(svgText) : null;
|
|
1525
|
+
return { html, url: resolvedUrl };
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
// src/transformers/shared/video-src-transformer.ts
|
|
1529
|
+
var import_wp_media3 = require("@elementor/wp-media");
|
|
1458
1530
|
var videoSrcTransformer = createTransformer(async (value) => {
|
|
1459
1531
|
const { id, url } = value;
|
|
1460
1532
|
if (!id) {
|
|
1461
1533
|
return { id: null, url };
|
|
1462
1534
|
}
|
|
1463
|
-
const attachment = await (0,
|
|
1535
|
+
const attachment = await (0, import_wp_media3.getMediaAttachment)({ id });
|
|
1464
1536
|
return {
|
|
1465
1537
|
id,
|
|
1466
1538
|
url: attachment?.url ?? url
|
|
@@ -1469,7 +1541,7 @@ var videoSrcTransformer = createTransformer(async (value) => {
|
|
|
1469
1541
|
|
|
1470
1542
|
// src/init-settings-transformers.ts
|
|
1471
1543
|
function initSettingsTransformers() {
|
|
1472
|
-
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);
|
|
1544
|
+
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);
|
|
1473
1545
|
}
|
|
1474
1546
|
|
|
1475
1547
|
// src/transformers/styles/background-color-overlay-transformer.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1153,6 +1153,9 @@ function getArgsElementType(args) {
|
|
|
1153
1153
|
function getElementType(element) {
|
|
1154
1154
|
return element?.model.get("widgetType") || element?.model.get("elType");
|
|
1155
1155
|
}
|
|
1156
|
+
function getClipboardElementType(element) {
|
|
1157
|
+
return element?.widgetType || element?.elType;
|
|
1158
|
+
}
|
|
1156
1159
|
function isElementWithinFormSelector(element) {
|
|
1157
1160
|
return !!element?.view?.el?.closest('form,[data-element_type="e-form"]');
|
|
1158
1161
|
}
|
|
@@ -1170,7 +1173,7 @@ function hasElementTypes(element, types) {
|
|
|
1170
1173
|
}
|
|
1171
1174
|
function hasClipboardElementType(elements, type) {
|
|
1172
1175
|
return elements.some((element) => {
|
|
1173
|
-
const elementType = element
|
|
1176
|
+
const elementType = getClipboardElementType(element);
|
|
1174
1177
|
if (elementType === type) {
|
|
1175
1178
|
return true;
|
|
1176
1179
|
}
|
|
@@ -1179,13 +1182,22 @@ function hasClipboardElementType(elements, type) {
|
|
|
1179
1182
|
}
|
|
1180
1183
|
function hasClipboardElementTypes(elements, types) {
|
|
1181
1184
|
return elements.some((element) => {
|
|
1182
|
-
const elementType = element
|
|
1185
|
+
const elementType = getClipboardElementType(element);
|
|
1183
1186
|
if (elementType && types.has(elementType)) {
|
|
1184
1187
|
return true;
|
|
1185
1188
|
}
|
|
1186
1189
|
return element.elements ? hasClipboardElementTypes(element.elements, types) : false;
|
|
1187
1190
|
});
|
|
1188
1191
|
}
|
|
1192
|
+
function movedContainersIncludeAtomicFormRoot(containers) {
|
|
1193
|
+
return containers.some((container) => getElementType(container) === FORM_ELEMENT_TYPE);
|
|
1194
|
+
}
|
|
1195
|
+
function clipboardRootsAreAtomicForms(elements) {
|
|
1196
|
+
if (!elements.length) {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
return elements.every((el) => getClipboardElementType(el) === FORM_ELEMENT_TYPE);
|
|
1200
|
+
}
|
|
1189
1201
|
|
|
1190
1202
|
// src/form-structure/enforce-form-ancestor-commands.ts
|
|
1191
1203
|
var FORM_FIELDS_OUTSIDE_ALERT = {
|
|
@@ -1223,7 +1235,7 @@ function blockFormFieldMove(args) {
|
|
|
1223
1235
|
const hasFormFieldElement = containers.some(
|
|
1224
1236
|
(container) => container ? hasElementTypes(container, FORM_FIELD_ELEMENT_TYPES) : false
|
|
1225
1237
|
);
|
|
1226
|
-
if (hasFormFieldElement && !isWithinForm(target)) {
|
|
1238
|
+
if (hasFormFieldElement && !isWithinForm(target) && !movedContainersIncludeAtomicFormRoot(containers)) {
|
|
1227
1239
|
handleBlockedFormField();
|
|
1228
1240
|
return true;
|
|
1229
1241
|
}
|
|
@@ -1239,7 +1251,7 @@ function blockFormFieldPaste(args) {
|
|
|
1239
1251
|
return false;
|
|
1240
1252
|
}
|
|
1241
1253
|
const hasFormFieldElement = hasClipboardElementTypes(data.clipboard.elements, FORM_FIELD_ELEMENT_TYPES);
|
|
1242
|
-
if (hasFormFieldElement && !isWithinForm(args.container)) {
|
|
1254
|
+
if (hasFormFieldElement && !isWithinForm(args.container) && !clipboardRootsAreAtomicForms(data.clipboard.elements)) {
|
|
1243
1255
|
handleBlockedFormField();
|
|
1244
1256
|
return true;
|
|
1245
1257
|
}
|
|
@@ -1419,14 +1431,74 @@ var plainTransformer = createTransformer((value) => {
|
|
|
1419
1431
|
return value;
|
|
1420
1432
|
});
|
|
1421
1433
|
|
|
1422
|
-
// src/transformers/shared/
|
|
1434
|
+
// src/transformers/shared/svg-src-transformer.ts
|
|
1435
|
+
import DOMPurify from "dompurify";
|
|
1423
1436
|
import { getMediaAttachment as getMediaAttachment2 } from "@elementor/wp-media";
|
|
1437
|
+
var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
|
|
1438
|
+
function processSvgContent(svgText) {
|
|
1439
|
+
const sanitized = DOMPurify.sanitize(svgText, {
|
|
1440
|
+
USE_PROFILES: { svg: true, svgFilters: true }
|
|
1441
|
+
});
|
|
1442
|
+
const parser = new DOMParser();
|
|
1443
|
+
const doc = parser.parseFromString(sanitized, "image/svg+xml");
|
|
1444
|
+
const svgElement = doc.querySelector("svg");
|
|
1445
|
+
if (!svgElement) {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
svgElement.setAttribute("fill", "currentColor");
|
|
1449
|
+
const existingStyle = svgElement.getAttribute("style") ?? "";
|
|
1450
|
+
const trimmed = existingStyle.trim();
|
|
1451
|
+
const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
|
|
1452
|
+
svgElement.setAttribute("style", merged);
|
|
1453
|
+
return svgElement.outerHTML;
|
|
1454
|
+
}
|
|
1455
|
+
async function fetchSvgContent(url, signal) {
|
|
1456
|
+
try {
|
|
1457
|
+
const response = await fetch(url, { signal });
|
|
1458
|
+
if (!response.ok) {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1462
|
+
const isSvg = contentType.includes("svg") || contentType.includes("xml") || url.endsWith(".svg");
|
|
1463
|
+
if (!isSvg) {
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
return await response.text();
|
|
1467
|
+
} catch {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function resolveSvgSrcId(id) {
|
|
1472
|
+
if (typeof id !== "number" || id <= 0) {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
return id;
|
|
1476
|
+
}
|
|
1477
|
+
var svgSrcTransformer = createTransformer(async (value, { signal }) => {
|
|
1478
|
+
const id = resolveSvgSrcId(value.id);
|
|
1479
|
+
const urlFromValue = typeof value.url === "string" ? value.url : null;
|
|
1480
|
+
let url = urlFromValue;
|
|
1481
|
+
if (id && !urlFromValue) {
|
|
1482
|
+
const attachment = await getMediaAttachment2({ id });
|
|
1483
|
+
url = attachment?.url ?? null;
|
|
1484
|
+
}
|
|
1485
|
+
const resolvedUrl = typeof url === "string" ? url : null;
|
|
1486
|
+
if (!resolvedUrl) {
|
|
1487
|
+
return { html: null, url: null };
|
|
1488
|
+
}
|
|
1489
|
+
const svgText = await fetchSvgContent(resolvedUrl, signal);
|
|
1490
|
+
const html = svgText ? processSvgContent(svgText) : null;
|
|
1491
|
+
return { html, url: resolvedUrl };
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
// src/transformers/shared/video-src-transformer.ts
|
|
1495
|
+
import { getMediaAttachment as getMediaAttachment3 } from "@elementor/wp-media";
|
|
1424
1496
|
var videoSrcTransformer = createTransformer(async (value) => {
|
|
1425
1497
|
const { id, url } = value;
|
|
1426
1498
|
if (!id) {
|
|
1427
1499
|
return { id: null, url };
|
|
1428
1500
|
}
|
|
1429
|
-
const attachment = await
|
|
1501
|
+
const attachment = await getMediaAttachment3({ id });
|
|
1430
1502
|
return {
|
|
1431
1503
|
id,
|
|
1432
1504
|
url: attachment?.url ?? url
|
|
@@ -1435,7 +1507,7 @@ var videoSrcTransformer = createTransformer(async (value) => {
|
|
|
1435
1507
|
|
|
1436
1508
|
// src/init-settings-transformers.ts
|
|
1437
1509
|
function initSettingsTransformers() {
|
|
1438
|
-
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);
|
|
1510
|
+
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);
|
|
1439
1511
|
}
|
|
1440
1512
|
|
|
1441
1513
|
// src/transformers/styles/background-color-overlay-transformer.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.1.0-
|
|
4
|
+
"version": "4.1.0-739",
|
|
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.1.0-
|
|
41
|
-
"
|
|
42
|
-
"@elementor/editor-
|
|
43
|
-
"@elementor/editor-
|
|
44
|
-
"@elementor/editor-
|
|
45
|
-
"@elementor/editor-
|
|
46
|
-
"@elementor/editor-
|
|
47
|
-
"@elementor/editor-
|
|
48
|
-
"@elementor/editor-
|
|
49
|
-
"@elementor/editor-
|
|
50
|
-
"@elementor/editor-styles
|
|
51
|
-
"@elementor/editor-
|
|
52
|
-
"@elementor/editor-
|
|
53
|
-
"@elementor/
|
|
54
|
-
"@elementor/
|
|
40
|
+
"@elementor/editor": "4.1.0-739",
|
|
41
|
+
"dompurify": "^3.2.6",
|
|
42
|
+
"@elementor/editor-controls": "4.1.0-739",
|
|
43
|
+
"@elementor/editor-documents": "4.1.0-739",
|
|
44
|
+
"@elementor/editor-elements": "4.1.0-739",
|
|
45
|
+
"@elementor/editor-interactions": "4.1.0-739",
|
|
46
|
+
"@elementor/editor-mcp": "4.1.0-739",
|
|
47
|
+
"@elementor/editor-notifications": "4.1.0-739",
|
|
48
|
+
"@elementor/editor-props": "4.1.0-739",
|
|
49
|
+
"@elementor/editor-responsive": "4.1.0-739",
|
|
50
|
+
"@elementor/editor-styles": "4.1.0-739",
|
|
51
|
+
"@elementor/editor-styles-repository": "4.1.0-739",
|
|
52
|
+
"@elementor/editor-ui": "4.1.0-739",
|
|
53
|
+
"@elementor/editor-v1-adapters": "4.1.0-739",
|
|
54
|
+
"@elementor/schema": "4.1.0-739",
|
|
55
|
+
"@elementor/twing": "4.1.0-739",
|
|
55
56
|
"@elementor/ui": "1.36.17",
|
|
56
|
-
"@elementor/utils": "4.1.0-
|
|
57
|
-
"@elementor/wp-media": "4.1.0-
|
|
57
|
+
"@elementor/utils": "4.1.0-739",
|
|
58
|
+
"@elementor/wp-media": "4.1.0-739",
|
|
58
59
|
"@floating-ui/react": "^0.27.5",
|
|
59
60
|
"@wordpress/i18n": "^5.13.0"
|
|
60
61
|
},
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { V1Element } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clipboardRootsAreAtomicForms,
|
|
5
|
+
FORM_ELEMENT_TYPE,
|
|
6
|
+
getClipboardElementType,
|
|
7
|
+
movedContainersIncludeAtomicFormRoot,
|
|
8
|
+
} from '../utils';
|
|
9
|
+
|
|
10
|
+
function mockElement( widgetType?: string, elType?: string ): V1Element {
|
|
11
|
+
return {
|
|
12
|
+
model: {
|
|
13
|
+
get: ( key: string ) => {
|
|
14
|
+
if ( key === 'widgetType' ) {
|
|
15
|
+
return widgetType;
|
|
16
|
+
}
|
|
17
|
+
if ( key === 'elType' ) {
|
|
18
|
+
return elType;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} as V1Element;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe( 'form-structure utils', () => {
|
|
27
|
+
describe( 'getClipboardElementType', () => {
|
|
28
|
+
it( 'prefers widgetType over elType', () => {
|
|
29
|
+
expect( getClipboardElementType( { widgetType: 'e-form-input', elType: 'widget' } ) ).toBe(
|
|
30
|
+
'e-form-input'
|
|
31
|
+
);
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'falls back to elType', () => {
|
|
35
|
+
expect( getClipboardElementType( { elType: FORM_ELEMENT_TYPE } ) ).toBe( FORM_ELEMENT_TYPE );
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'returns undefined when absent', () => {
|
|
39
|
+
expect( getClipboardElementType( {} ) ).toBeUndefined();
|
|
40
|
+
} );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
describe( 'movedContainersIncludeAtomicFormRoot', () => {
|
|
44
|
+
it( 'returns true when a top-level moved container is e-form', () => {
|
|
45
|
+
const form = mockElement( undefined, FORM_ELEMENT_TYPE );
|
|
46
|
+
|
|
47
|
+
expect( movedContainersIncludeAtomicFormRoot( [ form ] ) ).toBe( true );
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
it( 'returns false when moved containers are only form fields', () => {
|
|
51
|
+
const input = mockElement( 'e-form-input', 'widget' );
|
|
52
|
+
|
|
53
|
+
expect( movedContainersIncludeAtomicFormRoot( [ input ] ) ).toBe( false );
|
|
54
|
+
} );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
describe( 'clipboardRootsAreAtomicForms', () => {
|
|
58
|
+
it( 'returns true when every root is e-form', () => {
|
|
59
|
+
expect( clipboardRootsAreAtomicForms( [ { elType: FORM_ELEMENT_TYPE, elements: [] } ] ) ).toBe( true );
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
it( 'returns false when a root is a bare form field', () => {
|
|
63
|
+
expect( clipboardRootsAreAtomicForms( [ { widgetType: 'e-form-input', elements: [] } ] ) ).toBe( false );
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'returns false for empty roots', () => {
|
|
67
|
+
expect( clipboardRootsAreAtomicForms( [] ) ).toBe( false );
|
|
68
|
+
} );
|
|
69
|
+
} );
|
|
70
|
+
} );
|
|
@@ -3,6 +3,7 @@ import { blockCommand } from '@elementor/editor-v1-adapters';
|
|
|
3
3
|
import { __ } from '@wordpress/i18n';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
clipboardRootsAreAtomicForms,
|
|
6
7
|
type CreateArgs,
|
|
7
8
|
FORM_FIELD_ELEMENT_TYPES,
|
|
8
9
|
getArgsElementType,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
hasElementTypes,
|
|
11
12
|
isWithinForm,
|
|
12
13
|
type MoveArgs,
|
|
14
|
+
movedContainersIncludeAtomicFormRoot,
|
|
13
15
|
type PasteArgs,
|
|
14
16
|
type StorageContent,
|
|
15
17
|
} from './utils';
|
|
@@ -60,7 +62,7 @@ function blockFormFieldMove( args: MoveArgs ): boolean {
|
|
|
60
62
|
container ? hasElementTypes( container, FORM_FIELD_ELEMENT_TYPES ) : false
|
|
61
63
|
);
|
|
62
64
|
|
|
63
|
-
if ( hasFormFieldElement && ! isWithinForm( target ) ) {
|
|
65
|
+
if ( hasFormFieldElement && ! isWithinForm( target ) && ! movedContainersIncludeAtomicFormRoot( containers ) ) {
|
|
64
66
|
handleBlockedFormField();
|
|
65
67
|
|
|
66
68
|
return true;
|
|
@@ -86,7 +88,11 @@ function blockFormFieldPaste( args: PasteArgs ): boolean {
|
|
|
86
88
|
|
|
87
89
|
const hasFormFieldElement = hasClipboardElementTypes( data.clipboard.elements, FORM_FIELD_ELEMENT_TYPES );
|
|
88
90
|
|
|
89
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
hasFormFieldElement &&
|
|
93
|
+
! isWithinForm( args.container ) &&
|
|
94
|
+
! clipboardRootsAreAtomicForms( data.clipboard.elements )
|
|
95
|
+
) {
|
|
90
96
|
handleBlockedFormField();
|
|
91
97
|
|
|
92
98
|
return true;
|
|
@@ -50,6 +50,10 @@ export function getElementType( element?: V1Element ): string | undefined {
|
|
|
50
50
|
return element?.model.get( 'widgetType' ) || element?.model.get( 'elType' );
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export function getClipboardElementType( element?: ClipboardElement ): string | undefined {
|
|
54
|
+
return element?.widgetType || element?.elType;
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
export function isElementWithinFormSelector( element?: V1Element ): boolean {
|
|
54
58
|
return !! element?.view?.el?.closest( 'form,[data-element_type="e-form"]' );
|
|
55
59
|
}
|
|
@@ -72,7 +76,7 @@ export function hasElementTypes( element: V1Element, types: Set< string > ): boo
|
|
|
72
76
|
|
|
73
77
|
export function hasClipboardElementType( elements: ClipboardElement[], type: string ): boolean {
|
|
74
78
|
return elements.some( ( element ) => {
|
|
75
|
-
const elementType = element
|
|
79
|
+
const elementType = getClipboardElementType( element );
|
|
76
80
|
|
|
77
81
|
if ( elementType === type ) {
|
|
78
82
|
return true;
|
|
@@ -84,7 +88,7 @@ export function hasClipboardElementType( elements: ClipboardElement[], type: str
|
|
|
84
88
|
|
|
85
89
|
export function hasClipboardElementTypes( elements: ClipboardElement[], types: Set< string > ): boolean {
|
|
86
90
|
return elements.some( ( element ) => {
|
|
87
|
-
const elementType = element
|
|
91
|
+
const elementType = getClipboardElementType( element );
|
|
88
92
|
|
|
89
93
|
if ( elementType && types.has( elementType ) ) {
|
|
90
94
|
return true;
|
|
@@ -93,3 +97,15 @@ export function hasClipboardElementTypes( elements: ClipboardElement[], types: S
|
|
|
93
97
|
return element.elements ? hasClipboardElementTypes( element.elements, types ) : false;
|
|
94
98
|
} );
|
|
95
99
|
}
|
|
100
|
+
|
|
101
|
+
export function movedContainersIncludeAtomicFormRoot( containers: ( V1Element | undefined )[] ): boolean {
|
|
102
|
+
return containers.some( ( container ) => getElementType( container ) === FORM_ELEMENT_TYPE );
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function clipboardRootsAreAtomicForms( elements: ClipboardElement[] ): boolean {
|
|
106
|
+
if ( ! elements.length ) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return elements.every( ( el ) => getClipboardElementType( el ) === FORM_ELEMENT_TYPE );
|
|
111
|
+
}
|
|
@@ -9,6 +9,7 @@ import { queryTransformer } from './transformers/settings/query-transformer';
|
|
|
9
9
|
import { imageSrcTransformer } from './transformers/shared/image-src-transformer';
|
|
10
10
|
import { imageTransformer } from './transformers/shared/image-transformer';
|
|
11
11
|
import { plainTransformer } from './transformers/shared/plain-transformer';
|
|
12
|
+
import { svgSrcTransformer } from './transformers/shared/svg-src-transformer';
|
|
12
13
|
import { videoSrcTransformer } from './transformers/shared/video-src-transformer';
|
|
13
14
|
|
|
14
15
|
export function initSettingsTransformers() {
|
|
@@ -18,6 +19,7 @@ export function initSettingsTransformers() {
|
|
|
18
19
|
.register( 'query', queryTransformer )
|
|
19
20
|
.register( 'image', imageTransformer )
|
|
20
21
|
.register( 'image-src', imageSrcTransformer )
|
|
22
|
+
.register( 'svg-src', svgSrcTransformer )
|
|
21
23
|
.register( 'video-src', videoSrcTransformer )
|
|
22
24
|
.register( 'attributes', attributesTransformer )
|
|
23
25
|
.register( 'date-time', dateTimeTransformer )
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { getMediaAttachment } from '@elementor/wp-media';
|
|
2
|
+
|
|
3
|
+
import { svgSrcTransformer } from '../svg-src-transformer';
|
|
4
|
+
|
|
5
|
+
jest.mock( '@elementor/wp-media' );
|
|
6
|
+
|
|
7
|
+
const mockedGetMediaAttachment = jest.mocked( getMediaAttachment );
|
|
8
|
+
|
|
9
|
+
const SAMPLE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><path d="M0 0h100v100H0z"/></svg>';
|
|
10
|
+
|
|
11
|
+
const mockFetch = ( body: string, contentType = 'image/svg+xml', ok = true ) => {
|
|
12
|
+
global.fetch = jest.fn().mockResolvedValue( {
|
|
13
|
+
ok,
|
|
14
|
+
headers: new Headers( { 'content-type': contentType } ),
|
|
15
|
+
text: () => Promise.resolve( body ),
|
|
16
|
+
} );
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe( 'svgSrcTransformer', () => {
|
|
20
|
+
beforeEach( () => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
afterEach( () => {
|
|
25
|
+
jest.restoreAllMocks();
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'fetches SVG and returns processed html with currentColor fill', async () => {
|
|
29
|
+
// Arrange.
|
|
30
|
+
mockFetch( SAMPLE_SVG );
|
|
31
|
+
|
|
32
|
+
// Act.
|
|
33
|
+
const result = await svgSrcTransformer( { id: null, url: 'https://example.com/icon.svg' }, { key: 'svg' } );
|
|
34
|
+
|
|
35
|
+
// Assert.
|
|
36
|
+
expect( result ).toEqual( {
|
|
37
|
+
html: expect.stringContaining( 'fill="currentColor"' ),
|
|
38
|
+
url: 'https://example.com/icon.svg',
|
|
39
|
+
} );
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
it( 'adds inline styles to the svg element', async () => {
|
|
43
|
+
// Arrange.
|
|
44
|
+
mockFetch( SAMPLE_SVG );
|
|
45
|
+
|
|
46
|
+
// Act.
|
|
47
|
+
const result = ( await svgSrcTransformer(
|
|
48
|
+
{ id: null, url: 'https://example.com/icon.svg' },
|
|
49
|
+
{ key: 'svg' }
|
|
50
|
+
) ) as { html: string; url: string };
|
|
51
|
+
|
|
52
|
+
// Assert.
|
|
53
|
+
expect( result.html ).toContain( 'width: 100%' );
|
|
54
|
+
expect( result.html ).toContain( 'height: 100%' );
|
|
55
|
+
expect( result.html ).toContain( 'overflow: unset' );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'returns null html when url is null', async () => {
|
|
59
|
+
// Arrange & Act.
|
|
60
|
+
const result = await svgSrcTransformer( { id: null, url: null }, { key: 'svg' } );
|
|
61
|
+
|
|
62
|
+
// Assert.
|
|
63
|
+
expect( result ).toEqual( { html: null, url: null } );
|
|
64
|
+
expect( mockedGetMediaAttachment ).not.toHaveBeenCalled();
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
it( 'resolves url from attachment id then fetches SVG', async () => {
|
|
68
|
+
// Arrange.
|
|
69
|
+
mockedGetMediaAttachment.mockResolvedValue( {
|
|
70
|
+
url: 'https://example.com/resolved.svg',
|
|
71
|
+
} as never );
|
|
72
|
+
mockFetch( SAMPLE_SVG );
|
|
73
|
+
|
|
74
|
+
// Act.
|
|
75
|
+
const result = await svgSrcTransformer( { id: 42, url: null }, { key: 'svg' } );
|
|
76
|
+
|
|
77
|
+
// Assert.
|
|
78
|
+
expect( mockedGetMediaAttachment ).toHaveBeenCalledWith( { id: 42 } );
|
|
79
|
+
expect( result ).toEqual( {
|
|
80
|
+
html: expect.stringContaining( 'fill="currentColor"' ),
|
|
81
|
+
url: 'https://example.com/resolved.svg',
|
|
82
|
+
} );
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'falls back to provided url when attachment lookup fails', async () => {
|
|
86
|
+
// Arrange.
|
|
87
|
+
mockedGetMediaAttachment.mockResolvedValue( null as never );
|
|
88
|
+
mockFetch( SAMPLE_SVG );
|
|
89
|
+
|
|
90
|
+
// Act.
|
|
91
|
+
const result = ( await svgSrcTransformer(
|
|
92
|
+
{ id: 99, url: 'https://example.com/fallback.svg' },
|
|
93
|
+
{ key: 'svg' }
|
|
94
|
+
) ) as { html: string; url: string };
|
|
95
|
+
|
|
96
|
+
// Assert.
|
|
97
|
+
expect( result.url ).toBe( 'https://example.com/fallback.svg' );
|
|
98
|
+
expect( result.html ).toContain( 'fill="currentColor"' );
|
|
99
|
+
} );
|
|
100
|
+
|
|
101
|
+
it( 'returns null html when id is set but attachment and url are missing', async () => {
|
|
102
|
+
// Arrange.
|
|
103
|
+
mockedGetMediaAttachment.mockResolvedValue( null as never );
|
|
104
|
+
|
|
105
|
+
// Act.
|
|
106
|
+
const result = await svgSrcTransformer( { id: 7, url: null }, { key: 'svg' } );
|
|
107
|
+
|
|
108
|
+
// Assert.
|
|
109
|
+
expect( result ).toEqual( { html: null, url: null } );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'returns null html when fetch fails', async () => {
|
|
113
|
+
// Arrange.
|
|
114
|
+
mockFetch( '', 'image/svg+xml', false );
|
|
115
|
+
|
|
116
|
+
// Act.
|
|
117
|
+
const result = await svgSrcTransformer( { id: null, url: 'https://example.com/missing.svg' }, { key: 'svg' } );
|
|
118
|
+
|
|
119
|
+
// Assert.
|
|
120
|
+
expect( result ).toEqual( {
|
|
121
|
+
html: null,
|
|
122
|
+
url: 'https://example.com/missing.svg',
|
|
123
|
+
} );
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
it( 'returns null html when content is not SVG', async () => {
|
|
127
|
+
// Arrange.
|
|
128
|
+
mockFetch( '<html><body>Not SVG</body></html>', 'text/html' );
|
|
129
|
+
|
|
130
|
+
// Act.
|
|
131
|
+
const result = await svgSrcTransformer( { id: null, url: 'https://example.com/page.html' }, { key: 'svg' } );
|
|
132
|
+
|
|
133
|
+
// Assert.
|
|
134
|
+
expect( result ).toEqual( {
|
|
135
|
+
html: null,
|
|
136
|
+
url: 'https://example.com/page.html',
|
|
137
|
+
} );
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
it( 'merges with existing style attribute', async () => {
|
|
141
|
+
// Arrange.
|
|
142
|
+
const svgWithStyle = '<svg xmlns="http://www.w3.org/2000/svg" style="display: block"><path d="M0 0"/></svg>';
|
|
143
|
+
mockFetch( svgWithStyle );
|
|
144
|
+
|
|
145
|
+
// Act.
|
|
146
|
+
const result = ( await svgSrcTransformer(
|
|
147
|
+
{ id: null, url: 'https://example.com/styled.svg' },
|
|
148
|
+
{ key: 'svg' }
|
|
149
|
+
) ) as { html: string; url: string };
|
|
150
|
+
|
|
151
|
+
// Assert.
|
|
152
|
+
expect( result.html ).toContain( 'display: block' );
|
|
153
|
+
expect( result.html ).toContain( 'width: 100%' );
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
it( 'returns null html when response has no svg element', async () => {
|
|
157
|
+
// Arrange.
|
|
158
|
+
mockFetch( '<div>Not an SVG</div>' );
|
|
159
|
+
|
|
160
|
+
// Act.
|
|
161
|
+
const result = await svgSrcTransformer( { id: null, url: 'https://example.com/not-svg.svg' }, { key: 'svg' } );
|
|
162
|
+
|
|
163
|
+
// Assert.
|
|
164
|
+
expect( result ).toEqual( {
|
|
165
|
+
html: null,
|
|
166
|
+
url: 'https://example.com/not-svg.svg',
|
|
167
|
+
} );
|
|
168
|
+
} );
|
|
169
|
+
|
|
170
|
+
it( 'passes abort signal to fetch', async () => {
|
|
171
|
+
// Arrange.
|
|
172
|
+
mockFetch( SAMPLE_SVG );
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
|
|
175
|
+
// Act.
|
|
176
|
+
await svgSrcTransformer(
|
|
177
|
+
{ id: null, url: 'https://example.com/icon.svg' },
|
|
178
|
+
{ key: 'svg', signal: controller.signal }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Assert.
|
|
182
|
+
expect( global.fetch ).toHaveBeenCalledWith( 'https://example.com/icon.svg', { signal: controller.signal } );
|
|
183
|
+
} );
|
|
184
|
+
} );
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
import { getMediaAttachment } from '@elementor/wp-media';
|
|
3
|
+
|
|
4
|
+
import { createTransformer } from '../create-transformer';
|
|
5
|
+
import type { TransformerOptions } from '../types';
|
|
6
|
+
|
|
7
|
+
type SvgSrc = {
|
|
8
|
+
id?: unknown;
|
|
9
|
+
url?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SVG_INLINE_STYLES = 'width: 100%; height: 100%; overflow: unset;';
|
|
13
|
+
|
|
14
|
+
function processSvgContent( svgText: string ): string | null {
|
|
15
|
+
const sanitized = DOMPurify.sanitize( svgText, {
|
|
16
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
17
|
+
} );
|
|
18
|
+
|
|
19
|
+
const parser = new DOMParser();
|
|
20
|
+
const doc = parser.parseFromString( sanitized, 'image/svg+xml' );
|
|
21
|
+
const svgElement = doc.querySelector( 'svg' );
|
|
22
|
+
|
|
23
|
+
if ( ! svgElement ) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
svgElement.setAttribute( 'fill', 'currentColor' );
|
|
28
|
+
|
|
29
|
+
const existingStyle = svgElement.getAttribute( 'style' ) ?? '';
|
|
30
|
+
const trimmed = existingStyle.trim();
|
|
31
|
+
const merged = trimmed ? `${ trimmed.replace( /;$/, '' ) }; ${ SVG_INLINE_STYLES }` : SVG_INLINE_STYLES;
|
|
32
|
+
svgElement.setAttribute( 'style', merged );
|
|
33
|
+
|
|
34
|
+
return svgElement.outerHTML;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function fetchSvgContent( url: string, signal?: AbortSignal ): Promise< string | null > {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch( url, { signal } );
|
|
40
|
+
|
|
41
|
+
if ( ! response.ok ) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const contentType = response.headers.get( 'content-type' ) ?? '';
|
|
46
|
+
const isSvg = contentType.includes( 'svg' ) || contentType.includes( 'xml' ) || url.endsWith( '.svg' );
|
|
47
|
+
|
|
48
|
+
if ( ! isSvg ) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return await response.text();
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveSvgSrcId( id: unknown ): number | null {
|
|
59
|
+
if ( typeof id !== 'number' || id <= 0 ) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const svgSrcTransformer = createTransformer( async ( value: SvgSrc, { signal }: TransformerOptions ) => {
|
|
67
|
+
const id = resolveSvgSrcId( value.id );
|
|
68
|
+
const urlFromValue = typeof value.url === 'string' ? value.url : null;
|
|
69
|
+
|
|
70
|
+
let url: string | null | undefined = urlFromValue;
|
|
71
|
+
|
|
72
|
+
if ( id && ! urlFromValue ) {
|
|
73
|
+
const attachment = await getMediaAttachment( { id } );
|
|
74
|
+
url = attachment?.url ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const resolvedUrl = typeof url === 'string' ? url : null;
|
|
78
|
+
|
|
79
|
+
if ( ! resolvedUrl ) {
|
|
80
|
+
return { html: null, url: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const svgText = await fetchSvgContent( resolvedUrl, signal );
|
|
84
|
+
const html = svgText ? processSvgContent( svgText ) : null;
|
|
85
|
+
|
|
86
|
+
return { html, url: resolvedUrl };
|
|
87
|
+
} );
|