@dxos/react-ui-mosaic 0.3.3-main.006ca4d
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/LICENSE +8 -0
- package/README.md +45 -0
- package/dist/lib/browser/index.mjs +596 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/mosaic/Container.d.ts +65 -0
- package/dist/types/src/mosaic/Container.d.ts.map +1 -0
- package/dist/types/src/mosaic/Debug.d.ts +9 -0
- package/dist/types/src/mosaic/Debug.d.ts.map +1 -0
- package/dist/types/src/mosaic/DefaultComponent.d.ts +4 -0
- package/dist/types/src/mosaic/DefaultComponent.d.ts.map +1 -0
- package/dist/types/src/mosaic/DragOverlay.d.ts +11 -0
- package/dist/types/src/mosaic/DragOverlay.d.ts.map +1 -0
- package/dist/types/src/mosaic/Mosaic.d.ts +32 -0
- package/dist/types/src/mosaic/Mosaic.d.ts.map +1 -0
- package/dist/types/src/mosaic/Root.d.ts +24 -0
- package/dist/types/src/mosaic/Root.d.ts.map +1 -0
- package/dist/types/src/mosaic/SortableContext.d.ts +14 -0
- package/dist/types/src/mosaic/SortableContext.d.ts.map +1 -0
- package/dist/types/src/mosaic/Tile.d.ts +41 -0
- package/dist/types/src/mosaic/Tile.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/index.d.ts +5 -0
- package/dist/types/src/mosaic/hooks/index.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/useContainer.d.ts +4 -0
- package/dist/types/src/mosaic/hooks/useContainer.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/useItemsWithPreview.d.ts +10 -0
- package/dist/types/src/mosaic/hooks/useItemsWithPreview.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/useMosaic.d.ts +2 -0
- package/dist/types/src/mosaic/hooks/useMosaic.d.ts.map +1 -0
- package/dist/types/src/mosaic/hooks/useSortedItems.d.ts +6 -0
- package/dist/types/src/mosaic/hooks/useSortedItems.d.ts.map +1 -0
- package/dist/types/src/mosaic/index.d.ts +5 -0
- package/dist/types/src/mosaic/index.d.ts.map +1 -0
- package/dist/types/src/mosaic/types.d.ts +9 -0
- package/dist/types/src/mosaic/types.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/data.d.ts +3 -0
- package/dist/types/src/mosaic/util/data.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/index.d.ts +4 -0
- package/dist/types/src/mosaic/util/index.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/path.d.ts +14 -0
- package/dist/types/src/mosaic/util/path.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/path.test.d.ts +2 -0
- package/dist/types/src/mosaic/util/path.test.d.ts.map +1 -0
- package/dist/types/src/mosaic/util/styles.d.ts +3 -0
- package/dist/types/src/mosaic/util/styles.d.ts.map +1 -0
- package/dist/types/src/testing/Cards.d.ts +10 -0
- package/dist/types/src/testing/Cards.d.ts.map +1 -0
- package/dist/types/src/testing/decorators.d.ts +4 -0
- package/dist/types/src/testing/decorators.d.ts.map +1 -0
- package/dist/types/src/testing/generator.d.ts +43 -0
- package/dist/types/src/testing/generator.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +5 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/next-fractional-index.d.ts +5 -0
- package/dist/types/src/testing/next-fractional-index.d.ts.map +1 -0
- package/dist/types/src/views/Demo/Demo.stories.d.ts +13 -0
- package/dist/types/src/views/Demo/Demo.stories.d.ts.map +1 -0
- package/dist/types/src/views/Demo/Echo.stories.d.ts +12 -0
- package/dist/types/src/views/Demo/Echo.stories.d.ts.map +1 -0
- package/dist/types/src/views/Grid/Grid.d.ts +30 -0
- package/dist/types/src/views/Grid/Grid.d.ts.map +1 -0
- package/dist/types/src/views/Grid/Grid.stories.d.ts +13 -0
- package/dist/types/src/views/Grid/Grid.stories.d.ts.map +1 -0
- package/dist/types/src/views/Grid/index.d.ts +3 -0
- package/dist/types/src/views/Grid/index.d.ts.map +1 -0
- package/dist/types/src/views/Grid/layout.d.ts +27 -0
- package/dist/types/src/views/Grid/layout.d.ts.map +1 -0
- package/dist/types/src/views/Grid/testing.d.ts +10 -0
- package/dist/types/src/views/Grid/testing.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/Echo.stories.d.ts +18 -0
- package/dist/types/src/views/Kanban/Echo.stories.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/Kanban.d.ts +11 -0
- package/dist/types/src/views/Kanban/Kanban.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/Kanban.stories.d.ts +28 -0
- package/dist/types/src/views/Kanban/Kanban.stories.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/index.d.ts +2 -0
- package/dist/types/src/views/Kanban/index.d.ts.map +1 -0
- package/dist/types/src/views/Kanban/testing.d.ts +20 -0
- package/dist/types/src/views/Kanban/testing.d.ts.map +1 -0
- package/dist/types/src/views/Stack/Columns.stories.d.ts +34 -0
- package/dist/types/src/views/Stack/Columns.stories.d.ts.map +1 -0
- package/dist/types/src/views/Stack/Stack.d.ts +9 -0
- package/dist/types/src/views/Stack/Stack.d.ts.map +1 -0
- package/dist/types/src/views/Stack/Stack.stories.d.ts +39 -0
- package/dist/types/src/views/Stack/Stack.stories.d.ts.map +1 -0
- package/dist/types/src/views/Stack/index.d.ts +2 -0
- package/dist/types/src/views/Stack/index.d.ts.map +1 -0
- package/dist/types/src/views/Stack/testing.d.ts +9 -0
- package/dist/types/src/views/Stack/testing.d.ts.map +1 -0
- package/dist/types/src/views/Tree/Graph.stories.d.ts +21 -0
- package/dist/types/src/views/Tree/Graph.stories.d.ts.map +1 -0
- package/dist/types/src/views/Tree/Tree.d.ts +13 -0
- package/dist/types/src/views/Tree/Tree.d.ts.map +1 -0
- package/dist/types/src/views/Tree/Tree.stories.d.ts +41 -0
- package/dist/types/src/views/Tree/Tree.stories.d.ts.map +1 -0
- package/dist/types/src/views/Tree/index.d.ts +2 -0
- package/dist/types/src/views/Tree/index.d.ts.map +1 -0
- package/dist/types/src/views/Tree/testing.d.ts +15 -0
- package/dist/types/src/views/Tree/testing.d.ts.map +1 -0
- package/dist/types/src/views/index.d.ts +5 -0
- package/dist/types/src/views/index.d.ts.map +1 -0
- package/package.json +67 -0
- package/src/index.ts +5 -0
- package/src/mosaic/Container.tsx +137 -0
- package/src/mosaic/Debug.tsx +24 -0
- package/src/mosaic/DefaultComponent.tsx +26 -0
- package/src/mosaic/DragOverlay.tsx +136 -0
- package/src/mosaic/Mosaic.ts +84 -0
- package/src/mosaic/Root.tsx +255 -0
- package/src/mosaic/SortableContext.tsx +60 -0
- package/src/mosaic/Tile.tsx +189 -0
- package/src/mosaic/hooks/index.ts +8 -0
- package/src/mosaic/hooks/useContainer.ts +16 -0
- package/src/mosaic/hooks/useItemsWithPreview.ts +64 -0
- package/src/mosaic/hooks/useMosaic.ts +11 -0
- package/src/mosaic/hooks/useSortedItems.ts +14 -0
- package/src/mosaic/index.ts +9 -0
- package/src/mosaic/types.ts +13 -0
- package/src/mosaic/util/data.ts +19 -0
- package/src/mosaic/util/index.ts +7 -0
- package/src/mosaic/util/path.test.ts +26 -0
- package/src/mosaic/util/path.ts +21 -0
- package/src/mosaic/util/styles.ts +10 -0
- package/src/testing/Cards.tsx +88 -0
- package/src/testing/decorators.tsx +20 -0
- package/src/testing/generator.ts +135 -0
- package/src/testing/index.ts +9 -0
- package/src/testing/next-fractional-index.ts +36 -0
- package/src/views/Demo/Demo.stories.tsx +79 -0
- package/src/views/Demo/Echo.stories.tsx +74 -0
- package/src/views/Grid/Grid.stories.tsx +48 -0
- package/src/views/Grid/Grid.tsx +248 -0
- package/src/views/Grid/index.ts +7 -0
- package/src/views/Grid/layout.ts +42 -0
- package/src/views/Grid/testing.tsx +91 -0
- package/src/views/Kanban/Echo.stories.tsx +67 -0
- package/src/views/Kanban/Kanban.stories.tsx +48 -0
- package/src/views/Kanban/Kanban.tsx +139 -0
- package/src/views/Kanban/index.ts +5 -0
- package/src/views/Kanban/testing.tsx +231 -0
- package/src/views/Stack/Columns.stories.tsx +61 -0
- package/src/views/Stack/Stack.stories.tsx +62 -0
- package/src/views/Stack/Stack.tsx +58 -0
- package/src/views/Stack/index.ts +5 -0
- package/src/views/Stack/testing.tsx +77 -0
- package/src/views/Tree/Graph.stories.tsx +36 -0
- package/src/views/Tree/Tree.stories.tsx +92 -0
- package/src/views/Tree/Tree.tsx +128 -0
- package/src/views/Tree/index.ts +5 -0
- package/src/views/Tree/testing.tsx +156 -0
- package/src/views/index.ts +8 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type CollisionDetection,
|
|
7
|
+
DndContext,
|
|
8
|
+
type DragCancelEvent,
|
|
9
|
+
type DragEndEvent,
|
|
10
|
+
type DragMoveEvent,
|
|
11
|
+
type DragOverEvent,
|
|
12
|
+
type DragStartEvent,
|
|
13
|
+
KeyboardSensor,
|
|
14
|
+
type Modifier,
|
|
15
|
+
MouseSensor,
|
|
16
|
+
pointerWithin,
|
|
17
|
+
rectIntersection,
|
|
18
|
+
TouchSensor,
|
|
19
|
+
useSensor,
|
|
20
|
+
useSensors,
|
|
21
|
+
} from '@dnd-kit/core';
|
|
22
|
+
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
|
23
|
+
import pick from 'lodash.pick';
|
|
24
|
+
import React, { createContext, type FC, type PropsWithChildren, useState } from 'react';
|
|
25
|
+
import { createPortal } from 'react-dom';
|
|
26
|
+
|
|
27
|
+
import { DEFAULT_TRANSITION, type MosaicContainerProps, type MosaicMoveEvent, type MosaicOperation } from './Container';
|
|
28
|
+
import { Debug } from './Debug';
|
|
29
|
+
import { DefaultComponent } from './DefaultComponent';
|
|
30
|
+
import { type MosaicTileComponent } from './Tile';
|
|
31
|
+
import { type MosaicDraggedItem } from './types';
|
|
32
|
+
import { Path } from './util';
|
|
33
|
+
|
|
34
|
+
const DEFAULT_COMPONENT_ID = '__default';
|
|
35
|
+
|
|
36
|
+
export type MosaicContextType = {
|
|
37
|
+
containers: Record<string, MosaicContainerProps<any> | undefined>;
|
|
38
|
+
setContainer: (id: string, container?: MosaicContainerProps<any>) => void;
|
|
39
|
+
activeItem: MosaicDraggedItem | undefined;
|
|
40
|
+
overItem: MosaicDraggedItem | undefined;
|
|
41
|
+
operation: MosaicOperation;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const MosaicContext = createContext<MosaicContextType | undefined>(undefined);
|
|
45
|
+
|
|
46
|
+
export type MosaicRootProps = PropsWithChildren<{
|
|
47
|
+
/**
|
|
48
|
+
* Default component.
|
|
49
|
+
*/
|
|
50
|
+
Component?: MosaicTileComponent<any>;
|
|
51
|
+
debug?: boolean;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Root context provider.
|
|
56
|
+
*/
|
|
57
|
+
export const MosaicRoot: FC<MosaicRootProps> = ({ Component = DefaultComponent, debug, children }) => {
|
|
58
|
+
const [containers, setContainers] = useState<MosaicContextType['containers']>({
|
|
59
|
+
[DEFAULT_COMPONENT_ID]: { id: DEFAULT_COMPONENT_ID, Component },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const handleSetContainer = (id: string, container?: MosaicContainerProps<any>) => {
|
|
63
|
+
setContainers((containers) => {
|
|
64
|
+
if (container) {
|
|
65
|
+
return { ...containers, [id]: container };
|
|
66
|
+
} else {
|
|
67
|
+
const { [id]: _, ...rest } = containers;
|
|
68
|
+
return rest;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const [activeItem, setActiveItem] = useState<MosaicDraggedItem>();
|
|
74
|
+
const [overItem, setOverItem] = useState<MosaicDraggedItem>();
|
|
75
|
+
const [operation, setOperation] = useState<MosaicOperation>('reject');
|
|
76
|
+
|
|
77
|
+
//
|
|
78
|
+
// DndKit Defaults
|
|
79
|
+
//
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* See: https://docs.dndkit.com/api-documentation/context-provider/collision-detection-algorithms#custom-collision-detection-algorithms
|
|
83
|
+
* See: https://github.com/clauderic/dnd-kit/tree/master/packages/core/src/utilities/algorithms
|
|
84
|
+
*/
|
|
85
|
+
// TODO(burdon): Flickering: should not change while animation is happening.
|
|
86
|
+
const collisionDetection: CollisionDetection = (args) => {
|
|
87
|
+
// pointerWithin is the most accurate (since we're using drag handles), but only works with pointer sensors.
|
|
88
|
+
const pointerCollisions = pointerWithin(args);
|
|
89
|
+
if (pointerCollisions.length > 0) {
|
|
90
|
+
return pointerCollisions;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If there are no collisions with the pointer, return rectangle intersections.
|
|
94
|
+
return rectIntersection(args);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Dragging transformation modifiers adaptable by container.
|
|
99
|
+
* See: https://github.com/clauderic/dnd-kit/tree/master/packages/core/src/modifiers
|
|
100
|
+
*/
|
|
101
|
+
const modifiers: Modifier = (props) => {
|
|
102
|
+
const { transform } = props;
|
|
103
|
+
if (activeItem) {
|
|
104
|
+
const container = containers[Path.first(activeItem.path)];
|
|
105
|
+
return container?.modifier?.(activeItem, props) ?? transform;
|
|
106
|
+
} else {
|
|
107
|
+
return transform;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* See: https://github.com/clauderic/dnd-kit/tree/master/packages/core/src/sensors
|
|
113
|
+
*/
|
|
114
|
+
const sensors = useSensors(
|
|
115
|
+
useSensor(MouseSensor, {
|
|
116
|
+
activationConstraint: {
|
|
117
|
+
distance: 10,
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
useSensor(TouchSensor, {
|
|
121
|
+
activationConstraint: {
|
|
122
|
+
delay: 200,
|
|
123
|
+
tolerance: 5,
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
useSensor(KeyboardSensor, {
|
|
127
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
//
|
|
132
|
+
// Events
|
|
133
|
+
//
|
|
134
|
+
|
|
135
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
136
|
+
setActiveItem(pick(event.active.data.current as MosaicDraggedItem, 'path', 'item', 'position'));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleDragMove = (event: DragMoveEvent) => {};
|
|
140
|
+
|
|
141
|
+
const handleDragOver = (event: DragOverEvent) => {
|
|
142
|
+
const overItem = pick(event.over?.data.current as MosaicDraggedItem, 'path', 'item', 'position');
|
|
143
|
+
|
|
144
|
+
// If the over item is the same as the active item, do nothing.
|
|
145
|
+
// This happens when moving between containers where a placeholder of itself is rendered where it will be dropped.
|
|
146
|
+
if (overItem?.item?.id === activeItem?.item.id) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const activeContainer = activeItem && containers[Path.first(activeItem.path)];
|
|
151
|
+
const overContainer = overItem?.path && containers[Path.first(overItem.path)];
|
|
152
|
+
if (!event.over || !overItem || !overContainer || !activeItem || !activeContainer) {
|
|
153
|
+
setOperation('reject');
|
|
154
|
+
setOverItem(undefined);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const onOver = ({ active, over }: MosaicMoveEvent) => {
|
|
159
|
+
if (Path.parent(active.path) === Path.parent(over.path)) {
|
|
160
|
+
return 'rearrange';
|
|
161
|
+
} else if (overContainer.onOver) {
|
|
162
|
+
return overContainer.onOver({ active, over });
|
|
163
|
+
} else {
|
|
164
|
+
// TODO(wittjosiah): Default to reject.
|
|
165
|
+
return 'adopt';
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
setOperation(onOver({ active: activeItem, over: overItem }));
|
|
170
|
+
setOverItem(overItem);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleDragCancel = (event: DragCancelEvent) => {
|
|
174
|
+
setOperation('reject');
|
|
175
|
+
setActiveItem(undefined);
|
|
176
|
+
setOverItem(undefined);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
180
|
+
const overContainer = overItem && containers[Path.first(overItem.path)];
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
operation !== 'reject' &&
|
|
184
|
+
activeItem &&
|
|
185
|
+
overItem &&
|
|
186
|
+
(activeItem.path !== overItem.path || activeItem.position !== overItem.position)
|
|
187
|
+
) {
|
|
188
|
+
const activeContainer = containers[Path.first(activeItem.path)];
|
|
189
|
+
if (activeContainer) {
|
|
190
|
+
activeContainer.onDrop?.({ operation, active: activeItem, over: overItem });
|
|
191
|
+
|
|
192
|
+
if (overContainer && overContainer !== activeContainer) {
|
|
193
|
+
overContainer.onDrop?.({ operation, active: activeItem, over: overItem });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
setOperation('reject');
|
|
200
|
+
setOverItem(undefined);
|
|
201
|
+
setActiveItem(undefined);
|
|
202
|
+
}, overContainer?.transitionDuration ?? DEFAULT_TRANSITION);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<MosaicContext.Provider value={{ containers, setContainer: handleSetContainer, activeItem, overItem, operation }}>
|
|
207
|
+
<DndContext
|
|
208
|
+
collisionDetection={collisionDetection}
|
|
209
|
+
modifiers={[modifiers]}
|
|
210
|
+
sensors={sensors}
|
|
211
|
+
// TODO(burdon): Allow container to veto drop.
|
|
212
|
+
cancelDrop={(event) => false}
|
|
213
|
+
onDragStart={handleDragStart}
|
|
214
|
+
onDragMove={handleDragMove}
|
|
215
|
+
onDragOver={handleDragOver}
|
|
216
|
+
onDragCancel={handleDragCancel}
|
|
217
|
+
onDragEnd={handleDragEnd}
|
|
218
|
+
>
|
|
219
|
+
{children}
|
|
220
|
+
{debug &&
|
|
221
|
+
createPortal(
|
|
222
|
+
<MosaicDebug containers={containers} operation={operation} activeItem={activeItem} overItem={overItem} />,
|
|
223
|
+
document.body,
|
|
224
|
+
)}
|
|
225
|
+
</DndContext>
|
|
226
|
+
</MosaicContext.Provider>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const MosaicDebug: FC<{
|
|
231
|
+
containers: MosaicContextType['containers'];
|
|
232
|
+
operation: MosaicOperation;
|
|
233
|
+
activeItem?: MosaicDraggedItem;
|
|
234
|
+
overItem?: MosaicDraggedItem;
|
|
235
|
+
}> = ({ containers, operation, activeItem, overItem }) => {
|
|
236
|
+
return (
|
|
237
|
+
<Debug
|
|
238
|
+
position='bottom-right'
|
|
239
|
+
data={{
|
|
240
|
+
containers: Object.keys(containers).map((id) => id),
|
|
241
|
+
operation,
|
|
242
|
+
active: {
|
|
243
|
+
id: activeItem?.item?.id.slice(0, 16),
|
|
244
|
+
path: activeItem?.path && Path.create(...Path.parts(activeItem.path).map((part) => part.slice(0, 16))),
|
|
245
|
+
position: activeItem?.position,
|
|
246
|
+
},
|
|
247
|
+
over: {
|
|
248
|
+
id: overItem?.item?.id.slice(0, 16),
|
|
249
|
+
path: overItem?.path && Path.create(...Path.parts(overItem.path).map((part) => part.slice(0, 16))),
|
|
250
|
+
position: overItem?.position,
|
|
251
|
+
},
|
|
252
|
+
}}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { SortableContext, horizontalListSortingStrategy, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
6
|
+
import React, { type PropsWithChildren } from 'react';
|
|
7
|
+
|
|
8
|
+
import { useContainer, useMosaic } from './hooks';
|
|
9
|
+
import { type MosaicDataItem } from './types';
|
|
10
|
+
import { Path } from './util';
|
|
11
|
+
|
|
12
|
+
type Direction = 'horizontal' | 'vertical';
|
|
13
|
+
|
|
14
|
+
export type MosaicSortableProps<TData extends MosaicDataItem = MosaicDataItem> = PropsWithChildren<{
|
|
15
|
+
id?: string;
|
|
16
|
+
items?: TData[];
|
|
17
|
+
direction?: Direction;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mosaic convenience wrapper for dnd-kit SortableContext.
|
|
22
|
+
*/
|
|
23
|
+
// TODO(burdon): Remove since just obfuscates SortableContext unless more deeply integrated with useSortableItem.
|
|
24
|
+
// Otherwise accept same props as SortableContext (e.g., verticalListSortingStrategy).
|
|
25
|
+
export const MosaicSortableContext = ({ id, items = [], direction = 'vertical', children }: MosaicSortableProps) => {
|
|
26
|
+
const { operation, activeItem, overItem } = useMosaic();
|
|
27
|
+
const container = useContainer();
|
|
28
|
+
const contextId = id ?? container.id;
|
|
29
|
+
const Sortable = direction === 'vertical' ? Column : Row;
|
|
30
|
+
|
|
31
|
+
const ids = items.map((item) => {
|
|
32
|
+
const path = Path.create(contextId, item.id);
|
|
33
|
+
// TODO(wittjosiah): Copied from Tile.tsx. Consider factoring out to ensure logic is consistent.
|
|
34
|
+
const isPreview =
|
|
35
|
+
activeItem &&
|
|
36
|
+
activeItem.item.id === item.id &&
|
|
37
|
+
overItem &&
|
|
38
|
+
(Path.siblings(overItem.path, path) || Path.hasChild(overItem.path, path)) &&
|
|
39
|
+
operation !== 'reject';
|
|
40
|
+
return isPreview ? activeItem.path : path;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Sortable id={contextId} items={ids}>
|
|
45
|
+
{children}
|
|
46
|
+
</Sortable>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const Column = ({ children, id, items }: PropsWithChildren<{ id: string; items: string[] }>) => (
|
|
51
|
+
<SortableContext id={id} items={items} strategy={verticalListSortingStrategy}>
|
|
52
|
+
{children}
|
|
53
|
+
</SortableContext>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const Row = ({ children, id, items }: PropsWithChildren<{ id: string; items: string[] }>) => (
|
|
57
|
+
<SortableContext id={id} items={items} strategy={horizontalListSortingStrategy}>
|
|
58
|
+
{children}
|
|
59
|
+
</SortableContext>
|
|
60
|
+
);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type DraggableAttributes, useDraggable, useDroppable } from '@dnd-kit/core';
|
|
6
|
+
import { defaultAnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import type { CSSProperties, ForwardRefExoticComponent, HTMLAttributes, RefAttributes } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { MosaicOperation, MosaicTileOverlayProps } from './Container';
|
|
11
|
+
import { DefaultComponent } from './DefaultComponent';
|
|
12
|
+
import { useContainer, useMosaic } from './hooks';
|
|
13
|
+
import type { MosaicDataItem, MosaicDraggedItem } from './types';
|
|
14
|
+
import { getTransformCSS, Path } from './util';
|
|
15
|
+
|
|
16
|
+
export type MosaicActiveType = 'overlay' | 'rearrange' | 'origin' | 'destination';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props passed to mosaic tile.
|
|
20
|
+
*/
|
|
21
|
+
export type MosaicTileProps<TData extends MosaicDataItem = MosaicDataItem, TPosition = unknown> = Pick<
|
|
22
|
+
HTMLAttributes<HTMLDivElement>,
|
|
23
|
+
'className'
|
|
24
|
+
> &
|
|
25
|
+
MosaicTileOverlayProps & {
|
|
26
|
+
path: string;
|
|
27
|
+
item: TData;
|
|
28
|
+
Component?: MosaicTileComponent<TData, any>;
|
|
29
|
+
position?: TPosition;
|
|
30
|
+
operation?: MosaicOperation;
|
|
31
|
+
active?: MosaicActiveType; // TODO(burdon): Rename state?
|
|
32
|
+
|
|
33
|
+
// DndKit props.
|
|
34
|
+
isDragging?: boolean;
|
|
35
|
+
isOver?: boolean;
|
|
36
|
+
draggableStyle?: CSSProperties;
|
|
37
|
+
draggableProps?: DraggableAttributes &
|
|
38
|
+
// TODO(wittjosiah): SyntheticListenerMap.
|
|
39
|
+
HTMLAttributes<HTMLElement>;
|
|
40
|
+
|
|
41
|
+
onSelect?: () => void;
|
|
42
|
+
onRemove?: () => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mosaic Tile component.
|
|
47
|
+
*/
|
|
48
|
+
export type MosaicTileComponent<
|
|
49
|
+
TData extends MosaicDataItem = MosaicDataItem,
|
|
50
|
+
TElement extends HTMLElement = HTMLDivElement,
|
|
51
|
+
> = ForwardRefExoticComponent<
|
|
52
|
+
RefAttributes<TElement> & Omit<MosaicTileProps<TData>, 'Component' | 'operation'> & { operation: MosaicOperation }
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Basic draggable mosaic tile.
|
|
57
|
+
*/
|
|
58
|
+
export const DraggableTile = ({
|
|
59
|
+
path: parentPath,
|
|
60
|
+
item,
|
|
61
|
+
Component = DefaultComponent,
|
|
62
|
+
position,
|
|
63
|
+
draggableStyle,
|
|
64
|
+
...props
|
|
65
|
+
}: MosaicTileProps<any>) => {
|
|
66
|
+
const { operation } = useMosaic();
|
|
67
|
+
const path = Path.create(parentPath, item.id);
|
|
68
|
+
const { setNodeRef, attributes, listeners, /* transform, */ isDragging } = useDraggable({
|
|
69
|
+
id: path,
|
|
70
|
+
data: { path, item, position } satisfies MosaicDraggedItem,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Component
|
|
75
|
+
ref={setNodeRef}
|
|
76
|
+
item={item}
|
|
77
|
+
path={path}
|
|
78
|
+
position={position}
|
|
79
|
+
operation={operation}
|
|
80
|
+
isDragging={isDragging}
|
|
81
|
+
draggableStyle={{
|
|
82
|
+
// TODO(burdon): Override by container?
|
|
83
|
+
// transform: getTransformCSS(transform),
|
|
84
|
+
...draggableStyle,
|
|
85
|
+
}}
|
|
86
|
+
draggableProps={{ ...attributes, ...listeners }}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Basic droppable mosaic tile.
|
|
94
|
+
*/
|
|
95
|
+
export const DroppableTile = ({
|
|
96
|
+
path: parentPath,
|
|
97
|
+
item,
|
|
98
|
+
Component = DefaultComponent,
|
|
99
|
+
position,
|
|
100
|
+
...props
|
|
101
|
+
}: MosaicTileProps<any>) => {
|
|
102
|
+
const { operation } = useMosaic();
|
|
103
|
+
const path =
|
|
104
|
+
parentPath === item.id
|
|
105
|
+
? parentPath // If the path is the same as the item id, then this is the root tile.
|
|
106
|
+
: Path.create(parentPath, item.id);
|
|
107
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
108
|
+
id: path,
|
|
109
|
+
data: { path, item, position } satisfies MosaicDraggedItem,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Component
|
|
114
|
+
ref={setNodeRef}
|
|
115
|
+
item={item}
|
|
116
|
+
path={path}
|
|
117
|
+
position={position}
|
|
118
|
+
operation={operation}
|
|
119
|
+
isOver={isOver}
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Mosaic tile that can be sorted.
|
|
127
|
+
*/
|
|
128
|
+
export const SortableTile = ({
|
|
129
|
+
path: parentPath,
|
|
130
|
+
item,
|
|
131
|
+
Component: OverrideComponent,
|
|
132
|
+
position,
|
|
133
|
+
draggableStyle,
|
|
134
|
+
...props
|
|
135
|
+
}: MosaicTileProps<any, number>) => {
|
|
136
|
+
const { operation, activeItem, overItem } = useMosaic();
|
|
137
|
+
const { transitionDuration, Component: ContainerComponent } = useContainer();
|
|
138
|
+
|
|
139
|
+
const path = Path.create(parentPath, item.id);
|
|
140
|
+
// Re-use the active path if it's the same item and the item is a preview.
|
|
141
|
+
// This helps dndkit understand that the item is being moved and animate it appropriately.
|
|
142
|
+
const isPreview =
|
|
143
|
+
activeItem &&
|
|
144
|
+
activeItem.item.id === item.id &&
|
|
145
|
+
overItem &&
|
|
146
|
+
!Path.siblings(activeItem.path, overItem.path) &&
|
|
147
|
+
(Path.siblings(overItem.path, path) || Path.hasChild(overItem.path, path)) &&
|
|
148
|
+
operation !== 'reject';
|
|
149
|
+
|
|
150
|
+
const { setNodeRef, attributes, listeners, transform, isDragging, isOver } = useSortable({
|
|
151
|
+
id: isPreview ? activeItem.path : path,
|
|
152
|
+
data: { path, item, position } satisfies MosaicDraggedItem,
|
|
153
|
+
animateLayoutChanges: (args) =>
|
|
154
|
+
defaultAnimateLayoutChanges({ ...args, wasDragging: item.id !== activeItem?.item.id }),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let active: MosaicActiveType | undefined;
|
|
158
|
+
if (isDragging) {
|
|
159
|
+
if (operation === 'rearrange' || operation === 'reject') {
|
|
160
|
+
active = 'rearrange';
|
|
161
|
+
} else if (activeItem && Path.parent(activeItem.path) !== parentPath) {
|
|
162
|
+
active = 'destination';
|
|
163
|
+
} else {
|
|
164
|
+
active = 'origin';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const Component = OverrideComponent ?? ContainerComponent ?? DefaultComponent;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Component
|
|
172
|
+
ref={setNodeRef}
|
|
173
|
+
item={item}
|
|
174
|
+
path={path}
|
|
175
|
+
position={position}
|
|
176
|
+
operation={operation}
|
|
177
|
+
active={active}
|
|
178
|
+
isDragging={isDragging}
|
|
179
|
+
isOver={isOver}
|
|
180
|
+
draggableStyle={{
|
|
181
|
+
transform: getTransformCSS(transform),
|
|
182
|
+
transition: transform ? `transform ${transitionDuration}ms ease` : 'none',
|
|
183
|
+
...draggableStyle,
|
|
184
|
+
}}
|
|
185
|
+
draggableProps={{ ...attributes, ...listeners }}
|
|
186
|
+
{...props}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
import { raise } from '@dxos/debug';
|
|
8
|
+
|
|
9
|
+
import { MosaicContainerContext, type MosaicContainerProps } from '../Container';
|
|
10
|
+
import { type MosaicDataItem } from '../types';
|
|
11
|
+
|
|
12
|
+
export const useContainer = <
|
|
13
|
+
TData extends MosaicDataItem = MosaicDataItem,
|
|
14
|
+
TPosition = unknown,
|
|
15
|
+
>(): MosaicContainerProps<TData, TPosition> =>
|
|
16
|
+
useContext(MosaicContainerContext) ?? raise(new Error('Missing MosaicContainerContext'));
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useMosaic } from './useMosaic';
|
|
8
|
+
import { type MosaicDataItem } from '../types';
|
|
9
|
+
import { Path } from '../util';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns a spliced collection of items including a placeholder if items that could drop,
|
|
13
|
+
* and removing any item that is currently being dragged out.
|
|
14
|
+
*/
|
|
15
|
+
export const useItemsWithPreview = <T extends MosaicDataItem>({ path, items }: { path: string; items: T[] }): T[] => {
|
|
16
|
+
const { operation, activeItem, overItem } = useMosaic();
|
|
17
|
+
const [itemsWithPreview, setItemsWithPreview] = useState(items);
|
|
18
|
+
const overParent =
|
|
19
|
+
// If over the sortable context itself & active item is foreign, then treat self as the parent.
|
|
20
|
+
// This allows items to be appended to the end of the sortable.
|
|
21
|
+
overItem?.path === path && activeItem && Path.parent(activeItem.path) !== overItem.path
|
|
22
|
+
? path
|
|
23
|
+
: overItem && Path.parent(overItem.path);
|
|
24
|
+
const [lastOverParent, setLastOverParent] = useState(overParent);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (operation === 'reject' || operation === 'rearrange' || !activeItem || !overItem) {
|
|
28
|
+
setLastOverParent(undefined);
|
|
29
|
+
setItemsWithPreview(items);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (lastOverParent === overParent) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setLastOverParent(overParent);
|
|
38
|
+
|
|
39
|
+
const activeIsChild = Path.hasChild(path, activeItem.path);
|
|
40
|
+
const overIsChild = Path.hasChild(path, overItem.path);
|
|
41
|
+
const overSelf = overItem.path === path;
|
|
42
|
+
|
|
43
|
+
if (!activeIsChild && overIsChild) {
|
|
44
|
+
// Insert item into sortable.
|
|
45
|
+
setItemsWithPreview((items) => {
|
|
46
|
+
const position = overItem.position as number;
|
|
47
|
+
return [...items.slice(0, position), activeItem.item as T, ...items.slice(position)];
|
|
48
|
+
});
|
|
49
|
+
} else if (!activeIsChild && overSelf) {
|
|
50
|
+
// Append item to end of sortable.
|
|
51
|
+
setItemsWithPreview((items) => [...items, activeItem.item as T]);
|
|
52
|
+
} else if (activeIsChild && !overIsChild) {
|
|
53
|
+
// Remove item being dragged out.
|
|
54
|
+
setItemsWithPreview((items) => items.filter((item) => item.id !== activeItem.item.id));
|
|
55
|
+
} else {
|
|
56
|
+
// Reset items.
|
|
57
|
+
setItemsWithPreview(items);
|
|
58
|
+
}
|
|
59
|
+
}, [operation, activeItem, overItem, overParent, lastOverParent, path, items]);
|
|
60
|
+
|
|
61
|
+
// In order to avoid render glitching, rather than waiting for the effect to run,
|
|
62
|
+
// immediately return the new items after dropping an item into a new path.
|
|
63
|
+
return items.length === itemsWithPreview.length ? items : itemsWithPreview;
|
|
64
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
import { raise } from '@dxos/debug';
|
|
8
|
+
|
|
9
|
+
import { MosaicContext } from '../Root';
|
|
10
|
+
|
|
11
|
+
export const useMosaic = () => useContext(MosaicContext) ?? raise(new Error('Missing MosaicContext'));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useContainer } from './useContainer';
|
|
6
|
+
import { type MosaicDataItem } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns a sorted collection of items if a compare function is provided.
|
|
10
|
+
*/
|
|
11
|
+
export const useSortedItems = <T extends MosaicDataItem>(items: T[]): T[] => {
|
|
12
|
+
const { compare } = useContainer();
|
|
13
|
+
return compare ? [...items].sort(compare) : items;
|
|
14
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export type MosaicDataItem = { id: string };
|
|
6
|
+
|
|
7
|
+
export type MosaicDraggedItem<TPosition = unknown> = {
|
|
8
|
+
path: string;
|
|
9
|
+
// TODO(wittjosiah): Rename data? Otherwise item within item.
|
|
10
|
+
item: MosaicDataItem;
|
|
11
|
+
// Index or layout-specific positional information (stored separately from the item).
|
|
12
|
+
position?: TPosition;
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { arrayMove } from '@dxos/util';
|
|
6
|
+
|
|
7
|
+
import { type MosaicDataItem } from '../types';
|
|
8
|
+
|
|
9
|
+
// TODO(wittjosiah): Generalize and expand rearrange utilities.
|
|
10
|
+
|
|
11
|
+
export const swapItems = <TItem extends MosaicDataItem, TValue extends MosaicDataItem>(
|
|
12
|
+
items: TItem[],
|
|
13
|
+
from: TValue,
|
|
14
|
+
to: TValue,
|
|
15
|
+
): TItem[] => {
|
|
16
|
+
const fromIndex = items.findIndex((item) => item.id === from.id);
|
|
17
|
+
const toIndex = items.findIndex((item) => item.id === to.id);
|
|
18
|
+
return fromIndex !== -1 && toIndex !== -1 ? arrayMove(items, fromIndex, toIndex) : items;
|
|
19
|
+
};
|