Feat: 拖拽功能增加移动端支持
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { ref, type Ref } from "vue";
|
||||
|
||||
interface SortableItem {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface UsePhotoDragSortOptions<T extends SortableItem> {
|
||||
columns: Ref<T[][]>;
|
||||
onChange: (nextColumns: T[][]) => void;
|
||||
}
|
||||
|
||||
const MOVE_START_THRESHOLD = 8;
|
||||
|
||||
export const usePhotoDragSort = <T extends SortableItem>({
|
||||
columns,
|
||||
onChange,
|
||||
}: UsePhotoDragSortOptions<T>) => {
|
||||
const draggingPhotoId = ref<number | null>(null);
|
||||
const dragStartPoint = ref<Point | null>(null);
|
||||
const hasTouchMoved = ref(false);
|
||||
const suppressNextClick = ref(false);
|
||||
const lastTouchTargetKey = ref<string | null>(null);
|
||||
|
||||
const cloneColumns = (columnsValue: T[][]) =>
|
||||
columnsValue.map((column) => column.slice());
|
||||
|
||||
const emitColumns = (nextColumns: T[][]) => {
|
||||
onChange(cloneColumns(nextColumns));
|
||||
};
|
||||
|
||||
const getPhotoPosition = (columnsValue: T[][], photoId: number) => {
|
||||
for (let columnIndex = 0; columnIndex < columnsValue.length; columnIndex += 1) {
|
||||
const photoIndex = columnsValue[columnIndex].findIndex((photo) => photo.id === photoId);
|
||||
if (photoIndex !== -1) {
|
||||
return { columnIndex, photoIndex };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const movePhotoToColumn = (targetColumnIndex: number) => {
|
||||
if (draggingPhotoId.value === null) return;
|
||||
|
||||
const columnsSnapshot = cloneColumns(columns.value);
|
||||
const source = getPhotoPosition(columnsSnapshot, draggingPhotoId.value);
|
||||
const targetColumn = columnsSnapshot[targetColumnIndex];
|
||||
if (!source || !targetColumn) return;
|
||||
|
||||
const [draggedPhoto] = columnsSnapshot[source.columnIndex].splice(source.photoIndex, 1);
|
||||
if (!draggedPhoto) return;
|
||||
|
||||
targetColumn.push(draggedPhoto);
|
||||
emitColumns(columnsSnapshot);
|
||||
};
|
||||
|
||||
const movePhotoBeforeTarget = (targetColumnIndex: number, targetPhotoIndex: number) => {
|
||||
if (draggingPhotoId.value === null) return;
|
||||
|
||||
const columnsSnapshot = cloneColumns(columns.value);
|
||||
const source = getPhotoPosition(columnsSnapshot, draggingPhotoId.value);
|
||||
const targetColumn = columnsSnapshot[targetColumnIndex];
|
||||
if (!source || !targetColumn) return;
|
||||
|
||||
const [draggedPhoto] = columnsSnapshot[source.columnIndex].splice(source.photoIndex, 1);
|
||||
if (!draggedPhoto) return;
|
||||
|
||||
let insertIndex = Math.min(Math.max(targetPhotoIndex, 0), targetColumn.length);
|
||||
if (source.columnIndex === targetColumnIndex && source.photoIndex < targetPhotoIndex) {
|
||||
insertIndex -= 1;
|
||||
}
|
||||
|
||||
targetColumn.splice(insertIndex, 0, draggedPhoto);
|
||||
emitColumns(columnsSnapshot);
|
||||
};
|
||||
|
||||
const resetTouchDragState = () => {
|
||||
dragStartPoint.value = null;
|
||||
hasTouchMoved.value = false;
|
||||
lastTouchTargetKey.value = null;
|
||||
};
|
||||
|
||||
const handleDragStart = (photoId: number, event: DragEvent) => {
|
||||
draggingPhotoId.value = photoId;
|
||||
event.dataTransfer?.setData("text/plain", String(photoId));
|
||||
event.dataTransfer?.setDragImage?.((event.target as HTMLElement) ?? new Image(), 10, 10);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
draggingPhotoId.value = null;
|
||||
resetTouchDragState();
|
||||
};
|
||||
|
||||
const handleTouchStart = (photoId: number, event: TouchEvent) => {
|
||||
const touch = event.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
draggingPhotoId.value = photoId;
|
||||
dragStartPoint.value = { x: touch.clientX, y: touch.clientY };
|
||||
hasTouchMoved.value = false;
|
||||
lastTouchTargetKey.value = null;
|
||||
};
|
||||
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
if (draggingPhotoId.value === null) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
if (!hasTouchMoved.value && dragStartPoint.value) {
|
||||
const deltaX = touch.clientX - dragStartPoint.value.x;
|
||||
const deltaY = touch.clientY - dragStartPoint.value.y;
|
||||
const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
|
||||
if (distance < MOVE_START_THRESHOLD) return;
|
||||
}
|
||||
|
||||
hasTouchMoved.value = true;
|
||||
event.preventDefault();
|
||||
|
||||
const target = document.elementFromPoint(touch.clientX, touch.clientY) as HTMLElement | null;
|
||||
if (!target) return;
|
||||
|
||||
const photoElement = target.closest<HTMLElement>("[data-photo-index][data-column-index]");
|
||||
if (photoElement) {
|
||||
const targetColumn = Number(photoElement.dataset.columnIndex);
|
||||
const targetPhoto = Number(photoElement.dataset.photoIndex);
|
||||
if (Number.isNaN(targetColumn) || Number.isNaN(targetPhoto)) return;
|
||||
|
||||
const key = `photo:${targetColumn}:${targetPhoto}`;
|
||||
if (lastTouchTargetKey.value === key) return;
|
||||
lastTouchTargetKey.value = key;
|
||||
movePhotoBeforeTarget(targetColumn, targetPhoto);
|
||||
return;
|
||||
}
|
||||
|
||||
const columnElement = target.closest<HTMLElement>("[data-column-index]");
|
||||
if (columnElement) {
|
||||
const targetColumn = Number(columnElement.dataset.columnIndex);
|
||||
if (Number.isNaN(targetColumn)) return;
|
||||
|
||||
const key = `column:${targetColumn}`;
|
||||
if (lastTouchTargetKey.value === key) return;
|
||||
lastTouchTargetKey.value = key;
|
||||
movePhotoToColumn(targetColumn);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (hasTouchMoved.value) {
|
||||
suppressNextClick.value = true;
|
||||
}
|
||||
draggingPhotoId.value = null;
|
||||
resetTouchDragState();
|
||||
};
|
||||
|
||||
const consumeSuppressedClick = () => {
|
||||
if (!suppressNextClick.value) return false;
|
||||
suppressNextClick.value = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
draggingPhotoId,
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
movePhotoToColumn,
|
||||
movePhotoBeforeTarget,
|
||||
consumeSuppressedClick,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user