mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
feat: swith to pragmatic-drag-and-drop for better performance
This commit is contained in:
parent
aee0e936b6
commit
a1d25f7d6f
@ -25,13 +25,14 @@
|
||||
"test": "vitest -c vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.4.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@iconify-json/si": "^1.2.1",
|
||||
"@tanstack/react-query-devtools": "^5.59.9",
|
||||
"@tanstack/react-router": "^1.64.0",
|
||||
"@unocss/reset": "^0.63.4",
|
||||
"ahooks": "^3.8.1",
|
||||
"better-sqlite3": "^11.3.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
|
138
pnpm-lock.yaml
generated
138
pnpm-lock.yaml
generated
@ -17,15 +17,15 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@dnd-kit/sortable':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@18.3.1)
|
||||
'@atlaskit/pragmatic-drag-and-drop':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
'@atlaskit/pragmatic-drag-and-drop-auto-scroll':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
'@atlaskit/pragmatic-drag-and-drop-hitbox':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@iconify-json/si':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
@ -38,6 +38,9 @@ importers:
|
||||
'@unocss/reset':
|
||||
specifier: ^0.63.4
|
||||
version: 0.63.6
|
||||
ahooks:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1(react@18.3.1)
|
||||
better-sqlite3:
|
||||
specifier: ^11.3.0
|
||||
version: 11.5.0
|
||||
@ -244,6 +247,15 @@ packages:
|
||||
peerDependencies:
|
||||
ajv: '>=8'
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop-auto-scroll@1.4.0':
|
||||
resolution: {integrity: sha512-5GoikoTSW13UX76F9TDeWB8x3jbbGlp/Y+3aRkHe1MOBMkrWkwNpJ42MIVhhX/6NSeaZiPumP0KbGJVs2tOWSQ==}
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop-hitbox@1.0.3':
|
||||
resolution: {integrity: sha512-/Sbu/HqN2VGLYBhnsG7SbRNg98XKkbF6L7XDdBi+izRybfaK1FeMfodPpm/xnBHPJzwYMdkE0qtLyv6afhgMUA==}
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop@1.4.0':
|
||||
resolution: {integrity: sha512-qRY3PTJIcxfl/QB8Gwswz+BRvlmgAC5pB+J2hL6dkIxgqAgVwOhAamMUKsrOcFU/axG2Q7RbNs1xfoLKDuhoPg==}
|
||||
|
||||
'@babel/code-frame@7.25.9':
|
||||
resolution: {integrity: sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -801,28 +813,6 @@ packages:
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.0':
|
||||
resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/core@6.1.0':
|
||||
resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/sortable@8.0.0':
|
||||
resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
|
||||
peerDependencies:
|
||||
'@dnd-kit/core': ^6.1.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/utilities@3.2.2':
|
||||
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@es-joy/jsdoccomment@0.48.0':
|
||||
resolution: {integrity: sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==}
|
||||
engines: {node: '>=16'}
|
||||
@ -2634,6 +2624,12 @@ packages:
|
||||
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ahooks@3.8.1:
|
||||
resolution: {integrity: sha512-JoP9+/RWO7MnI/uSKdvQ8WB10Y3oo1PjLv+4Sv4Vpm19Z86VUMdXh+RhWvMGxZZs06sq2p0xVtFk8Oh5ZObsoA==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
@ -2765,6 +2761,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
bind-event-listener@3.0.0:
|
||||
resolution: {integrity: sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==}
|
||||
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
@ -4064,6 +4063,9 @@ packages:
|
||||
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
intersection-observer@0.12.2:
|
||||
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
|
||||
|
||||
invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||
|
||||
@ -4299,6 +4301,10 @@ packages:
|
||||
js-cookie@2.2.1:
|
||||
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
|
||||
|
||||
js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -4983,6 +4989,9 @@ packages:
|
||||
radix3@1.1.2:
|
||||
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
|
||||
|
||||
raf-schd@4.0.3:
|
||||
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
@ -5002,6 +5011,9 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
react-remove-scroll-bar@2.3.6:
|
||||
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
|
||||
engines: {node: '>=10'}
|
||||
@ -6247,6 +6259,22 @@ snapshots:
|
||||
jsonpointer: 5.0.1
|
||||
leven: 3.1.0
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop-auto-scroll@1.4.0':
|
||||
dependencies:
|
||||
'@atlaskit/pragmatic-drag-and-drop': 1.4.0
|
||||
'@babel/runtime': 7.25.9
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop-hitbox@1.0.3':
|
||||
dependencies:
|
||||
'@atlaskit/pragmatic-drag-and-drop': 1.4.0
|
||||
'@babel/runtime': 7.25.9
|
||||
|
||||
'@atlaskit/pragmatic-drag-and-drop@1.4.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.9
|
||||
bind-event-listener: 3.0.0
|
||||
raf-schd: 4.0.3
|
||||
|
||||
'@babel/code-frame@7.25.9':
|
||||
dependencies:
|
||||
'@babel/highlight': 7.25.9
|
||||
@ -6954,31 +6982,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@dnd-kit/accessibility@3.1.0(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
tslib: 2.8.0
|
||||
|
||||
'@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@dnd-kit/accessibility': 3.1.0(react@18.3.1)
|
||||
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
tslib: 2.8.0
|
||||
|
||||
'@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
|
||||
react: 18.3.1
|
||||
tslib: 2.8.0
|
||||
|
||||
'@dnd-kit/utilities@3.2.2(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
tslib: 2.8.0
|
||||
|
||||
'@es-joy/jsdoccomment@0.48.0':
|
||||
dependencies:
|
||||
comment-parser: 1.4.1
|
||||
@ -8694,6 +8697,19 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ahooks@3.8.1(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.9
|
||||
dayjs: 1.11.13(patch_hash=4wu3h3hwxidwuv4ovjl53zavle)
|
||||
intersection-observer: 0.12.2
|
||||
js-cookie: 3.0.5
|
||||
lodash: 4.17.21
|
||||
react: 18.3.1
|
||||
react-fast-compare: 3.2.2
|
||||
resize-observer-polyfill: 1.5.1
|
||||
screenfull: 5.2.0
|
||||
tslib: 2.8.0
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@ -8849,6 +8865,8 @@ snapshots:
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bind-event-listener@3.0.0: {}
|
||||
|
||||
bindings@1.5.0:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
@ -10437,6 +10455,8 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.0.6
|
||||
|
||||
intersection-observer@0.12.2: {}
|
||||
|
||||
invariant@2.2.4:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@ -10641,6 +10661,8 @@ snapshots:
|
||||
|
||||
js-cookie@2.2.1: {}
|
||||
|
||||
js-cookie@3.0.5: {}
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@ -11407,6 +11429,8 @@ snapshots:
|
||||
|
||||
radix3@1.1.2: {}
|
||||
|
||||
raf-schd@4.0.3: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@ -11431,6 +11455,8 @@ snapshots:
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-fast-compare@3.2.2: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { NewsItem, SourceID, SourceResponse } from "@shared/types"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { AnimatePresence, motion, useInView } from "framer-motion"
|
||||
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
||||
import { useWindowSize } from "react-use"
|
||||
import { forwardRef, useImperativeHandle } from "react"
|
||||
import { OverlayScrollbar } from "../common/overlay-scrollbar"
|
||||
@ -12,23 +11,23 @@ export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* 是否显示透明度,拖动时原卡片的样式
|
||||
*/
|
||||
isDragged?: boolean
|
||||
handleListeners?: SyntheticListenerMap
|
||||
isDragging?: boolean
|
||||
setHandleRef?: (ref: HTMLElement | null) => void
|
||||
}
|
||||
|
||||
interface NewsCardProps {
|
||||
id: SourceID
|
||||
handleListeners?: SyntheticListenerMap
|
||||
setHandleRef?: (ref: HTMLElement | null) => void
|
||||
}
|
||||
|
||||
export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragged, handleListeners, style, ...props }, dndRef) => {
|
||||
export const CardWrapper = forwardRef<HTMLElement, ItemsProps>(({ id, isDragging, setHandleRef, style, ...props }, dndRef) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const inView = useInView(ref, {
|
||||
once: true,
|
||||
})
|
||||
|
||||
useImperativeHandle(dndRef, () => ref.current!)
|
||||
useImperativeHandle(dndRef, () => ref.current! as HTMLDivElement)
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -36,7 +35,7 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
||||
className={$(
|
||||
"flex flex-col h-500px rounded-2xl p-4 cursor-default",
|
||||
"backdrop-blur-5 transition-opacity-300",
|
||||
isDragged && "op-50",
|
||||
isDragging && "op-50",
|
||||
`bg-${sources[id].color}-500 dark:bg-${sources[id].color} bg-op-40!`,
|
||||
)}
|
||||
style={{
|
||||
@ -45,12 +44,12 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{inView && <NewsCard id={id} handleListeners={handleListeners} />}
|
||||
{inView && <NewsCard id={id} setHandleRef={setHandleRef} />}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function NewsCard({ id, handleListeners }: NewsCardProps) {
|
||||
function NewsCard({ id, setHandleRef }: NewsCardProps) {
|
||||
const { refresh } = useRefetch()
|
||||
const { data, isFetching, isError } = useQuery({
|
||||
queryKey: ["source", id],
|
||||
@ -141,9 +140,9 @@ function NewsCard({ id, handleListeners }: NewsCardProps) {
|
||||
className={$("btn", isFocused ? "i-ph:star-fill" : "i-ph:star-duotone")}
|
||||
onClick={toggleFocus}
|
||||
/>
|
||||
{handleListeners && (
|
||||
{setHandleRef && (
|
||||
<button
|
||||
{...handleListeners}
|
||||
ref={setHandleRef}
|
||||
type="button"
|
||||
className={$("btn", "i-ph:dots-six-vertical-duotone", "cursor-grab")}
|
||||
/>
|
||||
|
@ -1,26 +1,18 @@
|
||||
import type { PropsWithChildren } from "react"
|
||||
import { useCallback, useState } from "react"
|
||||
import type { DragEndEvent, DragStartEvent } from "@dnd-kit/core"
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
MeasuringStrategy,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
closestCenter,
|
||||
defaultDropAnimationSideEffects,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core"
|
||||
import type { AnimateLayoutChanges } from "@dnd-kit/sortable"
|
||||
import { SortableContext, arrayMove, defaultAnimateLayoutChanges, rectSortingStrategy, useSortable } from "@dnd-kit/sortable"
|
||||
import type { SourceID } from "@shared/types"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { motion } from "framer-motion"
|
||||
import type { BaseEventPayload, ElementDragType } from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types"
|
||||
import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"
|
||||
import { reorderWithEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge"
|
||||
import { createPortal } from "react-dom"
|
||||
import { useThrottleFn } from "ahooks"
|
||||
import { DndContext } from "../common/dnd"
|
||||
import { useSortable } from "../common/dnd/useSortable"
|
||||
import type { ItemsProps } from "./card"
|
||||
import { CardWrapper } from "./card"
|
||||
import { currentSourcesAtom } from "~/atoms"
|
||||
|
||||
const AnimationDuration = 200
|
||||
export function Dnd() {
|
||||
const [items, setItems] = useAtom(currentSourcesAtom)
|
||||
useEntireQuery(items)
|
||||
@ -50,17 +42,17 @@ export function Dnd() {
|
||||
{items.map(id => (
|
||||
<motion.li
|
||||
key={id}
|
||||
layout
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: AnimationDuration / 1000,
|
||||
}}
|
||||
variants={{
|
||||
hidden: {
|
||||
y: 20,
|
||||
opacity: 0,
|
||||
display: "none",
|
||||
},
|
||||
visible: {
|
||||
display: "block",
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
@ -74,65 +66,35 @@ export function Dnd() {
|
||||
)
|
||||
}
|
||||
|
||||
interface DndProps {
|
||||
function DndWrapper({ items, setItems, children }: PropsWithChildren<{
|
||||
items: SourceID[]
|
||||
setItems: (update: SourceID[]) => void
|
||||
}
|
||||
|
||||
function DndWrapper({ items, setItems, children }: PropsWithChildren<DndProps>) {
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor))
|
||||
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string)
|
||||
}, [])
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
const oldIndex = items.indexOf(active.id as any)
|
||||
const newIndex = items.indexOf(over!.id as any)
|
||||
setItems(arrayMove(items, oldIndex, newIndex))
|
||||
}
|
||||
|
||||
setActiveId(null)
|
||||
}, [setItems, items])
|
||||
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setActiveId(null)
|
||||
}, [])
|
||||
|
||||
setItems: (items: SourceID[]) => void
|
||||
}>) {
|
||||
const onDropTargetChange = useCallback(({ location, source }: BaseEventPayload<ElementDragType>) => {
|
||||
const traget = location.current.dropTargets[0]
|
||||
if (!traget?.data || !source?.data) return
|
||||
const closestEdgeOfTarget = extractClosestEdge(traget.data)
|
||||
const fromIndex = items.indexOf(source.data.id as SourceID)
|
||||
const toIndex = items.indexOf(traget.data.id as SourceID)
|
||||
if (fromIndex === toIndex || fromIndex === -1 || toIndex === -1) return
|
||||
const update = reorderWithEdge({
|
||||
list: items,
|
||||
startIndex: fromIndex,
|
||||
indexOfTarget: toIndex,
|
||||
closestEdgeOfTarget,
|
||||
axis: "vertical",
|
||||
})
|
||||
setItems(update)
|
||||
}, [items, setItems])
|
||||
// 避免动画干扰
|
||||
const { run } = useThrottleFn(onDropTargetChange, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
wait: AnimationDuration,
|
||||
})
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
measuring={{
|
||||
droppable: {
|
||||
strategy: MeasuringStrategy.Always,
|
||||
},
|
||||
}}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext items={items} strategy={rectSortingStrategy}>
|
||||
<DndContext onDropTargetChange={run}>
|
||||
{children}
|
||||
</SortableContext>
|
||||
<DragOverlay
|
||||
className="transition-opacity-300"
|
||||
dropAnimation={{
|
||||
easing: "cubic-bezier(0.25, 1, 0.5, 1)",
|
||||
duration: 300,
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
className: {
|
||||
active: "op-100",
|
||||
dragOverlay: "op-0",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{!!activeId && <CardOverlay id={activeId as SourceID} />}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
@ -140,8 +102,9 @@ function DndWrapper({ items, setItems, children }: PropsWithChildren<DndProps>)
|
||||
function CardOverlay({ id }: { id: SourceID }) {
|
||||
return (
|
||||
<div className={$(
|
||||
"flex flex-col rounded-2xl p-4 backdrop-blur-5",
|
||||
"flex flex-col p-4 backdrop-blur-5",
|
||||
`bg-${sources[id].color}-500 dark:bg-${sources[id].color} bg-op-40!`,
|
||||
!isiOS() && "rounded-2xl",
|
||||
)}
|
||||
>
|
||||
<div className={$("flex justify-between mx-2 items-center")}>
|
||||
@ -173,45 +136,29 @@ function CardOverlay({ id }: { id: SourceID }) {
|
||||
)
|
||||
}
|
||||
|
||||
const animateLayoutChanges: AnimateLayoutChanges = (args) => {
|
||||
const { isSorting, wasDragging } = args
|
||||
if (isSorting || wasDragging) {
|
||||
return defaultAnimateLayoutChanges(args)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function SortableCardWrapper({ id, ...props }: ItemsProps) {
|
||||
function SortableCardWrapper({ id }: ItemsProps) {
|
||||
const {
|
||||
isDragging,
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({
|
||||
id,
|
||||
animateLayoutChanges,
|
||||
transition: {
|
||||
duration: 300,
|
||||
easing: "cubic-bezier(0.25, 1, 0.5, 1)",
|
||||
},
|
||||
})
|
||||
setHandleRef,
|
||||
OverlayContainer,
|
||||
} = useSortable({ id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
useEffect(() => {
|
||||
if (OverlayContainer) {
|
||||
OverlayContainer!.className += $(`bg-base`, !isiOS() && "rounded-2xl")
|
||||
}
|
||||
}, [OverlayContainer])
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardWrapper
|
||||
ref={setNodeRef}
|
||||
id={id}
|
||||
style={style}
|
||||
isDragged={isDragging}
|
||||
handleListeners={listeners}
|
||||
{...attributes}
|
||||
{...props}
|
||||
isDragging={isDragging}
|
||||
setHandleRef={setHandleRef}
|
||||
/>
|
||||
{OverlayContainer && createPortal(<CardOverlay id={id} />, OverlayContainer)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
28
src/components/common/dnd/index.tsx
Normal file
28
src/components/common/dnd/index.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"
|
||||
import type { PropsWithChildren } from "react"
|
||||
import type { AllEvents, ElementDragType } from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types"
|
||||
import { InstanceIdContext } from "./useSortable"
|
||||
|
||||
interface ContextProps extends Partial<AllEvents<ElementDragType>> {
|
||||
}
|
||||
export function DndContext({ children, ...callback }: PropsWithChildren<ContextProps>) {
|
||||
const [instanceId] = useState<string>(randomUUID())
|
||||
useEffect(() => {
|
||||
return (
|
||||
combine(
|
||||
monitorForElements({
|
||||
canMonitor({ source }) {
|
||||
return source.data.instanceId === instanceId
|
||||
},
|
||||
...callback,
|
||||
}),
|
||||
)
|
||||
)
|
||||
}, [callback, instanceId])
|
||||
return (
|
||||
<InstanceIdContext.Provider value={instanceId}>
|
||||
{children}
|
||||
</InstanceIdContext.Provider>
|
||||
)
|
||||
}
|
78
src/components/common/dnd/useSortable.ts
Normal file
78
src/components/common/dnd/useSortable.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"
|
||||
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"
|
||||
import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source"
|
||||
import { createContext } from "react"
|
||||
|
||||
export const InstanceIdContext = createContext<string | null>(null)
|
||||
|
||||
interface SortableProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
interface DraggableState {
|
||||
type: "idle" | "dragging"
|
||||
container?: HTMLElement
|
||||
}
|
||||
|
||||
export function useSortable(props: SortableProps) {
|
||||
const instanceId = useContext(InstanceIdContext)
|
||||
const [draggableState, setDraggableState] = useState<DraggableState>({
|
||||
type: "idle",
|
||||
})
|
||||
useEffect(() => {
|
||||
if (draggableState.type === "idle") {
|
||||
document.querySelector("html")?.classList.remove("grabbing")
|
||||
} else if (draggableState.type === "dragging") {
|
||||
// https://github.com/SortableJS/Vue.Draggable/issues/815#issuecomment-1552904628
|
||||
setTimeout(() => {
|
||||
document.querySelector("html")?.classList.add("grabbing")
|
||||
}, 50)
|
||||
}
|
||||
}, [draggableState])
|
||||
const [handleRef, setHandleRef] = useState<HTMLElement | null>(null)
|
||||
const [nodeRef, setNodeRef] = useState<HTMLElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (handleRef && nodeRef) {
|
||||
const cleanup = combine(
|
||||
draggable({
|
||||
// use custom drag preview
|
||||
element: handleRef,
|
||||
// element: ref,
|
||||
// dragHandle: handleRef,
|
||||
getInitialData: () => ({ id: props.id, instanceId }),
|
||||
onGenerateDragPreview({ nativeSetDragImage, location }) {
|
||||
setCustomNativeDragPreview({
|
||||
getOffset: preserveOffsetOnSource({
|
||||
element: nodeRef,
|
||||
input: location.current.input,
|
||||
}),
|
||||
render({ container }) {
|
||||
container.style.width = `${nodeRef.clientWidth}px`
|
||||
setDraggableState({ type: "dragging", container })
|
||||
},
|
||||
nativeSetDragImage,
|
||||
})
|
||||
},
|
||||
onDrop: () => {
|
||||
setDraggableState({ type: "idle" })
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element: nodeRef,
|
||||
getData: () => ({ id: props.id }),
|
||||
getIsSticky: () => true,
|
||||
canDrop: ({ source }) => source.data.instanceId === instanceId,
|
||||
}),
|
||||
)
|
||||
return cleanup
|
||||
}
|
||||
}, [props.id, instanceId, handleRef, nodeRef])
|
||||
return {
|
||||
setHandleRef,
|
||||
setNodeRef,
|
||||
isDragging: draggableState.type === "dragging",
|
||||
OverlayContainer: draggableState.container,
|
||||
}
|
||||
}
|
@ -51,3 +51,8 @@ button:disabled {
|
||||
#dropdown-menu li {
|
||||
--at-apply: hover:bg-neutral-400/10 rounded-md flex items-center p-1 gap-1;
|
||||
}
|
||||
|
||||
|
||||
.grabbing * {
|
||||
cursor: grabbing;
|
||||
}
|
@ -42,3 +42,15 @@ export const myFetch = $fetch.create({
|
||||
retry: 0,
|
||||
baseURL: "/api",
|
||||
})
|
||||
|
||||
export function isiOS() {
|
||||
return [
|
||||
"iPad Simulator",
|
||||
"iPhone Simulator",
|
||||
"iPod Simulator",
|
||||
"iPad",
|
||||
"iPhone",
|
||||
"iPod",
|
||||
].includes(navigator.platform)
|
||||
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user