From e077d69e774ea2bf8af17c96cff8a42939611350 Mon Sep 17 00:00:00 2001 From: Clauderic Demers Date: Wed, 9 Aug 2023 00:30:01 -0400 Subject: [PATCH] Latest updates --- apps/docs/.storybook/main.js | 2 +- apps/docs/.storybook/preview.jsx | 17 ++ apps/docs/package.json | 5 +- apps/docs/stories/Sortable/Sortable.tsx | 111 ------------- .../{ => react}/Droppable/Droppable.mdx | 4 +- .../Droppable/Droppable.stories.tsx | 0 .../Droppable/DroppableExample.tsx | 63 ++++---- .../{ => react}/Resizeable/Resizeable.css | 0 .../{ => react}/Resizeable/Resizeable.tsx | 8 +- .../stories/{ => react}/Resizeable/index.ts | 0 .../react/Sortable/Sortable.stories.tsx | 21 +++ .../react/Sortable/SortableExample.tsx | 86 ++++++++++ .../Virtualized/Virtualized.stories.tsx | 15 ++ .../VirtualizedSortableExample.tsx | 130 +++++++++++++++ .../docs/stories/react/Sortable/docs/Docs.mdx | 40 +++++ .../Sortable/docs/assets/useSortable.png | Bin 0 -> 59544 bytes .../Sortable/docs/examples/QuickStart.jsx | 37 +++++ .../components/Action/Action.module.css | 2 +- .../{ => react}/components/Action/Action.tsx | 8 +- .../{ => react}/components/Action/index.ts | 0 .../components/Button/Button.module.css | 2 +- .../{ => react}/components/Button/Button.tsx | 2 +- .../{ => react}/components/Button/index.ts | 0 .../react/components/Code/Code.module.css | 30 ++++ .../stories/react/components/Code/Code.tsx | 25 +++ .../CodeHighlighter.module.css | 136 ++++++++++++++++ .../CodeHighlighter/CodeHighlighter.tsx | 76 +++++++++ .../Code/components/CodeHighlighter/copy.svg | 15 ++ .../Code/components/CodeHighlighter/index.ts | 1 + .../react/components/Code/components/index.ts | 1 + .../stories/react/components/Code/index.ts | 1 + .../components/Dropzone/Dropzone.module.css | 36 ++--- .../components/Dropzone/Dropzone.tsx | 2 +- .../{ => react}/components/Dropzone/index.ts | 0 .../{ => react}/components/Handle/Handle.tsx | 0 .../{ => react}/components/Handle/index.ts | 0 .../react/components/Item/Item.module.css | 55 +++++++ .../stories/react/components/Item/Item.tsx | 35 ++++ .../stories/react/components/Item/index.ts | 1 + .../components/Preview/Preview.module.css | 10 ++ .../react/components/Preview/Preview.tsx | 18 +++ .../stories/react/components/Preview/index.ts | 1 + .../stories/{ => react}/components/index.ts | 8 + .../{ => react}/icons/DraggableIcon.tsx | 0 .../{ => react}/icons/DroppableIcon.tsx | 0 apps/docs/stories/{ => react}/icons/index.ts | 0 apps/docs/stories/utilities/createRange.ts | 3 + apps/docs/stories/utilities/index.ts | 1 + packages/abstract/src/collision/notifier.ts | 2 +- packages/abstract/src/index.ts | 2 +- packages/abstract/src/manager/manager.ts | 22 ++- packages/abstract/src/manager/monitor.ts | 64 +++++--- packages/abstract/src/manager/registry.ts | 18 +-- .../abstract/src/nodes/draggable/draggable.ts | 15 +- .../abstract/src/nodes/droppable/droppable.ts | 15 +- packages/abstract/src/nodes/node/node.ts | 20 ++- packages/abstract/src/plugins/registry.ts | 15 +- packages/dom-utilities/src/index.ts | 2 + .../src/scroll/getScrollableAncestors.ts | 6 +- packages/dom/src/index.ts | 14 +- packages/dom/src/manager/index.ts | 2 +- packages/dom/src/manager/manager.ts | 61 ++++--- packages/dom/src/nodes/draggable/draggable.ts | 65 +++++++- packages/dom/src/nodes/draggable/index.ts | 2 +- packages/dom/src/nodes/droppable/droppable.ts | 47 ++++-- packages/dom/src/plugins/debug/debug.ts | 104 ++++++++++++ packages/dom/src/plugins/debug/index.ts | 1 + .../dom/src/plugins/feedback/CloneFeedback.ts | 12 +- packages/dom/src/plugins/feedback/Overlay.ts | 2 +- .../plugins/feedback/PlaceholderFeedback.ts | 96 ++++++++++- packages/dom/src/plugins/index.ts | 7 + .../dom/src/plugins/scrolling/AutoScroller.ts | 14 +- .../dom/src/plugins/scrolling/Scroller.ts | 12 +- packages/dom/src/plugins/sortable/index.ts | 2 + packages/dom/src/plugins/sortable/sortable.ts | 151 ++++++++++++++++++ packages/dom/src/sensors/index.ts | 2 + .../src/sensors/keyboard/KeyboardSensor.ts | 6 +- .../dom/src/sensors/pointer/PointerSensor.ts | 5 + packages/dom/src/sensors/types.ts | 5 + packages/dom/src/shapes/DOMRectangle.ts | 29 +++- packages/react/package.json | 4 + .../{DndContext.tsx => DragDropProvider.tsx} | 12 +- packages/react/src/context/hook.ts | 2 +- packages/react/src/context/index.ts | 4 +- packages/react/src/draggable/index.ts | 1 + packages/react/src/draggable/useDraggable.ts | 76 ++++----- packages/react/src/droppable/index.ts | 1 + packages/react/src/droppable/useDroppable.ts | 47 +++--- packages/react/src/hooks/index.ts | 1 + packages/react/src/hooks/useOnValueChange.ts | 16 ++ packages/react/src/index.ts | 7 +- packages/react/src/sensors/index.ts | 1 - .../react/src/sensors/useConnectSensors.ts | 28 ---- packages/react/src/sortable/index.ts | 1 + packages/react/src/sortable/useSortable.ts | 66 ++++++-- packages/state/package.json | 2 +- packages/state/src/index.ts | 1 + packages/utilities/src/array/arrayMove.ts | 13 ++ packages/utilities/src/array/index.ts | 1 + packages/utilities/src/index.ts | 7 +- yarn.lock | 56 ++++++- 101 files changed, 1734 insertions(+), 441 deletions(-) create mode 100644 apps/docs/.storybook/preview.jsx delete mode 100644 apps/docs/stories/Sortable/Sortable.tsx rename apps/docs/stories/{ => react}/Droppable/Droppable.mdx (70%) rename apps/docs/stories/{ => react}/Droppable/Droppable.stories.tsx (100%) rename apps/docs/stories/{ => react}/Droppable/DroppableExample.tsx (73%) rename apps/docs/stories/{ => react}/Resizeable/Resizeable.css (100%) rename apps/docs/stories/{ => react}/Resizeable/Resizeable.tsx (98%) rename apps/docs/stories/{ => react}/Resizeable/index.ts (100%) create mode 100644 apps/docs/stories/react/Sortable/Sortable.stories.tsx create mode 100644 apps/docs/stories/react/Sortable/SortableExample.tsx create mode 100644 apps/docs/stories/react/Sortable/Virtualized/Virtualized.stories.tsx create mode 100644 apps/docs/stories/react/Sortable/Virtualized/VirtualizedSortableExample.tsx create mode 100644 apps/docs/stories/react/Sortable/docs/Docs.mdx create mode 100644 apps/docs/stories/react/Sortable/docs/assets/useSortable.png create mode 100644 apps/docs/stories/react/Sortable/docs/examples/QuickStart.jsx rename apps/docs/stories/{ => react}/components/Action/Action.module.css (98%) rename apps/docs/stories/{ => react}/components/Action/Action.tsx (69%) rename apps/docs/stories/{ => react}/components/Action/index.ts (100%) rename apps/docs/stories/{ => react}/components/Button/Button.module.css (97%) rename apps/docs/stories/{ => react}/components/Button/Button.tsx (94%) rename apps/docs/stories/{ => react}/components/Button/index.ts (100%) create mode 100644 apps/docs/stories/react/components/Code/Code.module.css create mode 100644 apps/docs/stories/react/components/Code/Code.tsx create mode 100644 apps/docs/stories/react/components/Code/components/CodeHighlighter/CodeHighlighter.module.css create mode 100644 apps/docs/stories/react/components/Code/components/CodeHighlighter/CodeHighlighter.tsx create mode 100644 apps/docs/stories/react/components/Code/components/CodeHighlighter/copy.svg create mode 100644 apps/docs/stories/react/components/Code/components/CodeHighlighter/index.ts create mode 100644 apps/docs/stories/react/components/Code/components/index.ts create mode 100644 apps/docs/stories/react/components/Code/index.ts rename apps/docs/stories/{ => react}/components/Dropzone/Dropzone.module.css (65%) rename apps/docs/stories/{ => react}/components/Dropzone/Dropzone.tsx (93%) rename apps/docs/stories/{ => react}/components/Dropzone/index.ts (100%) rename apps/docs/stories/{ => react}/components/Handle/Handle.tsx (100%) rename apps/docs/stories/{ => react}/components/Handle/index.ts (100%) create mode 100644 apps/docs/stories/react/components/Item/Item.module.css create mode 100644 apps/docs/stories/react/components/Item/Item.tsx create mode 100644 apps/docs/stories/react/components/Item/index.ts create mode 100644 apps/docs/stories/react/components/Preview/Preview.module.css create mode 100644 apps/docs/stories/react/components/Preview/Preview.tsx create mode 100644 apps/docs/stories/react/components/Preview/index.ts rename apps/docs/stories/{ => react}/components/index.ts (51%) rename apps/docs/stories/{ => react}/icons/DraggableIcon.tsx (100%) rename apps/docs/stories/{ => react}/icons/DroppableIcon.tsx (100%) rename apps/docs/stories/{ => react}/icons/index.ts (100%) create mode 100644 apps/docs/stories/utilities/createRange.ts create mode 100644 packages/dom/src/plugins/debug/debug.ts create mode 100644 packages/dom/src/plugins/debug/index.ts create mode 100644 packages/dom/src/plugins/sortable/index.ts create mode 100644 packages/dom/src/plugins/sortable/sortable.ts create mode 100644 packages/dom/src/sensors/types.ts rename packages/react/src/context/{DndContext.tsx => DragDropProvider.tsx} (74%) create mode 100644 packages/react/src/hooks/useOnValueChange.ts delete mode 100644 packages/react/src/sensors/index.ts delete mode 100644 packages/react/src/sensors/useConnectSensors.ts create mode 100644 packages/utilities/src/array/arrayMove.ts create mode 100644 packages/utilities/src/array/index.ts diff --git a/apps/docs/.storybook/main.js b/apps/docs/.storybook/main.js index 26e6a8dd..da9bd268 100644 --- a/apps/docs/.storybook/main.js +++ b/apps/docs/.storybook/main.js @@ -20,7 +20,7 @@ export default { }); }, docs: { - autodocs: true, + autodocs: 'tag', }, }; diff --git a/apps/docs/.storybook/preview.jsx b/apps/docs/.storybook/preview.jsx new file mode 100644 index 00000000..78d48c4b --- /dev/null +++ b/apps/docs/.storybook/preview.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {Unstyled} from '@storybook/blocks'; + +import {Code} from '../stories/react/components'; + +export const parameters = { + docs: { + components: { + pre: (props) => ( + +
+        
+      ),
+      code: Code,
+    },
+  },
+};
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 1a32822a..d12113ef 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -13,9 +13,12 @@
     "@dnd-kit/dom": "*",
     "@dnd-kit/react": "*",
     "@dnd-kit/utilities": "*",
