155 lines
5.0 KiB
TypeScript
155 lines
5.0 KiB
TypeScript
import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
|
import type { TicketModel } from '@/types/models/ticket-model';
|
|
|
|
/** Pointer capture keeps mouse, pen and touch drags on the same code path. */
|
|
export function useBoardDrag(options: {
|
|
canStart: (ticket: TicketModel) => boolean;
|
|
drop: (ticket: TicketModel, columnId: number) => void;
|
|
}) {
|
|
const ticket = shallowRef<TicketModel | null>(null);
|
|
const active = ref(false);
|
|
const x = ref(0);
|
|
const y = ref(0);
|
|
const columnId = ref<number | null>(null);
|
|
let pointerId: number | null = null;
|
|
let handle: HTMLElement | null = null;
|
|
let startX = 0;
|
|
let startY = 0;
|
|
let frame = 0;
|
|
|
|
const draggedTicket = computed(() => (active.value ? ticket.value : null));
|
|
|
|
function locateTarget(): void {
|
|
const target = document
|
|
.elementFromPoint(x.value, y.value)
|
|
?.closest<HTMLElement>('[data-board-drop-column]');
|
|
columnId.value = target ? Number(target.dataset.boardDropColumn) : null;
|
|
}
|
|
|
|
function scroll(): void {
|
|
if (!active.value) {
|
|
return;
|
|
}
|
|
const element = document.elementFromPoint(x.value, y.value);
|
|
const region = element?.closest<HTMLElement>(
|
|
'[data-board-drag-scroll]',
|
|
);
|
|
if (region) {
|
|
const bounds = region.getBoundingClientRect();
|
|
const left = Math.max(0, bounds.left);
|
|
const right = Math.min(window.innerWidth, bounds.right);
|
|
const delta =
|
|
x.value < left + 44 ? -10 : x.value > right - 44 ? 10 : 0;
|
|
region.scrollLeft += delta;
|
|
}
|
|
if (!element?.closest('[data-board-mobile-targets]')) {
|
|
const delta =
|
|
y.value < 64 ? -10 : y.value > window.innerHeight - 64 ? 10 : 0;
|
|
window.scrollBy(0, delta);
|
|
}
|
|
locateTarget();
|
|
frame = requestAnimationFrame(scroll);
|
|
}
|
|
|
|
function cancel(): void {
|
|
const previousPointer = pointerId;
|
|
pointerId = null;
|
|
active.value = false;
|
|
ticket.value = null;
|
|
columnId.value = null;
|
|
cancelAnimationFrame(frame);
|
|
if (
|
|
previousPointer !== null &&
|
|
handle?.hasPointerCapture(previousPointer)
|
|
) {
|
|
handle.releasePointerCapture(previousPointer);
|
|
}
|
|
handle = null;
|
|
}
|
|
|
|
function start(event: PointerEvent, source: TicketModel): void {
|
|
if (
|
|
pointerId !== null ||
|
|
!event.isPrimary ||
|
|
event.button !== 0 ||
|
|
!options.canStart(source)
|
|
) {
|
|
return;
|
|
}
|
|
handle = event.currentTarget as HTMLElement;
|
|
handle.focus({ preventScroll: true });
|
|
handle.setPointerCapture(event.pointerId);
|
|
pointerId = event.pointerId;
|
|
ticket.value = source;
|
|
startX = x.value = event.clientX;
|
|
startY = y.value = event.clientY;
|
|
}
|
|
|
|
function move(event: PointerEvent): void {
|
|
if (event.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
x.value = event.clientX;
|
|
y.value = event.clientY;
|
|
if (
|
|
!active.value &&
|
|
Math.hypot(x.value - startX, y.value - startY) >= 6
|
|
) {
|
|
active.value = true;
|
|
frame = requestAnimationFrame(scroll);
|
|
}
|
|
if (active.value) {
|
|
event.preventDefault();
|
|
locateTarget();
|
|
}
|
|
}
|
|
|
|
function finish(event: PointerEvent): void {
|
|
if (event.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
x.value = event.clientX;
|
|
y.value = event.clientY;
|
|
locateTarget();
|
|
const source = draggedTicket.value;
|
|
const target = columnId.value;
|
|
cancel();
|
|
if (source && target !== null) {
|
|
options.drop(source, target);
|
|
}
|
|
}
|
|
|
|
function cancelPointer(event: PointerEvent): void {
|
|
if (event.pointerId === pointerId) {
|
|
cancel();
|
|
}
|
|
}
|
|
|
|
function keydown(event: KeyboardEvent): void {
|
|
if (event.key === 'Escape' && pointerId !== null) {
|
|
event.preventDefault();
|
|
cancel();
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('pointermove', move, { passive: false });
|
|
document.addEventListener('pointerup', finish);
|
|
document.addEventListener('pointercancel', cancelPointer);
|
|
document.addEventListener('lostpointercapture', cancelPointer);
|
|
document.addEventListener('keydown', keydown);
|
|
window.addEventListener('blur', cancel);
|
|
});
|
|
onUnmounted(() => {
|
|
cancel();
|
|
document.removeEventListener('pointermove', move);
|
|
document.removeEventListener('pointerup', finish);
|
|
document.removeEventListener('pointercancel', cancelPointer);
|
|
document.removeEventListener('lostpointercapture', cancelPointer);
|
|
document.removeEventListener('keydown', keydown);
|
|
window.removeEventListener('blur', cancel);
|
|
});
|
|
|
|
return { draggedTicket, columnId, x, y, start, cancel };
|
|
}
|