@elementor/editor-canvas 4.2.0-beta1 → 4.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -460,8 +460,8 @@ function toGridTracks(computedStyle) {
460
460
  return {
461
461
  columns: parseTrackList(computedStyle.gridTemplateColumns),
462
462
  rows: parseTrackList(computedStyle.gridTemplateRows),
463
- columnGap: toPx(computedStyle.columnGap),
464
- rowGap: toPx(computedStyle.rowGap),
463
+ columnGap: resolveGapPx(computedStyle.columnGap, computedStyle.width),
464
+ rowGap: resolveGapPx(computedStyle.rowGap, computedStyle.height),
465
465
  padding: {
466
466
  top: toPx(computedStyle.paddingTop),
467
467
  right: toPx(computedStyle.paddingRight),
@@ -536,6 +536,14 @@ function toPx(value) {
536
536
  const parsed = parseFloat(value);
537
537
  return Number.isFinite(parsed) ? parsed : 0;
538
538
  }
539
+ function resolveGapPx(value, referenceSize) {
540
+ if (value.trim().endsWith("%")) {
541
+ const percent = parseFloat(value);
542
+ const reference = parseFloat(referenceSize);
543
+ return Number.isFinite(percent) && Number.isFinite(reference) ? percent / 100 * reference : 0;
544
+ }
545
+ return toPx(value);
546
+ }
539
547
 
540
548
  // src/hooks/use-grid-children.ts
541
549
  import { useEffect as useEffect3, useState as useState2 } from "react";
@@ -807,8 +815,7 @@ var GridEmptyCellPositioner = ({ element }) => {
807
815
 
808
816
  // src/components/grid-outline/grid-outline-overlay.tsx
809
817
  import * as React6 from "react";
810
- import { useSelectedElementSettings } from "@elementor/editor-elements";
811
- import { booleanPropTypeUtil } from "@elementor/editor-props";
818
+ import { useElementEditorSettings } from "@elementor/editor-elements";
812
819
  import { Box as Box2 } from "@elementor/ui";
813
820
  import { FloatingPortal as FloatingPortal2 } from "@floating-ui/react";
814
821
 
@@ -909,6 +916,16 @@ var useHasOverlapping = () => {
909
916
  return hasOverlapping;
910
917
  };
911
918
 
919
+ // src/utils/outline-offset-utils.ts
920
+ var THIN_ELEMENT_MAX_HEIGHT_PX = 1;
921
+ var SMALLER_OUTLINE_OFFSET_WIDGET_TYPES = /* @__PURE__ */ new Set(["e-form-input"]);
922
+ function shouldUseSmallerOutlineOffset(element, widgetType) {
923
+ if (element.offsetHeight <= THIN_ELEMENT_MAX_HEIGHT_PX) {
924
+ return true;
925
+ }
926
+ return widgetType !== void 0 && SMALLER_OUTLINE_OFFSET_WIDGET_TYPES.has(widgetType);
927
+ }
928
+
912
929
  // src/components/outline-overlay.tsx
913
930
  var CANVAS_WRAPPER_ID = "elementor-preview-responsive-wrapper";
914
931
  var OverlayBox = styled(Box, {
@@ -920,12 +937,18 @@ var OverlayBox = styled(Box, {
920
937
  pointerEvents: "none"
921
938
  })
922
939
  );
923
- var OutlineOverlay = ({ element, isSelected, id, isGlobal = false }) => {
940
+ var OutlineOverlay = ({
941
+ element,
942
+ isSelected,
943
+ id,
944
+ isGlobal = false,
945
+ widgetType
946
+ }) => {
924
947
  const { context, floating, isVisible } = useFloatingOnElement({ element, isSelected });
925
948
  const { getFloatingProps, getReferenceProps } = useInteractions([useHover(context)]);
926
949
  const hasOverlapping = useHasOverlapping();
927
950
  useBindReactPropsToElement(element, getReferenceProps);
928
- const isSmallerOffset = element.offsetHeight <= 1;
951
+ const isSmallerOffset = shouldUseSmallerOutlineOffset(element, widgetType);
929
952
  return isVisible && !hasOverlapping && /* @__PURE__ */ React.createElement(FloatingPortal, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React.createElement(
930
953
  OverlayBox,
931
954
  {
@@ -1081,8 +1104,8 @@ function GridOutline({ element, tracks, width, height }) {
1081
1104
 
1082
1105
  // src/components/grid-outline/grid-outline-overlay.tsx
1083
1106
  var GridOutlineOverlay = ({ element, id, isSelected }) => {
1084
- const { settings } = useSelectedElementSettings();
1085
- const enabled = booleanPropTypeUtil.extract(settings?.grid_outline);
1107
+ const settings = useElementEditorSettings(id);
1108
+ const enabled = settings?.grid_outline;
1086
1109
  const rect = useElementRect(element);
1087
1110
  const tracks = useGridTracks(element, rect);
1088
1111
  const { floating } = useFloatingOnElement({ element, isSelected });
@@ -1105,9 +1128,10 @@ var GridOutlineOverlay = ({ element, id, isSelected }) => {
1105
1128
  };
1106
1129
 
1107
1130
  // src/components/elements-overlays.tsx
1131
+ var hasGridStyleDisplay = (element) => {
1132
+ return element.computedStyleMap().get("display")?.toString() === "grid";
1133
+ };
1108
1134
  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
1135
  var overlayRegistry = [
1112
1136
  {
1113
1137
  component: OutlineOverlay,
@@ -1115,11 +1139,11 @@ var overlayRegistry = [
1115
1139
  },
1116
1140
  {
1117
1141
  component: GridEmptyCellPositioner,
1118
- shouldRender: ({ element }) => isGridElement(element)
1142
+ shouldRender: ({ element }) => hasGridStyleDisplay(element)
1119
1143
  },
1120
1144
  {
1121
1145
  component: GridOutlineOverlay,
1122
- shouldRender: ({ element, isSelected }) => isSelected && isGridElement(element)
1146
+ shouldRender: ({ isSelected, element }) => isSelected && hasGridStyleDisplay(element)
1123
1147
  }
1124
1148
  ];
1125
1149
  function ElementsOverlays() {
@@ -1132,17 +1156,18 @@ function ElementsOverlays() {
1132
1156
  if (!isActive) {
1133
1157
  return null;
1134
1158
  }
1135
- return elements.map(({ id, domElement, isGlobal }) => {
1159
+ return elements.map(({ id, domElement, isGlobal, widgetType }) => {
1136
1160
  const isSelected = selected.element?.id === id;
1137
1161
  return overlayRegistry.map(
1138
- ({ shouldRender, component: Overlay }, index) => shouldRender({ id, element: domElement, isSelected }) && /* @__PURE__ */ React7.createElement(
1162
+ ({ shouldRender, component: Overlay }, index) => shouldRender({ id, element: domElement, isSelected, widgetType }) && /* @__PURE__ */ React7.createElement(
1139
1163
  Overlay,
1140
1164
  {
1141
1165
  key: `${id}-${index}`,
1142
1166
  id,
1143
1167
  element: domElement,
1144
1168
  isSelected,
1145
- isGlobal
1169
+ isGlobal,
1170
+ widgetType
1146
1171
  }
1147
1172
  )
1148
1173
  );
@@ -1155,7 +1180,8 @@ function useElementsDom() {
1155
1180
  return getElements().filter((el) => isV4Element(el.view?.el?.dataset)).map((element) => ({
1156
1181
  id: element.id,
1157
1182
  domElement: element.view?.getDomElement?.()?.get?.(0),
1158
- isGlobal: element.model.get("isGlobal") ?? false
1183
+ isGlobal: element.model.get("isGlobal") ?? false,
1184
+ widgetType: element.model.get("widgetType")
1159
1185
  })).filter((item) => !!item.domElement);
1160
1186
  }
1161
1187
  );
@@ -1984,6 +2010,46 @@ function clipboardRootsAreAtomicForms(elements) {
1984
2010
  }
1985
2011
  return elements.every((el) => getClipboardElementType(el) === FORM_ELEMENT_TYPE);
1986
2012
  }
2013
+ function hasFormAncestor(node) {
2014
+ return node.closest(FORM_ELEMENT_TYPE) !== null;
2015
+ }
2016
+ function collectFormAncestorErrors(xml) {
2017
+ const errors = [];
2018
+ for (const node of xml.querySelectorAll("*")) {
2019
+ if (!FORM_FIELD_ELEMENT_TYPES.has(node.tagName.toLowerCase())) {
2020
+ continue;
2021
+ }
2022
+ if (hasFormAncestor(node)) {
2023
+ continue;
2024
+ }
2025
+ const id = node.getAttribute("configuration-id");
2026
+ errors.push(
2027
+ `<${node.tagName}${id ? ` configuration-id="${id}"` : ""}> must be nested inside <e-form> (any ancestor depth is allowed).`
2028
+ );
2029
+ }
2030
+ return errors;
2031
+ }
2032
+ function collectSubmitButtonErrors(xml) {
2033
+ const errors = [];
2034
+ for (const form of xml.querySelectorAll("e-form")) {
2035
+ const submitButtons = form.querySelectorAll("e-form-submit-button");
2036
+ if (submitButtons.length === 0) {
2037
+ errors.push(`<e-form> has no <e-form-submit-button>.`);
2038
+ } else if (submitButtons.length > 1) {
2039
+ errors.push(`<e-form> has ${submitButtons.length} submit buttons \u2014 only 1 is allowed.`);
2040
+ }
2041
+ }
2042
+ return errors;
2043
+ }
2044
+ function collectEmptyMessageErrors(xml) {
2045
+ const errors = [];
2046
+ for (const node of xml.querySelectorAll("e-form-success-message, e-form-error-message")) {
2047
+ if (node.children.length === 0) {
2048
+ errors.push(`<${node.tagName}> must have at least one child element (e.g. <e-atomic-paragraph>).`);
2049
+ }
2050
+ }
2051
+ return errors;
2052
+ }
1987
2053
 
1988
2054
  // src/form-structure/enforce-form-ancestor-commands.ts
1989
2055
  var FORM_FIELDS_OUTSIDE_ALERT = {
@@ -2010,7 +2076,8 @@ function blockFormFieldCreate(args) {
2010
2076
  if (!elementType || !FORM_FIELD_ELEMENT_TYPES.has(elementType)) {
2011
2077
  return false;
2012
2078
  }
2013
- if (!isWithinForm(args.container)) {
2079
+ const containers = args.containers ?? [args.container];
2080
+ if (containers.some((container) => !isWithinForm(container))) {
2014
2081
  handleBlockedFormField();
2015
2082
  return true;
2016
2083
  }
@@ -2018,10 +2085,10 @@ function blockFormFieldCreate(args) {
2018
2085
  }
2019
2086
  function blockFormFieldMove(args) {
2020
2087
  const { containers = [args.container], target } = args;
2021
- const hasFormFieldElement = containers.some(
2022
- (container) => container ? hasElementTypes(container, FORM_FIELD_ELEMENT_TYPES) : false
2088
+ const hasLooseFormFields = containers.some(
2089
+ (container) => container ? !hasElementType(container, FORM_ELEMENT_TYPE) && hasElementTypes(container, FORM_FIELD_ELEMENT_TYPES) : false
2023
2090
  );
2024
- if (hasFormFieldElement && !isWithinForm(target) && !movedContainersIncludeAtomicFormRoot(containers)) {
2091
+ if (hasLooseFormFields && !isWithinForm(target) && !movedContainersIncludeAtomicFormRoot(containers)) {
2025
2092
  handleBlockedFormField();
2026
2093
  return true;
2027
2094
  }
@@ -2677,13 +2744,13 @@ function initStyleTransformers() {
2677
2744
  "layout-direction",
2678
2745
  createMultiPropsTransformer(["row", "column"], ({ propKey, key }) => `${key}-${propKey}`)
2679
2746
  ).register("flex", flexTransformer).register(
2680
- "border-width",
2747
+ "border-width-v2",
2681
2748
  createMultiPropsTransformer(
2682
2749
  ["block-start", "block-end", "inline-start", "inline-end"],
2683
2750
  ({ key }) => `border-${key}-width`
2684
2751
  )
2685
2752
  ).register(
2686
- "border-radius",
2753
+ "border-radius-v2",
2687
2754
  createMultiPropsTransformer(
2688
2755
  ["start-start", "start-end", "end-start", "end-end"],
2689
2756
  ({ key }) => `border-${key}-radius`
@@ -5158,6 +5225,11 @@ var CompositionBuilder = class _CompositionBuilder {
5158
5225
  throw new Error(`Invalid element structure:
5159
5226
  ${childTypeErrors.join("\n")}`);
5160
5227
  }
5228
+ const formErrors = [
5229
+ ...collectFormAncestorErrors(this.xml),
5230
+ ...collectSubmitButtonErrors(this.xml),
5231
+ ...collectEmptyMessageErrors(this.xml)
5232
+ ];
5161
5233
  const children = Array.from(this.xml.children);
5162
5234
  for (const childNode of children) {
5163
5235
  const modelTree = this.buildModelTree(childNode, widgetsCache);
@@ -5192,6 +5264,7 @@ ${childTypeErrors.join("\n")}`);
5192
5264
  return {
5193
5265
  configErrors,
5194
5266
  styleErrors,
5267
+ formErrors,
5195
5268
  rootContainers: [...this.rootContainers]
5196
5269
  };
5197
5270
  }
@@ -5502,7 +5575,11 @@ var initBuildCompositionsTool = (reg) => {
5502
5575
  compositionBuilder.setElementConfig(elementConfig);
5503
5576
  compositionBuilder.setStylesConfig(stylesConfig);
5504
5577
  compositionBuilder.setCustomCSS(customCSS);
5505
- const { configErrors, rootContainers: generatedRootContainers } = await compositionBuilder.build(targetContainer);
5578
+ const {
5579
+ configErrors,
5580
+ formErrors,
5581
+ rootContainers: generatedRootContainers
5582
+ } = await compositionBuilder.build(targetContainer);
5506
5583
  rootContainers.push(...generatedRootContainers);
5507
5584
  generatedXML = new XMLSerializer().serializeToString(compositionBuilder.getXML());
5508
5585
  rootContainers.forEach((container) => {
@@ -5517,6 +5594,9 @@ var initBuildCompositionsTool = (reg) => {
5517
5594
  if (configErrors.length) {
5518
5595
  errors.push(...configErrors.map((msg) => new Error(msg)));
5519
5596
  }
5597
+ if (formErrors.length) {
5598
+ errors.push(...formErrors.map((msg) => new Error(msg)));
5599
+ }
5520
5600
  } catch (error) {
5521
5601
  errors.push(error);
5522
5602
  }
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-beta1",
4
+ "version": "4.2.3",
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-beta1",
40
+ "@elementor/editor": "4.2.3",
41
41
  "dompurify": "^3.2.6",
42
- "@elementor/editor-controls": "4.2.0-beta1",
43
- "@elementor/editor-documents": "4.2.0-beta1",
44
- "@elementor/editor-elements": "4.2.0-beta1",
45
- "@elementor/editor-interactions": "4.2.0-beta1",
46
- "@elementor/editor-mcp": "4.2.0-beta1",
47
- "@elementor/editor-notifications": "4.2.0-beta1",
48
- "@elementor/editor-props": "4.2.0-beta1",
49
- "@elementor/editor-responsive": "4.2.0-beta1",
50
- "@elementor/editor-styles": "4.2.0-beta1",
51
- "@elementor/editor-styles-repository": "4.2.0-beta1",
52
- "@elementor/editor-ui": "4.2.0-beta1",
53
- "@elementor/editor-v1-adapters": "4.2.0-beta1",
54
- "@elementor/events": "4.2.0-beta1",
55
- "@elementor/http-client": "4.2.0-beta1",
56
- "@elementor/schema": "4.2.0-beta1",
57
- "@elementor/twing": "4.2.0-beta1",
42
+ "@elementor/editor-controls": "4.2.3",
43
+ "@elementor/editor-documents": "4.2.3",
44
+ "@elementor/editor-elements": "4.2.3",
45
+ "@elementor/editor-interactions": "4.2.3",
46
+ "@elementor/editor-mcp": "4.2.3",
47
+ "@elementor/editor-notifications": "4.2.3",
48
+ "@elementor/editor-props": "4.2.3",
49
+ "@elementor/editor-responsive": "4.2.3",
50
+ "@elementor/editor-styles": "4.2.3",
51
+ "@elementor/editor-styles-repository": "4.2.3",
52
+ "@elementor/editor-ui": "4.2.3",
53
+ "@elementor/editor-v1-adapters": "4.2.3",
54
+ "@elementor/events": "4.2.3",
55
+ "@elementor/http-client": "4.2.3",
56
+ "@elementor/schema": "4.2.3",
57
+ "@elementor/twing": "4.2.3",
58
58
  "@elementor/ui": "1.37.5",
59
- "@elementor/utils": "4.2.0-beta1",
60
- "@elementor/wp-media": "4.2.0-beta1",
59
+ "@elementor/utils": "4.2.3",
60
+ "@elementor/wp-media": "4.2.3",
61
61
  "@floating-ui/react": "^0.27.5",
62
62
  "@wordpress/i18n": "^5.13.0"
63
63
  },
@@ -0,0 +1,2 @@
1
+ export const parseXml = ( xml: string ): Document =>
2
+ new DOMParser().parseFromString( `<root>${ xml }</root>`, 'application/xml' );
@@ -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 ELEMENTS_DATA_ATTR = 'atomic';
15
- const E_GRID_TYPE = 'e-grid';
14
+ const hasGridStyleDisplay = ( element: HTMLElement ): boolean => {
15
+ return element.computedStyleMap().get( 'display' )?.toString() === 'grid';
16
+ };
16
17
 
17
- const isGridElement = ( element: HTMLElement ): boolean =>
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 } ) => isGridElement( element ),
27
+ shouldRender: ( { element } ) => hasGridStyleDisplay( element ),
28
28
  },
29
29
  {
30
30
  component: GridOutlineOverlay,
31
- shouldRender: ( { element, isSelected } ) => isSelected && isGridElement( element ),
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 { useSelectedElementSettings } from '@elementor/editor-elements';
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( value: { $$type: 'boolean'; value: boolean } | null ) {
38
- jest.mocked( useSelectedElementSettings ).mockReturnValue( {
39
- element: { id: ID, type: 'e-grid' },
40
- elementType: {} as never,
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( { $$type: 'boolean', value: true } );
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 a null setting as default-on and renders the outline', () => {
101
- mockGridOutlineSetting( null );
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( { $$type: 'boolean', value: false } );
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( { $$type: 'boolean', value: true } );
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( null );
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( null );
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( null );
158
+ mockGridOutlineSetting( undefined );
161
159
 
162
160
  renderOverlay();
163
161
 
@@ -1,6 +1,5 @@
1
1
  import * as React from 'react';
2
- import { useSelectedElementSettings } from '@elementor/editor-elements';
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 { settings } = useSelectedElementSettings();
16
- const enabled = booleanPropTypeUtil.extract( settings?.grid_outline );
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 = ( { element, isSelected, id, isGlobal = false }: Props ): React.ReactElement | false => {
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.offsetHeight <= 1;
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
  }