mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
made sortable grid component
This commit is contained in:
parent
24ab0cb050
commit
3345939670
13 changed files with 243 additions and 170 deletions
|
@ -3,7 +3,7 @@ import React, { FC, useCallback } from 'react';
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
|
|
||||||
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
||||||
import { OnSortEnd, SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
import { SortableImageGrid } from '~/components/sortable';
|
||||||
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
|
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
|
||||||
import { useFileDropZone } from '~/hooks';
|
import { useFileDropZone } from '~/hooks';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
|
@ -29,16 +29,9 @@ const CommentFormAttaches: FC = () => {
|
||||||
const hasAudioAttaches = filesAudios.length > 0 || pendingAudios.length > 0;
|
const hasAudioAttaches = filesAudios.length > 0 || pendingAudios.length > 0;
|
||||||
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
||||||
|
|
||||||
const onImageMove = useCallback<OnSortEnd>(
|
const onImageMove = useCallback(
|
||||||
({ oldIndex, newIndex }) => {
|
(newFiles: IFile[]) => {
|
||||||
setFiles([
|
setFiles([...filesAudios, ...newFiles.filter(it => it)]);
|
||||||
...filesAudios,
|
|
||||||
...(moveArrItem(
|
|
||||||
oldIndex,
|
|
||||||
newIndex,
|
|
||||||
filesImages.filter(file => !!file)
|
|
||||||
) as IFile[]),
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
[setFiles, filesImages, filesAudios]
|
[setFiles, filesImages, filesAudios]
|
||||||
);
|
);
|
||||||
|
@ -85,6 +78,7 @@ const CommentFormAttaches: FC = () => {
|
||||||
onSortEnd={onImageMove}
|
onSortEnd={onImageMove}
|
||||||
items={filesImages}
|
items={filesImages}
|
||||||
locked={pendingImages}
|
locked={pendingImages}
|
||||||
|
size={160}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
import { SortableImageGrid } from '~/components/sortable';
|
||||||
|
|
||||||
import { OnSortEnd, SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
|
||||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { moveArrItem } from '~/utils/fn';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
|
@ -19,15 +14,9 @@ interface IProps {
|
||||||
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
const { innerWidth } = useWindowSize();
|
const { innerWidth } = useWindowSize();
|
||||||
|
|
||||||
const onMove = useCallback<OnSortEnd>(
|
const onMove = useCallback(
|
||||||
({ oldIndex, newIndex }) => {
|
(newFiles: IFile[]) => {
|
||||||
setFiles(
|
setFiles(newFiles.filter(it => it));
|
||||||
moveArrItem(
|
|
||||||
oldIndex,
|
|
||||||
newIndex,
|
|
||||||
files.filter(file => !!file)
|
|
||||||
) as IFile[]
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[setFiles, files]
|
[setFiles, files]
|
||||||
);
|
);
|
||||||
|
@ -39,7 +28,15 @@ const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
[setFiles, files]
|
[setFiles, files]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <SortableImageGrid onDelete={onDrop} onSortEnd={onMove} items={files} locked={locked} />;
|
return (
|
||||||
|
<SortableImageGrid
|
||||||
|
onDelete={onDrop}
|
||||||
|
onSortEnd={onMove}
|
||||||
|
items={files}
|
||||||
|
locked={locked}
|
||||||
|
size={innerWidth > 768 ? 220 : 160}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { ImageGrid };
|
export { ImageGrid };
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
KeyboardSensor,
|
|
||||||
MouseSensor,
|
|
||||||
PointerSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import { DragEndEvent } from '@dnd-kit/core/dist/types';
|
|
||||||
import {
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
sortableKeyboardCoordinates,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { SortableImageGridItem } from '~/components/editors/SortableImageGridItem';
|
|
||||||
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 styles from './styles.module.scss';
|
|
||||||
|
|
||||||
export type OnSortEnd = (props: { oldIndex: number; newIndex: number }) => void;
|
|
||||||
|
|
||||||
interface SortableImageGridProps {
|
|
||||||
onSortEnd: OnSortEnd;
|
|
||||||
items: IFile[];
|
|
||||||
locked: UploadStatus[];
|
|
||||||
onDelete: (file_id: IFile['id']) => void;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SortableImageGrid: FC<SortableImageGridProps> = ({
|
|
||||||
items,
|
|
||||||
locked,
|
|
||||||
onDelete,
|
|
||||||
className,
|
|
||||||
onSortEnd,
|
|
||||||
}) => {
|
|
||||||
const [draggingItem, setDraggingItem] = useState<IFile | null>(null);
|
|
||||||
|
|
||||||
const preventEvent = useCallback(event => event.preventDefault(), []);
|
|
||||||
const sensors = useSensors(
|
|
||||||
useSensor(TouchSensor, {
|
|
||||||
activationConstraint: {
|
|
||||||
delay: 200,
|
|
||||||
tolerance: 5,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
useSensor(MouseSensor)
|
|
||||||
);
|
|
||||||
|
|
||||||
const ids = useMemo(() => items.map(it => it.id ?? 0), [items]);
|
|
||||||
const onDragEnd = useCallback(
|
|
||||||
({ active, over }: DragEndEvent) => {
|
|
||||||
setDraggingItem(null);
|
|
||||||
|
|
||||||
if (active.id === over?.id || !over?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldIndex = items.findIndex(it => it.id === active.id);
|
|
||||||
const newIndex = items.findIndex(it => it.id === over.id);
|
|
||||||
|
|
||||||
onSortEnd({ oldIndex, newIndex });
|
|
||||||
},
|
|
||||||
[items]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDragStart = useCallback(
|
|
||||||
({ active }: DragStartEvent) => {
|
|
||||||
if (!active.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeItem = items.find(it => it.id === active.id);
|
|
||||||
|
|
||||||
setDraggingItem(activeItem ?? null);
|
|
||||||
},
|
|
||||||
[items]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
onDragStart={onDragStart}
|
|
||||||
>
|
|
||||||
<SortableContext items={ids} strategy={rectSortingStrategy}>
|
|
||||||
<div className={classNames(styles.grid, className)} onDropCapture={preventEvent}>
|
|
||||||
{items
|
|
||||||
.filter(file => file && file.id)
|
|
||||||
.map((file, index) => (
|
|
||||||
<SortableImageGridItem
|
|
||||||
key={file.id}
|
|
||||||
id={file.id!}
|
|
||||||
className={file.id === draggingItem?.id ? styles.dragging : undefined}
|
|
||||||
>
|
|
||||||
<ImageUpload
|
|
||||||
id={file.id}
|
|
||||||
thumb={getURL(file, ImagePresets.cover)}
|
|
||||||
onDrop={onDelete}
|
|
||||||
/>
|
|
||||||
</SortableImageGridItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{locked.map((item, index) => (
|
|
||||||
<ImageUpload
|
|
||||||
thumb={item.thumbnail}
|
|
||||||
progress={item.progress}
|
|
||||||
is_uploading
|
|
||||||
key={item.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<DragOverlay>
|
|
||||||
{draggingItem ? (
|
|
||||||
<div className={styles.overlay}>
|
|
||||||
<ImageUpload thumb={getURL(draggingItem, ImagePresets.cover)} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</div>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { SortableImageGrid };
|
|
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;
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import classNames from 'classnames';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface SortableImageGridItemProps {
|
interface SortableImageGridItemProps {
|
||||||
id: number;
|
id: number | string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
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 };
|
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';
|
|
@ -1,7 +1,7 @@
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
|
|
||||||
export const EMPTY_FILE: IFile = {
|
export const EMPTY_FILE: IFile = {
|
||||||
id: undefined,
|
id: 0,
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
node_id: undefined,
|
node_id: undefined,
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ export type UUID = string;
|
||||||
export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
|
export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
|
||||||
|
|
||||||
export interface IFile {
|
export interface IFile {
|
||||||
id?: number;
|
id: number;
|
||||||
temp_id?: UUID;
|
temp_id?: UUID;
|
||||||
user_id?: UUID;
|
user_id?: UUID;
|
||||||
node_id?: UUID;
|
node_id?: UUID;
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue