mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
made sortable grid component
This commit is contained in:
parent
24ab0cb050
commit
3345939670
13 changed files with 243 additions and 170 deletions
136
src/components/sortable/SortableGrid/index.tsx
Normal file
136
src/components/sortable/SortableGrid/index.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
import React, { createElement, FC, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { DragEndEvent } from '@dnd-kit/core/dist/types';
|
||||
import { rectSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { moveArrItem } from '~/utils/fn';
|
||||
import { DivProps } from '~/utils/types';
|
||||
|
||||
import { SortableImageGridItem } from '../SortableGridItem';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface SortableGridProps<T extends {}, R extends {}> {
|
||||
items: T[];
|
||||
locked: R[];
|
||||
getID: (item: T) => number | string;
|
||||
getLockedID: (locked: R) => number | string;
|
||||
renderItem: FC<{ item: T }>;
|
||||
renderLocked: FC<{ locked: R }>;
|
||||
onSortEnd: (newVal: T[]) => void;
|
||||
className?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const SortableGrid = <T, R>({
|
||||
items,
|
||||
locked,
|
||||
getID,
|
||||
getLockedID,
|
||||
className,
|
||||
renderItem,
|
||||
renderLocked,
|
||||
onSortEnd,
|
||||
size,
|
||||
}: SortableGridProps<T, R>) => {
|
||||
const [draggingItem, setDraggingItem] = useState<T | null>(null);
|
||||
|
||||
const ids = useMemo(() => items.map(getID), [items]);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
({ active, over }: DragEndEvent) => {
|
||||
setDraggingItem(null);
|
||||
|
||||
if (!over?.id || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldIndex = items.findIndex(it => getID(it) === active.id);
|
||||
const newIndex = items.findIndex(it => getID(it) === over.id);
|
||||
|
||||
onSortEnd(moveArrItem(oldIndex, newIndex, items));
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
const onDragStart = useCallback(
|
||||
({ active }: DragStartEvent) => {
|
||||
if (!active.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeItem = items.find(it => getID(it) === active.id);
|
||||
|
||||
setDraggingItem(activeItem ?? null);
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 200,
|
||||
tolerance: 5,
|
||||
},
|
||||
}),
|
||||
useSensor(MouseSensor)
|
||||
);
|
||||
|
||||
const gridStyle = useMemo<DivProps['style']>(
|
||||
() =>
|
||||
size
|
||||
? { gridTemplateColumns: size && `repeat(auto-fill, minmax(${size}px, 1fr))` }
|
||||
: undefined,
|
||||
[size]
|
||||
);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragStart={onDragStart}
|
||||
>
|
||||
<SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||
<div className={classNames(styles.grid, className)} style={gridStyle}>
|
||||
{items.map(item => (
|
||||
<SortableImageGridItem
|
||||
key={getID(item)}
|
||||
id={getID(item)}
|
||||
className={
|
||||
draggingItem && getID(item) === getID(draggingItem) ? styles.dragging : undefined
|
||||
}
|
||||
>
|
||||
{createElement(renderItem, { item })}
|
||||
</SortableImageGridItem>
|
||||
))}
|
||||
|
||||
{locked.map(item =>
|
||||
createElement(renderLocked, { locked: item, key: getLockedID(item) })
|
||||
)}
|
||||
|
||||
<DragOverlay>
|
||||
{draggingItem ? (
|
||||
<div className={styles.overlay}>
|
||||
{createElement(renderItem, { item: draggingItem })}
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export { SortableGrid };
|
19
src/components/sortable/SortableGrid/styles.module.scss
Normal file
19
src/components/sortable/SortableGrid/styles.module.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.grid {
|
||||
box-sizing: border-box;
|
||||
|
||||
display: grid;
|
||||
grid-column-gap: $gap;
|
||||
grid-row-gap: $gap;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
.overlay {
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 5px 5px 10px 2px;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
opacity: 0.1;
|
||||
}
|
44
src/components/sortable/SortableGridItem/index.tsx
Normal file
44
src/components/sortable/SortableGridItem/index.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface SortableImageGridItemProps {
|
||||
id: number | string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SortableImageGridItem: FC<SortableImageGridItemProps> = ({
|
||||
children,
|
||||
id,
|
||||
disabled = false,
|
||||
className,
|
||||
}) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
id,
|
||||
disabled,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={classNames(styles.item, className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { SortableImageGridItem };
|
|
@ -0,0 +1,6 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.item {
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
64
src/components/sortable/SortableImageGrid/index.tsx
Normal file
64
src/components/sortable/SortableImageGrid/index.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { ImageUpload } from '~/components/upload/ImageUpload';
|
||||
import { ImagePresets } from '~/constants/urls';
|
||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||
import { IFile } from '~/types';
|
||||
import { getURL } from '~/utils/dom';
|
||||
|
||||
import { SortableGrid } from '../SortableGrid';
|
||||
|
||||
type OnSortEnd = (newValue: IFile[]) => void;
|
||||
|
||||
interface SortableImageGridProps {
|
||||
onSortEnd: OnSortEnd;
|
||||
items: IFile[];
|
||||
locked: UploadStatus[];
|
||||
onDelete: (file_id: IFile['id']) => void;
|
||||
className?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const SortableImageGrid: FC<SortableImageGridProps> = ({
|
||||
items,
|
||||
locked,
|
||||
onDelete,
|
||||
className,
|
||||
onSortEnd,
|
||||
size,
|
||||
}) => {
|
||||
const renderItem = useCallback<FC<{ item: IFile }>>(
|
||||
({ item }) => (
|
||||
<ImageUpload id={item.id} thumb={getURL(item, ImagePresets.cover)} onDrop={onDelete} />
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const renderLocked = useCallback<FC<{ locked: UploadStatus }>>(
|
||||
({ locked }) => (
|
||||
<ImageUpload
|
||||
thumb={locked.thumbnail}
|
||||
onDrop={onDelete}
|
||||
progress={locked.progress}
|
||||
is_uploading
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<SortableGrid
|
||||
items={items}
|
||||
locked={locked}
|
||||
getID={it => it.id}
|
||||
getLockedID={it => it.id}
|
||||
renderItem={renderItem}
|
||||
renderLocked={renderLocked}
|
||||
onSortEnd={onSortEnd}
|
||||
className={className}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { SortableImageGrid };
|
23
src/components/sortable/SortableImageGrid/styles.module.scss
Normal file
23
src/components/sortable/SortableImageGrid/styles.module.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.grid {
|
||||
box-sizing: border-box;
|
||||
|
||||
display: grid;
|
||||
grid-column-gap: $gap;
|
||||
grid-row-gap: $gap;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(30vw, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 5px 5px 10px 2px;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
opacity: 0.1;
|
||||
}
|
2
src/components/sortable/index.ts
Normal file
2
src/components/sortable/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './SortableImageGrid';
|
||||
export * from './SortableGrid';
|
Loading…
Add table
Add a link
Reference in a new issue