@mbao01/common 0.0.45 → 0.0.46
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/types/components/DragAndDrop/Draggable/Draggable.d.ts +29 -0
- package/dist/types/components/DragAndDrop/Draggable/Draggable.example.d.ts +24 -0
- package/dist/types/components/DragAndDrop/Draggable/constants.d.ts +9 -0
- package/dist/types/components/DragAndDrop/Draggable/index.d.ts +1 -0
- package/dist/types/components/DragAndDrop/Draggable/types.d.ts +41 -0
- package/dist/types/components/DragAndDrop/Droppable/Droppable.d.ts +2 -0
- package/dist/types/components/DragAndDrop/Droppable/Droppable.example.d.ts +10 -0
- package/dist/types/components/DragAndDrop/Droppable/constants.d.ts +5 -0
- package/dist/types/components/DragAndDrop/Droppable/index.d.ts +1 -0
- package/dist/types/components/DragAndDrop/Droppable/types.d.ts +4 -0
- package/dist/types/components/DragAndDrop/Sortable/Sortable.d.ts +2 -0
- package/dist/types/components/DragAndDrop/Sortable/Sortable.example.d.ts +6 -0
- package/dist/types/components/DragAndDrop/Sortable/constants.d.ts +4 -0
- package/dist/types/components/DragAndDrop/Sortable/index.d.ts +1 -0
- package/dist/types/components/DragAndDrop/Sortable/types.d.ts +7 -0
- package/dist/types/components/DragAndDrop/index.d.ts +3 -0
- package/dist/types/components/Widget/InternalWidgetsContext.d.ts +2 -0
- package/dist/types/components/Widget/Widget.d.ts +2 -0
- package/dist/types/components/Widget/Widgets.example.d.ts +1 -0
- package/dist/types/components/Widget/WidgetsContext.d.ts +2 -0
- package/dist/types/components/Widget/hooks/index.d.ts +2 -0
- package/dist/types/components/Widget/hooks/useWidget/index.d.ts +1 -0
- package/dist/types/components/Widget/hooks/useWidget/useWidget.d.ts +7 -0
- package/dist/types/components/Widget/hooks/useWidgets/index.d.ts +1 -0
- package/dist/types/components/Widget/hooks/useWidgets/useWidgets.d.ts +1 -0
- package/dist/types/components/Widget/index.d.ts +3 -0
- package/dist/types/components/Widget/modifiers/index.d.ts +1 -0
- package/dist/types/components/Widget/modifiers/restrictToElement.d.ts +2 -0
- package/dist/types/components/Widget/types.d.ts +28 -0
- package/package.json +6 -2
- package/src/components/DragAndDrop/Draggable/Draggable.example.tsx +147 -0
- package/src/components/DragAndDrop/Draggable/Draggable.tsx +161 -0
- package/src/components/DragAndDrop/Draggable/constants.ts +47 -0
- package/src/components/DragAndDrop/Draggable/index.ts +1 -0
- package/src/components/DragAndDrop/Draggable/types.ts +56 -0
- package/src/components/DragAndDrop/Droppable/Droppable.example.tsx +86 -0
- package/src/components/DragAndDrop/Droppable/Droppable.tsx +38 -0
- package/src/components/DragAndDrop/Droppable/constants.ts +15 -0
- package/src/components/DragAndDrop/Droppable/index.ts +1 -0
- package/src/components/DragAndDrop/Droppable/types.ts +7 -0
- package/src/components/DragAndDrop/Sortable/Sortable.example.tsx +61 -0
- package/src/components/DragAndDrop/Sortable/Sortable.tsx +65 -0
- package/src/components/DragAndDrop/Sortable/constants.ts +12 -0
- package/src/components/DragAndDrop/Sortable/index.ts +1 -0
- package/src/components/DragAndDrop/Sortable/types.ts +11 -0
- package/src/components/DragAndDrop/index.ts +3 -0
- package/src/components/Widget/InternalWidgetsContext.tsx +4 -0
- package/src/components/Widget/Widget.tsx +17 -0
- package/src/components/Widget/Widgets.example.tsx +118 -0
- package/src/components/Widget/WidgetsContext.tsx +97 -0
- package/src/components/Widget/hooks/index.ts +2 -0
- package/src/components/Widget/hooks/useWidget/index.ts +1 -0
- package/src/components/Widget/hooks/useWidget/useWidget.ts +21 -0
- package/src/components/Widget/hooks/useWidgets/index.ts +1 -0
- package/src/components/Widget/hooks/useWidgets/useWidgets.ts +12 -0
- package/src/components/Widget/index.ts +3 -0
- package/src/components/Widget/modifiers/index.ts +1 -0
- package/src/components/Widget/modifiers/restrictToElement.ts +8 -0
- package/src/components/Widget/types.ts +30 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DraggableSyntheticListeners,
|
|
3
|
+
DragOverlayProps,
|
|
4
|
+
DropAnimation,
|
|
5
|
+
UseDraggableArguments,
|
|
6
|
+
} from "@dnd-kit/core";
|
|
7
|
+
import type { CSSProperties, ReactNode, Ref } from "react";
|
|
8
|
+
import { type Transform } from "@dnd-kit/utilities";
|
|
9
|
+
|
|
10
|
+
export enum DraggableAxis {
|
|
11
|
+
All,
|
|
12
|
+
Vertical,
|
|
13
|
+
Horizontal,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type DraggableHandleElement = JSX.Element | null;
|
|
17
|
+
|
|
18
|
+
export type DraggableActionsArgs = {
|
|
19
|
+
draggable: {
|
|
20
|
+
ref: Ref<HTMLElement> | undefined;
|
|
21
|
+
listeners: DraggableSyntheticListeners;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type BaseDraggableProps = Partial<{
|
|
26
|
+
axis: DraggableAxis;
|
|
27
|
+
style: CSSProperties;
|
|
28
|
+
handle: DraggableHandleElement;
|
|
29
|
+
actions: (args: Partial<DraggableActionsArgs>) => ReactNode;
|
|
30
|
+
}> &
|
|
31
|
+
React.HTMLAttributes<HTMLDivElement>;
|
|
32
|
+
|
|
33
|
+
export type DraggableProps = BaseDraggableProps & UseDraggableArguments;
|
|
34
|
+
|
|
35
|
+
export type DraggableRootProps = BaseDraggableProps &
|
|
36
|
+
Partial<{
|
|
37
|
+
isDragOverlay: boolean;
|
|
38
|
+
|
|
39
|
+
isDragging: boolean;
|
|
40
|
+
listeners: DraggableSyntheticListeners;
|
|
41
|
+
transform: Transform | null;
|
|
42
|
+
activatorNodeRef: Ref<HTMLElement>;
|
|
43
|
+
}>;
|
|
44
|
+
|
|
45
|
+
export type DraggableActionProps = React.HTMLAttributes<HTMLButtonElement> & {
|
|
46
|
+
active?: Partial<{
|
|
47
|
+
fill: string;
|
|
48
|
+
background: string;
|
|
49
|
+
}>;
|
|
50
|
+
cursor?: CSSProperties["cursor"];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type DraggableOverlayProps = {
|
|
54
|
+
axis?: DraggableRootProps["axis"];
|
|
55
|
+
dropAnimation?: DropAnimation | null;
|
|
56
|
+
} & DragOverlayProps;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Modifiers, UniqueIdentifier } from "@dnd-kit/core";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
closestCenter,
|
|
5
|
+
closestCorners,
|
|
6
|
+
DndContext,
|
|
7
|
+
pointerWithin,
|
|
8
|
+
rectIntersection,
|
|
9
|
+
} from "@dnd-kit/core";
|
|
10
|
+
import { Draggable } from "../Draggable";
|
|
11
|
+
import { Droppable } from "./Droppable";
|
|
12
|
+
|
|
13
|
+
type CollisionDetectionType =
|
|
14
|
+
| "rectIntersection"
|
|
15
|
+
| "closestCenter"
|
|
16
|
+
| "closestCorners"
|
|
17
|
+
| "pointerWithin";
|
|
18
|
+
|
|
19
|
+
const collisionDetectionAlgorithms = {
|
|
20
|
+
rectIntersection,
|
|
21
|
+
closestCenter,
|
|
22
|
+
closestCorners,
|
|
23
|
+
pointerWithin,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type DroppableExampleProps = {
|
|
27
|
+
collisionDetection?: CollisionDetectionType;
|
|
28
|
+
containers?: UniqueIdentifier[];
|
|
29
|
+
modifiers?: Modifiers;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const DroppableExample = ({
|
|
33
|
+
containers = ["A"],
|
|
34
|
+
collisionDetection = "rectIntersection",
|
|
35
|
+
modifiers,
|
|
36
|
+
}: DroppableExampleProps) => {
|
|
37
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
38
|
+
const [parentContainerId, setParentContainerId] = useState<UniqueIdentifier | null>(null);
|
|
39
|
+
|
|
40
|
+
const item = (
|
|
41
|
+
<Draggable
|
|
42
|
+
id="my-draggable"
|
|
43
|
+
className="w-fit border border-base-content rounded-md p-2 flex items-center gap-2 data-[draggable-active]:opacity-0"
|
|
44
|
+
>
|
|
45
|
+
Go ahead, drag me.
|
|
46
|
+
</Draggable>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<DndContext
|
|
51
|
+
collisionDetection={collisionDetectionAlgorithms[collisionDetection]}
|
|
52
|
+
modifiers={parentContainerId !== null ? undefined : modifiers}
|
|
53
|
+
onDragStart={() => setIsDragging(true)}
|
|
54
|
+
onDragEnd={({ over }) => {
|
|
55
|
+
setParentContainerId(over ? over.id : null);
|
|
56
|
+
setIsDragging(false);
|
|
57
|
+
}}
|
|
58
|
+
onDragCancel={() => setIsDragging(false)}
|
|
59
|
+
>
|
|
60
|
+
<div className="flex gap-6 items-center">
|
|
61
|
+
<div className="my-10 w-52">{parentContainerId === null ? item : null}</div>
|
|
62
|
+
<div className="grid grid-cols-2 gap-6">
|
|
63
|
+
{containers.map((id) => (
|
|
64
|
+
<Droppable
|
|
65
|
+
key={id}
|
|
66
|
+
id={id}
|
|
67
|
+
isDragging={isDragging}
|
|
68
|
+
className="relative p-6 border border-base-300 rounded-md w-72 h-72 bg-base-200 box-border data-[draggable-active]:opacity-80 data-[draggable-over]:opacity-100 transition-opacity"
|
|
69
|
+
>
|
|
70
|
+
<div className="absolute bottom-6">Container {id}</div>
|
|
71
|
+
{parentContainerId === id ? item : null}
|
|
72
|
+
</Droppable>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<Draggable.Overlay />
|
|
77
|
+
</DndContext>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const CollisionDetectionAlgorithmsDroppableExample = ({
|
|
82
|
+
collisionDetection,
|
|
83
|
+
...props
|
|
84
|
+
}: DroppableExampleProps) => {
|
|
85
|
+
return <DroppableExample {...props} collisionDetection={collisionDetection} />;
|
|
86
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
4
|
+
import { cn } from "../../../utilities";
|
|
5
|
+
import { getDroppableClasses } from "./constants";
|
|
6
|
+
import { type DroppableProps } from "./types";
|
|
7
|
+
|
|
8
|
+
export const Droppable = ({
|
|
9
|
+
children,
|
|
10
|
+
id,
|
|
11
|
+
disabled,
|
|
12
|
+
data,
|
|
13
|
+
resizeObserverConfig,
|
|
14
|
+
isDragging,
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}: DroppableProps) => {
|
|
18
|
+
const { isOver, setNodeRef } = useDroppable({
|
|
19
|
+
id,
|
|
20
|
+
data,
|
|
21
|
+
disabled,
|
|
22
|
+
resizeObserverConfig,
|
|
23
|
+
});
|
|
24
|
+
const isEmpty = !children || undefined;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={setNodeRef}
|
|
29
|
+
className={cn(className, getDroppableClasses({ isDragging, isOver, isEmpty }))}
|
|
30
|
+
data-empty={isEmpty}
|
|
31
|
+
data-draggable-over={isOver || undefined}
|
|
32
|
+
data-draggable-active={isDragging || undefined}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Droppable } from "./Droppable";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { UniqueIdentifier } from "@dnd-kit/core";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { DndContext } from "@dnd-kit/core";
|
|
4
|
+
import { arrayMove, SortableContext } from "@dnd-kit/sortable";
|
|
5
|
+
import { cn } from "../../../utilities";
|
|
6
|
+
import { Draggable } from "../Draggable";
|
|
7
|
+
import { Sortable } from "./Sortable";
|
|
8
|
+
import { type SortableProps } from "./types";
|
|
9
|
+
|
|
10
|
+
type SortableExampleProps = Partial<SortableProps> & { hasDraggableOverlay?: boolean };
|
|
11
|
+
|
|
12
|
+
export const SortableExample = ({ hasDraggableOverlay, ...props }: SortableExampleProps) => {
|
|
13
|
+
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
|
14
|
+
const [items, setItems] = useState<UniqueIdentifier[]>(["1", "2", "3", "4", "5", "6", "7", "8"]);
|
|
15
|
+
|
|
16
|
+
const activeIndex = activeId ? items.indexOf(activeId) : -1;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<DndContext
|
|
20
|
+
onDragStart={({ active }) => {
|
|
21
|
+
if (!active) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setActiveId(active.id);
|
|
26
|
+
}}
|
|
27
|
+
onDragEnd={({ over }) => {
|
|
28
|
+
setActiveId(null);
|
|
29
|
+
|
|
30
|
+
if (over) {
|
|
31
|
+
const overIndex = items.indexOf(over.id);
|
|
32
|
+
if (activeIndex !== overIndex) {
|
|
33
|
+
setItems((items) => arrayMove(items, activeIndex, overIndex));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}}
|
|
37
|
+
onDragCancel={() => setActiveId(null)}
|
|
38
|
+
>
|
|
39
|
+
<SortableContext items={items}>
|
|
40
|
+
<div className="grid grid-cols-3 gap-4">
|
|
41
|
+
{items.map((item) => (
|
|
42
|
+
<Sortable
|
|
43
|
+
{...props}
|
|
44
|
+
key={item}
|
|
45
|
+
id={item}
|
|
46
|
+
className={cn(
|
|
47
|
+
"flex items-center justify-center w-32 h-32 bg-base-100 border border-primary-content/30 rounded-md text-sm p-2 data-[draggable]:shadow data-[draggable-active]:z-20",
|
|
48
|
+
{
|
|
49
|
+
"data-[draggable-active]:opacity-30": hasDraggableOverlay,
|
|
50
|
+
}
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{item}
|
|
54
|
+
</Sortable>
|
|
55
|
+
))}
|
|
56
|
+
{hasDraggableOverlay ? <Draggable.Overlay className="bg-base-100 shadow-lg" /> : null}
|
|
57
|
+
</div>
|
|
58
|
+
</SortableContext>
|
|
59
|
+
</DndContext>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useSortable } from "@dnd-kit/sortable";
|
|
2
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
3
|
+
import { cn } from "../../../utilities";
|
|
4
|
+
import { Draggable } from "../Draggable";
|
|
5
|
+
import { getSortableClasses } from "./constants";
|
|
6
|
+
import { type SortableProps } from "./types";
|
|
7
|
+
|
|
8
|
+
export const Sortable = ({
|
|
9
|
+
id,
|
|
10
|
+
data,
|
|
11
|
+
style,
|
|
12
|
+
className,
|
|
13
|
+
children,
|
|
14
|
+
disabled,
|
|
15
|
+
attributes,
|
|
16
|
+
resizeObserverConfig,
|
|
17
|
+
animateLayoutChanges,
|
|
18
|
+
getNewIndex,
|
|
19
|
+
strategy,
|
|
20
|
+
transition,
|
|
21
|
+
...props
|
|
22
|
+
}: SortableProps) => {
|
|
23
|
+
const {
|
|
24
|
+
attributes: sortableAttributes,
|
|
25
|
+
isDragging,
|
|
26
|
+
isSorting,
|
|
27
|
+
listeners,
|
|
28
|
+
setNodeRef,
|
|
29
|
+
setActivatorNodeRef,
|
|
30
|
+
transform,
|
|
31
|
+
transition: sortableTransition,
|
|
32
|
+
} = useSortable({
|
|
33
|
+
id,
|
|
34
|
+
data,
|
|
35
|
+
disabled,
|
|
36
|
+
attributes,
|
|
37
|
+
transition,
|
|
38
|
+
resizeObserverConfig,
|
|
39
|
+
animateLayoutChanges,
|
|
40
|
+
getNewIndex,
|
|
41
|
+
strategy,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Draggable.Root
|
|
46
|
+
{...props}
|
|
47
|
+
{...sortableAttributes}
|
|
48
|
+
ref={setNodeRef}
|
|
49
|
+
activatorNodeRef={setActivatorNodeRef}
|
|
50
|
+
isDragging={isDragging}
|
|
51
|
+
listeners={listeners}
|
|
52
|
+
transform={transform}
|
|
53
|
+
style={{
|
|
54
|
+
...style,
|
|
55
|
+
transform: CSS.Transform.toString(transform),
|
|
56
|
+
transition: sortableTransition,
|
|
57
|
+
}}
|
|
58
|
+
className={cn(className, getSortableClasses({ isDragging, isSorting }))}
|
|
59
|
+
data-draggable={true}
|
|
60
|
+
data-draggable-active={isDragging || undefined}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</Draggable.Root>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Sortable } from "./Sortable";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { UseSortableArguments } from "@dnd-kit/sortable";
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
import type { DraggableAxis, DraggableRootProps } from "../Draggable/types";
|
|
4
|
+
|
|
5
|
+
export type SortableProps = Omit<React.HTMLAttributes<HTMLDivElement>, "id"> &
|
|
6
|
+
UseSortableArguments &
|
|
7
|
+
Pick<DraggableRootProps, "handle" | "actions"> &
|
|
8
|
+
Partial<{
|
|
9
|
+
axis: DraggableAxis;
|
|
10
|
+
style: CSSProperties;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Sortable } from "../DragAndDrop";
|
|
4
|
+
import { useWidget } from "./hooks";
|
|
5
|
+
import { WidgetProps } from "./types";
|
|
6
|
+
|
|
7
|
+
export const Widget = ({ id, children, actions, ...props }: WidgetProps) => {
|
|
8
|
+
const { widget, deleteWidget } = useWidget(id);
|
|
9
|
+
|
|
10
|
+
if (!widget) return null;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Sortable id={id} actions={(args) => actions?.({ deleteWidget, ...args })} {...props}>
|
|
14
|
+
{children}
|
|
15
|
+
</Sortable>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { MoveIcon, TrashIcon } from "@radix-ui/react-icons";
|
|
3
|
+
import { Draggable } from "../DragAndDrop";
|
|
4
|
+
import { useWidgets } from "./hooks/useWidgets/useWidgets";
|
|
5
|
+
import { Widget } from "./Widget";
|
|
6
|
+
import { WidgetsContext } from "./WidgetsContext";
|
|
7
|
+
|
|
8
|
+
export const WidgetsExample = () => {
|
|
9
|
+
return (
|
|
10
|
+
<WidgetsContext
|
|
11
|
+
initialWidgets={[
|
|
12
|
+
{ id: "A" },
|
|
13
|
+
{ id: "B" },
|
|
14
|
+
{ id: "C" },
|
|
15
|
+
{ id: "D" },
|
|
16
|
+
{ id: "E" },
|
|
17
|
+
{ id: "F" },
|
|
18
|
+
{ id: "G" },
|
|
19
|
+
]}
|
|
20
|
+
>
|
|
21
|
+
<Widgets />
|
|
22
|
+
</WidgetsContext>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const Widgets = () => {
|
|
27
|
+
const [widgetId, setWidgetId] = useState<string>("");
|
|
28
|
+
const [insertionIndex, setInsertionIndex] = useState<number>();
|
|
29
|
+
const { widgets, addWidget, addWidgets, deleteWidget, deleteWidgets, resetWidgets } =
|
|
30
|
+
useWidgets();
|
|
31
|
+
|
|
32
|
+
const widgetIds = widgetId.split(",").map((id) => ({
|
|
33
|
+
id,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="relative h-[calc(100dvh-50px)] border border-base-200 rounded-md p-6 overflow-y-auto">
|
|
38
|
+
<div className="flex flex-col gap-2 mb-6">
|
|
39
|
+
<span className="text-sm">Controls:</span>
|
|
40
|
+
<div className="flex gap-2">
|
|
41
|
+
<input
|
|
42
|
+
type="text"
|
|
43
|
+
placeholder="Enter Widget ID"
|
|
44
|
+
className="input input-xs input-bordered rounded-md"
|
|
45
|
+
value={widgetId}
|
|
46
|
+
onChange={(e) => setWidgetId(e.target.value)}
|
|
47
|
+
/>
|
|
48
|
+
<input
|
|
49
|
+
type="number"
|
|
50
|
+
placeholder="Enter Insertion Index"
|
|
51
|
+
className="input input-xs input-bordered rounded-md"
|
|
52
|
+
value={insertionIndex}
|
|
53
|
+
onChange={(e) => setInsertionIndex(Number(e.target.value))}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex gap-2">
|
|
57
|
+
<button
|
|
58
|
+
className="btn btn-outline btn-xs btn-success"
|
|
59
|
+
onClick={() => addWidget(widgetIds[0], insertionIndex)}
|
|
60
|
+
>
|
|
61
|
+
Add widget
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
className="btn btn-outline btn-xs btn-info"
|
|
65
|
+
onClick={() => addWidgets(widgetIds, insertionIndex)}
|
|
66
|
+
>
|
|
67
|
+
Add widgets
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
className="btn btn-outline btn-xs btn-warning"
|
|
71
|
+
onClick={() => deleteWidget(widgetIds[0])}
|
|
72
|
+
>
|
|
73
|
+
Delete widget
|
|
74
|
+
</button>
|
|
75
|
+
<button
|
|
76
|
+
className="btn btn-outline btn-xs btn-error"
|
|
77
|
+
onClick={() => deleteWidgets(widgetIds)}
|
|
78
|
+
>
|
|
79
|
+
Delete widgets
|
|
80
|
+
</button>
|
|
81
|
+
<button className="btn btn-outline btn-xs" onClick={() => resetWidgets()}>
|
|
82
|
+
Reset widgets
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="grid gap-4 grid-cols-3 w-fit h-fit">
|
|
87
|
+
{widgets.map((widget) => (
|
|
88
|
+
<Widget
|
|
89
|
+
key={widget.id}
|
|
90
|
+
id={widget.id}
|
|
91
|
+
actions={({ draggable, deleteWidget }) => (
|
|
92
|
+
<div className="absolute top-1 right-1 opacity-0 flex gap-1 [&>*]:cursor-pointer [&_svg]:size-4 pointer-events-none transition-opacity duration-300 hover:opacity-100 group-hover:pointer-events-auto">
|
|
93
|
+
<Draggable.Action
|
|
94
|
+
ref={draggable?.ref}
|
|
95
|
+
{...draggable?.listeners}
|
|
96
|
+
aria-label={`Drag widget ${widget.id}`}
|
|
97
|
+
className="transition-all hover:text-primary active:cursor-grabbing"
|
|
98
|
+
>
|
|
99
|
+
<MoveIcon />
|
|
100
|
+
</Draggable.Action>
|
|
101
|
+
<Draggable.Action
|
|
102
|
+
onClick={deleteWidget}
|
|
103
|
+
aria-label={`Delete widget ${widget.id}`}
|
|
104
|
+
className="transition-all hover:text-error"
|
|
105
|
+
>
|
|
106
|
+
<TrashIcon />
|
|
107
|
+
</Draggable.Action>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
className="group flex items-center justify-center w-32 h-32 bg-base-100 border border-primary-content/30 rounded-md text-sm p-2 cursor-default data-[draggable]:shadow data-[draggable-active]:z-20"
|
|
111
|
+
>
|
|
112
|
+
{widget.id}
|
|
113
|
+
</Widget>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { closestCenter, DndContext } from "@dnd-kit/core";
|
|
3
|
+
import { arrayMove, SortableContext } from "@dnd-kit/sortable";
|
|
4
|
+
import { InternalWidgetsContext } from "./InternalWidgetsContext";
|
|
5
|
+
import { restrictToElement } from "./modifiers";
|
|
6
|
+
import { Widget, WidgetsContextProps } from "./types";
|
|
7
|
+
|
|
8
|
+
export const WidgetsContext = ({ children, initialWidgets }: WidgetsContextProps) => {
|
|
9
|
+
const [activeId, setActiveId] = useState<Widget["id"] | null>(null);
|
|
10
|
+
const [widgets, setWidgets] = useState<Widget[]>(initialWidgets ?? []);
|
|
11
|
+
const boundingRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
const boundingRect = boundingRef.current?.getBoundingClientRect();
|
|
13
|
+
|
|
14
|
+
const activeIndex = activeId ? widgets.findIndex((item) => item.id === activeId) : -1;
|
|
15
|
+
|
|
16
|
+
const addWidget = (widget: Widget, insertIndex?: number) => {
|
|
17
|
+
const isValid = Boolean(widget.id);
|
|
18
|
+
const isExist = widgets.some((w) => w.id === widget.id);
|
|
19
|
+
|
|
20
|
+
if (!isExist && isValid) {
|
|
21
|
+
// if there's no existing widget in the list, then add it at the given index or at the end of the list
|
|
22
|
+
setWidgets((widgets) => widgets.toSpliced(insertIndex ?? Infinity, 0, widget));
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const addWidgets = (_widgets: Widget[], startIndex?: number) => {
|
|
27
|
+
setWidgets((widgets) => {
|
|
28
|
+
// filter widgets that do NOT already exist do avoid duplicate IDs
|
|
29
|
+
const newWidgets = _widgets.filter(
|
|
30
|
+
(widget) => widget.id && !widgets.some((w) => w.id === widget.id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// insert new widgets at the given start index or at the end of the list
|
|
34
|
+
return widgets.toSpliced(startIndex ?? Infinity, 0, ...newWidgets);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const deleteWidget = (widget: Widget) => {
|
|
39
|
+
const widgetIndex = widgets.findIndex((w) => w.id === widget.id);
|
|
40
|
+
|
|
41
|
+
if (widgetIndex >= 0) {
|
|
42
|
+
// if the widget to be deleted exists in the list, then delete it.
|
|
43
|
+
setWidgets((widgets) => widgets.toSpliced(widgetIndex, 1));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const deleteWidgets = (_widgets: Widget[]) => {
|
|
48
|
+
setWidgets((widgets) => {
|
|
49
|
+
// filter widgets that do NOT exist in the deletion list
|
|
50
|
+
return widgets.filter((widget) => !_widgets.some((w) => w.id === widget.id));
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const resetWidgets = () => {
|
|
55
|
+
setWidgets(initialWidgets ?? []);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<DndContext
|
|
60
|
+
modifiers={[restrictToElement(boundingRect)]}
|
|
61
|
+
collisionDetection={closestCenter}
|
|
62
|
+
onDragStart={({ active }) => {
|
|
63
|
+
if (!active) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setActiveId(active.id);
|
|
68
|
+
}}
|
|
69
|
+
onDragEnd={({ over }) => {
|
|
70
|
+
setActiveId(null);
|
|
71
|
+
|
|
72
|
+
if (over) {
|
|
73
|
+
const overIndex = widgets.findIndex((item) => item.id === over.id);
|
|
74
|
+
if (activeIndex !== overIndex) {
|
|
75
|
+
setWidgets((w) => arrayMove(w, activeIndex, overIndex));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
onDragCancel={() => setActiveId(null)}
|
|
80
|
+
>
|
|
81
|
+
<SortableContext items={widgets}>
|
|
82
|
+
<InternalWidgetsContext.Provider
|
|
83
|
+
value={{
|
|
84
|
+
widgets,
|
|
85
|
+
addWidget,
|
|
86
|
+
addWidgets,
|
|
87
|
+
deleteWidget,
|
|
88
|
+
deleteWidgets,
|
|
89
|
+
resetWidgets,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<div ref={boundingRef}>{children}</div>
|
|
93
|
+
</InternalWidgetsContext.Provider>
|
|
94
|
+
</SortableContext>
|
|
95
|
+
</DndContext>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useWidget } from "./useWidget";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { UniqueIdentifier } from "@dnd-kit/core";
|
|
3
|
+
import { InternalWidgetsContext } from "../../InternalWidgetsContext";
|
|
4
|
+
|
|
5
|
+
export const useWidget = (id: UniqueIdentifier) => {
|
|
6
|
+
const context = useContext(InternalWidgetsContext);
|
|
7
|
+
|
|
8
|
+
if (!context) {
|
|
9
|
+
throw new Error("useWidget must be used within a <WidgetsContext />");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const widget = context.widgets.find((widget) => widget.id === id);
|
|
13
|
+
|
|
14
|
+
const deleteWidget = () => {
|
|
15
|
+
if (widget) {
|
|
16
|
+
context.deleteWidget(widget);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return { widget, deleteWidget };
|
|
21
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useWidgets } from "./useWidgets";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { InternalWidgetsContext } from "../../InternalWidgetsContext";
|
|
3
|
+
|
|
4
|
+
export const useWidgets = () => {
|
|
5
|
+
const context = useContext(InternalWidgetsContext);
|
|
6
|
+
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error("useWidgets must be used within a <WidgetsContext />");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return context;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { restrictToElement } from "./restrictToElement";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Modifier } from "@dnd-kit/core";
|
|
2
|
+
import { restrictToParentElement } from "@dnd-kit/modifiers";
|
|
3
|
+
|
|
4
|
+
export const restrictToElement = (rect: DOMRect | null | undefined) => {
|
|
5
|
+
const modifier: Modifier = (args) =>
|
|
6
|
+
restrictToParentElement({ ...args, containerNodeRect: rect ?? null });
|
|
7
|
+
return modifier;
|
|
8
|
+
};
|