Skip to content

Commit 7d847d3

Browse files
amanosacrousllorenspujol
authored andcommitted
feat: multiple items drag/resize
Also closes #134
1 parent 3293c2b commit 7d847d3

14 files changed

+1532
-113
lines changed

projects/angular-grid-layout/src/lib/grid.component.ts

Lines changed: 167 additions & 97 deletions
Large diffs are not rendered by default.

projects/angular-grid-layout/src/lib/grid.definitions.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { InjectionToken } from '@angular/core';
22
import { CompactType } from './utils/react-grid-layout.utils';
33
import { KtdClientRect } from './utils/client-rect';
4+
import { KtdDictionary } from '../types';
45

56
export interface KtdGridLayoutItem {
67
id: string;
@@ -67,3 +68,11 @@ export interface KtdDraggingData {
6768
dragElemClientRect: KtdClientRect;
6869
scrollDifference: { top: number, left: number };
6970
}
71+
72+
export interface KtdDraggingMultipleData {
73+
pointerDownEvent: MouseEvent | TouchEvent;
74+
pointerDragEvent: MouseEvent | TouchEvent;
75+
gridElemClientRect: KtdClientRect;
76+
dragElementsClientRect: KtdDictionary<KtdClientRect>;
77+
scrollDifference: { top: number, left: number };
78+
}

projects/angular-grid-layout/src/lib/utils/grid.utils.ts

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { compact, CompactType, getFirstCollision, Layout, LayoutItem, moveElement } from './react-grid-layout.utils';
1+
import { compact, CompactType, getFirstCollision, Layout, LayoutItem, moveElement, sortLayoutItems } from './react-grid-layout.utils';
22
import {
3-
KtdDraggingData, KtdGridCfg, KtdGridCompactType, KtdGridItemRect, KtdGridItemRenderData, KtdGridLayout, KtdGridLayoutItem
3+
KtdDraggingData, KtdDraggingMultipleData, KtdGridCfg, KtdGridCompactType, KtdGridItemRect, KtdGridItemRenderData, KtdGridLayout, KtdGridLayoutItem
44
} from '../grid.definitions';
55
import { ktdPointerClientX, ktdPointerClientY } from './pointer.utils';
66
import { KtdDictionary } from '../../types';
77
import { KtdGridItemComponent } from '../grid-item/grid-item.component';
8+
import { KtdMoveMultipleElements } from './react-grid-layout-multiple.utils';
89

910
/** Tracks items by id. This function is mean to be used in conjunction with the ngFor that renders the 'ktd-grid-items' */
1011
export function ktdTrackById(index: number, item: {id: string}) {
@@ -31,6 +32,19 @@ export function ktdGridCompact(layout: KtdGridLayout, compactType: KtdGridCompac
3132
.map(item => ({ id: item.id, x: item.x, y: item.y, w: item.w, h: item.h, minW: item.minW, minH: item.minH, maxW: item.maxW, maxH: item.maxH }));
3233
}
3334

35+
/**
36+
* Call react-grid-layout utils 'sortLayoutItems()' function to return the 'layout' sorted by 'compactType'
37+
* @param {Layout} layout
38+
* @param {CompactType} compactType
39+
* @returns {Layout}
40+
*/
41+
export function ktdGridSortLayoutItems(
42+
layout: Layout,
43+
compactType: CompactType,
44+
): Layout {
45+
return sortLayoutItems(layout,compactType)
46+
}
47+
3448
function screenXToGridX(screenXPos: number, cols: number, width: number, gap: number): number {
3549
if (cols <= 1) {
3650
return 0;
@@ -152,6 +166,121 @@ export function ktdGridItemDragging(gridItem: KtdGridItemComponent, config: KtdG
152166
};
153167
}
154168

169+
170+
171+
/**
172+
* Given the grid config & layout data and the current drag position & information, returns the corresponding layout and drag item position
173+
* @param gridItem grid item that is been dragged
174+
* @param config current grid configuration
175+
* @param compactionType type of compaction that will be performed
176+
* @param draggingData contains all the information about the drag
177+
*/
178+
export function ktdGridItemsDragging(gridItems: KtdGridItemComponent[], config: KtdGridCfg, compactionType: CompactType, draggingData: KtdDraggingMultipleData): { layout: KtdGridLayoutItem[]; draggedItemPos: KtdDictionary<KtdGridItemRect> } {
179+
const {pointerDownEvent, pointerDragEvent, gridElemClientRect, dragElementsClientRect, scrollDifference} = draggingData;
180+
181+
const draggingElemPrevItem: KtdDictionary<KtdGridLayoutItem> = {}
182+
gridItems.forEach(gridItem=> {
183+
draggingElemPrevItem[gridItem.id] = config.layout.find(item => item.id === gridItem.id)!
184+
});
185+
186+
const clientStartX = ktdPointerClientX(pointerDownEvent);
187+
const clientStartY = ktdPointerClientY(pointerDownEvent);
188+
const clientX = ktdPointerClientX(pointerDragEvent);
189+
const clientY = ktdPointerClientY(pointerDragEvent);
190+
191+
// Grid element positions taking into account the possible scroll total difference from the beginning.
192+
const gridElementLeftPosition = gridElemClientRect.left + scrollDifference.left;
193+
const gridElementTopPosition = gridElemClientRect.top + scrollDifference.top;
194+
195+
const rowHeightInPixels = config.rowHeight === 'fit'
196+
? ktdGetGridItemRowHeight(config.layout, config.height ?? gridElemClientRect.height, config.gap)
197+
: config.rowHeight;
198+
199+
const layoutItemsToMove: KtdDictionary<KtdGridLayoutItem>={};
200+
const gridRelPos: KtdDictionary<{x:number,y:number}>={}
201+
let maxXMove: number = 0;
202+
let maxYMove: number = 0;
203+
gridItems.forEach((gridItem: KtdGridItemComponent)=>{
204+
const offsetX = clientStartX - dragElementsClientRect[gridItem.id].left;
205+
const offsetY = clientStartY - dragElementsClientRect[gridItem.id].top;
206+
// Calculate position relative to the grid element.
207+
gridRelPos[gridItem.id]={
208+
x: clientX - gridElementLeftPosition - offsetX,
209+
y: clientY - gridElementTopPosition - offsetY
210+
};
211+
// Get layout item position
212+
layoutItemsToMove[gridItem.id] = {
213+
...draggingElemPrevItem[gridItem.id],
214+
x: screenXToGridX(gridRelPos[gridItem.id].x , config.cols, gridElemClientRect.width, config.gap),
215+
y: screenYToGridY(gridRelPos[gridItem.id].y, rowHeightInPixels, gridElemClientRect.height, config.gap)
216+
};
217+
// Determine the maximum X and Y displacement where an item has gone outside the grid
218+
if(0>layoutItemsToMove[gridItem.id].x && maxXMove>layoutItemsToMove[gridItem.id].x){
219+
maxXMove = layoutItemsToMove[gridItem.id].x;
220+
}
221+
if(0>layoutItemsToMove[gridItem.id].y && maxYMove>layoutItemsToMove[gridItem.id].y){
222+
maxYMove = layoutItemsToMove[gridItem.id].y;
223+
}
224+
if(layoutItemsToMove[gridItem.id].x + layoutItemsToMove[gridItem.id].w > config.cols && maxXMove<layoutItemsToMove[gridItem.id].w + layoutItemsToMove[gridItem.id].x - config.cols){
225+
maxXMove = layoutItemsToMove[gridItem.id].w + layoutItemsToMove[gridItem.id].x - config.cols
226+
}
227+
})
228+
// Correct all the x and y position of the group decreasing/increasing the maximum overflow of an item, to maintain the structure
229+
Object.entries(layoutItemsToMove).forEach(([key, item]) => {
230+
layoutItemsToMove[key] = {
231+
...item,
232+
x: item.x - maxXMove,
233+
y: item.y - maxYMove
234+
};
235+
})
236+
237+
// Parse to LayoutItem array data in order to use 'react.grid-layout' utils
238+
const layoutItems: LayoutItem[] = config.layout;
239+
const draggedLayoutItems: {
240+
l: LayoutItem,
241+
x: number | null | undefined,
242+
y: number | null | undefined
243+
}[] = gridItems.map((gridItem:KtdGridItemComponent)=>{
244+
const draggedLayoutItem: LayoutItem = layoutItems.find(item => item.id === gridItem.id)!;
245+
draggedLayoutItem.static = true;
246+
return {
247+
l: draggedLayoutItem,
248+
x: layoutItemsToMove[gridItem.id].x,
249+
y: layoutItemsToMove[gridItem.id].y
250+
}
251+
});
252+
253+
// Move all elements in group
254+
let newLayoutItems: LayoutItem[] = KtdMoveMultipleElements(
255+
layoutItems,
256+
draggedLayoutItems,
257+
true,
258+
compactionType,
259+
config.cols,
260+
);
261+
262+
// Compact with selected items as static to preserve the structure of the selected items group
263+
newLayoutItems = compact(newLayoutItems, compactionType, config.cols);
264+
gridItems.forEach(gridItem=>newLayoutItems.find(layoutItem=>layoutItem.id === gridItem.id)!.static = false);
265+
// Compact normal to display the layout correctly
266+
newLayoutItems = compact(newLayoutItems, compactionType, config.cols);
267+
268+
const draggedItemPos: KtdDictionary<KtdGridItemRect>={};
269+
gridItems.forEach(gridItem=>
270+
draggedItemPos[gridItem.id]={
271+
left: gridRelPos[gridItem.id].x,
272+
top: gridRelPos[gridItem.id].y,
273+
width: dragElementsClientRect[gridItem.id].width,
274+
height: dragElementsClientRect[gridItem.id].height,
275+
}
276+
);
277+
278+
return {
279+
layout: newLayoutItems,
280+
draggedItemPos
281+
};
282+
}
283+
155284
/**
156285
* Given the grid config & layout data and the current drag position & information, returns the corresponding layout and drag item position
157286
* @param gridItem grid item that is been dragged
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* IMPORTANT:
3+
* This utils are taken from the project: https://github.com/STRML/react-grid-layout.
4+
* The code should be as less modified as possible for easy maintenance.
5+
*/
6+
7+
import { CompactType, getAllCollisions, getFirstCollision, Layout, LayoutItem, sortLayoutItems } from './react-grid-layout.utils';
8+
9+
const DEBUG = false;
10+
11+
12+
/**
13+
* Move a set of elements "items". Responsible for doing cascading movements of other elements.
14+
*
15+
* @export
16+
* @param {Layout} layout
17+
* @param {({
18+
* l: LayoutItem,
19+
* x: number | null | undefined,
20+
* y: number | null | undefined
21+
* }[])} items
22+
* @param {(boolean | null | undefined)} isUserAction
23+
* @param {(boolean | null | undefined)} preventCollision
24+
* @param {CompactType} compactType
25+
* @param {number} cols
26+
* @returns {Layout}
27+
*/
28+
export function KtdMoveMultipleElements(
29+
layout: Layout,
30+
items: {
31+
l: LayoutItem,
32+
x: number | null | undefined,
33+
y: number | null | undefined
34+
}[],
35+
isUserAction: boolean | null | undefined,
36+
compactType: CompactType,
37+
cols: number
38+
): Layout {
39+
let axes = compactType === 'vertical' ? 'y' : 'x';
40+
// Short-circuit if nothing to do.
41+
if (items.every((item) => item.l.y === item.y && item.l.x === item.x)) {
42+
return layout;
43+
}
44+
// Old coordinates to detect the cursor movement direction (up, down, left, right)
45+
const oldX = items[0].l.x;
46+
const oldY = items[0].l.y;
47+
// Old coordinates before mutation, to retrieve it if the element cant move
48+
const oldCoord = {}
49+
50+
// Move the selected elements
51+
items.forEach((item) => {
52+
oldCoord[item.l.id] = {
53+
x: item.l.x,
54+
y: item.l.y
55+
}
56+
if (typeof item.x === 'number') {
57+
item.l.x = item.x;
58+
}
59+
if (typeof item.y === 'number') {
60+
item.l.y = item.y;
61+
}
62+
item.l.moved = true;
63+
})
64+
65+
let sorted = sortLayoutItems(layout, compactType);
66+
let itemsSorted = sortLayoutItems(items.map(item => item.l), compactType);
67+
68+
// If this collides with anything, move it.
69+
// When doing this comparison, we have to sort the items we compare with
70+
// to ensure, in the case of multiple collisions, that we're getting the
71+
// nearest collision.
72+
const movingUp =
73+
compactType === 'vertical' && typeof items[0].y === 'number'
74+
? oldY >= items[0].y
75+
: compactType === 'horizontal' && typeof items[0].x === 'number'
76+
? oldX >= items[0].x
77+
: false;
78+
if (movingUp) {
79+
sorted = sorted.reverse();
80+
}
81+
82+
// Get the position of the first row/col of the moved block, to avoid repositioning elements between the moved block, only
83+
// can apply a repositioning if the collide item its on the first row/col
84+
let minAxe: number | undefined;
85+
if (itemsSorted && itemsSorted.length) {
86+
minAxe = itemsSorted[0][axes];
87+
}
88+
// For each element, detect collisions and move the collided element by +1
89+
itemsSorted.forEach((item) => {
90+
const collisions: LayoutItem[] = getAllCollisions(sorted, item);
91+
// Move each item that collides away from this element.
92+
for (let i = 0, len = collisions.length; i < len; i++) {
93+
const collision = collisions[i];
94+
logMulti(
95+
`Resolving collision between ${item.id}] and ${
96+
collision.id
97+
} at [${collision.x},${collision.y}]`,
98+
);
99+
// Short circuit so we can't infinite loop
100+
if (collision.moved) {
101+
continue;
102+
}
103+
// Don't move static items - we have to move *this* element away
104+
if (collision.static && !item.static) {
105+
layout = KtdMoveElementsAwayFromCollision(
106+
layout,
107+
collision,
108+
item,
109+
minAxe === item[axes] ? isUserAction : false, // We only allow repositioning the "item" element if "collision" is in the first row of the moved block
110+
compactType,
111+
cols
112+
);
113+
} else {
114+
layout = KtdMoveElementsAwayFromCollision(
115+
layout,
116+
item,
117+
collision,
118+
minAxe === item[axes] ? isUserAction : false, // We only allow repositioning the "collision" element if "item" is in the first row of the moved block
119+
compactType,
120+
cols
121+
);
122+
}
123+
}
124+
});
125+
126+
return layout;
127+
}
128+
129+
/**
130+
* Move the element "itemToMove" away from the collision with "collidesWith"
131+
* @export
132+
* @param {Layout} layout
133+
* @param {LayoutItem} collidesWith
134+
* @param {LayoutItem} itemToMove
135+
* @param {(boolean | null | undefined)} isUserAction
136+
* @param {CompactType} compactType
137+
* @param {number} cols
138+
* @returns {Layout}
139+
*/
140+
export function KtdMoveElementsAwayFromCollision(
141+
layout: Layout,
142+
collidesWith: LayoutItem,
143+
itemToMove: LayoutItem,
144+
isUserAction: boolean | null | undefined,
145+
compactType: CompactType,
146+
cols: number,
147+
): Layout {
148+
const compactH = compactType === 'horizontal';
149+
// Compact vertically if not set to horizontal
150+
const compactV = compactType !== 'horizontal';
151+
152+
// If there is enough space above the collision to put this element, move it there.
153+
// We only do this on the main collision as this can get funky in cascades and cause
154+
// unwanted swapping behavior.
155+
if (isUserAction) {
156+
// Reset isUserAction flag because we're not in the main collision anymore.
157+
isUserAction = false;
158+
159+
// Make a mock item so we don't modify the item here, only modify in moveElement.
160+
const fakeItem: LayoutItem = {
161+
x: compactH
162+
? Math.max(collidesWith.x - itemToMove.w, 0)
163+
: itemToMove.x,
164+
y: compactV
165+
? Math.max(collidesWith.y - itemToMove.h, 0)
166+
: itemToMove.y,
167+
w: itemToMove.w,
168+
h: itemToMove.h,
169+
id: '-1',
170+
};
171+
172+
// No collision? If so, we can go up there; otherwise, we'll end up moving down as normal
173+
if (!getFirstCollision(layout, fakeItem)) {
174+
logMulti(
175+
`Doing reverse collision on ${itemToMove.id} up to [${
176+
fakeItem.x
177+
},${fakeItem.y}].`,
178+
);
179+
return KtdMoveMultipleElements(
180+
layout,
181+
[{
182+
l: itemToMove,
183+
x: compactH ? fakeItem.x : undefined,
184+
y: compactV ? fakeItem.y : undefined,
185+
}],
186+
isUserAction,
187+
compactType,
188+
cols
189+
);
190+
}
191+
}
192+
193+
return KtdMoveMultipleElements(
194+
layout,
195+
[{
196+
l: itemToMove,
197+
x: compactH ? itemToMove.x + 1 : undefined,
198+
y: compactV ? itemToMove.y + 1 : undefined,
199+
}],
200+
isUserAction,
201+
compactType,
202+
cols
203+
);
204+
}
205+
206+
function logMulti(...args) {
207+
if (!DEBUG) {
208+
return;
209+
}
210+
// eslint-disable-next-line no-console
211+
console.log(...args);
212+
}
213+

0 commit comments

Comments
 (0)