@nocobase/flow-engine 2.0.0-alpha.3 → 2.0.0-alpha.5
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/lib/components/dnd/findModelUidPosition.d.ts +13 -0
- package/lib/components/dnd/findModelUidPosition.js +50 -0
- package/lib/components/dnd/gridDragPlanner.d.ts +130 -0
- package/lib/components/dnd/gridDragPlanner.js +497 -0
- package/lib/components/dnd/index.d.ts +2 -2
- package/lib/components/dnd/index.js +5 -5
- package/lib/data-source/index.js +4 -2
- package/lib/data-source/sortCollectionsByInherits.d.ts +10 -0
- package/lib/data-source/sortCollectionsByInherits.js +71 -0
- package/lib/flowContext.js +4 -2
- package/package.json +2 -2
- package/src/components/__tests__/gridDragPlanner.test.ts +494 -0
- package/src/components/dnd/README.md +149 -0
- package/src/components/dnd/findModelUidPosition.ts +26 -0
- package/src/components/dnd/gridDragPlanner.ts +659 -0
- package/src/components/dnd/index.tsx +3 -3
- package/src/data-source/__tests__/sortCollectionsByInherits.test.ts +125 -0
- package/src/data-source/index.ts +4 -2
- package/src/data-source/sortCollectionsByInherits.ts +61 -0
- package/src/flowContext.ts +6 -2
- package/lib/components/dnd/getMousePositionOnElement.d.ts +0 -50
- package/lib/components/dnd/getMousePositionOnElement.js +0 -95
- package/lib/components/dnd/moveBlock.d.ts +0 -33
- package/lib/components/dnd/moveBlock.js +0 -302
- package/src/components/dnd/getMousePositionOnElement.ts +0 -115
- package/src/components/dnd/moveBlock.ts +0 -379
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { uid } from '@formily/shared';
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
|
|
13
|
+
/** 栅格系统常量 */
|
|
14
|
+
export const DEFAULT_GRID_COLUMNS = 24;
|
|
15
|
+
|
|
16
|
+
/** 最小 slot 厚度 */
|
|
17
|
+
export const MIN_SLOT_THICKNESS = 16;
|
|
18
|
+
/** 最大 slot 厚度 */
|
|
19
|
+
export const MAX_SLOT_THICKNESS = 48;
|
|
20
|
+
|
|
21
|
+
/** 列边缘最小宽度 */
|
|
22
|
+
export const COLUMN_EDGE_MIN_WIDTH = 12;
|
|
23
|
+
/** 列边缘最大宽度 */
|
|
24
|
+
export const COLUMN_EDGE_MAX_WIDTH = 28;
|
|
25
|
+
/** 列边缘宽度占列宽的比例(原来是 1/5) */
|
|
26
|
+
export const COLUMN_EDGE_WIDTH_RATIO = 0.2;
|
|
27
|
+
|
|
28
|
+
/** 插入区域厚度比例常量 */
|
|
29
|
+
const COLUMN_INSERT_THICKNESS_RATIO = 0.5; // 元素高度的 1/2
|
|
30
|
+
|
|
31
|
+
/** 行间隙高度比例常量 */
|
|
32
|
+
const ROW_GAP_HEIGHT_RATIO = 0.33; // 行高的 1/3
|
|
33
|
+
|
|
34
|
+
export interface Rect {
|
|
35
|
+
top: number;
|
|
36
|
+
left: number;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Point {
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GridLayoutData {
|
|
47
|
+
rows: Record<string, string[][]>;
|
|
48
|
+
sizes: Record<string, number[]>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ColumnSlot {
|
|
52
|
+
type: 'column';
|
|
53
|
+
rowId: string;
|
|
54
|
+
columnIndex: number;
|
|
55
|
+
insertIndex: number;
|
|
56
|
+
position: 'before' | 'after';
|
|
57
|
+
rect: Rect;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ColumnEdgeSlot {
|
|
61
|
+
type: 'column-edge';
|
|
62
|
+
rowId: string;
|
|
63
|
+
columnIndex: number;
|
|
64
|
+
direction: 'left' | 'right';
|
|
65
|
+
rect: Rect;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RowGapSlot {
|
|
69
|
+
type: 'row-gap';
|
|
70
|
+
targetRowId: string;
|
|
71
|
+
position: 'above' | 'below';
|
|
72
|
+
rect: Rect;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface EmptyRowSlot {
|
|
76
|
+
type: 'empty-row';
|
|
77
|
+
rect: Rect;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EmptyColumnSlot {
|
|
81
|
+
type: 'empty-column';
|
|
82
|
+
rowId: string;
|
|
83
|
+
columnIndex: number;
|
|
84
|
+
rect: Rect;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type LayoutSlot = ColumnSlot | ColumnEdgeSlot | RowGapSlot | EmptyRowSlot | EmptyColumnSlot;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 列内插入的配置
|
|
91
|
+
*/
|
|
92
|
+
export interface ColumnInsertConfig {
|
|
93
|
+
/** 高亮区域的高度(像素) */
|
|
94
|
+
height?: number;
|
|
95
|
+
/** 垂直偏移(像素),正数向下,负数向上 */
|
|
96
|
+
offsetTop?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 列边缘的配置
|
|
101
|
+
*/
|
|
102
|
+
export interface ColumnEdgeConfig {
|
|
103
|
+
/** 高亮区域的宽度(像素) */
|
|
104
|
+
width?: number;
|
|
105
|
+
/** 水平偏移(像素),正数向右,负数向左 */
|
|
106
|
+
offsetLeft?: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 行间隙的配置
|
|
111
|
+
*/
|
|
112
|
+
export interface RowGapConfig {
|
|
113
|
+
/** 高亮区域的高度(像素) */
|
|
114
|
+
height?: number;
|
|
115
|
+
/** 垂直偏移(像素),正数向下,负数向上 */
|
|
116
|
+
offsetTop?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 拖拽高亮区域的全局配置
|
|
121
|
+
*/
|
|
122
|
+
export interface DragOverlayConfig {
|
|
123
|
+
/** 列内插入(before 表示在区块上方插入,after 表示在区块下方插入) */
|
|
124
|
+
columnInsert?: {
|
|
125
|
+
before?: ColumnInsertConfig;
|
|
126
|
+
after?: ColumnInsertConfig;
|
|
127
|
+
};
|
|
128
|
+
/** 列边缘(left 表示在左侧新建列,right 表示在右侧新建列) */
|
|
129
|
+
columnEdge?: {
|
|
130
|
+
left?: ColumnEdgeConfig;
|
|
131
|
+
right?: ColumnEdgeConfig;
|
|
132
|
+
};
|
|
133
|
+
/** 行间隙(above 表示在行上方插入,below 表示在行下方插入) */
|
|
134
|
+
rowGap?: {
|
|
135
|
+
above?: RowGapConfig;
|
|
136
|
+
below?: RowGapConfig;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface LayoutSnapshot {
|
|
141
|
+
slots: LayoutSlot[];
|
|
142
|
+
containerRect: Rect;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface BuildLayoutSnapshotOptions {
|
|
146
|
+
container: HTMLElement | null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const toRect = (domRect: DOMRect): Rect => ({
|
|
150
|
+
top: domRect.top,
|
|
151
|
+
left: domRect.left,
|
|
152
|
+
width: domRect.width,
|
|
153
|
+
height: domRect.height,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const clampSlotHeight = (value: number) => Math.max(MIN_SLOT_THICKNESS, Math.min(MAX_SLOT_THICKNESS, value));
|
|
157
|
+
|
|
158
|
+
const createRect = ({ top, left, width, height }: Rect): Rect => ({
|
|
159
|
+
top,
|
|
160
|
+
left,
|
|
161
|
+
width,
|
|
162
|
+
height,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const offsetRect = (rect: Rect, offsets: Partial<Rect>): Rect => ({
|
|
166
|
+
top: offsets.top ?? rect.top,
|
|
167
|
+
left: offsets.left ?? rect.left,
|
|
168
|
+
width: offsets.width ?? rect.width,
|
|
169
|
+
height: offsets.height ?? rect.height,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const createRowGapRect = (source: Rect, position: 'above' | 'below', containerRect: Rect): Rect => {
|
|
173
|
+
const baseHeight = clampSlotHeight(source.height * ROW_GAP_HEIGHT_RATIO);
|
|
174
|
+
if (position === 'above') {
|
|
175
|
+
const available = source.top - containerRect.top;
|
|
176
|
+
const height = clampSlotHeight(Math.max(baseHeight, available));
|
|
177
|
+
return createRect({
|
|
178
|
+
top: source.top - height,
|
|
179
|
+
left: containerRect.left,
|
|
180
|
+
width: containerRect.width,
|
|
181
|
+
height,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const available = containerRect.top + containerRect.height - (source.top + source.height);
|
|
186
|
+
const height = clampSlotHeight(Math.max(baseHeight, available));
|
|
187
|
+
return createRect({
|
|
188
|
+
top: source.top + source.height,
|
|
189
|
+
left: containerRect.left,
|
|
190
|
+
width: containerRect.width,
|
|
191
|
+
height,
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const createColumnEdgeRect = (source: Rect, side: 'left' | 'right'): Rect => {
|
|
196
|
+
const width = Math.min(
|
|
197
|
+
Math.max(COLUMN_EDGE_MIN_WIDTH, source.width * COLUMN_EDGE_WIDTH_RATIO),
|
|
198
|
+
COLUMN_EDGE_MAX_WIDTH,
|
|
199
|
+
);
|
|
200
|
+
return createRect({
|
|
201
|
+
top: source.top,
|
|
202
|
+
left: side === 'left' ? source.left : source.left + source.width - width,
|
|
203
|
+
width,
|
|
204
|
+
height: source.height,
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const createColumnInsertRect = (itemRect: Rect, position: 'before' | 'after'): Rect => {
|
|
209
|
+
const thickness = clampSlotHeight(itemRect.height * COLUMN_INSERT_THICKNESS_RATIO);
|
|
210
|
+
if (position === 'before') {
|
|
211
|
+
return createRect({
|
|
212
|
+
top: itemRect.top,
|
|
213
|
+
left: itemRect.left,
|
|
214
|
+
width: itemRect.width,
|
|
215
|
+
height: thickness,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return createRect({
|
|
219
|
+
top: itemRect.top + itemRect.height - thickness,
|
|
220
|
+
left: itemRect.left,
|
|
221
|
+
width: itemRect.width,
|
|
222
|
+
height: thickness,
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const expandColumnRect = (columnRect: Rect): Rect => ({
|
|
227
|
+
top: columnRect.top,
|
|
228
|
+
left: columnRect.left,
|
|
229
|
+
width: columnRect.width,
|
|
230
|
+
height: Math.max(columnRect.height, MIN_SLOT_THICKNESS),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export const buildLayoutSnapshot = ({ container }: BuildLayoutSnapshotOptions): LayoutSnapshot => {
|
|
234
|
+
if (!container) {
|
|
235
|
+
return {
|
|
236
|
+
slots: [],
|
|
237
|
+
containerRect: { top: 0, left: 0, width: 0, height: 0 },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const containerRect = toRect(container.getBoundingClientRect());
|
|
242
|
+
const slots: LayoutSlot[] = [];
|
|
243
|
+
|
|
244
|
+
// 获取所有行元素,但只保留直接属于当前容器的(不在子 Grid 中的)
|
|
245
|
+
const allRowElements = Array.from(container.querySelectorAll('[data-grid-row-id]'));
|
|
246
|
+
const rowElements = allRowElements.filter((el) => {
|
|
247
|
+
const htmlEl = el as HTMLElement;
|
|
248
|
+
// 查找该元素最近的带 data-grid-row-id 的祖先容器
|
|
249
|
+
let parent = htmlEl.parentElement;
|
|
250
|
+
while (parent && parent !== container) {
|
|
251
|
+
if (parent.hasAttribute('data-grid-row-id')) {
|
|
252
|
+
// 说明这个元素在另一个 Grid 行内,是嵌套的
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
parent = parent.parentElement;
|
|
256
|
+
}
|
|
257
|
+
// 如果遍历到 container 都没遇到其他 grid-row,说明是直接子元素
|
|
258
|
+
return true;
|
|
259
|
+
}) as HTMLElement[];
|
|
260
|
+
|
|
261
|
+
if (rowElements.length === 0) {
|
|
262
|
+
slots.push({
|
|
263
|
+
type: 'empty-row',
|
|
264
|
+
rect: createRect({
|
|
265
|
+
top: containerRect.top,
|
|
266
|
+
left: containerRect.left,
|
|
267
|
+
width: containerRect.width,
|
|
268
|
+
height: Math.max(containerRect.height, MIN_SLOT_THICKNESS),
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
return { slots, containerRect };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
rowElements.forEach((rowElement, rowIndex) => {
|
|
275
|
+
const rowId = rowElement.dataset.gridRowId;
|
|
276
|
+
if (!rowId) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const rowRect = toRect(rowElement.getBoundingClientRect());
|
|
280
|
+
|
|
281
|
+
if (rowIndex === 0) {
|
|
282
|
+
slots.push({
|
|
283
|
+
type: 'row-gap',
|
|
284
|
+
targetRowId: rowId,
|
|
285
|
+
position: 'above',
|
|
286
|
+
rect: createRowGapRect(rowRect, 'above', containerRect),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const columnElements = Array.from(
|
|
291
|
+
container.querySelectorAll(`[data-grid-column-row-id="${rowId}"][data-grid-column-index]`),
|
|
292
|
+
) as HTMLElement[];
|
|
293
|
+
|
|
294
|
+
const sortedColumns = columnElements.sort((a, b) => {
|
|
295
|
+
const indexA = Number(a.dataset.gridColumnIndex || 0);
|
|
296
|
+
const indexB = Number(b.dataset.gridColumnIndex || 0);
|
|
297
|
+
return indexA - indexB;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
sortedColumns.forEach((columnElement) => {
|
|
301
|
+
const columnIndex = Number(columnElement.dataset.gridColumnIndex || 0);
|
|
302
|
+
const columnRect = toRect(columnElement.getBoundingClientRect());
|
|
303
|
+
|
|
304
|
+
slots.push({
|
|
305
|
+
type: 'column-edge',
|
|
306
|
+
rowId,
|
|
307
|
+
columnIndex,
|
|
308
|
+
direction: 'left',
|
|
309
|
+
rect: createColumnEdgeRect(columnRect, 'left'),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
slots.push({
|
|
313
|
+
type: 'column-edge',
|
|
314
|
+
rowId,
|
|
315
|
+
columnIndex,
|
|
316
|
+
direction: 'right',
|
|
317
|
+
rect: createColumnEdgeRect(columnRect, 'right'),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const itemElements = Array.from(
|
|
321
|
+
columnElement.querySelectorAll(`[data-grid-item-row-id="${rowId}"][data-grid-column-index="${columnIndex}"]`),
|
|
322
|
+
) as HTMLElement[];
|
|
323
|
+
|
|
324
|
+
const sortedItems = itemElements.sort((a, b) => {
|
|
325
|
+
const indexA = Number(a.dataset.gridItemIndex || 0);
|
|
326
|
+
const indexB = Number(b.dataset.gridItemIndex || 0);
|
|
327
|
+
return indexA - indexB;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (sortedItems.length === 0) {
|
|
331
|
+
slots.push({
|
|
332
|
+
type: 'empty-column',
|
|
333
|
+
rowId,
|
|
334
|
+
columnIndex,
|
|
335
|
+
rect: expandColumnRect(columnRect),
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const firstItemRect = toRect(sortedItems[0].getBoundingClientRect());
|
|
341
|
+
slots.push({
|
|
342
|
+
type: 'column',
|
|
343
|
+
rowId,
|
|
344
|
+
columnIndex,
|
|
345
|
+
insertIndex: 0,
|
|
346
|
+
position: 'before',
|
|
347
|
+
rect: createColumnInsertRect(firstItemRect, 'before'),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
sortedItems.forEach((itemElement, itemIndex) => {
|
|
351
|
+
const itemRect = toRect(itemElement.getBoundingClientRect());
|
|
352
|
+
slots.push({
|
|
353
|
+
type: 'column',
|
|
354
|
+
rowId,
|
|
355
|
+
columnIndex,
|
|
356
|
+
insertIndex: itemIndex + 1,
|
|
357
|
+
position: 'after',
|
|
358
|
+
rect: createColumnInsertRect(itemRect, 'after'),
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
slots.push({
|
|
364
|
+
type: 'row-gap',
|
|
365
|
+
targetRowId: rowId,
|
|
366
|
+
position: 'below',
|
|
367
|
+
rect: createRowGapRect(rowRect, 'below', containerRect),
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
slots,
|
|
373
|
+
containerRect,
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export const getSlotKey = (slot: LayoutSlot): string => {
|
|
378
|
+
switch (slot.type) {
|
|
379
|
+
case 'column':
|
|
380
|
+
return `${slot.type}:${slot.rowId}:${slot.columnIndex}:${slot.insertIndex}:${slot.position}`;
|
|
381
|
+
case 'column-edge':
|
|
382
|
+
return `${slot.type}:${slot.rowId}:${slot.columnIndex}:${slot.direction}`;
|
|
383
|
+
case 'row-gap':
|
|
384
|
+
return `${slot.type}:${slot.targetRowId}:${slot.position}`;
|
|
385
|
+
case 'empty-row':
|
|
386
|
+
return `${slot.type}`;
|
|
387
|
+
case 'empty-column':
|
|
388
|
+
return `${slot.type}:${slot.rowId}:${slot.columnIndex}`;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const isPointInsideRect = (point: Point, rect: Rect): boolean => {
|
|
393
|
+
return (
|
|
394
|
+
point.x >= rect.left &&
|
|
395
|
+
point.x <= rect.left + rect.width &&
|
|
396
|
+
point.y >= rect.top &&
|
|
397
|
+
point.y <= rect.top + rect.height
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const distanceToRect = (point: Point, rect: Rect): number => {
|
|
402
|
+
const dx = Math.max(rect.left - point.x, 0, point.x - (rect.left + rect.width));
|
|
403
|
+
const dy = Math.max(rect.top - point.y, 0, point.y - (rect.top + rect.height));
|
|
404
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
export const resolveDropIntent = (point: Point, slots: LayoutSlot[]): LayoutSlot | null => {
|
|
408
|
+
if (!slots.length) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const insideSlot = slots.find((slot) => isPointInsideRect(point, slot.rect));
|
|
413
|
+
if (insideSlot) {
|
|
414
|
+
return insideSlot;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let closest: LayoutSlot | null = null;
|
|
418
|
+
let minDistance = Number.POSITIVE_INFINITY;
|
|
419
|
+
slots.forEach((slot) => {
|
|
420
|
+
const distance = distanceToRect(point, slot.rect);
|
|
421
|
+
if (distance < minDistance) {
|
|
422
|
+
minDistance = distance;
|
|
423
|
+
closest = slot;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return closest;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const findUidPosition = (rows: Record<string, string[][]>, uidValue: string) => {
|
|
431
|
+
for (const [rowId, columns] of Object.entries(rows)) {
|
|
432
|
+
for (let columnIndex = 0; columnIndex < columns.length; columnIndex += 1) {
|
|
433
|
+
const column = columns[columnIndex];
|
|
434
|
+
const itemIndex = column.indexOf(uidValue);
|
|
435
|
+
if (itemIndex !== -1) {
|
|
436
|
+
return { rowId, columnIndex, itemIndex };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const removeItemFromLayout = (layout: GridLayoutData, uidValue: string) => {
|
|
444
|
+
const position = findUidPosition(layout.rows, uidValue);
|
|
445
|
+
if (!position) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const { rowId, columnIndex, itemIndex } = position;
|
|
450
|
+
const columns = layout.rows[rowId];
|
|
451
|
+
const column = columns?.[columnIndex];
|
|
452
|
+
if (!column) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
column.splice(itemIndex, 1);
|
|
457
|
+
|
|
458
|
+
if (column.length === 0) {
|
|
459
|
+
columns.splice(columnIndex, 1);
|
|
460
|
+
if (layout.sizes[rowId]) {
|
|
461
|
+
layout.sizes[rowId].splice(columnIndex, 1);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (columns.length === 0) {
|
|
466
|
+
delete layout.rows[rowId];
|
|
467
|
+
delete layout.sizes[rowId];
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
normalizeRowSizes(rowId, layout);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const toIntSizes = (weights: number[], count: number): number[] => {
|
|
475
|
+
if (count === 0) {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const normalizedWeights = weights.map((weight) => (Number.isFinite(weight) && weight > 0 ? weight : 1));
|
|
480
|
+
const total = normalizedWeights.reduce((sum, weight) => sum + weight, 0) || count;
|
|
481
|
+
const ratios = normalizedWeights.map((weight) => weight / total);
|
|
482
|
+
const raw = ratios.map((ratio) => ratio * DEFAULT_GRID_COLUMNS);
|
|
483
|
+
const floors = raw.map((value) => Math.max(1, Math.floor(value)));
|
|
484
|
+
let remainder = DEFAULT_GRID_COLUMNS - floors.reduce((sum, value) => sum + value, 0);
|
|
485
|
+
|
|
486
|
+
if (remainder > 0) {
|
|
487
|
+
const decimals = raw
|
|
488
|
+
.map((value, index) => ({ index, decimal: value - Math.floor(value) }))
|
|
489
|
+
.sort((a, b) => b.decimal - a.decimal);
|
|
490
|
+
let pointer = 0;
|
|
491
|
+
while (remainder > 0 && decimals.length) {
|
|
492
|
+
const target = decimals[pointer % decimals.length].index;
|
|
493
|
+
floors[target] += 1;
|
|
494
|
+
remainder -= 1;
|
|
495
|
+
pointer += 1;
|
|
496
|
+
}
|
|
497
|
+
} else if (remainder < 0) {
|
|
498
|
+
const decimals = raw
|
|
499
|
+
.map((value, index) => ({ index, decimal: value - Math.floor(value) }))
|
|
500
|
+
.sort((a, b) => a.decimal - b.decimal);
|
|
501
|
+
let pointer = 0;
|
|
502
|
+
while (remainder < 0 && decimals.length) {
|
|
503
|
+
const target = decimals[pointer % decimals.length].index;
|
|
504
|
+
if (floors[target] > 1) {
|
|
505
|
+
floors[target] -= 1;
|
|
506
|
+
remainder += 1;
|
|
507
|
+
}
|
|
508
|
+
pointer += 1;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const diff = DEFAULT_GRID_COLUMNS - floors.reduce((sum, value) => sum + value, 0);
|
|
513
|
+
if (diff !== 0 && floors.length) {
|
|
514
|
+
floors[floors.length - 1] += diff;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return floors;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const normalizeRowSizes = (rowId: string, layout: GridLayoutData) => {
|
|
521
|
+
const columns = layout.rows[rowId];
|
|
522
|
+
if (!columns || columns.length === 0) {
|
|
523
|
+
delete layout.sizes[rowId];
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const current = layout.sizes[rowId] || new Array(columns.length).fill(DEFAULT_GRID_COLUMNS / columns.length);
|
|
528
|
+
const weights =
|
|
529
|
+
current.length === columns.length ? current : new Array(columns.length).fill(DEFAULT_GRID_COLUMNS / columns.length);
|
|
530
|
+
layout.sizes[rowId] = toIntSizes(weights, columns.length);
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const insertRow = (
|
|
534
|
+
rows: Record<string, string[][]>,
|
|
535
|
+
referenceRowId: string,
|
|
536
|
+
newRowId: string,
|
|
537
|
+
position: 'before' | 'after',
|
|
538
|
+
value: string[][],
|
|
539
|
+
): Record<string, string[][]> => {
|
|
540
|
+
const entries = Object.entries(rows);
|
|
541
|
+
const result: Record<string, string[][]> = {};
|
|
542
|
+
let inserted = false;
|
|
543
|
+
entries.forEach(([rowId, rowValue]) => {
|
|
544
|
+
if (!inserted && position === 'before' && rowId === referenceRowId) {
|
|
545
|
+
result[newRowId] = value;
|
|
546
|
+
inserted = true;
|
|
547
|
+
}
|
|
548
|
+
result[rowId] = rowValue;
|
|
549
|
+
if (!inserted && position === 'after' && rowId === referenceRowId) {
|
|
550
|
+
result[newRowId] = value;
|
|
551
|
+
inserted = true;
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
if (!inserted) {
|
|
556
|
+
result[newRowId] = value;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return result;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const distributeSizesWithNewColumn = (
|
|
563
|
+
sizes: number[] | undefined,
|
|
564
|
+
insertIndex: number,
|
|
565
|
+
columnCount: number,
|
|
566
|
+
): number[] => {
|
|
567
|
+
if (!sizes || sizes.length === 0) {
|
|
568
|
+
return toIntSizes(new Array(columnCount).fill(1), columnCount);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const normalized = sizes.map((size) => (Number.isFinite(size) && size > 0 ? size : 1));
|
|
572
|
+
const referenceIndex = Math.max(0, Math.min(insertIndex, normalized.length - 1));
|
|
573
|
+
const reference = normalized[referenceIndex] ?? 1;
|
|
574
|
+
const weights = [...normalized];
|
|
575
|
+
weights.splice(insertIndex, 0, reference);
|
|
576
|
+
return toIntSizes(weights, columnCount);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
export interface SimulateLayoutOptions {
|
|
580
|
+
slot: LayoutSlot;
|
|
581
|
+
sourceUid: string;
|
|
582
|
+
layout: GridLayoutData;
|
|
583
|
+
generateRowId?: () => string;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export const simulateLayoutForSlot = ({
|
|
587
|
+
slot,
|
|
588
|
+
sourceUid,
|
|
589
|
+
layout,
|
|
590
|
+
generateRowId,
|
|
591
|
+
}: SimulateLayoutOptions): GridLayoutData => {
|
|
592
|
+
const cloned: GridLayoutData = {
|
|
593
|
+
rows: _.cloneDeep(layout.rows),
|
|
594
|
+
sizes: _.cloneDeep(layout.sizes),
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
removeItemFromLayout(cloned, sourceUid);
|
|
598
|
+
|
|
599
|
+
const createRowId = generateRowId ?? uid;
|
|
600
|
+
|
|
601
|
+
switch (slot.type) {
|
|
602
|
+
case 'column': {
|
|
603
|
+
const columns = cloned.rows[slot.rowId] || [];
|
|
604
|
+
if (!cloned.rows[slot.rowId]) {
|
|
605
|
+
cloned.rows[slot.rowId] = columns;
|
|
606
|
+
}
|
|
607
|
+
if (!columns[slot.columnIndex]) {
|
|
608
|
+
columns[slot.columnIndex] = [];
|
|
609
|
+
}
|
|
610
|
+
const targetColumn = columns[slot.columnIndex];
|
|
611
|
+
targetColumn.splice(slot.insertIndex, 0, sourceUid);
|
|
612
|
+
normalizeRowSizes(slot.rowId, cloned);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case 'empty-column': {
|
|
616
|
+
const columns = cloned.rows[slot.rowId] || [];
|
|
617
|
+
if (!cloned.rows[slot.rowId]) {
|
|
618
|
+
cloned.rows[slot.rowId] = columns;
|
|
619
|
+
}
|
|
620
|
+
if (!columns[slot.columnIndex]) {
|
|
621
|
+
columns[slot.columnIndex] = [];
|
|
622
|
+
}
|
|
623
|
+
columns[slot.columnIndex] = [sourceUid];
|
|
624
|
+
normalizeRowSizes(slot.rowId, cloned);
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
case 'column-edge': {
|
|
628
|
+
const columns = cloned.rows[slot.rowId] || [];
|
|
629
|
+
if (!cloned.rows[slot.rowId]) {
|
|
630
|
+
cloned.rows[slot.rowId] = columns;
|
|
631
|
+
}
|
|
632
|
+
const insertIndex = slot.direction === 'left' ? slot.columnIndex : slot.columnIndex + 1;
|
|
633
|
+
columns.splice(insertIndex, 0, [sourceUid]);
|
|
634
|
+
cloned.sizes[slot.rowId] = distributeSizesWithNewColumn(cloned.sizes[slot.rowId], insertIndex, columns.length);
|
|
635
|
+
normalizeRowSizes(slot.rowId, cloned);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case 'row-gap': {
|
|
639
|
+
const newRowId = createRowId();
|
|
640
|
+
const rowPosition: 'before' | 'after' = slot.position === 'above' ? 'before' : 'after';
|
|
641
|
+
cloned.rows = insertRow(cloned.rows, slot.targetRowId, newRowId, rowPosition, [[sourceUid]]);
|
|
642
|
+
cloned.sizes[newRowId] = [DEFAULT_GRID_COLUMNS];
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
case 'empty-row': {
|
|
646
|
+
const newRowId = createRowId();
|
|
647
|
+
cloned.rows = {
|
|
648
|
+
...cloned.rows,
|
|
649
|
+
[newRowId]: [[sourceUid]],
|
|
650
|
+
};
|
|
651
|
+
cloned.sizes[newRowId] = [DEFAULT_GRID_COLUMNS];
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
default:
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return cloned;
|
|
659
|
+
};
|
|
@@ -15,8 +15,8 @@ import { FlowModel } from '../../models';
|
|
|
15
15
|
import { useFlowEngine } from '../../provider';
|
|
16
16
|
import { PersistOptions } from '../../types';
|
|
17
17
|
|
|
18
|
-
export * from './
|
|
19
|
-
export * from './
|
|
18
|
+
export * from './findModelUidPosition';
|
|
19
|
+
export * from './gridDragPlanner';
|
|
20
20
|
|
|
21
21
|
export const EMPTY_COLUMN_UID = 'EMPTY_COLUMN';
|
|
22
22
|
|
|
@@ -25,7 +25,7 @@ export const DragHandler: FC<{ model: FlowModel; children: React.ReactNode }> =
|
|
|
25
25
|
model,
|
|
26
26
|
children = <DragOutlined />,
|
|
27
27
|
}) => {
|
|
28
|
-
const { attributes, listeners, setNodeRef
|
|
28
|
+
const { attributes, listeners, setNodeRef } = useDraggable({ id: model.uid });
|
|
29
29
|
return (
|
|
30
30
|
<span
|
|
31
31
|
ref={setNodeRef}
|