@elementor/editor-canvas 4.2.0-898 → 4.2.0-899
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 +453 -271
- package/dist/index.mjs +332 -150
- package/package.json +18 -18
- package/src/components/elements-overlays.tsx +14 -1
- package/src/components/grid-outline/__tests__/grid-outline-overlay.test.tsx +151 -0
- package/src/components/grid-outline/__tests__/grid-outline.test.tsx +131 -0
- package/src/components/grid-outline/grid-outline-line.tsx +27 -0
- package/src/components/grid-outline/grid-outline-overlay.tsx +41 -0
- package/src/components/grid-outline/grid-outline.tsx +45 -0
- package/src/components/grid-outline/index.ts +1 -0
- package/src/hooks/__tests__/use-grid-tracks.test.ts +152 -0
- package/src/hooks/use-grid-tracks.ts +52 -0
- package/src/utils/__tests__/grid-outline-utils.test.ts +142 -0
- package/src/utils/grid-outline-utils.ts +70 -0
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-899",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -37,25 +37,25 @@
|
|
|
37
37
|
"react-dom": "^18.3.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@elementor/editor": "4.2.0-
|
|
40
|
+
"@elementor/editor": "4.2.0-899",
|
|
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/schema": "4.2.0-
|
|
55
|
-
"@elementor/twing": "4.2.0-
|
|
42
|
+
"@elementor/editor-controls": "4.2.0-899",
|
|
43
|
+
"@elementor/editor-documents": "4.2.0-899",
|
|
44
|
+
"@elementor/editor-elements": "4.2.0-899",
|
|
45
|
+
"@elementor/editor-interactions": "4.2.0-899",
|
|
46
|
+
"@elementor/editor-mcp": "4.2.0-899",
|
|
47
|
+
"@elementor/editor-notifications": "4.2.0-899",
|
|
48
|
+
"@elementor/editor-props": "4.2.0-899",
|
|
49
|
+
"@elementor/editor-responsive": "4.2.0-899",
|
|
50
|
+
"@elementor/editor-styles": "4.2.0-899",
|
|
51
|
+
"@elementor/editor-styles-repository": "4.2.0-899",
|
|
52
|
+
"@elementor/editor-ui": "4.2.0-899",
|
|
53
|
+
"@elementor/editor-v1-adapters": "4.2.0-899",
|
|
54
|
+
"@elementor/schema": "4.2.0-899",
|
|
55
|
+
"@elementor/twing": "4.2.0-899",
|
|
56
56
|
"@elementor/ui": "1.37.5",
|
|
57
|
-
"@elementor/utils": "4.2.0-
|
|
58
|
-
"@elementor/wp-media": "4.2.0-
|
|
57
|
+
"@elementor/utils": "4.2.0-899",
|
|
58
|
+
"@elementor/wp-media": "4.2.0-899",
|
|
59
59
|
"@floating-ui/react": "^0.27.5",
|
|
60
60
|
"@wordpress/i18n": "^5.13.0"
|
|
61
61
|
},
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '@elementor/editor-v1-adapters';
|
|
9
9
|
|
|
10
10
|
import type { ElementOverlayConfig } from '../types/element-overlay';
|
|
11
|
+
import { GridOutlineOverlay } from './grid-outline';
|
|
11
12
|
import { OutlineOverlay } from './outline-overlay';
|
|
12
13
|
|
|
13
14
|
const ELEMENTS_DATA_ATTR = 'atomic';
|
|
@@ -17,6 +18,10 @@ const overlayRegistry: ElementOverlayConfig[] = [
|
|
|
17
18
|
component: OutlineOverlay,
|
|
18
19
|
shouldRender: () => true,
|
|
19
20
|
},
|
|
21
|
+
{
|
|
22
|
+
component: GridOutlineOverlay,
|
|
23
|
+
shouldRender: ( { element, isSelected } ) => isSelected && element.dataset.eType === 'e-grid',
|
|
24
|
+
},
|
|
20
25
|
];
|
|
21
26
|
|
|
22
27
|
export function ElementsOverlays() {
|
|
@@ -61,7 +66,7 @@ function useElementsDom() {
|
|
|
61
66
|
[ windowEvent( 'elementor/editor/element-rendered' ), windowEvent( 'elementor/editor/element-destroyed' ) ],
|
|
62
67
|
() => {
|
|
63
68
|
return getElements()
|
|
64
|
-
.filter( ( el ) =>
|
|
69
|
+
.filter( ( el ) => isV4Element( el.view?.el?.dataset ) )
|
|
65
70
|
.map( ( element ) => ( {
|
|
66
71
|
id: element.id,
|
|
67
72
|
domElement: element.view?.getDomElement?.()?.get?.( 0 ),
|
|
@@ -71,3 +76,11 @@ function useElementsDom() {
|
|
|
71
76
|
}
|
|
72
77
|
);
|
|
73
78
|
}
|
|
79
|
+
|
|
80
|
+
function isV4Element( dataset: DOMStringMap | undefined ): boolean {
|
|
81
|
+
if ( ! dataset ) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return ELEMENTS_DATA_ATTR in dataset || 'eType' in dataset;
|
|
86
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createDOMElement, renderWithTheme } from 'test-utils';
|
|
3
|
+
import { useSelectedElementSettings } from '@elementor/editor-elements';
|
|
4
|
+
import { screen } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
import { useElementRect } from '../../../hooks/use-element-rect';
|
|
7
|
+
import { useFloatingOnElement } from '../../../hooks/use-floating-on-element';
|
|
8
|
+
import { type GridTracks, useGridTracks } from '../../../hooks/use-grid-tracks';
|
|
9
|
+
import { CANVAS_WRAPPER_ID } from '../../outline-overlay';
|
|
10
|
+
import { GridOutlineOverlay } from '../grid-outline-overlay';
|
|
11
|
+
|
|
12
|
+
jest.mock( '@elementor/editor-elements' );
|
|
13
|
+
jest.mock( '../../../hooks/use-element-rect' );
|
|
14
|
+
jest.mock( '../../../hooks/use-grid-tracks' );
|
|
15
|
+
jest.mock( '../../../hooks/use-floating-on-element' );
|
|
16
|
+
|
|
17
|
+
const ID = 'grid-1';
|
|
18
|
+
|
|
19
|
+
const NON_EMPTY_TRACKS: GridTracks = {
|
|
20
|
+
columns: [ 100, 100 ],
|
|
21
|
+
rows: [ 80, 80, 80 ],
|
|
22
|
+
columnGap: 0,
|
|
23
|
+
rowGap: 0,
|
|
24
|
+
padding: { top: 10, right: 10, bottom: 10, left: 10 },
|
|
25
|
+
borderColor: '#D5D8DC',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const EMPTY_TRACKS: GridTracks = {
|
|
29
|
+
columns: [],
|
|
30
|
+
rows: [],
|
|
31
|
+
columnGap: 0,
|
|
32
|
+
rowGap: 0,
|
|
33
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
34
|
+
borderColor: '',
|
|
35
|
+
};
|
|
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
|
+
} );
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mockFloating() {
|
|
46
|
+
jest.mocked( useFloatingOnElement ).mockReturnValue( {
|
|
47
|
+
isVisible: true,
|
|
48
|
+
context: {} as never,
|
|
49
|
+
floating: {
|
|
50
|
+
setRef: jest.fn(),
|
|
51
|
+
ref: { current: null } as never,
|
|
52
|
+
styles: { position: 'absolute', top: 0, left: 0 },
|
|
53
|
+
},
|
|
54
|
+
} );
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderOverlay( props: Partial< { id: string; isSelected: boolean; isGlobal: boolean } > = {} ) {
|
|
58
|
+
const element = createDOMElement( { tag: 'div', attrs: { 'data-e-type': 'e-grid' } } );
|
|
59
|
+
|
|
60
|
+
return renderWithTheme(
|
|
61
|
+
<GridOutlineOverlay
|
|
62
|
+
id={ props.id ?? ID }
|
|
63
|
+
element={ element }
|
|
64
|
+
isSelected={ props.isSelected ?? true }
|
|
65
|
+
isGlobal={ props.isGlobal ?? false }
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe( '<GridOutlineOverlay />', () => {
|
|
71
|
+
beforeEach( () => {
|
|
72
|
+
jest.mocked( useElementRect ).mockReturnValue( new DOMRect( 0, 0, 220, 260 ) );
|
|
73
|
+
jest.mocked( useGridTracks ).mockReturnValue( NON_EMPTY_TRACKS );
|
|
74
|
+
mockFloating();
|
|
75
|
+
|
|
76
|
+
window.document.body.appendChild(
|
|
77
|
+
createDOMElement( {
|
|
78
|
+
tag: 'div',
|
|
79
|
+
attrs: { id: CANVAS_WRAPPER_ID, 'data-testid': CANVAS_WRAPPER_ID },
|
|
80
|
+
} )
|
|
81
|
+
);
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
afterEach( () => {
|
|
85
|
+
window.document.body.innerHTML = '';
|
|
86
|
+
jest.clearAllMocks();
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
it( 'renders the outline svg with the element id when the setting is enabled', () => {
|
|
90
|
+
mockGridOutlineSetting( { $$type: 'boolean', value: true } );
|
|
91
|
+
|
|
92
|
+
renderOverlay();
|
|
93
|
+
|
|
94
|
+
const overlay = screen.getByRole( 'presentation' );
|
|
95
|
+
expect( overlay ).toHaveAttribute( 'data-grid-outline', ID );
|
|
96
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
97
|
+
expect( overlay.querySelector( 'svg' ) ).toBeInTheDocument();
|
|
98
|
+
} );
|
|
99
|
+
|
|
100
|
+
it( 'treats a null setting as default-on and renders the outline', () => {
|
|
101
|
+
mockGridOutlineSetting( null );
|
|
102
|
+
|
|
103
|
+
renderOverlay();
|
|
104
|
+
|
|
105
|
+
expect( screen.getByRole( 'presentation' ) ).toBeInTheDocument();
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
it( 'renders nothing when the setting is explicitly disabled', () => {
|
|
109
|
+
mockGridOutlineSetting( { $$type: 'boolean', value: false } );
|
|
110
|
+
|
|
111
|
+
renderOverlay();
|
|
112
|
+
|
|
113
|
+
expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
it( 'renders nothing while tracks have not resolved yet', () => {
|
|
117
|
+
mockGridOutlineSetting( { $$type: 'boolean', value: true } );
|
|
118
|
+
jest.mocked( useGridTracks ).mockReturnValue( EMPTY_TRACKS );
|
|
119
|
+
|
|
120
|
+
renderOverlay();
|
|
121
|
+
|
|
122
|
+
expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
|
|
123
|
+
} );
|
|
124
|
+
|
|
125
|
+
it( 'renders one <line> per track boundary plus both edges of every gap', () => {
|
|
126
|
+
mockGridOutlineSetting( null );
|
|
127
|
+
jest.mocked( useGridTracks ).mockReturnValue( {
|
|
128
|
+
...NON_EMPTY_TRACKS,
|
|
129
|
+
columns: [ 100, 100, 100 ],
|
|
130
|
+
rows: [ 80, 80 ],
|
|
131
|
+
columnGap: 10,
|
|
132
|
+
rowGap: 10,
|
|
133
|
+
} );
|
|
134
|
+
|
|
135
|
+
renderOverlay();
|
|
136
|
+
|
|
137
|
+
const overlay = screen.getByRole( 'presentation' );
|
|
138
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
139
|
+
expect( overlay.querySelectorAll( 'line' ) ).toHaveLength( 6 + 4 );
|
|
140
|
+
} );
|
|
141
|
+
|
|
142
|
+
it( 'mounts inside the canvas wrapper portal', () => {
|
|
143
|
+
mockGridOutlineSetting( null );
|
|
144
|
+
|
|
145
|
+
renderOverlay();
|
|
146
|
+
|
|
147
|
+
// eslint-disable-next-line testing-library/no-test-id-queries
|
|
148
|
+
const wrapper = screen.getByTestId( CANVAS_WRAPPER_ID );
|
|
149
|
+
expect( wrapper ).toContainElement( screen.getByRole( 'presentation' ) );
|
|
150
|
+
} );
|
|
151
|
+
} );
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* eslint-disable testing-library/no-container */
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { renderWithTheme } from 'test-utils';
|
|
4
|
+
|
|
5
|
+
import { type GridTracks } from '../../../hooks/use-grid-tracks';
|
|
6
|
+
import { GridOutline } from '../grid-outline';
|
|
7
|
+
|
|
8
|
+
function makeTracks( partial: Partial< GridTracks > = {} ): GridTracks {
|
|
9
|
+
return {
|
|
10
|
+
columns: [],
|
|
11
|
+
rows: [],
|
|
12
|
+
columnGap: 0,
|
|
13
|
+
rowGap: 0,
|
|
14
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
15
|
+
borderColor: '#D5D8DC',
|
|
16
|
+
...partial,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getSvg( container: HTMLElement ): SVGSVGElement {
|
|
21
|
+
const svg = container.querySelector( 'svg' );
|
|
22
|
+
if ( ! svg ) {
|
|
23
|
+
throw new Error( 'svg not found' );
|
|
24
|
+
}
|
|
25
|
+
return svg;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe( '<GridOutline />', () => {
|
|
29
|
+
it( 'sizes the svg to the element rect', () => {
|
|
30
|
+
const { container } = renderWithTheme(
|
|
31
|
+
<GridOutline tracks={ makeTracks( { columns: [ 100 ] } ) } width={ 320 } height={ 200 } />
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const svg = getSvg( container );
|
|
35
|
+
expect( svg ).toHaveAttribute( 'width', '320' );
|
|
36
|
+
expect( svg ).toHaveAttribute( 'height', '200' );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'draws N+M boundary lines for an N×M grid (no gaps)', () => {
|
|
40
|
+
const { container } = renderWithTheme(
|
|
41
|
+
<GridOutline
|
|
42
|
+
tracks={ makeTracks( { columns: [ 100, 100, 100 ], rows: [ 80, 80 ] } ) }
|
|
43
|
+
width={ 300 }
|
|
44
|
+
height={ 160 }
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect( container.querySelectorAll( 'line' ) ).toHaveLength( 4 + 3 );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'emits both edges of every gap between tracks', () => {
|
|
52
|
+
const { container } = renderWithTheme(
|
|
53
|
+
<GridOutline
|
|
54
|
+
tracks={ makeTracks( {
|
|
55
|
+
columns: [ 100, 100, 100 ],
|
|
56
|
+
rows: [ 80, 80 ],
|
|
57
|
+
columnGap: 10,
|
|
58
|
+
rowGap: 8,
|
|
59
|
+
} ) }
|
|
60
|
+
width={ 320 }
|
|
61
|
+
height={ 176 }
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect( container.querySelectorAll( 'line' ) ).toHaveLength( 6 + 4 );
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
it( 'snaps vertical line coordinates to half pixels for crisp 1px strokes', () => {
|
|
69
|
+
const { container } = renderWithTheme(
|
|
70
|
+
<GridOutline
|
|
71
|
+
tracks={ makeTracks( {
|
|
72
|
+
columns: [ 100, 100 ],
|
|
73
|
+
padding: { top: 10, right: 10, bottom: 10, left: 10 },
|
|
74
|
+
} ) }
|
|
75
|
+
width={ 220 }
|
|
76
|
+
height={ 100 }
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const verticals = Array.from( container.querySelectorAll( 'line' ) ).filter(
|
|
81
|
+
( line ) => line.getAttribute( 'x1' ) === line.getAttribute( 'x2' )
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect( verticals.map( ( line ) => line.getAttribute( 'x1' ) ) ).toEqual( [ '10.5', '110.5', '210.5' ] );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
it( 'spans horizontal lines across the padded content rect', () => {
|
|
88
|
+
const { container } = renderWithTheme(
|
|
89
|
+
<GridOutline
|
|
90
|
+
tracks={ makeTracks( {
|
|
91
|
+
rows: [ 50, 50 ],
|
|
92
|
+
padding: { top: 8, right: 12, bottom: 6, left: 4 },
|
|
93
|
+
} ) }
|
|
94
|
+
width={ 300 }
|
|
95
|
+
height={ 120 }
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const horizontals = Array.from( container.querySelectorAll( 'line' ) ).filter(
|
|
100
|
+
( line ) => line.getAttribute( 'y1' ) === line.getAttribute( 'y2' )
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect( horizontals.length ).toBeGreaterThan( 0 );
|
|
104
|
+
for ( const line of horizontals ) {
|
|
105
|
+
expect( line ).toHaveAttribute( 'x1', '4' );
|
|
106
|
+
expect( line ).toHaveAttribute( 'x2', '288' );
|
|
107
|
+
}
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
it( 'passes the resolved iframe border color through to each line', () => {
|
|
111
|
+
const { container } = renderWithTheme(
|
|
112
|
+
<GridOutline
|
|
113
|
+
tracks={ makeTracks( { columns: [ 100 ], borderColor: '#abcdef' } ) }
|
|
114
|
+
width={ 100 }
|
|
115
|
+
height={ 100 }
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const lines = container.querySelectorAll( 'line' );
|
|
120
|
+
expect( lines.length ).toBeGreaterThan( 0 );
|
|
121
|
+
lines.forEach( ( line ) => {
|
|
122
|
+
expect( line ).toHaveAttribute( 'stroke', '#abcdef' );
|
|
123
|
+
} );
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
it( 'renders no lines when there are no tracks on either axis', () => {
|
|
127
|
+
const { container } = renderWithTheme( <GridOutline tracks={ makeTracks() } width={ 100 } height={ 100 } /> );
|
|
128
|
+
|
|
129
|
+
expect( container.querySelectorAll( 'line' ) ).toHaveLength( 0 );
|
|
130
|
+
} );
|
|
131
|
+
} );
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
const FALLBACK_COLOR = 'rgba(0, 0, 0, 0.12)';
|
|
4
|
+
export const DASH = '2 2';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
x1: number;
|
|
8
|
+
x2: number;
|
|
9
|
+
y1: number;
|
|
10
|
+
y2: number;
|
|
11
|
+
color?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function GridOutlineLine( { x1, x2, y1, y2, color }: Props ) {
|
|
15
|
+
return (
|
|
16
|
+
<line
|
|
17
|
+
x1={ x1 }
|
|
18
|
+
x2={ x2 }
|
|
19
|
+
y1={ y1 }
|
|
20
|
+
y2={ y2 }
|
|
21
|
+
stroke={ color || FALLBACK_COLOR }
|
|
22
|
+
strokeWidth={ 1 }
|
|
23
|
+
strokeDasharray={ DASH }
|
|
24
|
+
vectorEffect="non-scaling-stroke"
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useSelectedElementSettings } from '@elementor/editor-elements';
|
|
3
|
+
import { booleanPropTypeUtil } from '@elementor/editor-props';
|
|
4
|
+
import { Box } from '@elementor/ui';
|
|
5
|
+
import { FloatingPortal } from '@floating-ui/react';
|
|
6
|
+
|
|
7
|
+
import { useElementRect } from '../../hooks/use-element-rect';
|
|
8
|
+
import { useFloatingOnElement } from '../../hooks/use-floating-on-element';
|
|
9
|
+
import { useGridTracks } from '../../hooks/use-grid-tracks';
|
|
10
|
+
import type { ElementOverlayProps } from '../../types/element-overlay';
|
|
11
|
+
import { CANVAS_WRAPPER_ID } from '../outline-overlay';
|
|
12
|
+
import { GridOutline } from './grid-outline';
|
|
13
|
+
|
|
14
|
+
export const GridOutlineOverlay = ( { element, id, isSelected }: ElementOverlayProps ): React.ReactElement | null => {
|
|
15
|
+
const { settings } = useSelectedElementSettings();
|
|
16
|
+
const enabled = booleanPropTypeUtil.extract( settings?.grid_outline );
|
|
17
|
+
const rect = useElementRect( element );
|
|
18
|
+
const tracks = useGridTracks( element, rect );
|
|
19
|
+
const { floating } = useFloatingOnElement( { element, isSelected } );
|
|
20
|
+
|
|
21
|
+
if ( enabled === false ) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if ( tracks.columns.length === 0 && tracks.rows.length === 0 ) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<FloatingPortal id={ CANVAS_WRAPPER_ID }>
|
|
31
|
+
<Box
|
|
32
|
+
ref={ floating.setRef }
|
|
33
|
+
style={ { ...floating.styles, pointerEvents: 'none' } }
|
|
34
|
+
data-grid-outline={ id }
|
|
35
|
+
role="presentation"
|
|
36
|
+
>
|
|
37
|
+
<GridOutline tracks={ tracks } width={ rect.width } height={ rect.height } />
|
|
38
|
+
</Box>
|
|
39
|
+
</FloatingPortal>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { type GridTracks } from '../../hooks/use-grid-tracks';
|
|
4
|
+
import { computeOutlineGeometry, snapToHalfPixel } from '../../utils/grid-outline-utils';
|
|
5
|
+
import { GridOutlineLine } from './grid-outline-line';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
tracks: GridTracks;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function GridOutline( { tracks, width, height }: Props ) {
|
|
14
|
+
const { vertical, horizontal, top, bottom, left, right } = computeOutlineGeometry( tracks, width, height );
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<svg
|
|
18
|
+
width={ width }
|
|
19
|
+
height={ height }
|
|
20
|
+
style={ { position: 'absolute', inset: 0, overflow: 'visible' } }
|
|
21
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
22
|
+
>
|
|
23
|
+
{ vertical.map( ( x, i ) => (
|
|
24
|
+
<GridOutlineLine
|
|
25
|
+
key={ `v-${ i }` }
|
|
26
|
+
x1={ snapToHalfPixel( x ) }
|
|
27
|
+
x2={ snapToHalfPixel( x ) }
|
|
28
|
+
y1={ top }
|
|
29
|
+
y2={ bottom }
|
|
30
|
+
color={ tracks.borderColor }
|
|
31
|
+
/>
|
|
32
|
+
) ) }
|
|
33
|
+
{ horizontal.map( ( y, i ) => (
|
|
34
|
+
<GridOutlineLine
|
|
35
|
+
key={ `h-${ i }` }
|
|
36
|
+
x1={ left }
|
|
37
|
+
x2={ right }
|
|
38
|
+
y1={ snapToHalfPixel( y ) }
|
|
39
|
+
y2={ snapToHalfPixel( y ) }
|
|
40
|
+
color={ tracks.borderColor }
|
|
41
|
+
/>
|
|
42
|
+
) ) }
|
|
43
|
+
</svg>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GridOutlineOverlay } from './grid-outline-overlay';
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { useGridTracks } from '../use-grid-tracks';
|
|
4
|
+
|
|
5
|
+
type Style = {
|
|
6
|
+
gridTemplateColumns: string;
|
|
7
|
+
gridTemplateRows: string;
|
|
8
|
+
columnGap: string;
|
|
9
|
+
rowGap: string;
|
|
10
|
+
paddingTop: string;
|
|
11
|
+
paddingRight: string;
|
|
12
|
+
paddingBottom: string;
|
|
13
|
+
paddingLeft: string;
|
|
14
|
+
'--e-a-border-color-bold': string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DEFAULT_STYLE: Style = {
|
|
18
|
+
gridTemplateColumns: 'none',
|
|
19
|
+
gridTemplateRows: 'none',
|
|
20
|
+
columnGap: 'normal',
|
|
21
|
+
rowGap: 'normal',
|
|
22
|
+
paddingTop: '0px',
|
|
23
|
+
paddingRight: '0px',
|
|
24
|
+
paddingBottom: '0px',
|
|
25
|
+
paddingLeft: '0px',
|
|
26
|
+
'--e-a-border-color-bold': '',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function mockElement( style: Partial< Style > = {} ): HTMLElement {
|
|
30
|
+
const resolved: Style = { ...DEFAULT_STYLE, ...style };
|
|
31
|
+
const getComputedStyle = jest.fn().mockReturnValue( {
|
|
32
|
+
gridTemplateColumns: resolved.gridTemplateColumns,
|
|
33
|
+
gridTemplateRows: resolved.gridTemplateRows,
|
|
34
|
+
columnGap: resolved.columnGap,
|
|
35
|
+
rowGap: resolved.rowGap,
|
|
36
|
+
paddingTop: resolved.paddingTop,
|
|
37
|
+
paddingRight: resolved.paddingRight,
|
|
38
|
+
paddingBottom: resolved.paddingBottom,
|
|
39
|
+
paddingLeft: resolved.paddingLeft,
|
|
40
|
+
getPropertyValue: ( name: string ) => ( name === '--e-a-border-color-bold' ? resolved[ name ] : '' ),
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
ownerDocument: {
|
|
45
|
+
defaultView: { getComputedStyle } as unknown as Window,
|
|
46
|
+
},
|
|
47
|
+
} as unknown as HTMLElement;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const RECT = new DOMRect( 0, 0, 320, 200 );
|
|
51
|
+
|
|
52
|
+
describe( 'useGridTracks', () => {
|
|
53
|
+
it( 'returns the empty snapshot when the element is null', () => {
|
|
54
|
+
const { result } = renderHook( () => useGridTracks( null, RECT ) );
|
|
55
|
+
|
|
56
|
+
expect( result.current ).toEqual( {
|
|
57
|
+
columns: [],
|
|
58
|
+
rows: [],
|
|
59
|
+
columnGap: 0,
|
|
60
|
+
rowGap: 0,
|
|
61
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
62
|
+
borderColor: '',
|
|
63
|
+
} );
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'returns the empty snapshot when the element has no owner window', () => {
|
|
67
|
+
const element = { ownerDocument: { defaultView: null } } as unknown as HTMLElement;
|
|
68
|
+
|
|
69
|
+
const { result } = renderHook( () => useGridTracks( element, RECT ) );
|
|
70
|
+
|
|
71
|
+
expect( result.current.columns ).toEqual( [] );
|
|
72
|
+
expect( result.current.rows ).toEqual( [] );
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'parses resolved track lists, gaps, and padding from computed style', () => {
|
|
76
|
+
const element = mockElement( {
|
|
77
|
+
gridTemplateColumns: '100px 100px 100px',
|
|
78
|
+
gridTemplateRows: '80px 80px',
|
|
79
|
+
columnGap: '10px',
|
|
80
|
+
rowGap: '8px',
|
|
81
|
+
paddingTop: '5px',
|
|
82
|
+
paddingRight: '6px',
|
|
83
|
+
paddingBottom: '7px',
|
|
84
|
+
paddingLeft: '8px',
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
const { result } = renderHook( () => useGridTracks( element, RECT ) );
|
|
88
|
+
|
|
89
|
+
expect( result.current ).toEqual( {
|
|
90
|
+
columns: [ 100, 100, 100 ],
|
|
91
|
+
rows: [ 80, 80 ],
|
|
92
|
+
columnGap: 10,
|
|
93
|
+
rowGap: 8,
|
|
94
|
+
padding: { top: 5, right: 6, bottom: 7, left: 8 },
|
|
95
|
+
borderColor: '',
|
|
96
|
+
} );
|
|
97
|
+
} );
|
|
98
|
+
|
|
99
|
+
it( 'reads the --e-a-border-color-bold CSS variable from the iframe', () => {
|
|
100
|
+
const element = mockElement( {
|
|
101
|
+
gridTemplateColumns: '100px',
|
|
102
|
+
'--e-a-border-color-bold': ' #d5d8dc ',
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
const { result } = renderHook( () => useGridTracks( element, RECT ) );
|
|
106
|
+
|
|
107
|
+
expect( result.current.borderColor ).toBe( '#d5d8dc' );
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
it( 'reports gap as 0 when computed style returns "normal"', () => {
|
|
111
|
+
const element = mockElement( {
|
|
112
|
+
gridTemplateColumns: '100px 100px',
|
|
113
|
+
columnGap: 'normal',
|
|
114
|
+
rowGap: 'normal',
|
|
115
|
+
} );
|
|
116
|
+
|
|
117
|
+
const { result } = renderHook( () => useGridTracks( element, RECT ) );
|
|
118
|
+
|
|
119
|
+
expect( result.current.columnGap ).toBe( 0 );
|
|
120
|
+
expect( result.current.rowGap ).toBe( 0 );
|
|
121
|
+
} );
|
|
122
|
+
|
|
123
|
+
it( 'recomputes when the rect dimensions change', () => {
|
|
124
|
+
const element = mockElement( { gridTemplateColumns: '100px' } );
|
|
125
|
+
const getComputedStyle = element.ownerDocument?.defaultView?.getComputedStyle as jest.Mock;
|
|
126
|
+
|
|
127
|
+
const { rerender } = renderHook( ( { rect } ) => useGridTracks( element, rect ), {
|
|
128
|
+
initialProps: { rect: new DOMRect( 0, 0, 320, 200 ) },
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
const callsBefore = getComputedStyle.mock.calls.length;
|
|
132
|
+
|
|
133
|
+
rerender( { rect: new DOMRect( 0, 0, 400, 200 ) } );
|
|
134
|
+
|
|
135
|
+
expect( getComputedStyle.mock.calls.length ).toBeGreaterThan( callsBefore );
|
|
136
|
+
} );
|
|
137
|
+
|
|
138
|
+
it( 'does not recompute when the same rect dimensions are passed again', () => {
|
|
139
|
+
const element = mockElement( { gridTemplateColumns: '100px' } );
|
|
140
|
+
const getComputedStyle = element.ownerDocument?.defaultView?.getComputedStyle as jest.Mock;
|
|
141
|
+
|
|
142
|
+
const { rerender } = renderHook( ( { rect } ) => useGridTracks( element, rect ), {
|
|
143
|
+
initialProps: { rect: new DOMRect( 0, 0, 320, 200 ) },
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
const callsBefore = getComputedStyle.mock.calls.length;
|
|
147
|
+
|
|
148
|
+
rerender( { rect: new DOMRect( 0, 0, 320, 200 ) } );
|
|
149
|
+
|
|
150
|
+
expect( getComputedStyle.mock.calls.length ).toBe( callsBefore );
|
|
151
|
+
} );
|
|
152
|
+
} );
|