@elementor/editor-canvas 4.2.0-926 → 4.2.0-928
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 +277 -77
- package/dist/index.mjs +251 -51
- package/package.json +18 -18
- package/src/components/grid-outline/__tests__/grid-outline.test.tsx +62 -2
- package/src/components/grid-outline/first-empty-cell.tsx +38 -0
- package/src/components/grid-outline/grid-outline-overlay.tsx +1 -1
- package/src/components/grid-outline/grid-outline.tsx +29 -13
- package/src/hooks/use-toolbar-rect.ts +30 -0
- package/src/legacy/create-pro-promotion-nested-type.ts +9 -6
- package/src/utils/__tests__/find-first-empty-cell.test.ts +132 -0
- package/src/utils/clip-path-cutout.ts +11 -0
- package/src/utils/find-first-empty-cell.ts +226 -0
|
@@ -28,7 +28,7 @@ function getSvg( container: HTMLElement ): SVGSVGElement {
|
|
|
28
28
|
describe( '<GridOutline />', () => {
|
|
29
29
|
it( 'sizes the svg to the element rect', () => {
|
|
30
30
|
const { container } = renderWithTheme(
|
|
31
|
-
<GridOutline tracks={ makeTracks( { columns: [ 100 ] } ) } width={ 320 } height={ 200 } />
|
|
31
|
+
<GridOutline element={ null } tracks={ makeTracks( { columns: [ 100 ] } ) } width={ 320 } height={ 200 } />
|
|
32
32
|
);
|
|
33
33
|
|
|
34
34
|
const svg = getSvg( container );
|
|
@@ -40,6 +40,7 @@ describe( '<GridOutline />', () => {
|
|
|
40
40
|
it( 'draws one line per unique boundary for an N×M grid', () => {
|
|
41
41
|
const { container } = renderWithTheme(
|
|
42
42
|
<GridOutline
|
|
43
|
+
element={ null }
|
|
43
44
|
tracks={ makeTracks( { columns: [ 100, 100, 100 ], rows: [ 80, 80 ] } ) }
|
|
44
45
|
width={ 300 }
|
|
45
46
|
height={ 160 }
|
|
@@ -53,6 +54,7 @@ describe( '<GridOutline />', () => {
|
|
|
53
54
|
it( 'snaps line coordinates to half pixels for crisp 1px strokes', () => {
|
|
54
55
|
const { container } = renderWithTheme(
|
|
55
56
|
<GridOutline
|
|
57
|
+
element={ null }
|
|
56
58
|
tracks={ makeTracks( {
|
|
57
59
|
columns: [ 100, 100 ],
|
|
58
60
|
rows: [ 80 ],
|
|
@@ -72,6 +74,7 @@ describe( '<GridOutline />', () => {
|
|
|
72
74
|
it( 'passes the resolved iframe border color through to each line', () => {
|
|
73
75
|
const { container } = renderWithTheme(
|
|
74
76
|
<GridOutline
|
|
77
|
+
element={ null }
|
|
75
78
|
tracks={ makeTracks( { columns: [ 100 ], rows: [ 100 ], borderColor: '#abcdef' } ) }
|
|
76
79
|
width={ 100 }
|
|
77
80
|
height={ 100 }
|
|
@@ -87,7 +90,7 @@ describe( '<GridOutline />', () => {
|
|
|
87
90
|
|
|
88
91
|
it( 'renders nothing when there are no tracks on either axis', () => {
|
|
89
92
|
const { container } = renderWithTheme(
|
|
90
|
-
<GridOutline tracks={ makeTracks() } width={ 100 } height={ 100 } />
|
|
93
|
+
<GridOutline element={ null } tracks={ makeTracks() } width={ 100 } height={ 100 } />
|
|
91
94
|
);
|
|
92
95
|
|
|
93
96
|
expect( container.querySelectorAll( 'line' ) ).toHaveLength( 0 );
|
|
@@ -95,10 +98,65 @@ describe( '<GridOutline />', () => {
|
|
|
95
98
|
} );
|
|
96
99
|
} );
|
|
97
100
|
|
|
101
|
+
describe( 'first-empty-cell indicator', () => {
|
|
102
|
+
it( 'renders a + glyph in the first empty cell when one exists', () => {
|
|
103
|
+
const element = document.createElement( 'div' );
|
|
104
|
+
document.body.appendChild( element );
|
|
105
|
+
|
|
106
|
+
const { container } = renderWithTheme(
|
|
107
|
+
<GridOutline
|
|
108
|
+
element={ element }
|
|
109
|
+
tracks={ makeTracks( { columns: [ 100, 100, 100 ], rows: [ 80, 80 ] } ) }
|
|
110
|
+
width={ 300 }
|
|
111
|
+
height={ 160 }
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect( container.querySelector( '.eicon-plus' ) ).not.toBeNull();
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'does not render the + glyph when the grid is fully occupied', () => {
|
|
119
|
+
const element = document.createElement( 'div' );
|
|
120
|
+
|
|
121
|
+
for ( let i = 0; i < 6; i++ ) {
|
|
122
|
+
const child = document.createElement( 'div' );
|
|
123
|
+
child.classList.add( 'elementor-element' );
|
|
124
|
+
element.appendChild( child );
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
document.body.appendChild( element );
|
|
128
|
+
|
|
129
|
+
const { container } = renderWithTheme(
|
|
130
|
+
<GridOutline
|
|
131
|
+
element={ element }
|
|
132
|
+
tracks={ makeTracks( { columns: [ 100, 100, 100 ], rows: [ 80, 80 ] } ) }
|
|
133
|
+
width={ 300 }
|
|
134
|
+
height={ 160 }
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect( container.querySelector( '.eicon-plus' ) ).toBeNull();
|
|
139
|
+
} );
|
|
140
|
+
|
|
141
|
+
it( 'does not render the + glyph when no element is provided', () => {
|
|
142
|
+
const { container } = renderWithTheme(
|
|
143
|
+
<GridOutline
|
|
144
|
+
element={ null }
|
|
145
|
+
tracks={ makeTracks( { columns: [ 100 ], rows: [ 80 ] } ) }
|
|
146
|
+
width={ 100 }
|
|
147
|
+
height={ 80 }
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect( container.querySelector( '.eicon-plus' ) ).toBeNull();
|
|
152
|
+
} );
|
|
153
|
+
} );
|
|
154
|
+
|
|
98
155
|
describe( 'with a gap', () => {
|
|
99
156
|
it( 'draws one rect per cell so each cell has its own framed perimeter', () => {
|
|
100
157
|
const { container } = renderWithTheme(
|
|
101
158
|
<GridOutline
|
|
159
|
+
element={ null }
|
|
102
160
|
tracks={ makeTracks( {
|
|
103
161
|
columns: [ 100, 100, 100 ],
|
|
104
162
|
rows: [ 80, 80 ],
|
|
@@ -117,6 +175,7 @@ describe( '<GridOutline />', () => {
|
|
|
117
175
|
it( 'offsets cells past the gap', () => {
|
|
118
176
|
const { container } = renderWithTheme(
|
|
119
177
|
<GridOutline
|
|
178
|
+
element={ null }
|
|
120
179
|
tracks={ makeTracks( {
|
|
121
180
|
columns: [ 100, 100, 100 ],
|
|
122
181
|
rows: [ 80 ],
|
|
@@ -134,6 +193,7 @@ describe( '<GridOutline />', () => {
|
|
|
134
193
|
it( 'passes the resolved iframe border color through to each cell', () => {
|
|
135
194
|
const { container } = renderWithTheme(
|
|
136
195
|
<GridOutline
|
|
196
|
+
element={ null }
|
|
137
197
|
tracks={ makeTracks( {
|
|
138
198
|
columns: [ 100, 100 ],
|
|
139
199
|
rows: [ 100 ],
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { type CellRect } from '../../utils/grid-outline-utils';
|
|
4
|
+
|
|
5
|
+
const GLYPH_SIZE = 19;
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
rect: CellRect;
|
|
9
|
+
color: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function FirstEmptyCell( { rect, color }: Props ) {
|
|
13
|
+
const size = Math.min( GLYPH_SIZE, rect.width, rect.height );
|
|
14
|
+
|
|
15
|
+
if ( size <= 0 ) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const centerX = rect.x + rect.width / 2;
|
|
20
|
+
const centerY = rect.y + rect.height / 2;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<i
|
|
24
|
+
className="eicon-plus"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
style={ {
|
|
27
|
+
position: 'absolute',
|
|
28
|
+
left: centerX,
|
|
29
|
+
top: centerY,
|
|
30
|
+
transform: 'translate(-50%, -50%)',
|
|
31
|
+
fontSize: size,
|
|
32
|
+
color,
|
|
33
|
+
lineHeight: 1,
|
|
34
|
+
pointerEvents: 'none',
|
|
35
|
+
} }
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -34,7 +34,7 @@ export const GridOutlineOverlay = ( { element, id, isSelected }: ElementOverlayP
|
|
|
34
34
|
data-grid-outline={ id }
|
|
35
35
|
role="presentation"
|
|
36
36
|
>
|
|
37
|
-
<GridOutline tracks={ tracks } width={ rect.width } height={ rect.height } />
|
|
37
|
+
<GridOutline element={ element } tracks={ tracks } width={ rect.width } height={ rect.height } />
|
|
38
38
|
</Box>
|
|
39
39
|
</FloatingPortal>
|
|
40
40
|
);
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { useMemo } from 'react';
|
|
2
3
|
|
|
3
4
|
import { type GridTracks } from '../../hooks/use-grid-tracks';
|
|
4
|
-
import {
|
|
5
|
+
import { findFirstEmptyCell } from '../../utils/find-first-empty-cell';
|
|
6
|
+
import { type CellRect, computeCellRects, computeGridLines, snapToHalfPixel } from '../../utils/grid-outline-utils';
|
|
5
7
|
import { Cell } from './cell';
|
|
8
|
+
import { FirstEmptyCell } from './first-empty-cell';
|
|
6
9
|
import { Line } from './line';
|
|
7
10
|
|
|
8
11
|
type Props = {
|
|
12
|
+
element: HTMLElement | null;
|
|
9
13
|
tracks: GridTracks;
|
|
10
14
|
width: number;
|
|
11
15
|
height: number;
|
|
12
16
|
};
|
|
13
17
|
|
|
14
|
-
const renderCells = (
|
|
15
|
-
|
|
18
|
+
const renderCells = ( cells: CellRect[], color: string ) =>
|
|
19
|
+
cells.map( ( cell, i ) => (
|
|
16
20
|
<Cell
|
|
17
21
|
key={ i }
|
|
18
22
|
x={ snapToHalfPixel( cell.x ) }
|
|
19
23
|
y={ snapToHalfPixel( cell.y ) }
|
|
20
24
|
width={ Math.round( cell.width ) }
|
|
21
25
|
height={ Math.round( cell.height ) }
|
|
22
|
-
color={
|
|
26
|
+
color={ color }
|
|
23
27
|
/>
|
|
24
28
|
) );
|
|
25
29
|
|
|
@@ -50,17 +54,29 @@ const renderLines = ( tracks: GridTracks, width: number, height: number ) => {
|
|
|
50
54
|
];
|
|
51
55
|
};
|
|
52
56
|
|
|
53
|
-
export function GridOutline( { tracks, width, height }: Props ) {
|
|
57
|
+
export function GridOutline( { element, tracks, width, height }: Props ) {
|
|
58
|
+
const cells = useMemo( () => computeCellRects( tracks, width, height ), [ tracks, width, height ] );
|
|
54
59
|
const hasGap = tracks.columnGap > 0 || tracks.rowGap > 0;
|
|
60
|
+
const firstEmpty = useMemo(
|
|
61
|
+
() => findFirstEmptyCell( element, tracks.columns.length, tracks.rows.length ),
|
|
62
|
+
[ element, tracks ]
|
|
63
|
+
);
|
|
64
|
+
const emptyCellRect =
|
|
65
|
+
firstEmpty && tracks.columns.length > 0
|
|
66
|
+
? cells[ firstEmpty.row * tracks.columns.length + firstEmpty.col ]
|
|
67
|
+
: null;
|
|
55
68
|
|
|
56
69
|
return (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
<>
|
|
71
|
+
<svg
|
|
72
|
+
width={ width }
|
|
73
|
+
height={ height }
|
|
74
|
+
style={ { position: 'absolute', inset: 0, overflow: 'visible' } }
|
|
75
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
76
|
+
>
|
|
77
|
+
{ hasGap ? renderCells( cells, tracks.borderColor ) : renderLines( tracks, width, height ) }
|
|
78
|
+
</svg>
|
|
79
|
+
{ emptyCellRect && <FirstEmptyCell rect={ emptyCellRect } color={ tracks.borderColor } /> }
|
|
80
|
+
</>
|
|
65
81
|
);
|
|
66
82
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const TOOLBAR_SELECTOR = ':scope > .elementor-element-overlay > .elementor-editor-element-settings';
|
|
2
|
+
const WING_OVERHANG = 14;
|
|
3
|
+
const OUTLINE_OVERHANG = 3;
|
|
4
|
+
|
|
5
|
+
export type ToolbarCutoutRect = { x: number; y: number; width: number; height: number };
|
|
6
|
+
|
|
7
|
+
export function useToolbarRect( element: HTMLElement | null, rect: DOMRect ): ToolbarCutoutRect | null {
|
|
8
|
+
if ( ! element ) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const toolbar = element.querySelector( TOOLBAR_SELECTOR );
|
|
13
|
+
|
|
14
|
+
if ( ! toolbar ) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const toolbarRect = toolbar.getBoundingClientRect();
|
|
19
|
+
|
|
20
|
+
if ( toolbarRect.width === 0 && toolbarRect.height === 0 ) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
x: toolbarRect.left - rect.left - WING_OVERHANG,
|
|
26
|
+
y: toolbarRect.top - rect.top - OUTLINE_OVERHANG,
|
|
27
|
+
width: toolbarRect.width + WING_OVERHANG * 2,
|
|
28
|
+
height: toolbarRect.height + OUTLINE_OVERHANG * 2,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -40,8 +40,11 @@ function createPromotionView( BaseView: typeof ElementView ): typeof ElementView
|
|
|
40
40
|
_afterRender() {
|
|
41
41
|
super._afterRender();
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const removeBtnSelector = '.e-pro-promotion-placeholder__remove-btn';
|
|
44
|
+
const unlockBtnSelector = '.e-pro-promotion-placeholder__unlock-btn';
|
|
45
|
+
|
|
46
|
+
this.$el.off( 'click', removeBtnSelector );
|
|
47
|
+
this.$el.on( 'click', removeBtnSelector, ( e: Event ) => {
|
|
45
48
|
e.preventDefault();
|
|
46
49
|
e.stopPropagation();
|
|
47
50
|
|
|
@@ -51,8 +54,8 @@ function createPromotionView( BaseView: typeof ElementView ): typeof ElementView
|
|
|
51
54
|
);
|
|
52
55
|
} );
|
|
53
56
|
|
|
54
|
-
this.$el.off( 'click',
|
|
55
|
-
this.$el.on( 'click',
|
|
57
|
+
this.$el.off( 'click', unlockBtnSelector );
|
|
58
|
+
this.$el.on( 'click', unlockBtnSelector, ( e: Event ) => {
|
|
56
59
|
e.stopPropagation();
|
|
57
60
|
} );
|
|
58
61
|
}
|
|
@@ -75,8 +78,8 @@ function createPromotionView( BaseView: typeof ElementView ): typeof ElementView
|
|
|
75
78
|
|
|
76
79
|
onDestroy( ...args: unknown[] ) {
|
|
77
80
|
super.onDestroy( ...args );
|
|
78
|
-
this.$el.off( 'click', '.e-
|
|
79
|
-
this.$el.off( 'click', '.e-
|
|
81
|
+
this.$el.off( 'click', '.e-pro-promotion-placeholder__remove-btn' );
|
|
82
|
+
this.$el.off( 'click', '.e-pro-promotion-placeholder__unlock-btn' );
|
|
80
83
|
}
|
|
81
84
|
} as unknown as typeof ElementView;
|
|
82
85
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { findFirstEmptyCell } from '../find-first-empty-cell';
|
|
2
|
+
|
|
3
|
+
type ChildSpec = {
|
|
4
|
+
gridColumnStart?: string;
|
|
5
|
+
gridColumnEnd?: string;
|
|
6
|
+
gridRowStart?: string;
|
|
7
|
+
gridRowEnd?: string;
|
|
8
|
+
display?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function makeGrid( {
|
|
12
|
+
autoFlow = 'row',
|
|
13
|
+
children = [],
|
|
14
|
+
}: { autoFlow?: string; children?: ChildSpec[] } = {} ): HTMLElement {
|
|
15
|
+
const element = document.createElement( 'div' );
|
|
16
|
+
element.style.gridAutoFlow = autoFlow;
|
|
17
|
+
|
|
18
|
+
for ( const spec of children ) {
|
|
19
|
+
const child = document.createElement( 'div' );
|
|
20
|
+
child.classList.add( 'elementor-element' );
|
|
21
|
+
|
|
22
|
+
if ( spec.gridColumnStart ) {
|
|
23
|
+
child.style.gridColumnStart = spec.gridColumnStart;
|
|
24
|
+
}
|
|
25
|
+
if ( spec.gridColumnEnd ) {
|
|
26
|
+
child.style.gridColumnEnd = spec.gridColumnEnd;
|
|
27
|
+
}
|
|
28
|
+
if ( spec.gridRowStart ) {
|
|
29
|
+
child.style.gridRowStart = spec.gridRowStart;
|
|
30
|
+
}
|
|
31
|
+
if ( spec.gridRowEnd ) {
|
|
32
|
+
child.style.gridRowEnd = spec.gridRowEnd;
|
|
33
|
+
}
|
|
34
|
+
if ( spec.display ) {
|
|
35
|
+
child.style.display = spec.display;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
element.appendChild( child );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
document.body.appendChild( element );
|
|
42
|
+
return element;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe( 'findFirstEmptyCell', () => {
|
|
46
|
+
it( 'returns null when the element is null', () => {
|
|
47
|
+
expect( findFirstEmptyCell( null, 3, 3 ) ).toBeNull();
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
it( 'returns null when either axis has zero tracks', () => {
|
|
51
|
+
const element = makeGrid();
|
|
52
|
+
expect( findFirstEmptyCell( element, 0, 3 ) ).toBeNull();
|
|
53
|
+
expect( findFirstEmptyCell( element, 3, 0 ) ).toBeNull();
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
it( 'returns the first cell when the grid is empty', () => {
|
|
57
|
+
const element = makeGrid();
|
|
58
|
+
expect( findFirstEmptyCell( element, 3, 2 ) ).toEqual( { row: 0, col: 0 } );
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'walks row-major when grid-auto-flow is row', () => {
|
|
62
|
+
const element = makeGrid( {
|
|
63
|
+
autoFlow: 'row',
|
|
64
|
+
children: [ {}, {} ],
|
|
65
|
+
} );
|
|
66
|
+
expect( findFirstEmptyCell( element, 3, 2 ) ).toEqual( { row: 0, col: 2 } );
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
it( 'walks column-major when grid-auto-flow is column', () => {
|
|
70
|
+
const element = makeGrid( {
|
|
71
|
+
autoFlow: 'column',
|
|
72
|
+
children: [ {}, {} ],
|
|
73
|
+
} );
|
|
74
|
+
expect( findFirstEmptyCell( element, 3, 2 ) ).toEqual( { row: 0, col: 1 } );
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
it( 'honors explicit grid-column-start / grid-row-start', () => {
|
|
78
|
+
const element = makeGrid( {
|
|
79
|
+
children: [ { gridColumnStart: '3', gridRowStart: '1' } ],
|
|
80
|
+
} );
|
|
81
|
+
expect( findFirstEmptyCell( element, 3, 2 ) ).toEqual( { row: 0, col: 0 } );
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
it( 'skips cells covered by an explicit span', () => {
|
|
85
|
+
const element = makeGrid( {
|
|
86
|
+
children: [ { gridColumnStart: '1', gridColumnEnd: 'span 2' } ],
|
|
87
|
+
} );
|
|
88
|
+
expect( findFirstEmptyCell( element, 3, 2 ) ).toEqual( { row: 0, col: 2 } );
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
it( 'skips rows covered by a row span', () => {
|
|
92
|
+
const element = makeGrid( {
|
|
93
|
+
children: [ { gridRowStart: '1', gridRowEnd: 'span 2' } ],
|
|
94
|
+
} );
|
|
95
|
+
expect( findFirstEmptyCell( element, 2, 2 ) ).toEqual( { row: 0, col: 1 } );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
it( 'auto-places remaining children around explicit placements', () => {
|
|
99
|
+
const element = makeGrid( {
|
|
100
|
+
children: [ { gridColumnStart: '2', gridRowStart: '1' }, {} ],
|
|
101
|
+
} );
|
|
102
|
+
expect( findFirstEmptyCell( element, 3, 2 ) ).toEqual( { row: 0, col: 2 } );
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
it( 'returns null when the grid is fully occupied', () => {
|
|
106
|
+
const element = makeGrid( {
|
|
107
|
+
children: [ {}, {}, {}, {}, {}, {} ],
|
|
108
|
+
} );
|
|
109
|
+
expect( findFirstEmptyCell( element, 3, 2 ) ).toBeNull();
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'ignores children with display: none', () => {
|
|
113
|
+
const element = makeGrid( {
|
|
114
|
+
children: [ { display: 'none' } ],
|
|
115
|
+
} );
|
|
116
|
+
expect( findFirstEmptyCell( element, 2, 1 ) ).toEqual( { row: 0, col: 0 } );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'ignores scaffolding children that are not .elementor-element', () => {
|
|
120
|
+
const element = makeGrid( { children: [ {} ] } );
|
|
121
|
+
|
|
122
|
+
const overlay = document.createElement( 'div' );
|
|
123
|
+
overlay.classList.add( 'elementor-element-overlay' );
|
|
124
|
+
element.insertBefore( overlay, element.firstChild );
|
|
125
|
+
|
|
126
|
+
const emptyView = document.createElement( 'div' );
|
|
127
|
+
emptyView.classList.add( 'elementor-empty-view' );
|
|
128
|
+
element.appendChild( emptyView );
|
|
129
|
+
|
|
130
|
+
expect( findFirstEmptyCell( element, 3, 1 ) ).toEqual( { row: 0, col: 1 } );
|
|
131
|
+
} );
|
|
132
|
+
} );
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Size = { width: number; height: number };
|
|
2
|
+
type CutoutRect = { x: number; y: number; width: number; height: number };
|
|
3
|
+
|
|
4
|
+
export function rectCutoutClipPath( outer: Size, inner: CutoutRect ): string {
|
|
5
|
+
const { width: ow, height: oh } = outer;
|
|
6
|
+
const { x, y, width: iw, height: ih } = inner;
|
|
7
|
+
|
|
8
|
+
return `path(evenodd, 'M 0 0 L ${ ow } 0 L ${ ow } ${ oh } L 0 ${ oh } Z M ${ x } ${ y } L ${ x + iw } ${ y } L ${
|
|
9
|
+
x + iw
|
|
10
|
+
} ${ y + ih } L ${ x } ${ y + ih } L ${ x } ${ y } Z')`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
export type EmptyCell = {
|
|
2
|
+
row: number;
|
|
3
|
+
col: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type LineValue = 'auto' | number | { kind: 'span'; n: number };
|
|
7
|
+
|
|
8
|
+
type ResolvedPlacement = {
|
|
9
|
+
start: number | null;
|
|
10
|
+
span: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function findFirstEmptyCell(
|
|
14
|
+
element: HTMLElement | null,
|
|
15
|
+
columnCount: number,
|
|
16
|
+
rowCount: number
|
|
17
|
+
): EmptyCell | null {
|
|
18
|
+
if ( ! element || columnCount === 0 || rowCount === 0 ) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const previewWindow = element.ownerDocument?.defaultView;
|
|
23
|
+
|
|
24
|
+
if ( ! previewWindow ) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const containerStyle = previewWindow.getComputedStyle( element );
|
|
29
|
+
const flowsByColumn = containerStyle.gridAutoFlow.trim().startsWith( 'column' );
|
|
30
|
+
|
|
31
|
+
const matrix: boolean[][] = Array.from( { length: rowCount }, () => new Array( columnCount ).fill( false ) );
|
|
32
|
+
|
|
33
|
+
const explicit: Array< { col: number | null; colSpan: number; row: number | null; rowSpan: number } > = [];
|
|
34
|
+
const autoPlaced: Array< { colSpan: number; rowSpan: number } > = [];
|
|
35
|
+
|
|
36
|
+
for ( const child of Array.from( element.children ) ) {
|
|
37
|
+
if ( ! child.classList.contains( 'elementor-element' ) ) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const style = previewWindow.getComputedStyle( child );
|
|
42
|
+
|
|
43
|
+
if ( style.display === 'none' ) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const col = resolvePlacement( style.gridColumnStart, style.gridColumnEnd );
|
|
48
|
+
const row = resolvePlacement( style.gridRowStart, style.gridRowEnd );
|
|
49
|
+
|
|
50
|
+
if ( col.start !== null || row.start !== null ) {
|
|
51
|
+
explicit.push( { col: col.start, colSpan: col.span, row: row.start, rowSpan: row.span } );
|
|
52
|
+
} else {
|
|
53
|
+
autoPlaced.push( { colSpan: col.span, rowSpan: row.span } );
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for ( const child of explicit ) {
|
|
58
|
+
fillMatrix( matrix, child.col ?? 0, child.row ?? 0, child.colSpan, child.rowSpan );
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for ( const child of autoPlaced ) {
|
|
62
|
+
const slot = findNextFreeSlot( matrix, child.colSpan, child.rowSpan, flowsByColumn );
|
|
63
|
+
|
|
64
|
+
if ( slot ) {
|
|
65
|
+
fillMatrix( matrix, slot.col, slot.row, child.colSpan, child.rowSpan );
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return scanFirstEmpty( matrix, flowsByColumn );
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolvePlacement( startRaw: string, endRaw: string ): ResolvedPlacement {
|
|
73
|
+
const start = parseLineValue( startRaw );
|
|
74
|
+
const end = parseLineValue( endRaw );
|
|
75
|
+
|
|
76
|
+
if ( typeof start === 'number' ) {
|
|
77
|
+
const zeroIndexedStart = start - 1;
|
|
78
|
+
|
|
79
|
+
if ( typeof end === 'number' ) {
|
|
80
|
+
return { start: zeroIndexedStart, span: Math.max( 1, end - start ) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if ( isSpan( end ) ) {
|
|
84
|
+
return { start: zeroIndexedStart, span: end.n };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { start: zeroIndexedStart, span: 1 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ( isSpan( start ) ) {
|
|
91
|
+
if ( typeof end === 'number' ) {
|
|
92
|
+
const zeroIndexedStart = end - 1 - start.n;
|
|
93
|
+
return { start: zeroIndexedStart >= 0 ? zeroIndexedStart : null, span: start.n };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { start: null, span: start.n };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if ( typeof end === 'number' ) {
|
|
100
|
+
const zeroIndexedStart = end - 2;
|
|
101
|
+
return { start: zeroIndexedStart >= 0 ? zeroIndexedStart : null, span: 1 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ( isSpan( end ) ) {
|
|
105
|
+
return { start: null, span: end.n };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { start: null, span: 1 };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseLineValue( raw: string ): LineValue {
|
|
112
|
+
const trimmed = raw.trim();
|
|
113
|
+
|
|
114
|
+
if ( trimmed === '' || trimmed === 'auto' ) {
|
|
115
|
+
return 'auto';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const spanMatch = trimmed.match( /^span\s+(\d+)$/ );
|
|
119
|
+
|
|
120
|
+
if ( spanMatch ) {
|
|
121
|
+
const n = parseInt( spanMatch[ 1 ], 10 );
|
|
122
|
+
return { kind: 'span', n: Math.max( 1, n ) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const parsed = parseInt( trimmed, 10 );
|
|
126
|
+
|
|
127
|
+
if ( Number.isFinite( parsed ) && parsed > 0 ) {
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return 'auto';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isSpan( value: LineValue ): value is { kind: 'span'; n: number } {
|
|
135
|
+
return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'span';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fillMatrix( matrix: boolean[][], col: number, row: number, colSpan: number, rowSpan: number ): void {
|
|
139
|
+
const rows = matrix.length;
|
|
140
|
+
const cols = rows > 0 ? matrix[ 0 ].length : 0;
|
|
141
|
+
|
|
142
|
+
const startRow = Math.max( 0, row );
|
|
143
|
+
const startCol = Math.max( 0, col );
|
|
144
|
+
const endRow = Math.min( rows, row + rowSpan );
|
|
145
|
+
const endCol = Math.min( cols, col + colSpan );
|
|
146
|
+
|
|
147
|
+
for ( let r = startRow; r < endRow; r++ ) {
|
|
148
|
+
for ( let c = startCol; c < endCol; c++ ) {
|
|
149
|
+
matrix[ r ][ c ] = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function findNextFreeSlot(
|
|
155
|
+
matrix: boolean[][],
|
|
156
|
+
colSpan: number,
|
|
157
|
+
rowSpan: number,
|
|
158
|
+
flowsByColumn: boolean
|
|
159
|
+
): EmptyCell | null {
|
|
160
|
+
const rows = matrix.length;
|
|
161
|
+
const cols = rows > 0 ? matrix[ 0 ].length : 0;
|
|
162
|
+
|
|
163
|
+
const maxCol = cols - colSpan;
|
|
164
|
+
const maxRow = rows - rowSpan;
|
|
165
|
+
|
|
166
|
+
if ( maxCol < 0 || maxRow < 0 ) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if ( flowsByColumn ) {
|
|
171
|
+
for ( let col = 0; col <= maxCol; col++ ) {
|
|
172
|
+
for ( let row = 0; row <= maxRow; row++ ) {
|
|
173
|
+
if ( canFit( matrix, col, row, colSpan, rowSpan ) ) {
|
|
174
|
+
return { row, col };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
for ( let row = 0; row <= maxRow; row++ ) {
|
|
180
|
+
for ( let col = 0; col <= maxCol; col++ ) {
|
|
181
|
+
if ( canFit( matrix, col, row, colSpan, rowSpan ) ) {
|
|
182
|
+
return { row, col };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function canFit( matrix: boolean[][], col: number, row: number, colSpan: number, rowSpan: number ): boolean {
|
|
192
|
+
for ( let r = row; r < row + rowSpan; r++ ) {
|
|
193
|
+
for ( let c = col; c < col + colSpan; c++ ) {
|
|
194
|
+
if ( matrix[ r ][ c ] ) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function scanFirstEmpty( matrix: boolean[][], flowsByColumn: boolean ): EmptyCell | null {
|
|
204
|
+
const rows = matrix.length;
|
|
205
|
+
const cols = rows > 0 ? matrix[ 0 ].length : 0;
|
|
206
|
+
|
|
207
|
+
if ( flowsByColumn ) {
|
|
208
|
+
for ( let col = 0; col < cols; col++ ) {
|
|
209
|
+
for ( let row = 0; row < rows; row++ ) {
|
|
210
|
+
if ( ! matrix[ row ][ col ] ) {
|
|
211
|
+
return { row, col };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
for ( let row = 0; row < rows; row++ ) {
|
|
217
|
+
for ( let col = 0; col < cols; col++ ) {
|
|
218
|
+
if ( ! matrix[ row ][ col ] ) {
|
|
219
|
+
return { row, col };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return null;
|
|
226
|
+
}
|