mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 00:10:22 +08:00
refactor(Sortable): improve sortable props, support custom modifiers (#9879)
* refactor(Sortable): improve props, support modifiers * refactor: update id and index
This commit is contained in:
parent
4b65dfa6ea
commit
33f8ea5acb
@ -1,13 +1,16 @@
|
||||
import { DraggableSyntheticListeners } from '@dnd-kit/core'
|
||||
import { Transform } from '@dnd-kit/utilities'
|
||||
import { CSS, Transform } from '@dnd-kit/utilities'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import React, { useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { RenderItemType } from './types'
|
||||
|
||||
interface ItemRendererProps<T> {
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
index?: number
|
||||
item: T
|
||||
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode
|
||||
renderItem: RenderItemType<T>
|
||||
dragging?: boolean
|
||||
dragOverlay?: boolean
|
||||
ghost?: boolean
|
||||
@ -18,6 +21,7 @@ interface ItemRendererProps<T> {
|
||||
|
||||
export function ItemRenderer<T>({
|
||||
ref,
|
||||
index,
|
||||
item,
|
||||
renderItem,
|
||||
dragging,
|
||||
@ -42,14 +46,15 @@ export function ItemRenderer<T>({
|
||||
|
||||
const wrapperStyle = {
|
||||
transition,
|
||||
'--translate-x': transform ? `${Math.round(transform.x)}px` : undefined,
|
||||
'--translate-y': transform ? `${Math.round(transform.y)}px` : undefined,
|
||||
'--scale-x': transform?.scaleX ? `${transform.scaleX}` : undefined,
|
||||
'--scale-y': transform?.scaleY ? `${transform.scaleY}` : undefined
|
||||
transform: CSS.Transform.toString(transform ?? null)
|
||||
} as React.CSSProperties
|
||||
|
||||
return (
|
||||
<ItemWrapper ref={ref} className={classNames({ dragOverlay: dragOverlay })} style={{ ...wrapperStyle }}>
|
||||
<ItemWrapper
|
||||
ref={ref}
|
||||
data-index={index}
|
||||
className={classNames({ dragOverlay: dragOverlay })}
|
||||
style={{ ...wrapperStyle }}>
|
||||
<DraggableItem
|
||||
className={classNames({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })}
|
||||
{...listeners}
|
||||
@ -62,8 +67,6 @@ export function ItemRenderer<T>({
|
||||
|
||||
const ItemWrapper = styled.div`
|
||||
box-sizing: border-box;
|
||||
transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0) scaleX(var(--scale-x, 1))
|
||||
scaleY(var(--scale-y, 1));
|
||||
transform-origin: 0 0;
|
||||
touch-action: manipulation;
|
||||
|
||||
|
||||
@ -5,13 +5,19 @@ import {
|
||||
DragOverlay,
|
||||
DropAnimation,
|
||||
KeyboardSensor,
|
||||
Modifier,
|
||||
Over,
|
||||
TouchSensor,
|
||||
UniqueIdentifier,
|
||||
useSensor,
|
||||
useSensors
|
||||
} from '@dnd-kit/core'
|
||||
import { restrictToHorizontalAxis, restrictToVerticalAxis } from '@dnd-kit/modifiers'
|
||||
import {
|
||||
restrictToFirstScrollableAncestor,
|
||||
restrictToHorizontalAxis,
|
||||
restrictToVerticalAxis,
|
||||
restrictToWindowEdges
|
||||
} from '@dnd-kit/modifiers'
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
rectSortingStrategy,
|
||||
@ -25,6 +31,7 @@ import styled from 'styled-components'
|
||||
|
||||
import { ItemRenderer } from './ItemRenderer'
|
||||
import { SortableItem } from './SortableItem'
|
||||
import { RenderItemType } from './types'
|
||||
import { PortalSafePointerSensor } from './utils'
|
||||
|
||||
interface SortableProps<T> {
|
||||
@ -39,7 +46,7 @@ interface SortableProps<T> {
|
||||
/** Callback when drag ends, will be passed to dnd-kit's onDragEnd */
|
||||
onDragEnd?: (event: { over: Over }) => void
|
||||
/** Function to render individual item, receives item data and drag state */
|
||||
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode
|
||||
renderItem: RenderItemType<T>
|
||||
/** Layout type - 'list' for vertical/horizontal list, 'grid' for grid layout */
|
||||
layout?: 'list' | 'grid'
|
||||
/** Whether sorting is horizontal */
|
||||
@ -54,10 +61,17 @@ interface SortableProps<T> {
|
||||
className?: string
|
||||
/** Item list style */
|
||||
listStyle?: React.CSSProperties
|
||||
/** Ghost item style */
|
||||
ghostItemStyle?: React.CSSProperties
|
||||
/** Item gap */
|
||||
gap?: number | string
|
||||
/** Restrictions, shortcuts for some modifiers */
|
||||
restrictions?: {
|
||||
/** Add modifier restrictToWindowEdges */
|
||||
windowEdges?: boolean
|
||||
/** Add modifier restrictToFirstScrollableAncestor */
|
||||
scrollableAncestor?: boolean
|
||||
}
|
||||
/** Additional modifiers */
|
||||
modifiers?: Modifier[]
|
||||
}
|
||||
|
||||
function Sortable<T>({
|
||||
@ -73,7 +87,9 @@ function Sortable<T>({
|
||||
showGhost = false,
|
||||
className,
|
||||
listStyle,
|
||||
gap
|
||||
gap,
|
||||
restrictions,
|
||||
modifiers: customModifiers
|
||||
}: SortableProps<T>) {
|
||||
const sensors = useSensors(
|
||||
useSensor(PortalSafePointerSensor, {
|
||||
@ -132,7 +148,18 @@ function Sortable<T>({
|
||||
|
||||
const strategy =
|
||||
layout === 'list' ? (horizontal ? horizontalListSortingStrategy : verticalListSortingStrategy) : rectSortingStrategy
|
||||
const modifiers = layout === 'list' ? (horizontal ? [restrictToHorizontalAxis] : [restrictToVerticalAxis]) : []
|
||||
|
||||
const { windowEdges = false, scrollableAncestor = false } = restrictions ?? {}
|
||||
|
||||
const modifiers = useMemo<Modifier[]>(
|
||||
() => [
|
||||
...(layout === 'list' ? [horizontal ? restrictToHorizontalAxis : restrictToVerticalAxis] : []),
|
||||
...(windowEdges ? [restrictToWindowEdges] : []),
|
||||
...(scrollableAncestor ? [restrictToFirstScrollableAncestor] : []),
|
||||
...(customModifiers ?? [])
|
||||
],
|
||||
[layout, horizontal, windowEdges, scrollableAncestor, customModifiers]
|
||||
)
|
||||
|
||||
const dropAnimation: DropAnimation = useMemo(
|
||||
() => ({
|
||||
@ -162,8 +189,9 @@ function Sortable<T>({
|
||||
{items.map((item, index) => (
|
||||
<SortableItem
|
||||
key={itemIds[index]}
|
||||
id={itemIds[index]}
|
||||
index={index}
|
||||
item={item}
|
||||
getId={getId}
|
||||
renderItem={renderItem}
|
||||
useDragOverlay={useDragOverlay}
|
||||
showGhost={showGhost}
|
||||
@ -200,14 +228,14 @@ const ListWrapper = styled.div<{ $gap?: number | string }>`
|
||||
&[data-layout='list'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-direction='horizontal'] {
|
||||
flex-direction: row;
|
||||
}
|
||||
&[data-layout='list'][data-direction='horizontal'] {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
[data-direction='vertical'] {
|
||||
flex-direction: column;
|
||||
}
|
||||
&[data-layout='list'][data-direction='vertical'] {
|
||||
flex-direction: column;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import React from 'react'
|
||||
|
||||
import { ItemRenderer } from './ItemRenderer'
|
||||
import { RenderItemType } from './types'
|
||||
|
||||
interface SortableItemProps<T> {
|
||||
item: T
|
||||
getId: (item: T) => string | number
|
||||
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode
|
||||
id: string | number
|
||||
index: number
|
||||
renderItem: RenderItemType<T>
|
||||
useDragOverlay?: boolean
|
||||
showGhost?: boolean
|
||||
}
|
||||
|
||||
export function SortableItem<T>({
|
||||
item,
|
||||
getId,
|
||||
id,
|
||||
index,
|
||||
renderItem,
|
||||
useDragOverlay = true,
|
||||
showGhost = true
|
||||
}: SortableItemProps<T>) {
|
||||
const id = getId(item)
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id
|
||||
})
|
||||
@ -28,6 +28,7 @@ export function SortableItem<T>({
|
||||
<ItemRenderer
|
||||
ref={setNodeRef}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
dragging={isDragging}
|
||||
dragOverlay={!useDragOverlay && isDragging}
|
||||
|
||||
1
src/renderer/src/components/dnd/types.ts
Normal file
1
src/renderer/src/components/dnd/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type RenderItemType<T> = (item: T, props: { dragging: boolean }) => React.ReactNode
|
||||
@ -1,4 +1,4 @@
|
||||
import { defaultDropAnimationSideEffects, type DropAnimation, PointerSensor } from '@dnd-kit/core'
|
||||
import { PointerSensor } from '@dnd-kit/core'
|
||||
|
||||
export const PORTAL_NO_DND_SELECTORS = [
|
||||
'.ant-dropdown',
|
||||
@ -8,20 +8,6 @@ export const PORTAL_NO_DND_SELECTORS = [
|
||||
'.ant-modal'
|
||||
].join(',')
|
||||
|
||||
/**
|
||||
* Default drop animation config.
|
||||
* The opacity is set so to match the drag overlay case.
|
||||
*/
|
||||
export const dropAnimationConfig: DropAnimation = {
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
styles: {
|
||||
active: {
|
||||
opacity: '0.25'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent drag on elements with specific classes or data-no-dnd attribute
|
||||
*/
|
||||
|
||||
@ -253,7 +253,8 @@ const McpServersList: FC = () => {
|
||||
itemKey="id"
|
||||
onSortEnd={onSortEnd}
|
||||
layout="grid"
|
||||
gap={'12px'}
|
||||
gap="12px"
|
||||
restrictions={{ scrollableAncestor: true }}
|
||||
useDragOverlay
|
||||
showGhost
|
||||
renderItem={(server) => (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user