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