@elementor/editor-canvas 4.2.0-beta1 → 4.2.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 +126 -54
- package/dist/index.mjs +91 -19
- package/package.json +20 -20
- package/src/__tests__/parse-xml.ts +2 -0
- package/src/components/__tests__/elements-overlays.test.tsx +7 -0
- package/src/components/elements-overlays.tsx +13 -10
- package/src/components/grid-outline/__tests__/grid-outline-overlay.test.tsx +13 -15
- package/src/components/grid-outline/grid-outline-overlay.tsx +3 -4
- package/src/components/outline-overlay.tsx +9 -2
- 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/enforce-form-ancestor-commands.ts +11 -4
- package/src/form-structure/utils.ts +47 -0
- package/src/mcp/tools/build-composition/tool.ts +9 -2
- package/src/types/element-overlay.ts +2 -0
- package/src/utils/__tests__/outline-offset-utils.test.ts +39 -0
- package/src/utils/outline-offset-utils.ts +11 -0
package/dist/index.mjs
CHANGED
|
@@ -807,8 +807,7 @@ var GridEmptyCellPositioner = ({ element }) => {
|
|
|
807
807
|
|
|
808
808
|
// src/components/grid-outline/grid-outline-overlay.tsx
|
|
809
809
|
import * as React6 from "react";
|
|
810
|
-
import {
|
|
811
|
-
import { booleanPropTypeUtil } from "@elementor/editor-props";
|
|
810
|
+
import { useElementEditorSettings } from "@elementor/editor-elements";
|
|
812
811
|
import { Box as Box2 } from "@elementor/ui";
|
|
813
812
|
import { FloatingPortal as FloatingPortal2 } from "@floating-ui/react";
|
|
814
813
|
|
|
@@ -909,6 +908,16 @@ var useHasOverlapping = () => {
|
|
|
909
908
|
return hasOverlapping;
|
|
910
909
|
};
|
|
911
910
|
|
|
911
|
+
// src/utils/outline-offset-utils.ts
|
|
912
|
+
var THIN_ELEMENT_MAX_HEIGHT_PX = 1;
|
|
913
|
+
var SMALLER_OUTLINE_OFFSET_WIDGET_TYPES = /* @__PURE__ */ new Set(["e-form-input"]);
|
|
914
|
+
function shouldUseSmallerOutlineOffset(element, widgetType) {
|
|
915
|
+
if (element.offsetHeight <= THIN_ELEMENT_MAX_HEIGHT_PX) {
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
return widgetType !== void 0 && SMALLER_OUTLINE_OFFSET_WIDGET_TYPES.has(widgetType);
|
|
919
|
+
}
|
|
920
|
+
|
|
912
921
|
// src/components/outline-overlay.tsx
|
|
913
922
|
var CANVAS_WRAPPER_ID = "elementor-preview-responsive-wrapper";
|
|
914
923
|
var OverlayBox = styled(Box, {
|
|
@@ -920,12 +929,18 @@ var OverlayBox = styled(Box, {
|
|
|
920
929
|
pointerEvents: "none"
|
|
921
930
|
})
|
|
922
931
|
);
|
|
923
|
-
var OutlineOverlay = ({
|
|
932
|
+
var OutlineOverlay = ({
|
|
933
|
+
element,
|
|
934
|
+
isSelected,
|
|
935
|
+
id,
|
|
936
|
+
isGlobal = false,
|
|
937
|
+
widgetType
|
|
938
|
+
}) => {
|
|
924
939
|
const { context, floating, isVisible } = useFloatingOnElement({ element, isSelected });
|
|
925
940
|
const { getFloatingProps, getReferenceProps } = useInteractions([useHover(context)]);
|
|
926
941
|
const hasOverlapping = useHasOverlapping();
|
|
927
942
|
useBindReactPropsToElement(element, getReferenceProps);
|
|
928
|
-
const isSmallerOffset = element
|
|
943
|
+
const isSmallerOffset = shouldUseSmallerOutlineOffset(element, widgetType);
|
|
929
944
|
return isVisible && !hasOverlapping && /* @__PURE__ */ React.createElement(FloatingPortal, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React.createElement(
|
|
930
945
|
OverlayBox,
|
|
931
946
|
{
|
|
@@ -1081,8 +1096,8 @@ function GridOutline({ element, tracks, width, height }) {
|
|
|
1081
1096
|
|
|
1082
1097
|
// src/components/grid-outline/grid-outline-overlay.tsx
|
|
1083
1098
|
var GridOutlineOverlay = ({ element, id, isSelected }) => {
|
|
1084
|
-
const
|
|
1085
|
-
const enabled =
|
|
1099
|
+
const settings = useElementEditorSettings(id);
|
|
1100
|
+
const enabled = settings?.grid_outline;
|
|
1086
1101
|
const rect = useElementRect(element);
|
|
1087
1102
|
const tracks = useGridTracks(element, rect);
|
|
1088
1103
|
const { floating } = useFloatingOnElement({ element, isSelected });
|
|
@@ -1105,9 +1120,10 @@ var GridOutlineOverlay = ({ element, id, isSelected }) => {
|
|
|
1105
1120
|
};
|
|
1106
1121
|
|
|
1107
1122
|
// src/components/elements-overlays.tsx
|
|
1123
|
+
var hasGridStyleDisplay = (element) => {
|
|
1124
|
+
return element.computedStyleMap().get("display")?.toString() === "grid";
|
|
1125
|
+
};
|
|
1108
1126
|
var ELEMENTS_DATA_ATTR = "atomic";
|
|
1109
|
-
var E_GRID_TYPE = "e-grid";
|
|
1110
|
-
var isGridElement = (element) => [element.dataset.eType, element.dataset.element_type].includes(E_GRID_TYPE);
|
|
1111
1127
|
var overlayRegistry = [
|
|
1112
1128
|
{
|
|
1113
1129
|
component: OutlineOverlay,
|
|
@@ -1115,11 +1131,11 @@ var overlayRegistry = [
|
|
|
1115
1131
|
},
|
|
1116
1132
|
{
|
|
1117
1133
|
component: GridEmptyCellPositioner,
|
|
1118
|
-
shouldRender: ({ element }) =>
|
|
1134
|
+
shouldRender: ({ element }) => hasGridStyleDisplay(element)
|
|
1119
1135
|
},
|
|
1120
1136
|
{
|
|
1121
1137
|
component: GridOutlineOverlay,
|
|
1122
|
-
shouldRender: ({
|
|
1138
|
+
shouldRender: ({ isSelected, element }) => isSelected && hasGridStyleDisplay(element)
|
|
1123
1139
|
}
|
|
1124
1140
|
];
|
|
1125
1141
|
function ElementsOverlays() {
|
|
@@ -1132,17 +1148,18 @@ function ElementsOverlays() {
|
|
|
1132
1148
|
if (!isActive) {
|
|
1133
1149
|
return null;
|
|
1134
1150
|
}
|
|
1135
|
-
return elements.map(({ id, domElement, isGlobal }) => {
|
|
1151
|
+
return elements.map(({ id, domElement, isGlobal, widgetType }) => {
|
|
1136
1152
|
const isSelected = selected.element?.id === id;
|
|
1137
1153
|
return overlayRegistry.map(
|
|
1138
|
-
({ shouldRender, component: Overlay }, index) => shouldRender({ id, element: domElement, isSelected }) && /* @__PURE__ */ React7.createElement(
|
|
1154
|
+
({ shouldRender, component: Overlay }, index) => shouldRender({ id, element: domElement, isSelected, widgetType }) && /* @__PURE__ */ React7.createElement(
|
|
1139
1155
|
Overlay,
|
|
1140
1156
|
{
|
|
1141
1157
|
key: `${id}-${index}`,
|
|
1142
1158
|
id,
|
|
1143
1159
|
element: domElement,
|
|
1144
1160
|
isSelected,
|
|
1145
|
-
isGlobal
|
|
1161
|
+
isGlobal,
|
|
1162
|
+
widgetType
|
|
1146
1163
|
}
|
|
1147
1164
|
)
|
|
1148
1165
|
);
|
|
@@ -1155,7 +1172,8 @@ function useElementsDom() {
|
|
|
1155
1172
|
return getElements().filter((el) => isV4Element(el.view?.el?.dataset)).map((element) => ({
|
|
1156
1173
|
id: element.id,
|
|
1157
1174
|
domElement: element.view?.getDomElement?.()?.get?.(0),
|
|
1158
|
-
isGlobal: element.model.get("isGlobal") ?? false
|
|
1175
|
+
isGlobal: element.model.get("isGlobal") ?? false,
|
|
1176
|
+
widgetType: element.model.get("widgetType")
|
|
1159
1177
|
})).filter((item) => !!item.domElement);
|
|
1160
1178
|
}
|
|
1161
1179
|
);
|
|
@@ -1984,6 +2002,46 @@ function clipboardRootsAreAtomicForms(elements) {
|
|
|
1984
2002
|
}
|
|
1985
2003
|
return elements.every((el) => getClipboardElementType(el) === FORM_ELEMENT_TYPE);
|
|
1986
2004
|
}
|
|
2005
|
+
function hasFormAncestor(node) {
|
|
2006
|
+
return node.closest(FORM_ELEMENT_TYPE) !== null;
|
|
2007
|
+
}
|
|
2008
|
+
function collectFormAncestorErrors(xml) {
|
|
2009
|
+
const errors = [];
|
|
2010
|
+
for (const node of xml.querySelectorAll("*")) {
|
|
2011
|
+
if (!FORM_FIELD_ELEMENT_TYPES.has(node.tagName.toLowerCase())) {
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
if (hasFormAncestor(node)) {
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
const id = node.getAttribute("configuration-id");
|
|
2018
|
+
errors.push(
|
|
2019
|
+
`<${node.tagName}${id ? ` configuration-id="${id}"` : ""}> must be nested inside <e-form> (any ancestor depth is allowed).`
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
return errors;
|
|
2023
|
+
}
|
|
2024
|
+
function collectSubmitButtonErrors(xml) {
|
|
2025
|
+
const errors = [];
|
|
2026
|
+
for (const form of xml.querySelectorAll("e-form")) {
|
|
2027
|
+
const submitButtons = form.querySelectorAll("e-form-submit-button");
|
|
2028
|
+
if (submitButtons.length === 0) {
|
|
2029
|
+
errors.push(`<e-form> has no <e-form-submit-button>.`);
|
|
2030
|
+
} else if (submitButtons.length > 1) {
|
|
2031
|
+
errors.push(`<e-form> has ${submitButtons.length} submit buttons \u2014 only 1 is allowed.`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return errors;
|
|
2035
|
+
}
|
|
2036
|
+
function collectEmptyMessageErrors(xml) {
|
|
2037
|
+
const errors = [];
|
|
2038
|
+
for (const node of xml.querySelectorAll("e-form-success-message, e-form-error-message")) {
|
|
2039
|
+
if (node.children.length === 0) {
|
|
2040
|
+
errors.push(`<${node.tagName}> must have at least one child element (e.g. <e-atomic-paragraph>).`);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return errors;
|
|
2044
|
+
}
|
|
1987
2045
|
|
|
1988
2046
|
// src/form-structure/enforce-form-ancestor-commands.ts
|
|
1989
2047
|
var FORM_FIELDS_OUTSIDE_ALERT = {
|
|
@@ -2010,7 +2068,8 @@ function blockFormFieldCreate(args) {
|
|
|
2010
2068
|
if (!elementType || !FORM_FIELD_ELEMENT_TYPES.has(elementType)) {
|
|
2011
2069
|
return false;
|
|
2012
2070
|
}
|
|
2013
|
-
|
|
2071
|
+
const containers = args.containers ?? [args.container];
|
|
2072
|
+
if (containers.some((container) => !isWithinForm(container))) {
|
|
2014
2073
|
handleBlockedFormField();
|
|
2015
2074
|
return true;
|
|
2016
2075
|
}
|
|
@@ -2018,10 +2077,10 @@ function blockFormFieldCreate(args) {
|
|
|
2018
2077
|
}
|
|
2019
2078
|
function blockFormFieldMove(args) {
|
|
2020
2079
|
const { containers = [args.container], target } = args;
|
|
2021
|
-
const
|
|
2022
|
-
(container) => container ? hasElementTypes(container, FORM_FIELD_ELEMENT_TYPES) : false
|
|
2080
|
+
const hasLooseFormFields = containers.some(
|
|
2081
|
+
(container) => container ? !hasElementType(container, FORM_ELEMENT_TYPE) && hasElementTypes(container, FORM_FIELD_ELEMENT_TYPES) : false
|
|
2023
2082
|
);
|
|
2024
|
-
if (
|
|
2083
|
+
if (hasLooseFormFields && !isWithinForm(target) && !movedContainersIncludeAtomicFormRoot(containers)) {
|
|
2025
2084
|
handleBlockedFormField();
|
|
2026
2085
|
return true;
|
|
2027
2086
|
}
|
|
@@ -5158,6 +5217,11 @@ var CompositionBuilder = class _CompositionBuilder {
|
|
|
5158
5217
|
throw new Error(`Invalid element structure:
|
|
5159
5218
|
${childTypeErrors.join("\n")}`);
|
|
5160
5219
|
}
|
|
5220
|
+
const formErrors = [
|
|
5221
|
+
...collectFormAncestorErrors(this.xml),
|
|
5222
|
+
...collectSubmitButtonErrors(this.xml),
|
|
5223
|
+
...collectEmptyMessageErrors(this.xml)
|
|
5224
|
+
];
|
|
5161
5225
|
const children = Array.from(this.xml.children);
|
|
5162
5226
|
for (const childNode of children) {
|
|
5163
5227
|
const modelTree = this.buildModelTree(childNode, widgetsCache);
|
|
@@ -5192,6 +5256,7 @@ ${childTypeErrors.join("\n")}`);
|
|
|
5192
5256
|
return {
|
|
5193
5257
|
configErrors,
|
|
5194
5258
|
styleErrors,
|
|
5259
|
+
formErrors,
|
|
5195
5260
|
rootContainers: [...this.rootContainers]
|
|
5196
5261
|
};
|
|
5197
5262
|
}
|
|
@@ -5502,7 +5567,11 @@ var initBuildCompositionsTool = (reg) => {
|
|
|
5502
5567
|
compositionBuilder.setElementConfig(elementConfig);
|
|
5503
5568
|
compositionBuilder.setStylesConfig(stylesConfig);
|
|
5504
5569
|
compositionBuilder.setCustomCSS(customCSS);
|
|
5505
|
-
const {
|
|
5570
|
+
const {
|
|
5571
|
+
configErrors,
|
|
5572
|
+
formErrors,
|
|
5573
|
+
rootContainers: generatedRootContainers
|
|
5574
|
+
} = await compositionBuilder.build(targetContainer);
|
|
5506
5575
|
rootContainers.push(...generatedRootContainers);
|
|
5507
5576
|
generatedXML = new XMLSerializer().serializeToString(compositionBuilder.getXML());
|
|
5508
5577
|
rootContainers.forEach((container) => {
|
|
@@ -5517,6 +5586,9 @@ var initBuildCompositionsTool = (reg) => {
|
|
|
5517
5586
|
if (configErrors.length) {
|
|
5518
5587
|
errors.push(...configErrors.map((msg) => new Error(msg)));
|
|
5519
5588
|
}
|
|
5589
|
+
if (formErrors.length) {
|
|
5590
|
+
errors.push(...formErrors.map((msg) => new Error(msg)));
|
|
5591
|
+
}
|
|
5520
5592
|
} catch (error) {
|
|
5521
5593
|
errors.push(error);
|
|
5522
5594
|
}
|
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.2.0-
|
|
4
|
+
"version": "4.2.0-beta2",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -37,27 +37,27 @@
|
|
|
37
37
|
"react-dom": "^18.3.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@elementor/editor": "4.2.0-
|
|
40
|
+
"@elementor/editor": "4.2.0-beta2",
|
|
41
41
|
"dompurify": "^3.2.6",
|
|
42
|
-
"@elementor/editor-controls": "4.2.0-
|
|
43
|
-
"@elementor/editor-documents": "4.2.0-
|
|
44
|
-
"@elementor/editor-elements": "4.2.0-
|
|
45
|
-
"@elementor/editor-interactions": "4.2.0-
|
|
46
|
-
"@elementor/editor-mcp": "4.2.0-
|
|
47
|
-
"@elementor/editor-notifications": "4.2.0-
|
|
48
|
-
"@elementor/editor-props": "4.2.0-
|
|
49
|
-
"@elementor/editor-responsive": "4.2.0-
|
|
50
|
-
"@elementor/editor-styles": "4.2.0-
|
|
51
|
-
"@elementor/editor-styles-repository": "4.2.0-
|
|
52
|
-
"@elementor/editor-ui": "4.2.0-
|
|
53
|
-
"@elementor/editor-v1-adapters": "4.2.0-
|
|
54
|
-
"@elementor/events": "4.2.0-
|
|
55
|
-
"@elementor/http-client": "4.2.0-
|
|
56
|
-
"@elementor/schema": "4.2.0-
|
|
57
|
-
"@elementor/twing": "4.2.0-
|
|
42
|
+
"@elementor/editor-controls": "4.2.0-beta2",
|
|
43
|
+
"@elementor/editor-documents": "4.2.0-beta2",
|
|
44
|
+
"@elementor/editor-elements": "4.2.0-beta2",
|
|
45
|
+
"@elementor/editor-interactions": "4.2.0-beta2",
|
|
46
|
+
"@elementor/editor-mcp": "4.2.0-beta2",
|
|
47
|
+
"@elementor/editor-notifications": "4.2.0-beta2",
|
|
48
|
+
"@elementor/editor-props": "4.2.0-beta2",
|
|
49
|
+
"@elementor/editor-responsive": "4.2.0-beta2",
|
|
50
|
+
"@elementor/editor-styles": "4.2.0-beta2",
|
|
51
|
+
"@elementor/editor-styles-repository": "4.2.0-beta2",
|
|
52
|
+
"@elementor/editor-ui": "4.2.0-beta2",
|
|
53
|
+
"@elementor/editor-v1-adapters": "4.2.0-beta2",
|
|
54
|
+
"@elementor/events": "4.2.0-beta2",
|
|
55
|
+
"@elementor/http-client": "4.2.0-beta2",
|
|
56
|
+
"@elementor/schema": "4.2.0-beta2",
|
|
57
|
+
"@elementor/twing": "4.2.0-beta2",
|
|
58
58
|
"@elementor/ui": "1.37.5",
|
|
59
|
-
"@elementor/utils": "4.2.0-
|
|
60
|
-
"@elementor/wp-media": "4.2.0-
|
|
59
|
+
"@elementor/utils": "4.2.0-beta2",
|
|
60
|
+
"@elementor/wp-media": "4.2.0-beta2",
|
|
61
61
|
"@floating-ui/react": "^0.27.5",
|
|
62
62
|
"@wordpress/i18n": "^5.13.0"
|
|
63
63
|
},
|
|
@@ -19,6 +19,10 @@ jest.mock( '@elementor/editor-v1-adapters', () => ( {
|
|
|
19
19
|
isExperimentActive: jest.fn(),
|
|
20
20
|
} ) );
|
|
21
21
|
|
|
22
|
+
function mockGridComputedStyle( element: HTMLElement ) {
|
|
23
|
+
element.computedStyleMap = jest.fn().mockReturnValue( new Map( [ [ 'display', { toString: () => 'grid' } ] ] ) );
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
describe( '<ElementsOverlays />', () => {
|
|
23
27
|
beforeEach( () => {
|
|
24
28
|
const documentEl = createDOMElement( { tag: 'div' } );
|
|
@@ -26,6 +30,9 @@ describe( '<ElementsOverlays />', () => {
|
|
|
26
30
|
const atomic1El = createDOMElement( { tag: 'div', attrs: { 'data-atomic': '', id: '10' } } );
|
|
27
31
|
const atomic2El = createDOMElement( { tag: 'div', attrs: { 'data-atomic': '', id: '20' } } );
|
|
28
32
|
|
|
33
|
+
mockGridComputedStyle( atomic1El );
|
|
34
|
+
mockGridComputedStyle( atomic2El );
|
|
35
|
+
|
|
29
36
|
jest.mocked( useEditMode ).mockReturnValue( 'edit' );
|
|
30
37
|
jest.mocked( useIsRouteActive ).mockReturnValue( false );
|
|
31
38
|
jest.mocked( isExperimentActive ).mockReturnValue( true );
|
|
@@ -11,11 +11,11 @@ import type { ElementOverlayConfig } from '../types/element-overlay';
|
|
|
11
11
|
import { GridEmptyCellPositioner, GridOutlineOverlay } from './grid-outline';
|
|
12
12
|
import { OutlineOverlay } from './outline-overlay';
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const hasGridStyleDisplay = ( element: HTMLElement ): boolean => {
|
|
15
|
+
return element.computedStyleMap().get( 'display' )?.toString() === 'grid';
|
|
16
|
+
};
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
[ element.dataset.eType, element.dataset.element_type ].includes( E_GRID_TYPE );
|
|
18
|
+
const ELEMENTS_DATA_ATTR = 'atomic';
|
|
19
19
|
|
|
20
20
|
const overlayRegistry: ElementOverlayConfig[] = [
|
|
21
21
|
{
|
|
@@ -24,11 +24,11 @@ const overlayRegistry: ElementOverlayConfig[] = [
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
component: GridEmptyCellPositioner,
|
|
27
|
-
shouldRender: ( { element } ) =>
|
|
27
|
+
shouldRender: ( { element } ) => hasGridStyleDisplay( element ),
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
component: GridOutlineOverlay,
|
|
31
|
-
shouldRender: ( {
|
|
31
|
+
shouldRender: ( { isSelected, element } ) => isSelected && hasGridStyleDisplay( element ),
|
|
32
32
|
},
|
|
33
33
|
];
|
|
34
34
|
|
|
@@ -45,18 +45,19 @@ export function ElementsOverlays() {
|
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
return elements.map( ( { id, domElement, isGlobal } ) => {
|
|
48
|
+
return elements.map( ( { id, domElement, isGlobal, widgetType } ) => {
|
|
49
49
|
const isSelected = selected.element?.id === id;
|
|
50
50
|
|
|
51
51
|
return overlayRegistry.map(
|
|
52
52
|
( { shouldRender, component: Overlay }, index ) =>
|
|
53
|
-
shouldRender( { id, element: domElement, isSelected } ) && (
|
|
53
|
+
shouldRender( { id, element: domElement, isSelected, widgetType } ) && (
|
|
54
54
|
<Overlay
|
|
55
55
|
key={ `${ id }-${ index }` }
|
|
56
56
|
id={ id }
|
|
57
57
|
element={ domElement }
|
|
58
58
|
isSelected={ isSelected }
|
|
59
59
|
isGlobal={ isGlobal }
|
|
60
|
+
widgetType={ widgetType }
|
|
60
61
|
/>
|
|
61
62
|
)
|
|
62
63
|
);
|
|
@@ -67,18 +68,20 @@ type ElementData = {
|
|
|
67
68
|
id: string;
|
|
68
69
|
domElement: HTMLElement;
|
|
69
70
|
isGlobal: boolean;
|
|
71
|
+
widgetType: string | undefined;
|
|
70
72
|
};
|
|
71
73
|
|
|
72
|
-
function useElementsDom() {
|
|
74
|
+
function useElementsDom(): ElementData[] {
|
|
73
75
|
return useListenTo(
|
|
74
76
|
[ windowEvent( 'elementor/editor/element-rendered' ), windowEvent( 'elementor/editor/element-destroyed' ) ],
|
|
75
|
-
() => {
|
|
77
|
+
(): ElementData[] => {
|
|
76
78
|
return getElements()
|
|
77
79
|
.filter( ( el ) => isV4Element( el.view?.el?.dataset ) )
|
|
78
80
|
.map( ( element ) => ( {
|
|
79
81
|
id: element.id,
|
|
80
82
|
domElement: element.view?.getDomElement?.()?.get?.( 0 ),
|
|
81
83
|
isGlobal: element.model.get( 'isGlobal' ) ?? false,
|
|
84
|
+
widgetType: element.model.get( 'widgetType' ),
|
|
82
85
|
} ) )
|
|
83
86
|
.filter( ( item ): item is ElementData => !! item.domElement );
|
|
84
87
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { createDOMElement, renderWithTheme } from 'test-utils';
|
|
3
|
-
import {
|
|
3
|
+
import { useElementEditorSettings } from '@elementor/editor-elements';
|
|
4
4
|
import { screen } from '@testing-library/react';
|
|
5
5
|
|
|
6
6
|
import { useElementRect } from '../../../hooks/use-element-rect';
|
|
@@ -34,12 +34,10 @@ const EMPTY_TRACKS: GridTracks = {
|
|
|
34
34
|
borderColor: '',
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
function mockGridOutlineSetting(
|
|
38
|
-
jest.mocked(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
settings: { grid_outline: value },
|
|
42
|
-
} );
|
|
37
|
+
function mockGridOutlineSetting( gridOutline: boolean | undefined ) {
|
|
38
|
+
jest.mocked( useElementEditorSettings ).mockReturnValue(
|
|
39
|
+
gridOutline === undefined ? {} : { grid_outline: gridOutline }
|
|
40
|
+
);
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
function mockFloating() {
|
|
@@ -87,7 +85,7 @@ describe( '<GridOutlineOverlay />', () => {
|
|
|
87
85
|
} );
|
|
88
86
|
|
|
89
87
|
it( 'renders the outline svg with the element id when the setting is enabled', () => {
|
|
90
|
-
mockGridOutlineSetting(
|
|
88
|
+
mockGridOutlineSetting( true );
|
|
91
89
|
|
|
92
90
|
renderOverlay();
|
|
93
91
|
|
|
@@ -97,8 +95,8 @@ describe( '<GridOutlineOverlay />', () => {
|
|
|
97
95
|
expect( overlay.querySelector( 'svg' ) ).toBeInTheDocument();
|
|
98
96
|
} );
|
|
99
97
|
|
|
100
|
-
it( 'treats
|
|
101
|
-
mockGridOutlineSetting(
|
|
98
|
+
it( 'treats an unset setting as default-on and renders the outline', () => {
|
|
99
|
+
mockGridOutlineSetting( undefined );
|
|
102
100
|
|
|
103
101
|
renderOverlay();
|
|
104
102
|
|
|
@@ -106,7 +104,7 @@ describe( '<GridOutlineOverlay />', () => {
|
|
|
106
104
|
} );
|
|
107
105
|
|
|
108
106
|
it( 'renders nothing when the setting is explicitly disabled', () => {
|
|
109
|
-
mockGridOutlineSetting(
|
|
107
|
+
mockGridOutlineSetting( false );
|
|
110
108
|
|
|
111
109
|
renderOverlay();
|
|
112
110
|
|
|
@@ -114,7 +112,7 @@ describe( '<GridOutlineOverlay />', () => {
|
|
|
114
112
|
} );
|
|
115
113
|
|
|
116
114
|
it( 'renders nothing while tracks have not resolved yet', () => {
|
|
117
|
-
mockGridOutlineSetting(
|
|
115
|
+
mockGridOutlineSetting( true );
|
|
118
116
|
jest.mocked( useGridTracks ).mockReturnValue( EMPTY_TRACKS );
|
|
119
117
|
|
|
120
118
|
renderOverlay();
|
|
@@ -123,7 +121,7 @@ describe( '<GridOutlineOverlay />', () => {
|
|
|
123
121
|
} );
|
|
124
122
|
|
|
125
123
|
it( 'renders one rect per cell when the grid has a gap', () => {
|
|
126
|
-
mockGridOutlineSetting(
|
|
124
|
+
mockGridOutlineSetting( undefined );
|
|
127
125
|
jest.mocked( useGridTracks ).mockReturnValue( {
|
|
128
126
|
...NON_EMPTY_TRACKS,
|
|
129
127
|
columns: [ 100, 100, 100 ],
|
|
@@ -140,7 +138,7 @@ describe( '<GridOutlineOverlay />', () => {
|
|
|
140
138
|
} );
|
|
141
139
|
|
|
142
140
|
it( 'renders boundary lines when the grid has no gap', () => {
|
|
143
|
-
mockGridOutlineSetting(
|
|
141
|
+
mockGridOutlineSetting( undefined );
|
|
144
142
|
jest.mocked( useGridTracks ).mockReturnValue( {
|
|
145
143
|
...NON_EMPTY_TRACKS,
|
|
146
144
|
columns: [ 100, 100, 100 ],
|
|
@@ -157,7 +155,7 @@ describe( '<GridOutlineOverlay />', () => {
|
|
|
157
155
|
} );
|
|
158
156
|
|
|
159
157
|
it( 'mounts inside the canvas wrapper portal', () => {
|
|
160
|
-
mockGridOutlineSetting(
|
|
158
|
+
mockGridOutlineSetting( undefined );
|
|
161
159
|
|
|
162
160
|
renderOverlay();
|
|
163
161
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { booleanPropTypeUtil } from '@elementor/editor-props';
|
|
2
|
+
import { useElementEditorSettings } from '@elementor/editor-elements';
|
|
4
3
|
import { Box } from '@elementor/ui';
|
|
5
4
|
import { FloatingPortal } from '@floating-ui/react';
|
|
6
5
|
|
|
@@ -12,8 +11,8 @@ import { CANVAS_WRAPPER_ID } from '../outline-overlay';
|
|
|
12
11
|
import { GridOutline } from './grid-outline';
|
|
13
12
|
|
|
14
13
|
export const GridOutlineOverlay = ( { element, id, isSelected }: ElementOverlayProps ): React.ReactElement | null => {
|
|
15
|
-
const
|
|
16
|
-
const enabled =
|
|
14
|
+
const settings = useElementEditorSettings( id );
|
|
15
|
+
const enabled = settings?.grid_outline;
|
|
17
16
|
const rect = useElementRect( element );
|
|
18
17
|
const tracks = useGridTracks( element, rect );
|
|
19
18
|
const { floating } = useFloatingOnElement( { element, isSelected } );
|
|
@@ -6,6 +6,7 @@ import { useBindReactPropsToElement } from '../hooks/use-bind-react-props-to-ele
|
|
|
6
6
|
import { useFloatingOnElement } from '../hooks/use-floating-on-element';
|
|
7
7
|
import { useHasOverlapping } from '../hooks/use-has-overlapping';
|
|
8
8
|
import type { ElementOverlayProps } from '../types/element-overlay';
|
|
9
|
+
import { shouldUseSmallerOutlineOffset } from '../utils/outline-offset-utils';
|
|
9
10
|
|
|
10
11
|
export const CANVAS_WRAPPER_ID = 'elementor-preview-responsive-wrapper';
|
|
11
12
|
|
|
@@ -25,13 +26,19 @@ const OverlayBox = styled( Box, {
|
|
|
25
26
|
} )
|
|
26
27
|
);
|
|
27
28
|
|
|
28
|
-
export const OutlineOverlay = ( {
|
|
29
|
+
export const OutlineOverlay = ( {
|
|
30
|
+
element,
|
|
31
|
+
isSelected,
|
|
32
|
+
id,
|
|
33
|
+
isGlobal = false,
|
|
34
|
+
widgetType,
|
|
35
|
+
}: Props ): React.ReactElement | false => {
|
|
29
36
|
const { context, floating, isVisible } = useFloatingOnElement( { element, isSelected } );
|
|
30
37
|
const { getFloatingProps, getReferenceProps } = useInteractions( [ useHover( context ) ] );
|
|
31
38
|
const hasOverlapping = useHasOverlapping();
|
|
32
39
|
|
|
33
40
|
useBindReactPropsToElement( element, getReferenceProps );
|
|
34
|
-
const isSmallerOffset = element
|
|
41
|
+
const isSmallerOffset = shouldUseSmallerOutlineOffset( element, widgetType );
|
|
35
42
|
|
|
36
43
|
return (
|
|
37
44
|
isVisible &&
|
|
@@ -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
|
}
|