Compare commits

...

1 Commits

Author SHA1 Message Date
Peng Xiao
adaee0ef5f feat(component): sortable 2025-03-31 17:21:52 +08:00
5 changed files with 259 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
# Sortable
Migrated from https://github.com/clauderic/dnd-kit

View File

@@ -0,0 +1,123 @@
import type { ElementDragType } from '@atlaskit/pragmatic-drag-and-drop/types';
import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { rectSortingStrategy } from './strategies';
import type {
ClientRect,
Disabled,
SortingStrategy,
UniqueIdentifier,
} from './types';
import {
getSortedRects,
itemsEqual,
normalizeDisabled,
useUniqueId,
} from './utilities';
export interface Props {
children: React.ReactNode;
items: (UniqueIdentifier | { id: UniqueIdentifier })[];
strategy?: SortingStrategy;
disabled?: boolean | Disabled;
}
const ID_PREFIX = 'Sortable';
interface ContextDescriptor {
activeIndex: number;
containerId: string;
disableTransforms: boolean;
items: {
id: UniqueIdentifier;
}[];
overIndex: number;
sortedRects: ClientRect[];
strategy: SortingStrategy;
disabled: Disabled;
}
export const Context = React.createContext<ContextDescriptor>({
activeIndex: -1,
containerId: ID_PREFIX,
disableTransforms: false,
items: [],
overIndex: -1,
sortedRects: [],
strategy: rectSortingStrategy,
disabled: {
draggable: false,
droppable: false,
},
});
export function SortableContext({
children,
items: userDefinedItems,
strategy = rectSortingStrategy,
disabled: disabledProp = false,
}: Props) {
const [active, setActive] = useState<ElementDragType | null>(null);
const { active, droppableRects, over, measureDroppableContainers } =
useDndContext();
const containerId = useUniqueId(ID_PREFIX, id);
const items = useMemo<UniqueIdentifier[]>(
() =>
userDefinedItems.map(item =>
typeof item === 'object' && 'id' in item ? item.id : item
),
[userDefinedItems]
);
const isDragging = active != null;
const activeIndex = active ? items.indexOf(active.id) : -1;
const overIndex = over ? items.indexOf(over.id) : -1;
const previousItemsRef = useRef(items);
const itemsHaveChanged = !itemsEqual(items, previousItemsRef.current);
const disableTransforms =
(overIndex !== -1 && activeIndex === -1) || itemsHaveChanged;
const disabled = normalizeDisabled(disabledProp);
useLayoutEffect(() => {
if (itemsHaveChanged && isDragging) {
measureDroppableContainers(items);
}
}, [itemsHaveChanged, items, isDragging, measureDroppableContainers]);
useEffect(() => {
previousItemsRef.current = items;
}, [items]);
const contextValue = useMemo(
(): ContextDescriptor => ({
activeIndex,
containerId,
disabled,
disableTransforms,
items,
overIndex,
sortedRects: getSortedRects(items, droppableRects),
strategy,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
activeIndex,
containerId,
disabled.draggable,
disabled.droppable,
disableTransforms,
items,
overIndex,
droppableRects,
strategy,
]
);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

View File

@@ -0,0 +1,25 @@
import type { SortingStrategy } from './types';
import { arrayMove } from './utilities';
export const rectSortingStrategy: SortingStrategy = ({
rects,
activeIndex,
overIndex,
index,
}) => {
const newRects = arrayMove(rects, overIndex, activeIndex);
const oldRect = rects[index];
const newRect = newRects[index];
if (!newRect || !oldRect) {
return null;
}
return {
x: newRect.left - oldRect.left,
y: newRect.top - oldRect.top,
scaleX: newRect.width / oldRect.width,
scaleY: newRect.height / oldRect.height,
};
};

View File

@@ -0,0 +1,32 @@
export type Transform = {
x: number;
y: number;
scaleX: number;
scaleY: number;
};
export interface ClientRect {
width: number;
height: number;
top: number;
left: number;
right: number;
bottom: number;
}
export type SortingStrategy = (args: {
activeNodeRect: ClientRect | null;
activeIndex: number;
index: number;
rects: ClientRect[];
overIndex: number;
}) => Transform | null;
export type UniqueIdentifier = string | number;
export type RectMap = Map<UniqueIdentifier, ClientRect>;
export interface Disabled {
draggable?: boolean;
droppable?: boolean;
}

View File

@@ -0,0 +1,76 @@
import { useMemo } from 'react';
import type { ClientRect, Disabled, RectMap, UniqueIdentifier } from './types';
let ids: Record<string, number> = {};
export function useUniqueId(prefix: string, value?: string) {
return useMemo(() => {
if (value) {
return value;
}
const id = ids[prefix] == null ? 0 : ids[prefix] + 1;
ids[prefix] = id;
return `${prefix}-${id}`;
}, [prefix, value]);
}
/**
* Move an array item to a different position. Returns a new array with the item moved to the new position.
*/
export function arrayMove<T>(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;
}
export function getSortedRects(items: UniqueIdentifier[], rects: RectMap) {
return items.reduce<ClientRect[]>(
(accumulator, id, index) => {
const rect = rects.get(id);
if (rect) {
accumulator[index] = rect;
}
return accumulator;
},
Array.from({ length: items.length })
);
}
export function itemsEqual(a: UniqueIdentifier[], b: UniqueIdentifier[]) {
if (a === b) {
return true;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
export function normalizeDisabled(disabled: boolean | Disabled): Disabled {
if (typeof disabled === 'boolean') {
return {
draggable: disabled,
droppable: disabled,
};
}
return disabled;
}