@elementor/editor-canvas 4.3.0-984 → 4.3.0-986
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 +64 -3
- package/dist/index.mjs +64 -3
- package/package.json +20 -20
- package/src/__tests__/parse-xml.ts +2 -0
- package/src/composition-builder/composition-builder.ts +12 -0
- package/src/form-structure/__tests__/form-structure-utils.test.ts +218 -0
- package/src/form-structure/utils.ts +46 -0
- package/src/mcp/tools/build-composition/tool.ts +9 -2
- package/src/utils/__tests__/grid-outline-utils.test.ts +39 -0
- package/src/utils/grid-outline-utils.ts +13 -2
package/dist/index.js
CHANGED
|
@@ -495,8 +495,8 @@ function toGridTracks(computedStyle) {
|
|
|
495
495
|
return {
|
|
496
496
|
columns: parseTrackList(computedStyle.gridTemplateColumns),
|
|
497
497
|
rows: parseTrackList(computedStyle.gridTemplateRows),
|
|
498
|
-
columnGap:
|
|
499
|
-
rowGap:
|
|
498
|
+
columnGap: resolveGapPx(computedStyle.columnGap, computedStyle.width),
|
|
499
|
+
rowGap: resolveGapPx(computedStyle.rowGap, computedStyle.height),
|
|
500
500
|
padding: {
|
|
501
501
|
top: toPx(computedStyle.paddingTop),
|
|
502
502
|
right: toPx(computedStyle.paddingRight),
|
|
@@ -571,6 +571,14 @@ function toPx(value) {
|
|
|
571
571
|
const parsed = parseFloat(value);
|
|
572
572
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
573
573
|
}
|
|
574
|
+
function resolveGapPx(value, referenceSize) {
|
|
575
|
+
if (value.trim().endsWith("%")) {
|
|
576
|
+
const percent = parseFloat(value);
|
|
577
|
+
const reference = parseFloat(referenceSize);
|
|
578
|
+
return Number.isFinite(percent) && Number.isFinite(reference) ? percent / 100 * reference : 0;
|
|
579
|
+
}
|
|
580
|
+
return toPx(value);
|
|
581
|
+
}
|
|
574
582
|
|
|
575
583
|
// src/hooks/use-grid-children.ts
|
|
576
584
|
var import_react3 = require("react");
|
|
@@ -2021,6 +2029,46 @@ function clipboardRootsAreAtomicForms(elements) {
|
|
|
2021
2029
|
}
|
|
2022
2030
|
return elements.every((el) => getClipboardElementType(el) === FORM_ELEMENT_TYPE);
|
|
2023
2031
|
}
|
|
2032
|
+
function hasFormAncestor(node) {
|
|
2033
|
+
return node.closest(FORM_ELEMENT_TYPE) !== null;
|
|
2034
|
+
}
|
|
2035
|
+
function collectFormAncestorErrors(xml) {
|
|
2036
|
+
const errors = [];
|
|
2037
|
+
for (const node of xml.querySelectorAll("*")) {
|
|
2038
|
+
if (!FORM_FIELD_ELEMENT_TYPES.has(node.tagName.toLowerCase())) {
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
if (hasFormAncestor(node)) {
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
const id = node.getAttribute("configuration-id");
|
|
2045
|
+
errors.push(
|
|
2046
|
+
`<${node.tagName}${id ? ` configuration-id="${id}"` : ""}> must be nested inside <e-form> (any ancestor depth is allowed).`
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
return errors;
|
|
2050
|
+
}
|
|
2051
|
+
function collectSubmitButtonErrors(xml) {
|
|
2052
|
+
const errors = [];
|
|
2053
|
+
for (const form of xml.querySelectorAll("e-form")) {
|
|
2054
|
+
const submitButtons = form.querySelectorAll("e-form-submit-button");
|
|
2055
|
+
if (submitButtons.length === 0) {
|
|
2056
|
+
errors.push(`<e-form> has no <e-form-submit-button>.`);
|
|
2057
|
+
} else if (submitButtons.length > 1) {
|
|
2058
|
+
errors.push(`<e-form> has ${submitButtons.length} submit buttons \u2014 only 1 is allowed.`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return errors;
|
|
2062
|
+
}
|
|
2063
|
+
function collectEmptyMessageErrors(xml) {
|
|
2064
|
+
const errors = [];
|
|
2065
|
+
for (const node of xml.querySelectorAll("e-form-success-message, e-form-error-message")) {
|
|
2066
|
+
if (node.children.length === 0) {
|
|
2067
|
+
errors.push(`<${node.tagName}> must have at least one child element (e.g. <e-atomic-paragraph>).`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return errors;
|
|
2071
|
+
}
|
|
2024
2072
|
|
|
2025
2073
|
// src/form-structure/enforce-form-ancestor-commands.ts
|
|
2026
2074
|
var FORM_FIELDS_OUTSIDE_ALERT = {
|
|
@@ -5195,6 +5243,11 @@ var CompositionBuilder = class _CompositionBuilder {
|
|
|
5195
5243
|
throw new Error(`Invalid element structure:
|
|
5196
5244
|
${childTypeErrors.join("\n")}`);
|
|
5197
5245
|
}
|
|
5246
|
+
const formErrors = [
|
|
5247
|
+
...collectFormAncestorErrors(this.xml),
|
|
5248
|
+
...collectSubmitButtonErrors(this.xml),
|
|
5249
|
+
...collectEmptyMessageErrors(this.xml)
|
|
5250
|
+
];
|
|
5198
5251
|
const children = Array.from(this.xml.children);
|
|
5199
5252
|
for (const childNode of children) {
|
|
5200
5253
|
const modelTree = this.buildModelTree(childNode, widgetsCache);
|
|
@@ -5229,6 +5282,7 @@ ${childTypeErrors.join("\n")}`);
|
|
|
5229
5282
|
return {
|
|
5230
5283
|
configErrors,
|
|
5231
5284
|
styleErrors,
|
|
5285
|
+
formErrors,
|
|
5232
5286
|
rootContainers: [...this.rootContainers]
|
|
5233
5287
|
};
|
|
5234
5288
|
}
|
|
@@ -5539,7 +5593,11 @@ var initBuildCompositionsTool = (reg) => {
|
|
|
5539
5593
|
compositionBuilder.setElementConfig(elementConfig);
|
|
5540
5594
|
compositionBuilder.setStylesConfig(stylesConfig);
|
|
5541
5595
|
compositionBuilder.setCustomCSS(customCSS);
|
|
5542
|
-
const {
|
|
5596
|
+
const {
|
|
5597
|
+
configErrors,
|
|
5598
|
+
formErrors,
|
|
5599
|
+
rootContainers: generatedRootContainers
|
|
5600
|
+
} = await compositionBuilder.build(targetContainer);
|
|
5543
5601
|
rootContainers.push(...generatedRootContainers);
|
|
5544
5602
|
generatedXML = new XMLSerializer().serializeToString(compositionBuilder.getXML());
|
|
5545
5603
|
rootContainers.forEach((container) => {
|
|
@@ -5554,6 +5612,9 @@ var initBuildCompositionsTool = (reg) => {
|
|
|
5554
5612
|
if (configErrors.length) {
|
|
5555
5613
|
errors.push(...configErrors.map((msg) => new Error(msg)));
|
|
5556
5614
|
}
|
|
5615
|
+
if (formErrors.length) {
|
|
5616
|
+
errors.push(...formErrors.map((msg) => new Error(msg)));
|
|
5617
|
+
}
|
|
5557
5618
|
} catch (error) {
|
|
5558
5619
|
errors.push(error);
|
|
5559
5620
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -433,8 +433,8 @@ function toGridTracks(computedStyle) {
|
|
|
433
433
|
return {
|
|
434
434
|
columns: parseTrackList(computedStyle.gridTemplateColumns),
|
|
435
435
|
rows: parseTrackList(computedStyle.gridTemplateRows),
|
|
436
|
-
columnGap:
|
|
437
|
-
rowGap:
|
|
436
|
+
columnGap: resolveGapPx(computedStyle.columnGap, computedStyle.width),
|
|
437
|
+
rowGap: resolveGapPx(computedStyle.rowGap, computedStyle.height),
|
|
438
438
|
padding: {
|
|
439
439
|
top: toPx(computedStyle.paddingTop),
|
|
440
440
|
right: toPx(computedStyle.paddingRight),
|
|
@@ -509,6 +509,14 @@ function toPx(value) {
|
|
|
509
509
|
const parsed = parseFloat(value);
|
|
510
510
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
511
511
|
}
|
|
512
|
+
function resolveGapPx(value, referenceSize) {
|
|
513
|
+
if (value.trim().endsWith("%")) {
|
|
514
|
+
const percent = parseFloat(value);
|
|
515
|
+
const reference = parseFloat(referenceSize);
|
|
516
|
+
return Number.isFinite(percent) && Number.isFinite(reference) ? percent / 100 * reference : 0;
|
|
517
|
+
}
|
|
518
|
+
return toPx(value);
|
|
519
|
+
}
|
|
512
520
|
|
|
513
521
|
// src/hooks/use-grid-children.ts
|
|
514
522
|
import { useEffect as useEffect3, useState as useState2 } from "react";
|
|
@@ -1975,6 +1983,46 @@ function clipboardRootsAreAtomicForms(elements) {
|
|
|
1975
1983
|
}
|
|
1976
1984
|
return elements.every((el) => getClipboardElementType(el) === FORM_ELEMENT_TYPE);
|
|
1977
1985
|
}
|
|
1986
|
+
function hasFormAncestor(node) {
|
|
1987
|
+
return node.closest(FORM_ELEMENT_TYPE) !== null;
|
|
1988
|
+
}
|
|
1989
|
+
function collectFormAncestorErrors(xml) {
|
|
1990
|
+
const errors = [];
|
|
1991
|
+
for (const node of xml.querySelectorAll("*")) {
|
|
1992
|
+
if (!FORM_FIELD_ELEMENT_TYPES.has(node.tagName.toLowerCase())) {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
if (hasFormAncestor(node)) {
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
const id = node.getAttribute("configuration-id");
|
|
1999
|
+
errors.push(
|
|
2000
|
+
`<${node.tagName}${id ? ` configuration-id="${id}"` : ""}> must be nested inside <e-form> (any ancestor depth is allowed).`
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
return errors;
|
|
2004
|
+
}
|
|
2005
|
+
function collectSubmitButtonErrors(xml) {
|
|
2006
|
+
const errors = [];
|
|
2007
|
+
for (const form of xml.querySelectorAll("e-form")) {
|
|
2008
|
+
const submitButtons = form.querySelectorAll("e-form-submit-button");
|
|
2009
|
+
if (submitButtons.length === 0) {
|
|
2010
|
+
errors.push(`<e-form> has no <e-form-submit-button>.`);
|
|
2011
|
+
} else if (submitButtons.length > 1) {
|
|
2012
|
+
errors.push(`<e-form> has ${submitButtons.length} submit buttons \u2014 only 1 is allowed.`);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return errors;
|
|
2016
|
+
}
|
|
2017
|
+
function collectEmptyMessageErrors(xml) {
|
|
2018
|
+
const errors = [];
|
|
2019
|
+
for (const node of xml.querySelectorAll("e-form-success-message, e-form-error-message")) {
|
|
2020
|
+
if (node.children.length === 0) {
|
|
2021
|
+
errors.push(`<${node.tagName}> must have at least one child element (e.g. <e-atomic-paragraph>).`);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return errors;
|
|
2025
|
+
}
|
|
1978
2026
|
|
|
1979
2027
|
// src/form-structure/enforce-form-ancestor-commands.ts
|
|
1980
2028
|
var FORM_FIELDS_OUTSIDE_ALERT = {
|
|
@@ -5180,6 +5228,11 @@ var CompositionBuilder = class _CompositionBuilder {
|
|
|
5180
5228
|
throw new Error(`Invalid element structure:
|
|
5181
5229
|
${childTypeErrors.join("\n")}`);
|
|
5182
5230
|
}
|
|
5231
|
+
const formErrors = [
|
|
5232
|
+
...collectFormAncestorErrors(this.xml),
|
|
5233
|
+
...collectSubmitButtonErrors(this.xml),
|
|
5234
|
+
...collectEmptyMessageErrors(this.xml)
|
|
5235
|
+
];
|
|
5183
5236
|
const children = Array.from(this.xml.children);
|
|
5184
5237
|
for (const childNode of children) {
|
|
5185
5238
|
const modelTree = this.buildModelTree(childNode, widgetsCache);
|
|
@@ -5214,6 +5267,7 @@ ${childTypeErrors.join("\n")}`);
|
|
|
5214
5267
|
return {
|
|
5215
5268
|
configErrors,
|
|
5216
5269
|
styleErrors,
|
|
5270
|
+
formErrors,
|
|
5217
5271
|
rootContainers: [...this.rootContainers]
|
|
5218
5272
|
};
|
|
5219
5273
|
}
|
|
@@ -5524,7 +5578,11 @@ var initBuildCompositionsTool = (reg) => {
|
|
|
5524
5578
|
compositionBuilder.setElementConfig(elementConfig);
|
|
5525
5579
|
compositionBuilder.setStylesConfig(stylesConfig);
|
|
5526
5580
|
compositionBuilder.setCustomCSS(customCSS);
|
|
5527
|
-
const {
|
|
5581
|
+
const {
|
|
5582
|
+
configErrors,
|
|
5583
|
+
formErrors,
|
|
5584
|
+
rootContainers: generatedRootContainers
|
|
5585
|
+
} = await compositionBuilder.build(targetContainer);
|
|
5528
5586
|
rootContainers.push(...generatedRootContainers);
|
|
5529
5587
|
generatedXML = new XMLSerializer().serializeToString(compositionBuilder.getXML());
|
|
5530
5588
|
rootContainers.forEach((container) => {
|
|
@@ -5539,6 +5597,9 @@ var initBuildCompositionsTool = (reg) => {
|
|
|
5539
5597
|
if (configErrors.length) {
|
|
5540
5598
|
errors.push(...configErrors.map((msg) => new Error(msg)));
|
|
5541
5599
|
}
|
|
5600
|
+
if (formErrors.length) {
|
|
5601
|
+
errors.push(...formErrors.map((msg) => new Error(msg)));
|
|
5602
|
+
}
|
|
5542
5603
|
} catch (error) {
|
|
5543
5604
|
errors.push(error);
|
|
5544
5605
|
}
|
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.3.0-
|
|
4
|
+
"version": "4.3.0-986",
|
|
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.3.0-
|
|
41
|
-
"@elementor/editor-controls": "4.3.0-
|
|
42
|
-
"@elementor/editor-documents": "4.3.0-
|
|
43
|
-
"@elementor/editor-elements": "4.3.0-
|
|
44
|
-
"@elementor/editor-interactions": "4.3.0-
|
|
45
|
-
"@elementor/editor-mcp": "4.3.0-
|
|
46
|
-
"@elementor/editor-notifications": "4.3.0-
|
|
47
|
-
"@elementor/editor-props": "4.3.0-
|
|
48
|
-
"@elementor/editor-responsive": "4.3.0-
|
|
49
|
-
"@elementor/editor-styles": "4.3.0-
|
|
50
|
-
"@elementor/editor-styles-repository": "4.3.0-
|
|
51
|
-
"@elementor/editor-ui": "4.3.0-
|
|
52
|
-
"@elementor/editor-v1-adapters": "4.3.0-
|
|
53
|
-
"@elementor/events": "4.3.0-
|
|
54
|
-
"@elementor/http-client": "4.3.0-
|
|
55
|
-
"@elementor/schema": "4.3.0-
|
|
56
|
-
"@elementor/twing": "4.3.0-
|
|
40
|
+
"@elementor/editor": "4.3.0-986",
|
|
41
|
+
"@elementor/editor-controls": "4.3.0-986",
|
|
42
|
+
"@elementor/editor-documents": "4.3.0-986",
|
|
43
|
+
"@elementor/editor-elements": "4.3.0-986",
|
|
44
|
+
"@elementor/editor-interactions": "4.3.0-986",
|
|
45
|
+
"@elementor/editor-mcp": "4.3.0-986",
|
|
46
|
+
"@elementor/editor-notifications": "4.3.0-986",
|
|
47
|
+
"@elementor/editor-props": "4.3.0-986",
|
|
48
|
+
"@elementor/editor-responsive": "4.3.0-986",
|
|
49
|
+
"@elementor/editor-styles": "4.3.0-986",
|
|
50
|
+
"@elementor/editor-styles-repository": "4.3.0-986",
|
|
51
|
+
"@elementor/editor-ui": "4.3.0-986",
|
|
52
|
+
"@elementor/editor-v1-adapters": "4.3.0-986",
|
|
53
|
+
"@elementor/events": "4.3.0-986",
|
|
54
|
+
"@elementor/http-client": "4.3.0-986",
|
|
55
|
+
"@elementor/schema": "4.3.0-986",
|
|
56
|
+
"@elementor/twing": "4.3.0-986",
|
|
57
57
|
"@elementor/ui": "1.37.5",
|
|
58
|
-
"@elementor/utils": "4.3.0-
|
|
59
|
-
"@elementor/wp-media": "4.3.0-
|
|
58
|
+
"@elementor/utils": "4.3.0-986",
|
|
59
|
+
"@elementor/wp-media": "4.3.0-986",
|
|
60
60
|
"@floating-ui/react": "^0.27.5",
|
|
61
61
|
"@wordpress/i18n": "^5.13.0",
|
|
62
62
|
"dompurify": "^3.2.6"
|
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
} from '@elementor/editor-elements';
|
|
12
12
|
import { type z } from '@elementor/schema';
|
|
13
13
|
|
|
14
|
+
import {
|
|
15
|
+
collectEmptyMessageErrors,
|
|
16
|
+
collectFormAncestorErrors,
|
|
17
|
+
collectSubmitButtonErrors,
|
|
18
|
+
} from '../form-structure/utils';
|
|
14
19
|
import { doUpdateElementProperty } from '../mcp/utils/do-update-element-property';
|
|
15
20
|
import { mergeCustomCssText } from '../mcp/utils/merge-custom-css';
|
|
16
21
|
import { RequiredChildrenEnforcer } from './utils/required-children-enforcer';
|
|
@@ -294,6 +299,12 @@ export class CompositionBuilder {
|
|
|
294
299
|
throw new Error( `Invalid element structure:\n${ childTypeErrors.join( '\n' ) }` );
|
|
295
300
|
}
|
|
296
301
|
|
|
302
|
+
const formErrors = [
|
|
303
|
+
...collectFormAncestorErrors( this.xml ),
|
|
304
|
+
...collectSubmitButtonErrors( this.xml ),
|
|
305
|
+
...collectEmptyMessageErrors( this.xml ),
|
|
306
|
+
];
|
|
307
|
+
|
|
297
308
|
const children = Array.from( this.xml.children );
|
|
298
309
|
for ( const childNode of children ) {
|
|
299
310
|
const modelTree = this.buildModelTree( childNode, widgetsCache );
|
|
@@ -332,6 +343,7 @@ export class CompositionBuilder {
|
|
|
332
343
|
return {
|
|
333
344
|
configErrors,
|
|
334
345
|
styleErrors,
|
|
346
|
+
formErrors,
|
|
335
347
|
rootContainers: [ ...this.rootContainers ],
|
|
336
348
|
};
|
|
337
349
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { V1Element } from '@elementor/editor-elements';
|
|
2
2
|
|
|
3
|
+
import { parseXml } from '../../__tests__/parse-xml';
|
|
3
4
|
import {
|
|
4
5
|
clipboardRootsAreAtomicForms,
|
|
6
|
+
collectEmptyMessageErrors,
|
|
7
|
+
collectFormAncestorErrors,
|
|
8
|
+
collectSubmitButtonErrors,
|
|
5
9
|
FORM_ELEMENT_TYPE,
|
|
6
10
|
getClipboardElementType,
|
|
7
11
|
movedContainersIncludeAtomicFormRoot,
|
|
@@ -67,4 +71,218 @@ describe( 'form-structure utils', () => {
|
|
|
67
71
|
expect( clipboardRootsAreAtomicForms( [] ) ).toBe( false );
|
|
68
72
|
} );
|
|
69
73
|
} );
|
|
74
|
+
|
|
75
|
+
describe( 'collectFormAncestorErrors', () => {
|
|
76
|
+
it( 'returns no errors when form field is a direct child of e-form', () => {
|
|
77
|
+
const xml = parseXml( `
|
|
78
|
+
<e-form>
|
|
79
|
+
<e-form-input />
|
|
80
|
+
</e-form>
|
|
81
|
+
` );
|
|
82
|
+
expect( collectFormAncestorErrors( xml ) ).toHaveLength( 0 );
|
|
83
|
+
} );
|
|
84
|
+
it( 'returns no errors when form field is a deep descendant of e-form (valid)', () => {
|
|
85
|
+
const xml = parseXml( `
|
|
86
|
+
<e-form>
|
|
87
|
+
<e-flexbox>
|
|
88
|
+
<e-form-input />
|
|
89
|
+
</e-flexbox>
|
|
90
|
+
</e-form>
|
|
91
|
+
` );
|
|
92
|
+
expect( collectFormAncestorErrors( xml ) ).toHaveLength( 0 );
|
|
93
|
+
} );
|
|
94
|
+
it( 'returns no errors when e-form wraps multiple containers with fields (valid)', () => {
|
|
95
|
+
const xml = parseXml( `
|
|
96
|
+
<e-flexbox>
|
|
97
|
+
<e-form>
|
|
98
|
+
<e-form-input />
|
|
99
|
+
</e-form>
|
|
100
|
+
</e-flexbox>
|
|
101
|
+
` );
|
|
102
|
+
expect( collectFormAncestorErrors( xml ) ).toHaveLength( 0 );
|
|
103
|
+
} );
|
|
104
|
+
it( 'returns an error when form field has no e-form ancestor (invalid)', () => {
|
|
105
|
+
const xml = parseXml( `
|
|
106
|
+
<e-flexbox>
|
|
107
|
+
<e-flexbox>
|
|
108
|
+
<e-form-input />
|
|
109
|
+
</e-flexbox>
|
|
110
|
+
</e-flexbox>
|
|
111
|
+
` );
|
|
112
|
+
const errors = collectFormAncestorErrors( xml );
|
|
113
|
+
expect( errors ).toHaveLength( 1 );
|
|
114
|
+
expect( errors[ 0 ] ).toContain( 'e-form-input' );
|
|
115
|
+
expect( errors[ 0 ] ).toContain( 'e-form' );
|
|
116
|
+
} );
|
|
117
|
+
it( 'returns an error for each orphaned form field', () => {
|
|
118
|
+
const xml = parseXml( `
|
|
119
|
+
<e-flexbox>
|
|
120
|
+
<e-form-input />
|
|
121
|
+
<e-form-label />
|
|
122
|
+
</e-flexbox>
|
|
123
|
+
` );
|
|
124
|
+
expect( collectFormAncestorErrors( xml ) ).toHaveLength( 2 );
|
|
125
|
+
} );
|
|
126
|
+
it( 'includes configuration-id in the error message when present', () => {
|
|
127
|
+
const xml = parseXml( `
|
|
128
|
+
<e-flexbox>
|
|
129
|
+
<e-form-input configuration-id="my-input" />
|
|
130
|
+
</e-flexbox>
|
|
131
|
+
` );
|
|
132
|
+
const errors = collectFormAncestorErrors( xml );
|
|
133
|
+
expect( errors[ 0 ] ).toContain( 'configuration-id="my-input"' );
|
|
134
|
+
} );
|
|
135
|
+
it( 'returns no errors when there are no form fields at all', () => {
|
|
136
|
+
const xml = parseXml( `
|
|
137
|
+
<e-flexbox>
|
|
138
|
+
<e-heading />
|
|
139
|
+
</e-flexbox>
|
|
140
|
+
` );
|
|
141
|
+
expect( collectFormAncestorErrors( xml ) ).toHaveLength( 0 );
|
|
142
|
+
} );
|
|
143
|
+
} );
|
|
144
|
+
|
|
145
|
+
describe( 'collectSubmitButtonErrors', () => {
|
|
146
|
+
it( 'returns no errors when e-form has exactly one submit button', () => {
|
|
147
|
+
const xml = parseXml( `
|
|
148
|
+
<e-form>
|
|
149
|
+
<e-form-input />
|
|
150
|
+
<e-form-submit-button />
|
|
151
|
+
</e-form>
|
|
152
|
+
` );
|
|
153
|
+
expect( collectSubmitButtonErrors( xml ) ).toHaveLength( 0 );
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
it( 'returns no errors when submit button is nested inside a container within e-form', () => {
|
|
157
|
+
const xml = parseXml( `
|
|
158
|
+
<e-form>
|
|
159
|
+
<e-flexbox>
|
|
160
|
+
<e-form-submit-button />
|
|
161
|
+
</e-flexbox>
|
|
162
|
+
</e-form>
|
|
163
|
+
` );
|
|
164
|
+
expect( collectSubmitButtonErrors( xml ) ).toHaveLength( 0 );
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'returns an error when e-form has no submit button', () => {
|
|
168
|
+
const xml = parseXml( `
|
|
169
|
+
<e-form>
|
|
170
|
+
<e-form-input />
|
|
171
|
+
</e-form>
|
|
172
|
+
` );
|
|
173
|
+
const errors = collectSubmitButtonErrors( xml );
|
|
174
|
+
expect( errors ).toHaveLength( 1 );
|
|
175
|
+
expect( errors[ 0 ] ).toContain( 'e-form-submit-button' );
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
it( 'returns an error when e-form has more than one submit button', () => {
|
|
179
|
+
const xml = parseXml( `
|
|
180
|
+
<e-form>
|
|
181
|
+
<e-form-submit-button />
|
|
182
|
+
<e-form-submit-button />
|
|
183
|
+
</e-form>
|
|
184
|
+
` );
|
|
185
|
+
const errors = collectSubmitButtonErrors( xml );
|
|
186
|
+
expect( errors ).toHaveLength( 1 );
|
|
187
|
+
expect( errors[ 0 ] ).toContain( '2' );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
it( 'returns one error per e-form that is missing a submit button', () => {
|
|
191
|
+
const xml = parseXml( `
|
|
192
|
+
<e-form>
|
|
193
|
+
<e-form-input />
|
|
194
|
+
</e-form>
|
|
195
|
+
<e-form>
|
|
196
|
+
<e-form-input />
|
|
197
|
+
</e-form>
|
|
198
|
+
` );
|
|
199
|
+
expect( collectSubmitButtonErrors( xml ) ).toHaveLength( 2 );
|
|
200
|
+
} );
|
|
201
|
+
|
|
202
|
+
it( 'returns no errors when there are no e-form elements', () => {
|
|
203
|
+
const xml = parseXml( `
|
|
204
|
+
<e-flexbox>
|
|
205
|
+
<e-heading />
|
|
206
|
+
</e-flexbox>
|
|
207
|
+
` );
|
|
208
|
+
expect( collectSubmitButtonErrors( xml ) ).toHaveLength( 0 );
|
|
209
|
+
} );
|
|
210
|
+
} );
|
|
211
|
+
|
|
212
|
+
describe( 'collectEmptyMessageErrors', () => {
|
|
213
|
+
it( 'returns no errors when success message has children', () => {
|
|
214
|
+
const xml = parseXml( `
|
|
215
|
+
<e-form>
|
|
216
|
+
<e-form-success-message>
|
|
217
|
+
<e-atomic-paragraph />
|
|
218
|
+
</e-form-success-message>
|
|
219
|
+
</e-form>
|
|
220
|
+
` );
|
|
221
|
+
expect( collectEmptyMessageErrors( xml ) ).toHaveLength( 0 );
|
|
222
|
+
} );
|
|
223
|
+
|
|
224
|
+
it( 'returns no errors when error message has children', () => {
|
|
225
|
+
const xml = parseXml( `
|
|
226
|
+
<e-form>
|
|
227
|
+
<e-form-error-message>
|
|
228
|
+
<e-atomic-paragraph />
|
|
229
|
+
</e-form-error-message>
|
|
230
|
+
</e-form>
|
|
231
|
+
` );
|
|
232
|
+
expect( collectEmptyMessageErrors( xml ) ).toHaveLength( 0 );
|
|
233
|
+
} );
|
|
234
|
+
|
|
235
|
+
it( 'returns an error when success message is empty', () => {
|
|
236
|
+
const xml = parseXml( `
|
|
237
|
+
<e-form>
|
|
238
|
+
<e-form-success-message />
|
|
239
|
+
</e-form>
|
|
240
|
+
` );
|
|
241
|
+
const errors = collectEmptyMessageErrors( xml );
|
|
242
|
+
expect( errors ).toHaveLength( 1 );
|
|
243
|
+
expect( errors[ 0 ] ).toContain( 'e-form-success-message' );
|
|
244
|
+
} );
|
|
245
|
+
|
|
246
|
+
it( 'returns an error when error message is empty', () => {
|
|
247
|
+
const xml = parseXml( `
|
|
248
|
+
<e-form>
|
|
249
|
+
<e-form-error-message />
|
|
250
|
+
</e-form>
|
|
251
|
+
` );
|
|
252
|
+
const errors = collectEmptyMessageErrors( xml );
|
|
253
|
+
expect( errors ).toHaveLength( 1 );
|
|
254
|
+
expect( errors[ 0 ] ).toContain( 'e-form-error-message' );
|
|
255
|
+
} );
|
|
256
|
+
|
|
257
|
+
it( 'returns two errors when both success and error messages are empty', () => {
|
|
258
|
+
const xml = parseXml( `
|
|
259
|
+
<e-form>
|
|
260
|
+
<e-form-success-message />
|
|
261
|
+
<e-form-error-message />
|
|
262
|
+
</e-form>
|
|
263
|
+
` );
|
|
264
|
+
expect( collectEmptyMessageErrors( xml ) ).toHaveLength( 2 );
|
|
265
|
+
} );
|
|
266
|
+
|
|
267
|
+
it( 'returns no errors when there are no message elements', () => {
|
|
268
|
+
const xml = parseXml( `
|
|
269
|
+
<e-form>
|
|
270
|
+
<e-form-input />
|
|
271
|
+
<e-form-submit-button />
|
|
272
|
+
</e-form>
|
|
273
|
+
` );
|
|
274
|
+
expect( collectEmptyMessageErrors( xml ) ).toHaveLength( 0 );
|
|
275
|
+
} );
|
|
276
|
+
|
|
277
|
+
it( 'returns no errors when message element is outside e-form but has children', () => {
|
|
278
|
+
const xml = parseXml( `
|
|
279
|
+
<e-flexbox>
|
|
280
|
+
<e-form-success-message>
|
|
281
|
+
<e-atomic-paragraph />
|
|
282
|
+
</e-form-success-message>
|
|
283
|
+
</e-flexbox>
|
|
284
|
+
` );
|
|
285
|
+
expect( collectEmptyMessageErrors( xml ) ).toHaveLength( 0 );
|
|
286
|
+
} );
|
|
287
|
+
} );
|
|
70
288
|
} );
|
|
@@ -115,3 +115,49 @@ export function clipboardRootsAreAtomicForms( elements: ClipboardElement[] ): bo
|
|
|
115
115
|
|
|
116
116
|
return elements.every( ( el ) => getClipboardElementType( el ) === FORM_ELEMENT_TYPE );
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
export function hasFormAncestor( node: Element ): boolean {
|
|
120
|
+
return node.closest( FORM_ELEMENT_TYPE ) !== null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function collectFormAncestorErrors( xml: Document ): string[] {
|
|
124
|
+
const errors: string[] = [];
|
|
125
|
+
for ( const node of xml.querySelectorAll( '*' ) ) {
|
|
126
|
+
if ( ! FORM_FIELD_ELEMENT_TYPES.has( node.tagName.toLowerCase() ) ) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if ( hasFormAncestor( node ) ) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const id = node.getAttribute( 'configuration-id' );
|
|
133
|
+
errors.push(
|
|
134
|
+
`<${ node.tagName }${
|
|
135
|
+
id ? ` configuration-id="${ id }"` : ''
|
|
136
|
+
}> must be nested inside <e-form> (any ancestor depth is allowed).`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return errors;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function collectSubmitButtonErrors( xml: Document ): string[] {
|
|
143
|
+
const errors: string[] = [];
|
|
144
|
+
for ( const form of xml.querySelectorAll( 'e-form' ) ) {
|
|
145
|
+
const submitButtons = form.querySelectorAll( 'e-form-submit-button' );
|
|
146
|
+
if ( submitButtons.length === 0 ) {
|
|
147
|
+
errors.push( `<e-form> has no <e-form-submit-button>.` );
|
|
148
|
+
} else if ( submitButtons.length > 1 ) {
|
|
149
|
+
errors.push( `<e-form> has ${ submitButtons.length } submit buttons — only 1 is allowed.` );
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return errors;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function collectEmptyMessageErrors( xml: Document ): string[] {
|
|
156
|
+
const errors: string[] = [];
|
|
157
|
+
for ( const node of xml.querySelectorAll( 'e-form-success-message, e-form-error-message' ) ) {
|
|
158
|
+
if ( node.children.length === 0 ) {
|
|
159
|
+
errors.push( `<${ node.tagName }> must have at least one child element (e.g. <e-atomic-paragraph>).` );
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return errors;
|
|
163
|
+
}
|
|
@@ -84,8 +84,11 @@ export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
|
|
|
84
84
|
compositionBuilder.setStylesConfig( stylesConfig );
|
|
85
85
|
compositionBuilder.setCustomCSS( customCSS );
|
|
86
86
|
|
|
87
|
-
const {
|
|
88
|
-
|
|
87
|
+
const {
|
|
88
|
+
configErrors,
|
|
89
|
+
formErrors,
|
|
90
|
+
rootContainers: generatedRootContainers,
|
|
91
|
+
} = await compositionBuilder.build( targetContainer );
|
|
89
92
|
|
|
90
93
|
rootContainers.push( ...generatedRootContainers );
|
|
91
94
|
generatedXML = new XMLSerializer().serializeToString( compositionBuilder.getXML() );
|
|
@@ -105,6 +108,10 @@ export const initBuildCompositionsTool = ( reg: MCPRegistryEntry ) => {
|
|
|
105
108
|
if ( configErrors.length ) {
|
|
106
109
|
errors.push( ...configErrors.map( ( msg ) => new Error( msg ) ) );
|
|
107
110
|
}
|
|
111
|
+
|
|
112
|
+
if ( formErrors.length ) {
|
|
113
|
+
errors.push( ...formErrors.map( ( msg ) => new Error( msg ) ) );
|
|
114
|
+
}
|
|
108
115
|
} catch ( error ) {
|
|
109
116
|
errors.push( error as Error );
|
|
110
117
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
computeCellRects,
|
|
4
4
|
computeGridLines,
|
|
5
5
|
parseTrackList,
|
|
6
|
+
resolveGapPx,
|
|
6
7
|
snapToHalfPixel,
|
|
7
8
|
toGridTracks,
|
|
8
9
|
toPx,
|
|
@@ -215,11 +216,29 @@ describe( 'toPx', () => {
|
|
|
215
216
|
} );
|
|
216
217
|
} );
|
|
217
218
|
|
|
219
|
+
describe( 'resolveGapPx', () => {
|
|
220
|
+
it( 'resolves a percentage gap against the reference size', () => {
|
|
221
|
+
expect( resolveGapPx( '20%', '1000px' ) ).toBe( 200 );
|
|
222
|
+
expect( resolveGapPx( '10%', '500px' ) ).toBe( 50 );
|
|
223
|
+
} );
|
|
224
|
+
|
|
225
|
+
it( 'falls back to px parsing for non-percentage gaps', () => {
|
|
226
|
+
expect( resolveGapPx( '16px', '1000px' ) ).toBe( 16 );
|
|
227
|
+
expect( resolveGapPx( 'normal', '1000px' ) ).toBe( 0 );
|
|
228
|
+
} );
|
|
229
|
+
|
|
230
|
+
it( 'returns 0 when the reference size is not a finite number', () => {
|
|
231
|
+
expect( resolveGapPx( '20%', 'auto' ) ).toBe( 0 );
|
|
232
|
+
} );
|
|
233
|
+
} );
|
|
234
|
+
|
|
218
235
|
type ComputedStyleParts = {
|
|
219
236
|
gridTemplateColumns: string;
|
|
220
237
|
gridTemplateRows: string;
|
|
221
238
|
columnGap: string;
|
|
222
239
|
rowGap: string;
|
|
240
|
+
width: string;
|
|
241
|
+
height: string;
|
|
223
242
|
paddingTop: string;
|
|
224
243
|
paddingRight: string;
|
|
225
244
|
paddingBottom: string;
|
|
@@ -233,6 +252,8 @@ function mockComputedStyle( parts: Partial< ComputedStyleParts > = {} ): CSSStyl
|
|
|
233
252
|
gridTemplateRows: 'none',
|
|
234
253
|
columnGap: 'normal',
|
|
235
254
|
rowGap: 'normal',
|
|
255
|
+
width: '1000px',
|
|
256
|
+
height: '500px',
|
|
236
257
|
paddingTop: '0px',
|
|
237
258
|
paddingRight: '0px',
|
|
238
259
|
paddingBottom: '0px',
|
|
@@ -246,6 +267,8 @@ function mockComputedStyle( parts: Partial< ComputedStyleParts > = {} ): CSSStyl
|
|
|
246
267
|
gridTemplateRows: resolved.gridTemplateRows,
|
|
247
268
|
columnGap: resolved.columnGap,
|
|
248
269
|
rowGap: resolved.rowGap,
|
|
270
|
+
width: resolved.width,
|
|
271
|
+
height: resolved.height,
|
|
249
272
|
paddingTop: resolved.paddingTop,
|
|
250
273
|
paddingRight: resolved.paddingRight,
|
|
251
274
|
paddingBottom: resolved.paddingBottom,
|
|
@@ -286,6 +309,22 @@ describe( 'toGridTracks', () => {
|
|
|
286
309
|
expect( tracks.rows ).toEqual( [] );
|
|
287
310
|
} );
|
|
288
311
|
|
|
312
|
+
it( 'resolves percentage gaps against the content-box width and height', () => {
|
|
313
|
+
const tracks = toGridTracks(
|
|
314
|
+
mockComputedStyle( {
|
|
315
|
+
gridTemplateColumns: '100px 100px',
|
|
316
|
+
gridTemplateRows: '80px 80px',
|
|
317
|
+
columnGap: '20%',
|
|
318
|
+
rowGap: '10%',
|
|
319
|
+
width: '1000px',
|
|
320
|
+
height: '500px',
|
|
321
|
+
} )
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
expect( tracks.columnGap ).toBe( 200 );
|
|
325
|
+
expect( tracks.rowGap ).toBe( 50 );
|
|
326
|
+
} );
|
|
327
|
+
|
|
289
328
|
it( 'reports gap as 0 when computed style returns "normal"', () => {
|
|
290
329
|
const tracks = toGridTracks(
|
|
291
330
|
mockComputedStyle( { gridTemplateColumns: '100px 100px', columnGap: 'normal', rowGap: 'normal' } )
|
|
@@ -28,8 +28,8 @@ export function toGridTracks( computedStyle: CSSStyleDeclaration ): GridTracks {
|
|
|
28
28
|
return {
|
|
29
29
|
columns: parseTrackList( computedStyle.gridTemplateColumns ),
|
|
30
30
|
rows: parseTrackList( computedStyle.gridTemplateRows ),
|
|
31
|
-
columnGap:
|
|
32
|
-
rowGap:
|
|
31
|
+
columnGap: resolveGapPx( computedStyle.columnGap, computedStyle.width ),
|
|
32
|
+
rowGap: resolveGapPx( computedStyle.rowGap, computedStyle.height ),
|
|
33
33
|
padding: {
|
|
34
34
|
top: toPx( computedStyle.paddingTop ),
|
|
35
35
|
right: toPx( computedStyle.paddingRight ),
|
|
@@ -142,3 +142,14 @@ export function toPx( value: string ): number {
|
|
|
142
142
|
|
|
143
143
|
return Number.isFinite( parsed ) ? parsed : 0;
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
export function resolveGapPx( value: string, referenceSize: string ): number {
|
|
147
|
+
if ( value.trim().endsWith( '%' ) ) {
|
|
148
|
+
const percent = parseFloat( value );
|
|
149
|
+
const reference = parseFloat( referenceSize );
|
|
150
|
+
|
|
151
|
+
return Number.isFinite( percent ) && Number.isFinite( reference ) ? ( percent / 100 ) * reference : 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return toPx( value );
|
|
155
|
+
}
|