+    "prismjs": "^1.29.0",
+    "clipboard": "^2.0.11",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
-    "use-pan-and-zoom": "^0.6.5"
+    "use-pan-and-zoom": "^0.6.5",
+    "@tanstack/react-virtual": "^3.0.0-beta.54"
   },
   "devDependencies": {
     "@dnd-kit/abstract": "*",
diff --git a/apps/docs/stories/Sortable/Sortable.tsx b/apps/docs/stories/Sortable/Sortable.tsx
deleted file mode 100644
index c27becff..00000000
--- a/apps/docs/stories/Sortable/Sortable.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import React, {useRef, useState} from 'react';
-import {flushSync} from 'react-dom';
-import type {PropsWithChildren} from 'react';
-import type {UniqueIdentifier} from '@dnd-kit/types';
-import {DndContext, useDraggable, useDroppable} from '@dnd-kit/react';
-import {closestCenter, CollisionDetector} from '@dnd-kit/collision';
-
-const items = {
-  A: ['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8'],
-  B: ['B1', 'B2', 'B3', 'B4', 'B5'],
-  C: ['C1', 'C2', 'C3'],
-};
-
-export function App() {
-  const draggableMarkup = ;
-
-  return (
-    
-      
-
- {items.map((id) => ( - - {parent === `A${id}` ? draggableMarkup : null} - - ))} -
- -
- {parent == null ? draggableMarkup : null} -
- {/* */} - {items.map((id) => ( - - {parent === `B${id}` ? draggableMarkup : null} - - ))} -
-
- - ); -} - -interface DraggableProps { - id: UniqueIdentifier; - type?: UniqueIdentifier; -} - -function Draggable({id, type}: DraggableProps) { - const [element, setElement] = useState(null); - const activatorRef = useRef(null); - - const {isDragging} = useDraggable({ - id, - element, - activator: activatorRef, - type, - }); - - return ( -
- I am draggable - -
- ); -} - -interface DroppableProps { - id: UniqueIdentifier; - accept?: UniqueIdentifier[]; - collisionDetector?: CollisionDetector; -} - -function Droppable({ - id, - accept, - collisionDetector, - children, -}: PropsWithChildren) { - const {ref, isDropTarget} = useDroppable({id, accept, collisionDetector}); - - return ( -
-
Container: {id}
- -
{children}
-
- ); -} diff --git a/apps/docs/stories/Droppable/Droppable.mdx b/apps/docs/stories/react/Droppable/Droppable.mdx similarity index 70% rename from apps/docs/stories/Droppable/Droppable.mdx rename to apps/docs/stories/react/Droppable/Droppable.mdx index 8ff555ac..9bc25f09 100644 --- a/apps/docs/stories/Droppable/Droppable.mdx +++ b/apps/docs/stories/react/Droppable/Droppable.mdx @@ -1,6 +1,4 @@ -{/* Checkbox.mdx */} - -import { Canvas, Meta } from '@storybook/blocks'; +import {Canvas, Meta} from '@storybook/blocks'; import * as DroppableStories from './Droppable.stories'; diff --git a/apps/docs/stories/Droppable/Droppable.stories.tsx b/apps/docs/stories/react/Droppable/Droppable.stories.tsx similarity index 100% rename from apps/docs/stories/Droppable/Droppable.stories.tsx rename to apps/docs/stories/react/Droppable/Droppable.stories.tsx diff --git a/apps/docs/stories/Droppable/DroppableExample.tsx b/apps/docs/stories/react/Droppable/DroppableExample.tsx similarity index 73% rename from apps/docs/stories/Droppable/DroppableExample.tsx rename to apps/docs/stories/react/Droppable/DroppableExample.tsx index 260d5c32..702372d2 100644 --- a/apps/docs/stories/Droppable/DroppableExample.tsx +++ b/apps/docs/stories/react/Droppable/DroppableExample.tsx @@ -1,12 +1,11 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; import type {PropsWithChildren} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/types'; -import {DndContext, useDraggable, useDroppable} from '@dnd-kit/react'; +import {DragDropProvider, useDraggable, useDroppable} from '@dnd-kit/react'; import {closestCenter, CollisionDetector} from '@dnd-kit/collision'; import {Button, Dropzone, Handle} from '../components'; import {DraggableIcon} from '../icons'; -import {cloneDeep} from '../utilities'; export function DroppableExample() { const [items, setItems] = useState({ @@ -32,28 +31,23 @@ export function DroppableExample() { C4: [], }, }); - const snapshot = useRef(cloneDeep(items)); return ( - { - snapshot.current = cloneDeep(items); - }} - // onCollision={(event, manager) => { - // const [_, secondCollision] = event.collisions; - - // event.preventDefault(); - // manager.actions.setDropTarget(secondCollision?.id); - // }} + { const {source, target} = event.operation; if (event.canceled) { - return; - } + const {abort, resume} = event.suspend(); + const cancel = confirm('Cancel drop?'); - const {resume} = event.suspend(); - resume(); + resume(); + + if (cancel) { + abort(); + return; + } + } if (source && target) { const targetRowId = target.id; @@ -81,11 +75,6 @@ export function DroppableExample() { } } }} - // onDragEnd={(event) => { - // if (event.canceled) { - // setItems(snapshot.current); - // } - // }} >
- {children.map((child) => ( - - ))} + {children.length + ? children.map((child) => ( + + )) + : null} ))}
))}
-
+ ); } @@ -133,7 +124,7 @@ function Draggable({id, parent, type}: DraggableProps) { const [element, setElement] = useState(null); const activatorRef = useRef(null); - const {isDragging} = useDraggable({ + const {isDragSource} = useDraggable({ id, data: {parent}, element, @@ -144,8 +135,10 @@ function Draggable({id, parent, type}: DraggableProps) { return ( diff --git a/apps/docs/stories/Resizeable/Resizeable.css b/apps/docs/stories/react/Resizeable/Resizeable.css similarity index 100% rename from apps/docs/stories/Resizeable/Resizeable.css rename to apps/docs/stories/react/Resizeable/Resizeable.css diff --git a/apps/docs/stories/Resizeable/Resizeable.tsx b/apps/docs/stories/react/Resizeable/Resizeable.tsx similarity index 98% rename from apps/docs/stories/Resizeable/Resizeable.tsx rename to apps/docs/stories/react/Resizeable/Resizeable.tsx index e3fcb97a..86c807ae 100644 --- a/apps/docs/stories/Resizeable/Resizeable.tsx +++ b/apps/docs/stories/react/Resizeable/Resizeable.tsx @@ -8,7 +8,7 @@ import React, { } from 'react'; import type {PropsWithChildren} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/types'; -import {DndContext, useDraggable} from '@dnd-kit/react'; +import {DragDropProvider, useDraggable} from '@dnd-kit/react'; import './Resizeable.css'; import {Coordinates} from '@dnd-kit/geometry'; @@ -173,7 +173,7 @@ export function App() { ); return ( - { if (selectedIds?.length) { manager.actions.initalize(selectedIds); @@ -202,7 +202,7 @@ export function App() { ))} - + ); } @@ -316,7 +316,7 @@ export function Resizeable({disabled, onResize}: ResizeableProps) { ); - return {handles}; + return {handles}; } type Direction = 'NE' | 'N' | 'NW' | 'SE' | 'S' | 'SW' | 'E' | 'W'; diff --git a/apps/docs/stories/Resizeable/index.ts b/apps/docs/stories/react/Resizeable/index.ts similarity index 100% rename from apps/docs/stories/Resizeable/index.ts rename to apps/docs/stories/react/Resizeable/index.ts diff --git a/apps/docs/stories/react/Sortable/Sortable.stories.tsx b/apps/docs/stories/react/Sortable/Sortable.stories.tsx new file mode 100644 index 00000000..70358d42 --- /dev/null +++ b/apps/docs/stories/react/Sortable/Sortable.stories.tsx @@ -0,0 +1,21 @@ +import type {Meta, StoryObj} from '@storybook/react'; + +import {SortableExample} from './SortableExample'; +import docs from './docs/Docs.mdx'; + +const meta: Meta = { + component: SortableExample, + tags: ['autodocs'], + parameters: { + docs: { + page: docs, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const BasicSetup: Story = { + render: SortableExample, +}; diff --git a/apps/docs/stories/react/Sortable/SortableExample.tsx b/apps/docs/stories/react/Sortable/SortableExample.tsx new file mode 100644 index 00000000..2c4a5801 --- /dev/null +++ b/apps/docs/stories/react/Sortable/SortableExample.tsx @@ -0,0 +1,86 @@ +import React, {useRef, useState} from 'react'; +import type {PropsWithChildren} from 'react'; +import type {UniqueIdentifier} from '@dnd-kit/types'; +import {DragDropProvider, useSortable} from '@dnd-kit/react'; +import {arrayMove} from '@dnd-kit/utilities'; + +import {Item, Handle} from '../components'; +import {createRange, cloneDeep} from '../../utilities'; + +interface Props { + itemCount?: number; +} + +export function SortableExample({itemCount = 15}: Props) { + const [items, setItems] = useState( + createRange(itemCount) + ); + const snapshot = useRef(cloneDeep(items)); + + return ( + { + snapshot.current = cloneDeep(items); + }} + onDragOver={(event) => { + const {source, target} = event.operation; + + if (!source || !target) { + return; + } + + const sourceIndex = items.indexOf(source.id); + const targetIndex = items.indexOf(target.id); + + if (sourceIndex !== targetIndex) { + setItems((items) => arrayMove(items, sourceIndex, targetIndex)); + } + }} + onDragEnd={(event) => { + if (event.canceled) { + setItems(snapshot.current); + } + }} + > +
+ {items.map((id, index) => ( + + ))} +
+
+ ); +} + +interface SortableProps { + id: UniqueIdentifier; + index: number; +} + +function Sortable({id, index}: PropsWithChildren) { + const [element, setElement] = useState(null); + const activatorRef = useRef(null); + + const {isDragSource} = useSortable({ + id, + index, + element, + activator: activatorRef, + }); + + return ( + } + > + {id} + + ); +} diff --git a/apps/docs/stories/react/Sortable/Virtualized/Virtualized.stories.tsx b/apps/docs/stories/react/Sortable/Virtualized/Virtualized.stories.tsx new file mode 100644 index 00000000..862cb4a4 --- /dev/null +++ b/apps/docs/stories/react/Sortable/Virtualized/Virtualized.stories.tsx @@ -0,0 +1,15 @@ +import type {Meta, StoryObj} from '@storybook/react'; + +import {VirtualizedSortableExample} from './VirtualizedSortableExample'; + +const meta: Meta = { + component: VirtualizedSortableExample, +}; + +export default meta; +type Story = StoryObj; + +export const ReactVirtual: Story = { + name: 'react-virtual', + render: VirtualizedSortableExample, +}; diff --git a/apps/docs/stories/react/Sortable/Virtualized/VirtualizedSortableExample.tsx b/apps/docs/stories/react/Sortable/Virtualized/VirtualizedSortableExample.tsx new file mode 100644 index 00000000..02779f95 --- /dev/null +++ b/apps/docs/stories/react/Sortable/Virtualized/VirtualizedSortableExample.tsx @@ -0,0 +1,130 @@ +import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'; +import type {PropsWithChildren} from 'react'; +import type {UniqueIdentifier} from '@dnd-kit/types'; +import {DragDropProvider, useSortable} from '@dnd-kit/react'; +import {arrayMove} from '@dnd-kit/utilities'; +import {useWindowVirtualizer} from '@tanstack/react-virtual'; + +import {Item, Handle} from '../../components'; +import {createRange, cloneDeep} from '../../../utilities'; + +export function VirtualizedSortableExample() { + const [items, setItems] = useState(createRange(1000)); + const snapshot = useRef(cloneDeep(items)); + + const parentRef = React.useRef(null); + const parentOffsetRef = React.useRef(0); + + const virtualizer = useWindowVirtualizer({ + count: items.length, + estimateSize: () => 62, + scrollMargin: parentOffsetRef.current, + getItemKey: (index) => items[index], + }); + const virtualItems = virtualizer.getVirtualItems(); + + useLayoutEffect(() => { + parentOffsetRef.current = parentRef.current?.offsetTop ?? 0; + }, []); + + return ( + { + snapshot.current = cloneDeep(items); + }} + onDragOver={(event) => { + const {source, target} = event.operation; + + if (!source || !target) { + return; + } + + const sourceIndex = items.indexOf(source.id); + const targetIndex = items.indexOf(target.id); + + if (sourceIndex !== targetIndex) { + setItems((items) => arrayMove(items, sourceIndex, targetIndex)); + } + }} + onDragEnd={(event) => { + if (event.canceled) { + setItems(snapshot.current); + } + }} + > +
+
+
+ {virtualItems.map(({key, index}) => { + return ( + + ); + })} +
+
+
+
+ ); +} + +interface SortableProps { + id: UniqueIdentifier; + index: number; +} + +const Sortable = forwardRef>( + function Sortable({id, index}, ref) { + const [element, setElement] = useState(null); + const activatorRef = useRef(null); + + const {isDragSource} = useSortable({ + id, + index, + element, + activator: activatorRef, + }); + + return ( + { + if (typeof ref === 'function') { + ref(el); + } else if (ref) { + ref.current = el; + } + + setElement(el); + }} + shadow={isDragSource} + actions={} + data-index={index} + > + {id} + + ); + } +); diff --git a/apps/docs/stories/react/Sortable/docs/Docs.mdx b/apps/docs/stories/react/Sortable/docs/Docs.mdx new file mode 100644 index 00000000..3f0e7a85 --- /dev/null +++ b/apps/docs/stories/react/Sortable/docs/Docs.mdx @@ -0,0 +1,40 @@ +import {Canvas} from '@storybook/blocks'; + +import {Code, Preview} from '../../components'; +import {BasicSetup} from '../Sortable.stories'; +import BasicSetupSource from '../SortableExample?raw'; +import {Example} from './examples/QuickStart'; +import QuickStartSource from './examples/QuickStart?raw'; +import image from './assets/useSortable.png'; + +# Sortable + +> The sortable plugin provides the building blocks to build sortable interfaces. + + + +## Installation + +To get started, install the `@dnd-kit/react` package. + +## Getting started + + + + + +{QuickStartSource} + +To create a vertical list, we can simply update the CSS value for `flex-direction` to `column`, without any other configuration changes. + + + + + +## Architecture + +The sortable plugin composes the `Draggable` and `Droppable` nodes to provide a simple API for building sortable interfaces. + +The `useSortable` hook is an abstraction that composes the `useDroppable` and `useDraggable` hooks: + + diff --git a/apps/docs/stories/react/Sortable/docs/assets/useSortable.png b/apps/docs/stories/react/Sortable/docs/assets/useSortable.png new file mode 100644 index 0000000000000000000000000000000000000000..a5de498fd3823ba8c4cffdaf87eecb961bdc314e GIT binary patch literal 59544 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 B4ln=! literal 0 HcmV?d00001 diff --git a/apps/docs/stories/react/Sortable/docs/examples/QuickStart.jsx b/apps/docs/stories/react/Sortable/docs/examples/QuickStart.jsx new file mode 100644 index 00000000..6fcc1929 --- /dev/null +++ b/apps/docs/stories/react/Sortable/docs/examples/QuickStart.jsx @@ -0,0 +1,37 @@ +import React, {useState} from 'react'; +import {DragDropProvider, useSortable} from '@dnd-kit/react'; +import {arrayMove} from '@dnd-kit/utilities'; + +export function Example({vertical}) { + const [items, setItems] = useState([0, 1, 2, 3]); + + return ( + { + const {source, target} = event.operation; + + if (source && target && source.id !== target.id) { + setItems((items) => + arrayMove(items, items.indexOf(source.id), items.indexOf(target.id)) + ); + } + }} + > +
+ {items.map((id, index) => ( + + ))} +
+
+ ); +} + +function Sortable({id, index}) { + const {ref} = useSortable({id, index}); + + return ( + + ); +} diff --git a/apps/docs/stories/components/Action/Action.module.css b/apps/docs/stories/react/components/Action/Action.module.css similarity index 98% rename from apps/docs/stories/components/Action/Action.module.css rename to apps/docs/stories/react/components/Action/Action.module.css index ebd038ba..acf85914 100644 --- a/apps/docs/stories/components/Action/Action.module.css +++ b/apps/docs/stories/react/components/Action/Action.module.css @@ -36,6 +36,6 @@ fill: #6f7b88; } -.light { +.dark { --background: rgba(255, 255, 255, 0.1); } diff --git a/apps/docs/stories/components/Action/Action.tsx b/apps/docs/stories/react/components/Action/Action.tsx similarity index 69% rename from apps/docs/stories/components/Action/Action.tsx rename to apps/docs/stories/react/components/Action/Action.tsx index 12df4483..50864e18 100644 --- a/apps/docs/stories/components/Action/Action.tsx +++ b/apps/docs/stories/react/components/Action/Action.tsx @@ -1,21 +1,21 @@ import React, {forwardRef, CSSProperties, Ref} from 'react'; -import {classNames} from '../../utilities'; +import {classNames} from '../../../utilities'; import styles from './Action.module.css'; export interface Props extends React.HTMLAttributes { - light?: boolean; + variant?: 'light' | 'dark'; cursor?: CSSProperties['cursor']; } export const Action = forwardRef( - ({className, cursor, style, light, ...props}, ref) => { + ({className, cursor, style, variant = 'light', ...props}, ref) => { return ( + + ); +} + +function syntaxReplacements(value: string) { + const markup = (string, newAttribute = '') => + `${string}`; + const replacements = [ + ['const', 'token', 'const'], + ['null', 'token', 'null'], + ['function', 'token', 'function'], + ['[(]', 'punctuation', 'parentheses opening', '('], + ['[)]', 'punctuation', 'parentheses closing', ')'], + ['{', 'punctuation', 'braces opening'], + ['}', 'punctuation', 'braces closing'], + [';', 'punctuation', 'semicolon'], + ['=>', 'operator', 'arrow-function'], + ]; + + return replacements.reduce( + (accumulator, [value, label, annotation, replacement]) => + accumulator.replace( + new RegExp(markup(value, label), 'g'), + markup(replacement ?? value, `${label} ${annotation}`) + ), + value + ); +} + +const lineNumbers = createRange(50); diff --git a/apps/docs/stories/react/components/Code/components/CodeHighlighter/copy.svg b/apps/docs/stories/react/components/Code/components/CodeHighlighter/copy.svg new file mode 100644 index 00000000..04b3d7f8 --- /dev/null +++ b/apps/docs/stories/react/components/Code/components/CodeHighlighter/copy.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/docs/stories/react/components/Code/components/CodeHighlighter/index.ts b/apps/docs/stories/react/components/Code/components/CodeHighlighter/index.ts new file mode 100644 index 00000000..44b8492c --- /dev/null +++ b/apps/docs/stories/react/components/Code/components/CodeHighlighter/index.ts @@ -0,0 +1 @@ +export {CodeHighlighter} from './CodeHighlighter'; diff --git a/apps/docs/stories/react/components/Code/components/index.ts b/apps/docs/stories/react/components/Code/components/index.ts new file mode 100644 index 00000000..44b8492c --- /dev/null +++ b/apps/docs/stories/react/components/Code/components/index.ts @@ -0,0 +1 @@ +export {CodeHighlighter} from './CodeHighlighter'; diff --git a/apps/docs/stories/react/components/Code/index.ts b/apps/docs/stories/react/components/Code/index.ts new file mode 100644 index 00000000..2807cb65 --- /dev/null +++ b/apps/docs/stories/react/components/Code/index.ts @@ -0,0 +1 @@ +export {Code} from './Code'; diff --git a/apps/docs/stories/components/Dropzone/Dropzone.module.css b/apps/docs/stories/react/components/Dropzone/Dropzone.module.css similarity index 65% rename from apps/docs/stories/components/Dropzone/Dropzone.module.css rename to apps/docs/stories/react/components/Dropzone/Dropzone.module.css index fcb9fee2..ed09ce7d 100644 --- a/apps/docs/stories/components/Dropzone/Dropzone.module.css +++ b/apps/docs/stories/react/components/Dropzone/Dropzone.module.css @@ -15,25 +15,23 @@ box-shadow: inset rgba(201, 211, 219, 0.5) 0 0 0 2px, rgba(255, 255, 255, 0) 0 0 0 1px, rgba(201, 211, 219, 0.25) 20px 14px 24px; transition: box-shadow 250ms ease; +} - > svg { - position: absolute; - left: 50%; - top: 50%; - width: 200px; - transform: translate3d(-50%, -50%, 0); - opacity: 0.8; - transition: opacity 300ms ease, transform 200ms ease; - user-select: none; - pointer-events: none; - } +.Dropzone > svg { + position: absolute; + left: 50%; + top: 50%; + width: 200px; + transform: translate3d(-50%, -50%, 0); + opacity: 0.3; + transition: opacity 300ms ease, transform 200ms ease; + user-select: none; + pointer-events: none; } -.lift { - > svg { - opacity: 0.8; - } +.lift > svg { + opacity: 0.8; } .highlight { @@ -49,9 +47,7 @@ } } -.dropped { - > svg { - opacity: 0.2; - transform: translate3d(-50%, 100%, 0) scale(0.8); - } +.dropped > svg { + opacity: 0.2; + transform: translate3d(-50%, 100%, 0) scale(0.8); } diff --git a/apps/docs/stories/components/Dropzone/Dropzone.tsx b/apps/docs/stories/react/components/Dropzone/Dropzone.tsx similarity index 93% rename from apps/docs/stories/components/Dropzone/Dropzone.tsx rename to apps/docs/stories/react/components/Dropzone/Dropzone.tsx index d9ea7d0b..03eff451 100644 --- a/apps/docs/stories/components/Dropzone/Dropzone.tsx +++ b/apps/docs/stories/react/components/Dropzone/Dropzone.tsx @@ -1,6 +1,6 @@ import React, {forwardRef} from 'react'; -import {classNames} from '../../utilities'; +import {classNames} from '../../../utilities'; import {DroppableIcon} from '../../icons'; import styles from './Dropzone.module.css'; diff --git a/apps/docs/stories/components/Dropzone/index.ts b/apps/docs/stories/react/components/Dropzone/index.ts similarity index 100% rename from apps/docs/stories/components/Dropzone/index.ts rename to apps/docs/stories/react/components/Dropzone/index.ts diff --git a/apps/docs/stories/components/Handle/Handle.tsx b/apps/docs/stories/react/components/Handle/Handle.tsx similarity index 100% rename from apps/docs/stories/components/Handle/Handle.tsx rename to apps/docs/stories/react/components/Handle/Handle.tsx diff --git a/apps/docs/stories/components/Handle/index.ts b/apps/docs/stories/react/components/Handle/index.ts similarity index 100% rename from apps/docs/stories/components/Handle/index.ts rename to apps/docs/stories/react/components/Handle/index.ts diff --git a/apps/docs/stories/react/components/Item/Item.module.css b/apps/docs/stories/react/components/Item/Item.module.css new file mode 100644 index 00000000..c479fdcf --- /dev/null +++ b/apps/docs/stories/react/components/Item/Item.module.css @@ -0,0 +1,55 @@ +.Item { + --box-shadow: 0 0 0 calc(1px / var(--scale-x, 1)) rgba(63, 63, 68, 0.05), 0 1px calc(3px / var(--scale-x, 1)) 0 rgba(34, 33, 81, 0.15); + + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + padding: 10px 20px; + border: none; + gap: 10px; + background-color: #FFFFFF; + border-radius: 8px; + font-size: 14px !important; + font-weight: 600; + color: #333333; + cursor: grab; + outline: none; + transition: background 0.4s ease, box-shadow 0.3s ease, transform 0.25s ease; + min-height: 62px; + box-shadow: var(--box-shadow); + font-family: sans-serif !important; + font-weight: 300; + width: 100%; + max-width: 300px; +} + +.Item:focus-visible:not(.shadow) { + --box-shadow: inset 0 0 0 3px #4c9ffe; + transition: background 0.4s ease; +} + +.shadow { + animation: fadeShadowIn 0.3s ease; + transform: scale(1.03); + --box-shadow: -1px 0 15px 0 rgba(34, 33, 81, 0.01), 0px 15px 15px 0 rgba(34, 33, 81, 0.25); +} + +.hasActions { + padding-right: 10px; +} + +.Item:not(.hasActions) { + touch-action: none; +} + +@keyframes fadeShadowIn { + from { + transform: scale(1); + box-shadow: none; + } + to { + transform: scale(1.03); + box-shadow: var(--box-shadow); + } +} diff --git a/apps/docs/stories/react/components/Item/Item.tsx b/apps/docs/stories/react/components/Item/Item.tsx new file mode 100644 index 00000000..3b3542b2 --- /dev/null +++ b/apps/docs/stories/react/components/Item/Item.tsx @@ -0,0 +1,35 @@ +import React, { + forwardRef, + type HTMLAttributes, + type PropsWithChildren, +} from 'react'; + +import {classNames} from '../../../utilities'; + +import styles from './Item.module.css'; + +export interface Props extends HTMLAttributes { + actions?: React.ReactNode; + shadow?: boolean; +} + +export const Item = forwardRef>( + function Button({actions, children, shadow, ...props}, ref) { + const Element = actions ? 'div' : 'button'; + + return ( + + {children} + {actions} + + ); + } +); diff --git a/apps/docs/stories/react/components/Item/index.ts b/apps/docs/stories/react/components/Item/index.ts new file mode 100644 index 00000000..374108f2 --- /dev/null +++ b/apps/docs/stories/react/components/Item/index.ts @@ -0,0 +1 @@ +export {Item} from './Item'; diff --git a/apps/docs/stories/react/components/Preview/Preview.module.css b/apps/docs/stories/react/components/Preview/Preview.module.css new file mode 100644 index 00000000..f788818a --- /dev/null +++ b/apps/docs/stories/react/components/Preview/Preview.module.css @@ -0,0 +1,10 @@ +.Preview { + max-height: 400px; + overflow-y: auto; + padding: 25px; + margin: 25px 0 40px; + border-radius: 4px; + background: #FFFFFF; + box-shadow: rgba(0, 0, 0, 0.10) 0 1px 3px 0; + border: 1px solid hsla(203, 50%, 30%, 0.15); +} diff --git a/apps/docs/stories/react/components/Preview/Preview.tsx b/apps/docs/stories/react/components/Preview/Preview.tsx new file mode 100644 index 00000000..c45497cf --- /dev/null +++ b/apps/docs/stories/react/components/Preview/Preview.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import {Story, Unstyled} from '@storybook/blocks'; +import {type StoryFn} from '@storybook/react'; + +import styles from './Preview.module.css'; + +interface Props { + of?: StoryFn; + children?: React.ReactNode; +} + +export function Preview({children, of}: Props) { + return ( + +
{children ?? }
+
+ ); +} diff --git a/apps/docs/stories/react/components/Preview/index.ts b/apps/docs/stories/react/components/Preview/index.ts new file mode 100644 index 00000000..f9f8c610 --- /dev/null +++ b/apps/docs/stories/react/components/Preview/index.ts @@ -0,0 +1 @@ +export {Preview} from './Preview'; diff --git a/apps/docs/stories/components/index.ts b/apps/docs/stories/react/components/index.ts similarity index 51% rename from apps/docs/stories/components/index.ts rename to apps/docs/stories/react/components/index.ts index 94267024..eb22044f 100644 --- a/apps/docs/stories/components/index.ts +++ b/apps/docs/stories/react/components/index.ts @@ -1,3 +1,11 @@ export {Button} from './Button'; + +export {Code} from './Code'; + export {Dropzone} from './Dropzone'; + export {Handle} from './Handle'; + +export {Item} from './Item'; + +export {Preview} from './Preview'; diff --git a/apps/docs/stories/icons/DraggableIcon.tsx b/apps/docs/stories/react/icons/DraggableIcon.tsx similarity index 100% rename from apps/docs/stories/icons/DraggableIcon.tsx rename to apps/docs/stories/react/icons/DraggableIcon.tsx diff --git a/apps/docs/stories/icons/DroppableIcon.tsx b/apps/docs/stories/react/icons/DroppableIcon.tsx similarity index 100% rename from apps/docs/stories/icons/DroppableIcon.tsx rename to apps/docs/stories/react/icons/DroppableIcon.tsx diff --git a/apps/docs/stories/icons/index.ts b/apps/docs/stories/react/icons/index.ts similarity index 100% rename from apps/docs/stories/icons/index.ts rename to apps/docs/stories/react/icons/index.ts diff --git a/apps/docs/stories/utilities/createRange.ts b/apps/docs/stories/utilities/createRange.ts new file mode 100644 index 00000000..51f48387 --- /dev/null +++ b/apps/docs/stories/utilities/createRange.ts @@ -0,0 +1,3 @@ +export function createRange(count: number) { + return Array.from(Array(count).keys()); +} diff --git a/apps/docs/stories/utilities/index.ts b/apps/docs/stories/utilities/index.ts index 74480129..669346af 100644 --- a/apps/docs/stories/utilities/index.ts +++ b/apps/docs/stories/utilities/index.ts @@ -1,2 +1,3 @@ export {classNames} from './classnames'; export {cloneDeep} from './cloneDeep'; +export {createRange} from './createRange'; diff --git a/packages/abstract/src/collision/notifier.ts b/packages/abstract/src/collision/notifier.ts index a896b084..648c0f80 100644 --- a/packages/abstract/src/collision/notifier.ts +++ b/packages/abstract/src/collision/notifier.ts @@ -1,4 +1,5 @@ import {effect} from '@dnd-kit/state'; + import {DragDropManager} from '../manager'; import {Plugin} from '../plugins'; @@ -14,7 +15,6 @@ export class CollisionNotifier extends Plugin { monitor.dispatch('collision', { collisions, preventDefault() { - console.log('prevent default'); defaultPrevented = true; }, }); diff --git a/packages/abstract/src/index.ts b/packages/abstract/src/index.ts index bae2d703..97365e13 100644 --- a/packages/abstract/src/index.ts +++ b/packages/abstract/src/index.ts @@ -15,7 +15,7 @@ export type {ModifierConstructor} from './modifiers'; export {Draggable, Droppable} from './nodes'; export type {Data, Node, DraggableInput, DroppableInput} from './nodes'; -export {Plugin, configure, descriptor} from './plugins'; +export {Plugin, PluginRegistry, configure, descriptor} from './plugins'; export type { Plugins, PluginConstructor, diff --git a/packages/abstract/src/manager/manager.ts b/packages/abstract/src/manager/manager.ts index a3dc05d2..9b2aa5e8 100644 --- a/packages/abstract/src/manager/manager.ts +++ b/packages/abstract/src/manager/manager.ts @@ -8,11 +8,16 @@ import { type DragActions, } from './dragOperation'; import {DragDropMonitor} from './monitor'; -import {PluginRegistry, descriptor, type Plugins} from '../plugins'; +import { + PluginRegistry, + descriptor, + type Plugins, + PluginConstructor, +} from '../plugins'; import type {SensorConstructor, Sensors} from '../sensors'; import type {ModifierConstructor} from '../modifiers'; -export interface DragDropConfiguration> { +export interface DragDropConfiguration { plugins: Plugins; sensors: Sensors; modifiers: ModifierConstructor[]; @@ -31,7 +36,10 @@ export class DragDropManager< public dragOperation: DragOperation; public registry: DragDropRegistry; public monitor: DragDropMonitor>; - public plugins: PluginRegistry>; + public plugins: PluginRegistry< + DragDropManager, + PluginConstructor> + >; public sensors: PluginRegistry< DragDropManager, SensorConstructor> @@ -51,9 +59,9 @@ export class DragDropManager< this.registry = registry; this.monitor = monitor; - this.plugins = new PluginRegistry(this); - this.sensors = new PluginRegistry(this); - this.modifiers = new PluginRegistry(this); + this.plugins = new PluginRegistry>(this); + this.sensors = new PluginRegistry>(this); + this.modifiers = new PluginRegistry>(this); const {actions, operation} = DragOperationManager(this); const collisionObserver = new CollisionObserver({ @@ -71,7 +79,7 @@ export class DragDropManager< for (const entry of plugins) { const {plugin, options} = descriptor(entry); - this.plugins.register(plugin, options); + this.plugins.register(plugin as PluginConstructor, options); } for (const entry of sensors) { diff --git a/packages/abstract/src/manager/monitor.ts b/packages/abstract/src/manager/monitor.ts index 9f6a1d2a..c623b22e 100644 --- a/packages/abstract/src/manager/monitor.ts +++ b/packages/abstract/src/manager/monitor.ts @@ -5,15 +5,12 @@ import type {DragOperation} from './dragOperation'; import type {Draggable, Droppable} from '../nodes'; import type {Collisions} from '../collision'; -export type Events = Record; +export type Events = Record void>; class Monitor { - private registry = new Map>(); + private registry = new Map>(); - public addEventListener( - name: U, - handler: (event: T[U]) => void - ) { + public addEventListener(name: U, handler: T[U]) { const {registry} = this; const listeners = new Set(registry.get(name)); @@ -23,7 +20,7 @@ class Monitor { return () => this.removeEventListener(name, handler); } - public removeEventListener(name: keyof T, handler: AnyFunction) { + public removeEventListener(name: keyof T, handler: T[keyof T]) { const {registry} = this; const listeners = new Set(registry.get(name)); @@ -31,7 +28,7 @@ class Monitor { registry.set(name, listeners); } - public dispatch(name: U, event?: T[U], ...args: any[]) { + protected __dispatch(name: U, ...args: Parameters) { const {registry} = this; const listeners = registry.get(name); @@ -40,36 +37,57 @@ class Monitor { } for (const listener of listeners) { - listener(event, ...args); + listener(...args); } } } -export type DragDropEvents = { - collision: {collisions: Collisions; preventDefault(): void}; - dragstart: {}; - dragmove: {}; - dragover: {operation: DragOperation}; - dragend: { - operation: DragOperation; - canceled: boolean; - suspend(): {resume(): void; abort(): void}; - }; +type DragDropEvent< + T extends Draggable, + U extends Droppable, + V extends DragDropManager, +> = (event: Record, manager: V) => void; + +export type DragDropEvents< + T extends Draggable, + U extends Droppable, + V extends DragDropManager, +> = { + collision( + event: {collisions: Collisions; preventDefault(): void}, + manager: V + ): void; + dragstart(event: {}, manager: V): void; + dragmove(event: {}, manager: V): void; + dragover(event: {operation: DragOperation}, manager: V): void; + dragend( + event: { + operation: DragOperation; + canceled: boolean; + suspend(): {resume(): void; abort(): void}; + }, + manager: V + ): void; }; export class DragDropMonitor< T extends Draggable, U extends Droppable, V extends DragDropManager, -> extends Monitor> { +> extends Monitor> { constructor(private manager: V) { super(); } - public dispatch>( + public dispatch>( type: Key, - event: DragDropEvents[Key] + event: Parameters[Key]>[0] ) { - super.dispatch(type, event, this.manager); + const args = [event, this.manager] as any; + + super.__dispatch( + type, + ...(args as Parameters[Key]>) + ); } } diff --git a/packages/abstract/src/manager/registry.ts b/packages/abstract/src/manager/registry.ts index 8638c3ff..72564e69 100644 --- a/packages/abstract/src/manager/registry.ts +++ b/packages/abstract/src/manager/registry.ts @@ -2,7 +2,7 @@ import {signal} from '@dnd-kit/state'; import type {UniqueIdentifier} from '@dnd-kit/types'; import {PubSub} from '@dnd-kit/utilities'; -import {Draggable, Droppable} from '../nodes'; +import {Draggable, Droppable, Node} from '../nodes'; class Registry { private map = signal>(new Map()); @@ -13,11 +13,11 @@ class Registry { } public get(identifier: UniqueIdentifier): T | undefined { - return this.map.value.get(identifier); + return this.map.peek().get(identifier); } public pick(...identifiers: UniqueIdentifier[]): T[] | undefined { - const map = this.map.value; + const map = this.map.peek(); return identifiers.map((identifier) => { const entry = map.get(identifier); @@ -63,25 +63,25 @@ export class DragDropRegistry { public draggable: Registry = new Registry(); public droppable: Registry = new Registry(); - public register(instance: T | U) { + public register(instance: V) { if (instance instanceof Draggable) { - return this.draggable.register(instance.id, instance); + return this.draggable.register(instance.id, instance as any); } if (instance instanceof Droppable) { - return this.droppable.register(instance.id, instance); + return this.droppable.register(instance.id, instance as any); } throw new Error('Invalid instance type'); } - public unregister(instance: T | U) { + public unregister(instance: V) { if (instance instanceof Draggable) { - this.draggable.unregister(instance.id, instance); + this.draggable.unregister(instance.id, instance as any); } if (instance instanceof Droppable) { - this.droppable.unregister(instance.id, instance); + this.droppable.unregister(instance.id, instance as any); } } } diff --git a/packages/abstract/src/nodes/draggable/draggable.ts b/packages/abstract/src/nodes/draggable/draggable.ts index 3cc9454c..ad1c00f8 100644 --- a/packages/abstract/src/nodes/draggable/draggable.ts +++ b/packages/abstract/src/nodes/draggable/draggable.ts @@ -1,6 +1,7 @@ -import {reactive} from '@dnd-kit/state'; +import {derived, reactive} from '@dnd-kit/state'; import type {Type} from '@dnd-kit/types'; +import type {DragDropManager} from '../../manager'; import {Node} from '../node'; import type {NodeInput, Data} from '../node'; @@ -9,12 +10,20 @@ export interface Input extends NodeInput { } export class Draggable extends Node { - constructor({type, ...input}: Input) { - super(input); + constructor( + {type, ...input}: Input, + protected manager: DragDropManager + ) { + super(input, manager); this.type = type; } @reactive public type: Type | undefined; + + @derived + public get isDragSource() { + return this.manager.dragOperation.source?.id === this.id; + } } diff --git a/packages/abstract/src/nodes/droppable/droppable.ts b/packages/abstract/src/nodes/droppable/droppable.ts index ba6c46ef..f9532213 100644 --- a/packages/abstract/src/nodes/droppable/droppable.ts +++ b/packages/abstract/src/nodes/droppable/droppable.ts @@ -1,11 +1,12 @@ import type {Type} from '@dnd-kit/types'; -import {reactive} from '@dnd-kit/state'; +import {derived, reactive} from '@dnd-kit/state'; import type {Shape} from '@dnd-kit/geometry'; import {Node} from '../node'; import type {NodeInput, Data} from '../node'; import type {CollisionDetector} from '../../collision'; +import type {DragDropManager} from '../../manager'; export interface Input extends NodeInput { accept?: Type[]; @@ -13,8 +14,11 @@ export interface Input extends NodeInput { } export class Droppable extends Node { - constructor({accept, collisionDetector, ...input}: Input) { - super(input); + constructor( + {accept, collisionDetector, ...input}: Input, + protected manager: DragDropManager + ) { + super(input, manager); this.accept = accept; this.collisionDetector = collisionDetector; @@ -51,4 +55,9 @@ export class Droppable extends Node { @reactive public shape: Shape | null = null; + + @derived + public get isDropTarget() { + return this.manager.dragOperation.target?.id === this.id; + } } diff --git a/packages/abstract/src/nodes/node/node.ts b/packages/abstract/src/nodes/node/node.ts index e938c40b..2c22232e 100644 --- a/packages/abstract/src/nodes/node/node.ts +++ b/packages/abstract/src/nodes/node/node.ts @@ -1,6 +1,7 @@ -import {reactive} from '@dnd-kit/state'; -import type {UniqueIdentifier} from '@dnd-kit/types'; +import {effect, reactive} from '@dnd-kit/state'; +import type {CleanupFunction, UniqueIdentifier} from '@dnd-kit/types'; +import type {DragDropManager} from '../../manager'; import type {Data} from './types'; export interface Input { @@ -10,12 +11,23 @@ export interface Input { } export class Node { - constructor(input: Input) { + constructor( + input: Input, + protected manager: DragDropManager + ) { const {id, data = null, disabled = false} = input; this.id = id; this.data = data; this.disabled = disabled; + + this.destroy = effect(() => { + // Re-run this effect whenever the `id` changes + const {id: _} = this; + const unregister = manager.registry.register(this); + + return unregister; + }); } @reactive @@ -27,5 +39,5 @@ export class Node { @reactive public disabled: boolean; - public destroy() {} + public destroy: CleanupFunction; } diff --git a/packages/abstract/src/plugins/registry.ts b/packages/abstract/src/plugins/registry.ts index 39666e1c..adf999af 100644 --- a/packages/abstract/src/plugins/registry.ts +++ b/packages/abstract/src/plugins/registry.ts @@ -5,8 +5,8 @@ import type {InferPluginOptions, PluginConstructor} from './types'; export class PluginRegistry< T extends DragDropManager, - W extends PluginConstructor = PluginConstructor, - U extends Plugin = InstanceType, + W extends PluginConstructor = PluginConstructor, + U extends Plugin = InstanceType, > { private instances: Map = new Map(); @@ -16,18 +16,21 @@ export class PluginRegistry< return this.instances.values(); } - public get(plugin: W): U | undefined { + public get(plugin: X): InstanceType | undefined { const instance = this.instances.get(plugin); - return instance; + return instance as any; } - public register(plugin: W, options?: InferPluginOptions): U { + public register( + plugin: X, + options?: InferPluginOptions + ): InstanceType { const instance = new plugin(this.manager, options) as U; this.instances.set(plugin, instance); - return instance; + return instance as InstanceType; } public destroy() { diff --git a/packages/dom-utilities/src/index.ts b/packages/dom-utilities/src/index.ts index 3b6fa0bd..ca468d63 100644 --- a/packages/dom-utilities/src/index.ts +++ b/packages/dom-utilities/src/index.ts @@ -22,3 +22,5 @@ export {scheduler, Scheduler} from './scheduler'; export {InlineStyles} from './styles'; export {supportsViewTransition} from './type-guards'; + +export {inverseTransform} from './transform'; diff --git a/packages/dom-utilities/src/scroll/getScrollableAncestors.ts b/packages/dom-utilities/src/scroll/getScrollableAncestors.ts index 48e21e4a..f23f363e 100644 --- a/packages/dom-utilities/src/scroll/getScrollableAncestors.ts +++ b/packages/dom-utilities/src/scroll/getScrollableAncestors.ts @@ -39,7 +39,11 @@ export function getScrollableAncestors( return scrollParents; } - if (!isHTMLElement(node) || isSVGElement(node)) { + if (!isHTMLElement(node)) { + if (isSVGElement(node)) { + return findScrollableAncestors(node.parentElement); + } + return scrollParents; } diff --git a/packages/dom/src/index.ts b/packages/dom/src/index.ts index f1ae983b..5c2f0366 100644 --- a/packages/dom/src/index.ts +++ b/packages/dom/src/index.ts @@ -1,7 +1,17 @@ -export {DragDropManager} from './manager'; +export {DragDropManager, defaultPreset} from './manager'; export type {DragDropManagerInput} from './manager'; export {Draggable, Droppable} from './nodes'; -export type {DraggableInput, DroppableInput} from './nodes'; +export type {DraggableInput, DraggableFeedback, DroppableInput} from './nodes'; export {DragSensor, PointerSensor, KeyboardSensor} from './sensors'; +export type {Sensors} from './sensors'; + +export { + AutoScroller, + PlaceholderFeedback, + CloneFeedback, + Debug, + Sortable, +} from './plugins'; +export type {SortableInput} from './plugins'; diff --git a/packages/dom/src/manager/index.ts b/packages/dom/src/manager/index.ts index bdebd390..67de9918 100644 --- a/packages/dom/src/manager/index.ts +++ b/packages/dom/src/manager/index.ts @@ -1,2 +1,2 @@ -export {DragDropManager} from './manager'; +export {DragDropManager, defaultPreset} from './manager'; export type {Input as DragDropManagerInput} from './manager'; diff --git a/packages/dom/src/manager/manager.ts b/packages/dom/src/manager/manager.ts index e44eae11..49392e79 100644 --- a/packages/dom/src/manager/manager.ts +++ b/packages/dom/src/manager/manager.ts @@ -15,51 +15,52 @@ import { RestoreFocus, ScrollManager, Scroller, + Debug, } from '../plugins'; import {KeyboardSensor, PointerSensor} from '../sensors'; import {DragSourceDeltaModifier} from '../modifiers'; -export interface Input extends DragDropManagerInput {} +export interface Input extends DragDropManagerInput {} -const defaultPlugins: Plugins = [ - AutoScroller, - CloneFeedback, - PlaceholderFeedback, - RestoreFocus, -]; - -const defaultSensors: Sensors = [ - configure(PointerSensor, { - activationConstraints: { - delay: {value: 200, tolerance: 10}, - distance: {value: 5}, - }, - }), - KeyboardSensor, -]; +export const defaultPreset: { + plugins: Plugins; + sensors: Sensors; +} = { + plugins: [ + // Debug, + AutoScroller, + CloneFeedback, + PlaceholderFeedback, + RestoreFocus, + ], + sensors: [ + configure(PointerSensor, { + activationConstraints: { + delay: {value: 200, tolerance: 10}, + distance: {value: 5}, + }, + }), + KeyboardSensor, + ], +}; export class DragDropManager< T extends Draggable = Draggable, U extends Droppable = Droppable, > extends AbstractDragDropManager { - public scroller: Scroller; - constructor({ - plugins = defaultPlugins, - sensors = defaultSensors, + plugins = defaultPreset.plugins, + sensors = defaultPreset.sensors, modifiers = [], ...input }: Input = {}) { super({ ...input, - plugins, + plugins: [ScrollManager, Scroller, ...plugins], sensors, modifiers: [DragSourceDeltaModifier, ...modifiers], }); - const scrollManager = new ScrollManager(this); - this.scroller = new Scroller(this); - const effectCleanup = effect(() => { if (this.dragOperation.status === 'initializing') { batch(() => { @@ -70,17 +71,9 @@ export class DragDropManager< } }); - const {destroy} = this; - this.destroy = () => { effectCleanup(); - scrollManager.destroy(); - destroy(); + super.destroy(); }; } - - public destroy = () => { - super.destroy(); - this.scroller.destroy(); - }; } diff --git a/packages/dom/src/nodes/draggable/draggable.ts b/packages/dom/src/nodes/draggable/draggable.ts index f2392fb5..576271c6 100644 --- a/packages/dom/src/nodes/draggable/draggable.ts +++ b/packages/dom/src/nodes/draggable/draggable.ts @@ -1,13 +1,27 @@ import { Draggable as AbstractDraggable, + Sensor, + descriptor, +} from '@dnd-kit/abstract'; +import type { + Data, DraggableInput, + DragDropManager as AbstractDragDropManager, + PluginConstructor, } from '@dnd-kit/abstract'; -import type {Data} from '@dnd-kit/abstract'; -import {reactive} from '@dnd-kit/state'; +import {effect, reactive} from '@dnd-kit/state'; + +import {CloneFeedback} from '../../plugins'; +import type {Sensors} from '../../sensors'; -export interface Input extends DraggableInput {} +export interface Input extends DraggableInput { + activator?: Element; + element?: Element; + feedback?: DraggableFeedback; + sensors?: Sensors; +} -export type DraggableFeedback = 'none' | 'clone' | 'move' | 'placeholder'; +export type DraggableFeedback = PluginConstructor | null; export class Draggable extends AbstractDraggable { @reactive @@ -17,9 +31,46 @@ export class Draggable extends AbstractDraggable { public element: Element | undefined; @reactive - public feedback: string = 'placeholder'; + public feedback: DraggableFeedback; + + @reactive + public sensors: Sensors | undefined; + + constructor( + {activator, element, feedback = CloneFeedback, sensors, ...input}: Input, + protected manager: AbstractDragDropManager + ) { + super(input, manager); + + this.activator = activator; + this.element = element; + this.feedback = feedback; + this.sensors = sensors; + + const effectCleanup = effect(() => { + const sensors = this.sensors?.map(descriptor) ?? [...manager.sensors]; + const unbindFunctions = sensors.map((entry) => { + const sensorInstance = + entry instanceof Sensor + ? entry + : manager.sensors.get(entry.plugin) ?? + manager.sensors.register(entry.plugin); + const options = entry instanceof Sensor ? undefined : entry.options; + + const unbind = sensorInstance.bind(this, options); + return unbind; + }); + + return function cleanup() { + unbindFunctions.forEach((unbind) => unbind()); + }; + }); + + const {destroy} = this; - constructor(input: Input) { - super(input); + this.destroy = () => { + effectCleanup(); + destroy(); + }; } } diff --git a/packages/dom/src/nodes/draggable/index.ts b/packages/dom/src/nodes/draggable/index.ts index 262de3e6..aa4371ec 100644 --- a/packages/dom/src/nodes/draggable/index.ts +++ b/packages/dom/src/nodes/draggable/index.ts @@ -1,2 +1,2 @@ export {Draggable} from './draggable'; -export type {Input as DraggableInput} from './draggable'; +export type {Input as DraggableInput, DraggableFeedback} from './draggable'; diff --git a/packages/dom/src/nodes/droppable/droppable.ts b/packages/dom/src/nodes/droppable/droppable.ts index 4e8f53f2..e835c05f 100644 --- a/packages/dom/src/nodes/droppable/droppable.ts +++ b/packages/dom/src/nodes/droppable/droppable.ts @@ -1,15 +1,13 @@ -import { - DragOperationStatus, - Droppable as AbstractDroppable, -} from '@dnd-kit/abstract'; +import {Droppable as AbstractDroppable} from '@dnd-kit/abstract'; import type { Data, DroppableInput as AbstractDroppableInput, + DragDropManager as AbstractDragDropManager, } from '@dnd-kit/abstract'; -import {Shape} from '@dnd-kit/geometry'; import {defaultCollisionDetection} from '@dnd-kit/collision'; import type {CollisionDetector} from '@dnd-kit/collision'; import {effect, reactive} from '@dnd-kit/state'; +import type {Shape} from '@dnd-kit/geometry'; import {DOMRectangle} from '../../shapes'; @@ -18,36 +16,51 @@ type OptionalInput = 'collisionDetector'; export interface Input extends Omit, OptionalInput> { collisionDetector?: CollisionDetector; - shape?: Shape; + element?: Element; + ignoreTransform?: boolean; } export class Droppable extends AbstractDroppable { @reactive public element: Element | undefined; - constructor({ - collisionDetector = defaultCollisionDetection, - ...input - }: Input) { - super({...input, collisionDetector}); + @reactive + public ignoreTransform: boolean; + + constructor( + { + collisionDetector = defaultCollisionDetection, + element, + ignoreTransform = false, + ...input + }: Input, + protected manager: AbstractDragDropManager + ) { + super({...input, collisionDetector}, manager); + this.element = element; + this.ignoreTransform = ignoreTransform; + this.updateShape = this.updateShape.bind(this); this.destroy = effect(this.updateShape); } - public updateShape = () => { + public updateShape(): Shape | null { const {disabled, element} = this; if (!element || disabled) { this.shape = null; - return; + return null; } - const updatedShape = new DOMRectangle(element); + const {shape} = this; + const updatedShape = new DOMRectangle(element, this.ignoreTransform); - if (this.shape?.equals(updatedShape)) { - return; + if (shape?.equals(updatedShape)) { + return shape; } this.shape = updatedShape; - }; + + return updatedShape; + } } diff --git a/packages/dom/src/plugins/debug/debug.ts b/packages/dom/src/plugins/debug/debug.ts new file mode 100644 index 00000000..00a26b67 --- /dev/null +++ b/packages/dom/src/plugins/debug/debug.ts @@ -0,0 +1,104 @@ +import {effect} from '@dnd-kit/state'; +import {Plugin, type Node} from '@dnd-kit/abstract'; + +import {DragDropManager} from '../../manager'; + +export class Debug extends Plugin { + constructor(manager: DragDropManager) { + super(manager); + + const elements = new Map(); + + this.destroy = effect(() => { + const {dragOperation} = manager; + const {status} = dragOperation; + const isDragging = status === 'dragging'; + + if (!isDragging) { + return () => { + for (const element of elements.values()) { + element.remove(); + } + + elements.clear(); + }; + } + + const draggable = dragOperation.source; + + if (draggable && dragOperation.shape) { + const draggableElement = elements.get(draggable); + const element = draggableElement ?? document.createElement('dialog'); + const {boundingRectangle} = dragOperation.shape; + + if (!draggableElement) { + elements.set(draggable, element); + + const style = document.createElement('style'); + style.innerText = `dialog[data-dnd-kit-debug]::backdrop {display: none;}`; + + element.innerText = `${draggable.id}`; + element.setAttribute('data-dnd-kit-debug', ''); + element.appendChild(style); + element.style.all = 'initial'; + element.style.position = 'fixed'; + element.style.display = 'flex'; + element.style.alignItems = 'center'; + element.style.justifyContent = 'center'; + element.style.backgroundColor = 'rgba(118, 190, 250, 0.5)'; + element.style.border = '1px solid rgba(0, 0, 0, 0.1)'; + element.style.boxSizing = 'border-box'; + element.style.pointerEvents = 'none'; + element.style.color = 'rgba(0,0,0,0.9)'; + element.style.textShadow = '0 0 3px rgba(255,255,255,0.8)'; + element.style.fontFamily = 'sans-serif'; + + document.body.appendChild(element); + + if (element instanceof HTMLDialogElement) { + element.showModal(); + } + } + + element.style.top = `${boundingRectangle.top}px`; + element.style.left = `${boundingRectangle.left}px`; + element.style.width = `${boundingRectangle.width}px`; + element.style.height = `${boundingRectangle.height}px`; + } + + for (const droppable of manager.registry.droppable) { + const element = elements.get(droppable); + + if (droppable.shape) { + const {boundingRectangle} = droppable.shape; + const debugElement = element ?? document.createElement('div'); + + if (!element) { + elements.set(droppable, debugElement); + debugElement.style.position = 'fixed'; + debugElement.style.display = 'flex'; + debugElement.style.alignItems = 'center'; + debugElement.style.justifyContent = 'center'; + debugElement.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'; + debugElement.style.border = '1px solid rgba(0, 0, 0, 0.1)'; + debugElement.style.boxSizing = 'border-box'; + debugElement.style.pointerEvents = 'none'; + debugElement.style.zIndex = '9999'; + debugElement.style.color = 'rgba(0,0,0,0.5)'; + debugElement.style.fontFamily = 'sans-serif'; + document.body.appendChild(debugElement); + } + + debugElement.style.top = `${boundingRectangle.top}px`; + debugElement.style.left = `${boundingRectangle.left}px`; + debugElement.style.width = `${boundingRectangle.width}px`; + debugElement.style.height = `${boundingRectangle.height}px`; + debugElement.innerText = `${droppable.id}`; + } else if (element) { + element.remove(); + elements.delete(droppable); + } + } + }); + } +} diff --git a/packages/dom/src/plugins/debug/index.ts b/packages/dom/src/plugins/debug/index.ts new file mode 100644 index 00000000..b1cf078e --- /dev/null +++ b/packages/dom/src/plugins/debug/index.ts @@ -0,0 +1 @@ +export {Debug} from './debug'; diff --git a/packages/dom/src/plugins/feedback/CloneFeedback.ts b/packages/dom/src/plugins/feedback/CloneFeedback.ts index 7df073a9..c27c515d 100644 --- a/packages/dom/src/plugins/feedback/CloneFeedback.ts +++ b/packages/dom/src/plugins/feedback/CloneFeedback.ts @@ -25,7 +25,7 @@ export class CloneFeedback extends Plugin { !isDragging || !source || !source.element || - source.feedback !== 'clone' + source.feedback !== CloneFeedback ) { return; } @@ -33,13 +33,21 @@ export class CloneFeedback extends Plugin { const {element} = source; const {boundingRectangle} = new DOMRectangle(element); const overlay = createOverlay(manager, boundingRectangle); - const clonedElement = cloneElement(element); + const clonedElement = cloneElement(element); overlay.appendChild(clonedElement); overlay.appendTo(document.body); + if (element instanceof HTMLElement) { + element.style.visibility = 'hidden'; + } + return () => { overlay.remove(); + + if (element instanceof HTMLElement) { + element.style.visibility = ''; + } }; }); } diff --git a/packages/dom/src/plugins/feedback/Overlay.ts b/packages/dom/src/plugins/feedback/Overlay.ts index 1b608eaa..f82f31a4 100644 --- a/packages/dom/src/plugins/feedback/Overlay.ts +++ b/packages/dom/src/plugins/feedback/Overlay.ts @@ -33,7 +33,7 @@ class Overlay { const style = document.createElement('style'); element.style.setProperty('all', 'initial'); - element.style.pointerEvents = 'none'; + element.style.setProperty('pointer-events', 'none'); element.style.setProperty('position', 'fixed'); element.style.setProperty('top', `${top}px`); element.style.setProperty('left', `${left}px`); diff --git a/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts b/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts index 7cd346f8..8f2517aa 100644 --- a/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts +++ b/packages/dom/src/plugins/feedback/PlaceholderFeedback.ts @@ -1,6 +1,6 @@ import {Plugin} from '@dnd-kit/abstract'; import type {CleanupFunction} from '@dnd-kit/types'; -import {effect} from '@dnd-kit/state'; +import {effect, untracked} from '@dnd-kit/state'; import {cloneElement} from '@dnd-kit/dom-utilities'; import type {DragDropManager} from '../../manager'; @@ -25,11 +25,12 @@ export class PlaceholderFeedback extends Plugin { !isDragging || !source || !source.element || - source.feedback !== 'placeholder' + source.feedback !== PlaceholderFeedback ) { return; } + const cleanupFns: CleanupFunction[] = []; const {element} = source; const {boundingRectangle} = new DOMRectangle(element); const overlay = createOverlay(manager, boundingRectangle); @@ -38,15 +39,106 @@ export class PlaceholderFeedback extends Plugin { placeholder.style.width = `${boundingRectangle.width}px`; placeholder.style.height = `${boundingRectangle.height}px`; + const {parentElement} = element; + element.replaceWith(placeholder); overlay.appendChild(element); overlay.appendTo(document.body); + let ignoreNextMutation = false; + + const {id} = source; + + if (parentElement) { + const {insertBefore, appendChild} = parentElement; + + parentElement.appendChild = ((node: Node) => { + try { + if (node === element) { + return appendChild.call(parentElement, placeholder); + } + + return appendChild.call(parentElement, node); + } catch (error) { + // no-op + console.error(error, node); + } + }) as Node['appendChild']; + + parentElement.insertBefore = (( + node: Node, + referenceNode: Node | null + ) => { + try { + if (node === element) { + insertBefore.call(parentElement, placeholder, referenceNode); + return; + } + + if (referenceNode === element) { + insertBefore.call(parentElement, node, placeholder); + return; + } + + return insertBefore.call(parentElement, node, referenceNode); + } catch (error) { + // no-op + console.error(error, node, referenceNode); + } + }) as Node['insertBefore']; + + cleanupFns.push(() => { + parentElement.appendChild = appendChild; + parentElement.insertBefore = insertBefore; + }); + } + + const placeholderReplacement = () => { + const droppable = manager.registry.droppable.get(id); + + if (droppable && droppable.element === source.element) { + droppable.element = placeholder; + } + }; + + untracked(placeholderReplacement); + + const mutationObserver = new MutationObserver((mutations) => { + if (ignoreNextMutation) { + ignoreNextMutation = false; + return; + } + + for (const mutation of mutations) { + if (Array.from(mutation.addedNodes).includes(element)) { + ignoreNextMutation = true; + + console.log(mutation); + + element.replaceWith(placeholder); + overlay.appendChild(element); + + placeholderReplacement(); + } + } + }); + + mutationObserver.observe(document, {childList: true, subtree: true}); + return () => { + mutationObserver.disconnect(); + cleanupFns.forEach((cleanup) => cleanup()); + const clone = cloneElement(element); element.replaceWith(clone); placeholder.replaceWith(element); overlay.remove(); + + const droppable = manager.registry.droppable.get(id); + + if (droppable && droppable.element === placeholder) { + droppable.element = element; + } }; }); } diff --git a/packages/dom/src/plugins/index.ts b/packages/dom/src/plugins/index.ts index 400c42e3..66b5ceef 100644 --- a/packages/dom/src/plugins/index.ts +++ b/packages/dom/src/plugins/index.ts @@ -1,3 +1,10 @@ +export {Debug} from './debug'; + export {CloneFeedback, PlaceholderFeedback} from './feedback'; + export {RestoreFocus} from './focus'; + export {AutoScroller, Scroller, ScrollManager} from './scrolling'; + +export {Sortable} from './sortable'; +export type {SortableInput} from './sortable'; diff --git a/packages/dom/src/plugins/scrolling/AutoScroller.ts b/packages/dom/src/plugins/scrolling/AutoScroller.ts index 85488a98..b4b3b1c5 100644 --- a/packages/dom/src/plugins/scrolling/AutoScroller.ts +++ b/packages/dom/src/plugins/scrolling/AutoScroller.ts @@ -3,6 +3,7 @@ import type {CleanupFunction} from '@dnd-kit/types'; import {effect} from '@dnd-kit/state'; import type {DragDropManager} from '../../manager'; +import {Scroller} from './Scroller'; interface Options {} @@ -14,6 +15,12 @@ export class AutoScroller extends Plugin { constructor(manager: DragDropManager, _options?: Options) { super(manager); + const scroller = manager.plugins.get(Scroller); + + if (!scroller) { + throw new Error('AutoScroller plugin depends on Scroller plugin'); + } + this.destroy = effect(() => { if (this.disabled) { return; @@ -24,13 +31,10 @@ export class AutoScroller extends Plugin { const {position: _, status} = manager.dragOperation; if (status === 'dragging') { - const canScroll = manager.scroller.scroll(); + const canScroll = scroller.scroll(); if (canScroll) { - const interval = setInterval( - manager.scroller.scroll, - AUTOSCROLL_INTERVAL - ); + const interval = setInterval(scroller.scroll, AUTOSCROLL_INTERVAL); return () => { clearInterval(interval); diff --git a/packages/dom/src/plugins/scrolling/Scroller.ts b/packages/dom/src/plugins/scrolling/Scroller.ts index 0e9ffda6..831c1b18 100644 --- a/packages/dom/src/plugins/scrolling/Scroller.ts +++ b/packages/dom/src/plugins/scrolling/Scroller.ts @@ -38,12 +38,22 @@ export class Scroller extends Plugin { const scrollableElements = computed(() => { const element = elementFromPoint.value; + if (!element || element === document.documentElement) { + const targetElement = manager.dragOperation.target?.element; + + if (targetElement) { + return getScrollableAncestors(targetElement, {excludeElement: false}); + } + } + return element ? getScrollableAncestors(element, {excludeElement: false}) : null; }, isEqual); - this.getScrollableElements = () => scrollableElements.value; + this.getScrollableElements = () => { + return scrollableElements.value; + }; this.scrollIntentTracker = new ScrollIntentTracker(manager); } diff --git a/packages/dom/src/plugins/sortable/index.ts b/packages/dom/src/plugins/sortable/index.ts new file mode 100644 index 00000000..ccf5f89f --- /dev/null +++ b/packages/dom/src/plugins/sortable/index.ts @@ -0,0 +1,2 @@ +export {Sortable} from './sortable'; +export type {SortableInput} from './sortable'; diff --git a/packages/dom/src/plugins/sortable/sortable.ts b/packages/dom/src/plugins/sortable/sortable.ts new file mode 100644 index 00000000..d5262d29 --- /dev/null +++ b/packages/dom/src/plugins/sortable/sortable.ts @@ -0,0 +1,151 @@ +import type { + Data, + DragDropManager as AbstractDragDropManager, +} from '@dnd-kit/abstract'; +import {effect, reactive, untracked} from '@dnd-kit/state'; +import type {Type, UniqueIdentifier} from '@dnd-kit/types'; + +import {Draggable, Droppable} from '../../nodes'; +import type { + DraggableInput, + DraggableFeedback, + DroppableInput, +} from '../../nodes'; +import type {Sensors} from '../../sensors'; +import {DOMRectangle} from '../../shapes'; + +export interface SortableInput + extends DraggableInput, + DroppableInput { + index: number; +} + +export class Sortable { + protected draggable: Draggable; + protected droppable: Droppable; + + @reactive + index: number; + + constructor( + {index, ...input}: SortableInput, + protected manager: AbstractDragDropManager + ) { + this.draggable = new Draggable(input, manager); + this.droppable = new Droppable({...input, ignoreTransform: true}, manager); + + let previousIndex = index; + + this.index = index; + + const {destroy} = this; + + const animate = () => { + const {shape, element} = this.droppable; + + if (!shape || !element) { + return; + } + + this.droppable.updateShape(); + const updatedShape = this.droppable.shape; + + if (!updatedShape) { + return; + } + + const delta = { + x: shape.boundingRectangle.left - updatedShape.boundingRectangle.left, + y: shape.boundingRectangle.top - updatedShape.boundingRectangle.top, + }; + + if (delta.x || delta.y) { + element.animate( + { + transform: [ + `translate3d(${delta.x}px, ${delta.y}px, 0)`, + 'translate3d(0, 0, 0)', + ], + }, + {duration: 150, easing: 'ease'} + ); + } + }; + + const effectCleanup = effect(() => { + const {index} = this; + + // Re-run this effect whenever the index changes + if (index === previousIndex) { + return; + } + + previousIndex = index; + + untracked(animate); + }); + + this.destroy = () => { + destroy.bind(this)(); + effectCleanup(); + }; + } + + public get disabled() { + return this.draggable.disabled && this.droppable.disabled; + } + + public set feedback(value: DraggableFeedback) { + this.draggable.feedback = value; + } + + public set disabled(value: boolean) { + this.draggable.disabled = value; + this.droppable.disabled = value; + } + + public set activator(activator: Element | undefined) { + this.draggable.activator = activator; + } + + public set element(element: Element | undefined) { + this.draggable.element = element; + this.droppable.element = element; + } + + public set id(id: UniqueIdentifier) { + this.draggable.id = id; + this.droppable.id = id; + } + + public set sensors(value: Sensors | undefined) { + this.draggable.sensors = value; + } + + public set type(type: Type | undefined) { + this.draggable.type = type; + } + + public set accept(value: Type[] | undefined) { + this.droppable.accept = value; + } + + public get isDropTarget() { + return this.droppable.isDropTarget; + } + + public get isDragSource() { + return this.draggable.isDragSource; + } + + public accepts(types: Type | Type[]) { + return this.droppable.accepts(types); + } + + public destroy() { + this.draggable.destroy(); + this.droppable.destroy(); + } +} + +function noop() {} diff --git a/packages/dom/src/sensors/index.ts b/packages/dom/src/sensors/index.ts index 35a83a02..ab989cb9 100644 --- a/packages/dom/src/sensors/index.ts +++ b/packages/dom/src/sensors/index.ts @@ -3,3 +3,5 @@ export {DragSensor} from './drag'; export {KeyboardSensor} from './keyboard'; export {PointerSensor} from './pointer'; + +export type {Sensors} from './types'; diff --git a/packages/dom/src/sensors/keyboard/KeyboardSensor.ts b/packages/dom/src/sensors/keyboard/KeyboardSensor.ts index bd471e14..3b817330 100644 --- a/packages/dom/src/sensors/keyboard/KeyboardSensor.ts +++ b/packages/dom/src/sensors/keyboard/KeyboardSensor.ts @@ -5,7 +5,7 @@ import {Listeners, getOwnerDocument} from '@dnd-kit/dom-utilities'; import type {DragDropManager} from '../../manager'; import type {Draggable} from '../../nodes'; -import {AutoScroller} from '../../plugins'; +import {AutoScroller, Scroller} from '../../plugins'; import {DOMRectangle} from '../../shapes'; export type KeyCode = KeyboardEvent['code']; @@ -159,7 +159,9 @@ export class KeyboardSensor extends Sensor< if (offset.x || offset.y) { event.preventDefault(); - if (!this.manager.scroller.scroll({by: offset})) { + const scroller = this.manager.plugins.get(Scroller); + + if (!scroller?.scroll({by: offset})) { this.manager.actions.move({ coordinates: { x: center.x + offset.x, diff --git a/packages/dom/src/sensors/pointer/PointerSensor.ts b/packages/dom/src/sensors/pointer/PointerSensor.ts index d29f7b2d..109b474f 100644 --- a/packages/dom/src/sensors/pointer/PointerSensor.ts +++ b/packages/dom/src/sensors/pointer/PointerSensor.ts @@ -150,6 +150,11 @@ export class PointerSensor extends Sensor< passive: false, }, }, + { + // Cancel activation if there is a competing Drag and Drop interaction + type: 'dragstart', + listener: this.handleCancel.bind(this), + }, { type: 'keydown', listener: this.handleKeyDown.bind(this), diff --git a/packages/dom/src/sensors/types.ts b/packages/dom/src/sensors/types.ts new file mode 100644 index 00000000..3e6b5fd4 --- /dev/null +++ b/packages/dom/src/sensors/types.ts @@ -0,0 +1,5 @@ +import type {Sensors as AbstractSensors} from '@dnd-kit/abstract'; + +import type {DragDropManager} from '../manager'; + +export type Sensors = AbstractSensors; diff --git a/packages/dom/src/shapes/DOMRectangle.ts b/packages/dom/src/shapes/DOMRectangle.ts index f3992f24..e8ff5363 100644 --- a/packages/dom/src/shapes/DOMRectangle.ts +++ b/packages/dom/src/shapes/DOMRectangle.ts @@ -1,9 +1,32 @@ import {Rectangle} from '@dnd-kit/geometry'; -import {getBoundingRectangle} from '@dnd-kit/dom-utilities'; +import { + getWindow, + getBoundingRectangle, + inverseTransform, +} from '@dnd-kit/dom-utilities'; export class DOMRectangle extends Rectangle { - constructor(element: Element) { - const {top, left, width, height} = getBoundingRectangle(element); + constructor(element: Element, ignoreTransforms = false) { + let {top, left, right, bottom, width, height} = + getBoundingRectangle(element); + + if (ignoreTransforms) { + const {transform, transformOrigin} = + getWindow(element).getComputedStyle(element); + + if (transform) { + const updated = inverseTransform( + {top, left, right, bottom, width, height}, + transform, + transformOrigin + ); + + top = updated.top; + left = updated.left; + width = updated.width; + height = updated.height; + } + } super(left, top, width, height); diff --git a/packages/react/package.json b/packages/react/package.json index 224057a9..459ecfc8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -20,6 +20,10 @@ "@dnd-kit/dom": "*", "@dnd-kit/state": "*" }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, "devDependencies": { "@dnd-kit/config-ts": "*", "@types/react": "^18.0.9", diff --git a/packages/react/src/context/DndContext.tsx b/packages/react/src/context/DragDropProvider.tsx similarity index 74% rename from packages/react/src/context/DndContext.tsx rename to packages/react/src/context/DragDropProvider.tsx index c8c3d041..0ec34a4c 100644 --- a/packages/react/src/context/DndContext.tsx +++ b/packages/react/src/context/DragDropProvider.tsx @@ -6,16 +6,16 @@ import {useConstant, useEvent} from '../hooks'; import {DragDropContext} from './context'; -type Events = DragDropEvents; +type Events = DragDropEvents; export interface Props { - onCollision?(event: Events['collision'], manager: DragDropManager): void; - onDragStart?(event: Events['dragstart'], manager: DragDropManager): void; - onDragOver?(event: Events['dragover'], manager: DragDropManager): void; - onDragEnd?(event: Events['dragend'], manager: DragDropManager): void; + onCollision?: Events['collision']; + onDragStart?: Events['dragstart']; + onDragOver?: Events['dragover']; + onDragEnd?: Events['dragend']; } -export function DndContext({ +export function DragDropProvider({ children, onCollision, onDragStart, diff --git a/packages/react/src/context/hook.ts b/packages/react/src/context/hook.ts index 5d994be7..d9d41d45 100644 --- a/packages/react/src/context/hook.ts +++ b/packages/react/src/context/hook.ts @@ -2,6 +2,6 @@ import {useContext} from 'react'; import {DragDropContext} from './context'; -export function useDndContext() { +export function useDragDropManager() { return useContext(DragDropContext); } diff --git a/packages/react/src/context/index.ts b/packages/react/src/context/index.ts index 539c861d..2e8a120c 100644 --- a/packages/react/src/context/index.ts +++ b/packages/react/src/context/index.ts @@ -1,2 +1,2 @@ -export {DndContext} from './DndContext'; -export {useDndContext} from './hook'; +export {DragDropProvider} from './DragDropProvider'; +export {useDragDropManager} from './hook'; diff --git a/packages/react/src/draggable/index.ts b/packages/react/src/draggable/index.ts index 667ece84..c52f71f4 100644 --- a/packages/react/src/draggable/index.ts +++ b/packages/react/src/draggable/index.ts @@ -1 +1,2 @@ export {useDraggable} from './useDraggable'; +export type {UseDraggableInput} from './useDraggable'; diff --git a/packages/react/src/draggable/useDraggable.ts b/packages/react/src/draggable/useDraggable.ts index 77360f69..38bfcee2 100644 --- a/packages/react/src/draggable/useDraggable.ts +++ b/packages/react/src/draggable/useDraggable.ts @@ -1,55 +1,45 @@ -import type {Data, Sensors} from '@dnd-kit/abstract'; -import {Draggable} from '@dnd-kit/dom'; -import type {DragDropManager, DraggableInput} from '@dnd-kit/dom'; - -import {useDndContext} from '../context'; -import {useComputed, useConstant, useIsomorphicLayoutEffect} from '../hooks'; -import {useConnectSensors} from '../sensors'; +import {useCallback, useEffect} from 'react'; +import type {Data} from '@dnd-kit/abstract'; +import {CloneFeedback, Draggable} from '@dnd-kit/dom'; +import type {DraggableInput} from '@dnd-kit/dom'; + +import {useDragDropManager} from '../context'; +import { + useComputed, + useConstant, + useIsomorphicLayoutEffect, + useOnValueChange, +} from '../hooks'; import {getCurrentValue, type RefOrValue} from '../utilities'; -import {useEffect} from 'react'; export interface UseDraggableInput - extends DraggableInput { + extends Omit, 'activator' | 'element'> { activator?: RefOrValue; - element: RefOrValue; - sensors?: Sensors; + element?: RefOrValue; } export function useDraggable( input: UseDraggableInput ) { - const manager = useDndContext(); - const {disabled, sensors, id} = input; - const draggable = useConstant(() => new Draggable(input)); + const manager = useDragDropManager(); + const {disabled, id, sensors, feedback = CloneFeedback} = input; const activator = getCurrentValue(input.activator); const element = getCurrentValue(input.element); - const isDragging = useComputed(() => { - const {dragOperation} = manager; - - return dragOperation.source?.id === draggable.id; - }); + const draggable = useConstant( + () => new Draggable({...input, activator, element, feedback: null}, manager) + ); + const isDragSource = useComputed(() => draggable.isDragSource).value; - useIsomorphicLayoutEffect(() => { - draggable.activator = activator; - draggable.element = element; - draggable.disabled = Boolean(disabled); - }, [activator, disabled, element, id]); + useOnValueChange(id, () => (draggable.id = id)); + useOnValueChange(activator, () => (draggable.activator = activator)); + useOnValueChange(element, () => (draggable.element = element)); + useOnValueChange(disabled, () => (draggable.disabled = disabled === true)); + useOnValueChange(sensors, () => (draggable.sensors = sensors)); useIsomorphicLayoutEffect(() => { - const {registry} = manager; - - if (draggable.id !== id) { - draggable.id = id; - } - - const unregister = registry.register(draggable); - - return () => { - unregister(); - }; - }, [manager, id]); - - useConnectSensors(sensors, manager, draggable); + // Wait until React has had a chance to re-render before updating the feedback + draggable.feedback = isDragSource ? feedback ?? null : null; + }, [isDragSource]); useEffect(() => { // Cleanup on unmount @@ -57,8 +47,12 @@ export function useDraggable( }, [draggable]); return { - get isDragging() { - return isDragging.value; - }, + isDragSource, + ref: useCallback( + (element: Element | null) => { + draggable.element = element ?? undefined; + }, + [draggable] + ), }; } diff --git a/packages/react/src/droppable/index.ts b/packages/react/src/droppable/index.ts index 05538598..af24653d 100644 --- a/packages/react/src/droppable/index.ts +++ b/packages/react/src/droppable/index.ts @@ -1 +1,2 @@ export {useDroppable} from './useDroppable'; +export type {UseDroppableInput} from './useDroppable'; diff --git a/packages/react/src/droppable/useDroppable.ts b/packages/react/src/droppable/useDroppable.ts index ea05ea0d..2aee9011 100644 --- a/packages/react/src/droppable/useDroppable.ts +++ b/packages/react/src/droppable/useDroppable.ts @@ -3,35 +3,40 @@ import type {Data} from '@dnd-kit/abstract'; import {Droppable} from '@dnd-kit/dom'; import type {DroppableInput} from '@dnd-kit/dom'; -import {useDndContext} from '../context'; -import {useComputed, useConstant} from '../hooks'; +import {useDragDropManager} from '../context'; +import {useComputed, useConstant, useOnValueChange} from '../hooks'; +import {getCurrentValue, type RefOrValue} from '../utilities'; -export function useDroppable(input: DroppableInput) { - const manager = useDndContext(); - const {disabled} = input; - const {registry} = manager; - const droppable = useConstant(() => new Droppable(input)); - const isDropTarget = useComputed(() => { - const {dragOperation} = manager; +export interface UseDroppableInput + extends Omit, 'element'> { + element?: RefOrValue; +} - return dragOperation.target === droppable; - }); +export function useDroppable( + input: UseDroppableInput +) { + const manager = useDragDropManager(); + const {disabled, id} = input; + const element = getCurrentValue(input.element); + const droppable = useConstant( + () => new Droppable({...input, element}, manager) + ); + const isDisabled = useComputed(() => droppable.disabled); + const isDropTarget = useComputed(() => droppable.isDropTarget); - useEffect(() => { - droppable.disabled = disabled === true; - }, [disabled]); + useOnValueChange(id, () => (droppable.id = id)); + useOnValueChange(element, () => (droppable.element = element)); + useOnValueChange(disabled, () => (droppable.disabled = disabled === true)); useEffect(() => { - const unregister = registry.register(droppable); - - return () => { - unregister(); - droppable.destroy(); - }; + // Cleanup on unmount + return droppable.destroy; }, [droppable]); return { - disabled: droppable.disabled, + get isDisabled() { + return isDisabled.value; + }, get isDropTarget() { return isDropTarget.value; }, diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 11c88e96..c55842c6 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -1,5 +1,6 @@ export {useConstant} from './useConstant'; export {useComputed} from './useComputed'; export {useEvent} from './useEvent'; +export {useOnValueChange} from './useOnValueChange'; export {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect'; export {useSignalEffect} from './useSignalEffect'; diff --git a/packages/react/src/hooks/useOnValueChange.ts b/packages/react/src/hooks/useOnValueChange.ts new file mode 100644 index 00000000..037d9d4f --- /dev/null +++ b/packages/react/src/hooks/useOnValueChange.ts @@ -0,0 +1,16 @@ +import {useRef, useEffect} from 'react'; + +export function useOnValueChange( + value: T, + onChange: (value: T, oldValue: T) => void +) { + const tracked = useRef(value); + + useEffect(() => { + const oldValue = tracked.current; + if (value !== tracked.current) { + tracked.current = value; + onChange(value, oldValue); + } + }, [value, onChange]); +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 1ff5b83d..de5b306d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,4 +1,9 @@ -export {DndContext} from './context'; +export {DragDropProvider} from './context'; + export {useDraggable} from './draggable'; + export {useDroppable} from './droppable'; + +export {useSortable} from './sortable'; + export {useComputed, useEvent, useIsomorphicLayoutEffect} from './hooks'; diff --git a/packages/react/src/sensors/index.ts b/packages/react/src/sensors/index.ts deleted file mode 100644 index e9e87a3b..00000000 --- a/packages/react/src/sensors/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {useConnectSensors} from './useConnectSensors'; diff --git a/packages/react/src/sensors/useConnectSensors.ts b/packages/react/src/sensors/useConnectSensors.ts deleted file mode 100644 index 66a19b4e..00000000 --- a/packages/react/src/sensors/useConnectSensors.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type {DragDropManager, Draggable} from '@dnd-kit/dom'; -import {Sensor, descriptor, type Sensors} from '@dnd-kit/abstract'; -import {useIsomorphicLayoutEffect} from '../hooks'; - -export function useConnectSensors( - localSensors: Sensors | undefined, - manager: DragDropManager, - draggable: Draggable -) { - useIsomorphicLayoutEffect(() => { - const sensors = localSensors?.map(descriptor) ?? [...manager.sensors]; - const unbindFunctions = sensors.map((entry) => { - const sensorInstance = - entry instanceof Sensor - ? entry - : manager.sensors.get(entry.plugin) ?? - manager.sensors.register(entry.plugin); - const options = entry instanceof Sensor ? undefined : entry.options; - - const unbind = sensorInstance.bind(draggable, options); - return unbind; - }); - - return function cleanup() { - unbindFunctions.forEach((unbind) => unbind()); - }; - }, [localSensors, manager, draggable]); -} diff --git a/packages/react/src/sortable/index.ts b/packages/react/src/sortable/index.ts index 54fc0b2c..73f652b3 100644 --- a/packages/react/src/sortable/index.ts +++ b/packages/react/src/sortable/index.ts @@ -1 +1,2 @@ export {useSortable} from './useSortable'; +export type {UseSortableInput} from './useSortable'; diff --git a/packages/react/src/sortable/useSortable.ts b/packages/react/src/sortable/useSortable.ts index 06199734..eb505854 100644 --- a/packages/react/src/sortable/useSortable.ts +++ b/packages/react/src/sortable/useSortable.ts @@ -1,28 +1,66 @@ import {useCallback, useEffect} from 'react'; import type {Data} from '@dnd-kit/abstract'; -import {Droppable} from '@dnd-kit/dom'; -import type {UniqueIdentifier} from '@dnd-kit/types'; -import type {DraggableInput, DroppableInput} from '@dnd-kit/dom'; +import {CloneFeedback, Sortable} from '@dnd-kit/dom'; +import type {SortableInput} from '@dnd-kit/dom'; -import {useDraggable} from '../draggable'; -import {useDroppable} from '../droppable'; +import {useDragDropManager} from '../context'; +import { + useComputed, + useConstant, + useIsomorphicLayoutEffect, + useOnValueChange, +} from '../hooks'; +import {getCurrentValue, type RefOrValue} from '../utilities'; -interface SortableInput { - id: UniqueIdentifier; - type?: string; +export interface UseSortableInput + extends Omit, 'activator' | 'element'> { + activator?: RefOrValue; + element?: RefOrValue; } -export function useSortable(input: SortableInput) { - const draggable = useDraggable(input); - const droppable = useDroppable(input); +export function useSortable(input: UseSortableInput) { + const {id, index, disabled, feedback = CloneFeedback, sensors} = input; + const activator = getCurrentValue(input.activator); + const element = getCurrentValue(input.element); + const manager = useDragDropManager(); + const sortable = useConstant( + () => new Sortable({...input, activator, element, feedback: null}, manager) + ); + + const isDisabled = useComputed(() => sortable.disabled); + const isDropTarget = useComputed(() => sortable.isDropTarget); + const isDragSource = useComputed(() => sortable.isDragSource).value; + + useOnValueChange(id, () => (sortable.id = id)); + useOnValueChange(index, () => (sortable.index = index)); + useOnValueChange(activator, () => (sortable.activator = activator)); + useOnValueChange(element, () => (sortable.element = element)); + useOnValueChange(disabled, () => (sortable.disabled = disabled === true)); + useOnValueChange(sensors, () => (sortable.sensors = sensors)); + + useIsomorphicLayoutEffect(() => { + // Wait until React has had a chance to re-render before updating the feedback + sortable.feedback = isDragSource ? feedback ?? null : null; + }, [isDragSource]); + + useEffect(() => { + // Cleanup on unmount + return sortable.destroy; + }, [sortable]); return { - ...droppable, + get isDisabled() { + return isDisabled.value; + }, + isDragSource, + get isDropTarget() { + return isDropTarget.value; + }, ref: useCallback( (element: Element | null) => { - droppable.element = element ?? undefined; + sortable.element = element ?? undefined; }, - [droppable] + [sortable] ), }; } diff --git a/packages/state/package.json b/packages/state/package.json index 4f683364..593791c9 100644 --- a/packages/state/package.json +++ b/packages/state/package.json @@ -16,7 +16,7 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { - "@preact/signals-core": "^1.3.0" + "@preact/signals-core": "^1.4.0" }, "devDependencies": { "@dnd-kit/config-eslint": "*", diff --git a/packages/state/src/index.ts b/packages/state/src/index.ts index 6e92e061..cfe4a814 100644 --- a/packages/state/src/index.ts +++ b/packages/state/src/index.ts @@ -1,6 +1,7 @@ export { batch, effect, + untracked, signal, Signal, type ReadonlySignal, diff --git a/packages/utilities/src/array/arrayMove.ts b/packages/utilities/src/array/arrayMove.ts new file mode 100644 index 00000000..d0f89b6a --- /dev/null +++ b/packages/utilities/src/array/arrayMove.ts @@ -0,0 +1,13 @@ +/** + * Move an array item to a different position. Returns a new array with the item moved to the new position. + */ +export function arrayMove(array: T[], from: number, to: number): T[] { + const newArray = array.slice(); + newArray.splice( + to < 0 ? newArray.length + to : to, + 0, + newArray.splice(from, 1)[0] + ); + + return newArray; +} diff --git a/packages/utilities/src/array/index.ts b/packages/utilities/src/array/index.ts new file mode 100644 index 00000000..644ffbcf --- /dev/null +++ b/packages/utilities/src/array/index.ts @@ -0,0 +1 @@ +export {arrayMove} from './arrayMove'; diff --git a/packages/utilities/src/index.ts b/packages/utilities/src/index.ts index 2a4e8d7e..d938668e 100644 --- a/packages/utilities/src/index.ts +++ b/packages/utilities/src/index.ts @@ -1,2 +1,5 @@ -export * from './math'; -export * from './other'; +export {arrayMove} from './array'; + +export {add, subtract} from './math'; + +export {debounce, isEqual, PubSub} from './other'; diff --git a/yarn.lock b/yarn.lock index f71f46d1..b4128f8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1886,10 +1886,10 @@ resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@preact/signals-core@^1.3.0": - version "1.3.0" - resolved "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.3.0.tgz" - integrity sha512-M+M3ZOtd1dtV/uasyk4SZu1vbfEJ4NeENv0F7F12nijZYedB5wSgbtZcuACyssnTznhF4ctUyrR0dZHuHfyWKA== +"@preact/signals-core@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@preact/signals-core/-/signals-core-1.4.0.tgz#ff67fca60f144437f3894ab60272eb57cf8cafaf" + integrity sha512-5iYoZBhELLIhUQceZI7sDTQWPb+xcVSn2qk8T/aNl/VMh+A4AiPX9YRSh4XO7fZ6pncrVxl1Iln82poVqYVbbw== "@radix-ui/number@1.0.1": version "1.0.1" @@ -2901,6 +2901,18 @@ "@types/express" "^4.7.0" file-system-cache "2.3.0" +"@tanstack/react-virtual@^3.0.0-beta.54": + version "3.0.0-beta.54" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.54.tgz#755979455adf13f2584937204a3f38703e446037" + integrity sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ== + dependencies: + "@tanstack/virtual-core" "3.0.0-beta.54" + +"@tanstack/virtual-core@3.0.0-beta.54": + version "3.0.0-beta.54" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.54.tgz#12259d007911ad9fce1388385c54a9141f4ecdc4" + integrity sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g== + "@types/babel__core@^7.0.0": version "7.20.1" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz" @@ -4008,6 +4020,15 @@ cli-table3@^0.6.1: optionalDependencies: "@colors/colors" "1.5.0" +clipboard@^2.0.11: + version "2.0.11" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" + integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + clipboardy@2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz" @@ -4379,6 +4400,11 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +delegate@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" + integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== + depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" @@ -5592,6 +5618,13 @@ globby@^11.0.0, globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== + dependencies: + delegate "^3.1.2" + gopd@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" @@ -7300,6 +7333,11 @@ pretty-hrtime@^1.0.3: resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz" integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== +prismjs@^1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" @@ -7864,6 +7902,11 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== + "semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" @@ -8411,6 +8454,11 @@ through2@^2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" +tiny-emitter@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + tiny-invariant@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz"