@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.
Files changed (59) hide show
  1. package/dist/types/components/DragAndDrop/Draggable/Draggable.d.ts +29 -0
  2. package/dist/types/components/DragAndDrop/Draggable/Draggable.example.d.ts +24 -0
  3. package/dist/types/components/DragAndDrop/Draggable/constants.d.ts +9 -0
  4. package/dist/types/components/DragAndDrop/Draggable/index.d.ts +1 -0
  5. package/dist/types/components/DragAndDrop/Draggable/types.d.ts +41 -0
  6. package/dist/types/components/DragAndDrop/Droppable/Droppable.d.ts +2 -0
  7. package/dist/types/components/DragAndDrop/Droppable/Droppable.example.d.ts +10 -0
  8. package/dist/types/components/DragAndDrop/Droppable/constants.d.ts +5 -0
  9. package/dist/types/components/DragAndDrop/Droppable/index.d.ts +1 -0
  10. package/dist/types/components/DragAndDrop/Droppable/types.d.ts +4 -0
  11. package/dist/types/components/DragAndDrop/Sortable/Sortable.d.ts +2 -0
  12. package/dist/types/components/DragAndDrop/Sortable/Sortable.example.d.ts +6 -0
  13. package/dist/types/components/DragAndDrop/Sortable/constants.d.ts +4 -0
  14. package/dist/types/components/DragAndDrop/Sortable/index.d.ts +1 -0
  15. package/dist/types/components/DragAndDrop/Sortable/types.d.ts +7 -0
  16. package/dist/types/components/DragAndDrop/index.d.ts +3 -0
  17. package/dist/types/components/Widget/InternalWidgetsContext.d.ts +2 -0
  18. package/dist/types/components/Widget/Widget.d.ts +2 -0
  19. package/dist/types/components/Widget/Widgets.example.d.ts +1 -0
  20. package/dist/types/components/Widget/WidgetsContext.d.ts +2 -0
  21. package/dist/types/components/Widget/hooks/index.d.ts +2 -0
  22. package/dist/types/components/Widget/hooks/useWidget/index.d.ts +1 -0
  23. package/dist/types/components/Widget/hooks/useWidget/useWidget.d.ts +7 -0
  24. package/dist/types/components/Widget/hooks/useWidgets/index.d.ts +1 -0
  25. package/dist/types/components/Widget/hooks/useWidgets/useWidgets.d.ts +1 -0
  26. package/dist/types/components/Widget/index.d.ts +3 -0
  27. package/dist/types/components/Widget/modifiers/index.d.ts +1 -0
  28. package/dist/types/components/Widget/modifiers/restrictToElement.d.ts +2 -0
  29. package/dist/types/components/Widget/types.d.ts +28 -0
  30. package/package.json +6 -2
  31. package/src/components/DragAndDrop/Draggable/Draggable.example.tsx +147 -0
  32. package/src/components/DragAndDrop/Draggable/Draggable.tsx +161 -0
  33. package/src/components/DragAndDrop/Draggable/constants.ts +47 -0
  34. package/src/components/DragAndDrop/Draggable/index.ts +1 -0
  35. package/src/components/DragAndDrop/Draggable/types.ts +56 -0
  36. package/src/components/DragAndDrop/Droppable/Droppable.example.tsx +86 -0
  37. package/src/components/DragAndDrop/Droppable/Droppable.tsx +38 -0
  38. package/src/components/DragAndDrop/Droppable/constants.ts +15 -0
  39. package/src/components/DragAndDrop/Droppable/index.ts +1 -0
  40. package/src/components/DragAndDrop/Droppable/types.ts +7 -0
  41. package/src/components/DragAndDrop/Sortable/Sortable.example.tsx +61 -0
  42. package/src/components/DragAndDrop/Sortable/Sortable.tsx +65 -0
  43. package/src/components/DragAndDrop/Sortable/constants.ts +12 -0
  44. package/src/components/DragAndDrop/Sortable/index.ts +1 -0
  45. package/src/components/DragAndDrop/Sortable/types.ts +11 -0
  46. package/src/components/DragAndDrop/index.ts +3 -0
  47. package/src/components/Widget/InternalWidgetsContext.tsx +4 -0
  48. package/src/components/Widget/Widget.tsx +17 -0
  49. package/src/components/Widget/Widgets.example.tsx +118 -0
  50. package/src/components/Widget/WidgetsContext.tsx +97 -0
  51. package/src/components/Widget/hooks/index.ts +2 -0
  52. package/src/components/Widget/hooks/useWidget/index.ts +1 -0
  53. package/src/components/Widget/hooks/useWidget/useWidget.ts +21 -0
  54. package/src/components/Widget/hooks/useWidgets/index.ts +1 -0
  55. package/src/components/Widget/hooks/useWidgets/useWidgets.ts +12 -0
  56. package/src/components/Widget/index.ts +3 -0
  57. package/src/components/Widget/modifiers/index.ts +1 -0
  58. package/src/components/Widget/modifiers/restrictToElement.ts +8 -0
  59. 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,15 @@
1
+ import { cva } from "../../../libs";
2
+
3
+ export const getDroppableClasses = cva("relative", {
4
+ variants: {
5
+ isDragging: {
6
+ true: "",
7
+ },
8
+ isOver: {
9
+ true: "",
10
+ },
11
+ isEmpty: {
12
+ true: "",
13
+ },
14
+ },
15
+ });
@@ -0,0 +1 @@
1
+ export { Droppable } from "./Droppable";
@@ -0,0 +1,7 @@
1
+ import { type UseDroppableArguments } from "@dnd-kit/core";
2
+
3
+ export type DroppableProps = Omit<React.HTMLAttributes<HTMLDivElement>, "id"> &
4
+ UseDroppableArguments &
5
+ Partial<{
6
+ isDragging: boolean;
7
+ }>;
@@ -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,12 @@
1
+ import { cva } from "../../../libs";
2
+
3
+ export const getSortableClasses = cva("", {
4
+ variants: {
5
+ isDragging: {
6
+ true: "",
7
+ },
8
+ isSorting: {
9
+ true: "",
10
+ },
11
+ },
12
+ });
@@ -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,3 @@
1
+ export { Draggable } from "./Draggable";
2
+ export { Droppable } from "./Droppable";
3
+ export { Sortable } from "./Sortable";
@@ -0,0 +1,4 @@
1
+ import { createContext } from "react";
2
+ import { type InternalWidgetsContextProps } from "./types";
3
+
4
+ export const InternalWidgetsContext = createContext<InternalWidgetsContextProps | null>(null);
@@ -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,2 @@
1
+ export { useWidget } from "./useWidget";
2
+ export { useWidgets } from "./useWidgets";
@@ -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,3 @@
1
+ export * from "./hooks";
2
+ export { Widget } from "./Widget";
3
+ export { WidgetsContext } from "./WidgetsContext";
@@ -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
+ };