mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 05:21:10 +02:00
refactor(calendar): extract WeekView inline logic into composables (1600→903 LOC)
Replace 697 lines of inline drag/drop/resize/create/keyboard handlers in WeekView.svelte with existing composables: - useEventDragDrop: event drag & resize (was ~220 LOC inline) - useTaskDragDrop: task drag & resize (was ~180 LOC inline) - useSidebarDrop: sidebar task drop (was ~70 LOC inline) - useDragToCreate: new composable for click-drag event creation (was ~105 LOC) - useCalendarKeyboard: Escape key cancel (was ~50 LOC inline) Also adds getResizePreviewTime() to useEventDragDrop return value so WeekView doesn't need access to internal resize state. WeekView.svelte: 1600 → 903 lines (-44%) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
70671e2b2b
commit
aeabdcaf8e
4 changed files with 339 additions and 797 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,9 @@ export { useTaskDragDrop, type TaskDragDropConfig } from './useTaskDragDrop.svel
|
|||
// Sidebar task drop handling
|
||||
export { useSidebarDrop, type SidebarDropConfig } from './useSidebarDrop.svelte';
|
||||
|
||||
// Drag-to-create
|
||||
export { useDragToCreate, type DragToCreateConfig } from './useDragToCreate.svelte';
|
||||
|
||||
// Keyboard handling
|
||||
export { useCalendarKeyboard, type CancellableOperation } from './useCalendarKeyboard.svelte';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* Drag-to-Create Composable
|
||||
* Handles click-and-drag on the calendar grid to create new events
|
||||
*/
|
||||
|
||||
import { SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants';
|
||||
|
||||
export interface DragToCreateConfig {
|
||||
containerEl: HTMLElement | null;
|
||||
days: Date[];
|
||||
firstVisibleHour: number;
|
||||
lastVisibleHour: number;
|
||||
totalVisibleHours: number;
|
||||
hourHeight: number;
|
||||
minutesToPercent: (minutes: number) => number;
|
||||
snapMinutes?: number;
|
||||
isOtherOperationActive: () => boolean;
|
||||
onCreateEnd?: (startTime: Date, endTime: Date, position: { x: number; y: number }) => void;
|
||||
}
|
||||
|
||||
export function useDragToCreate(getConfig: () => DragToCreateConfig) {
|
||||
let isCreating = $state(false);
|
||||
let createTargetDay = $state<Date | null>(null);
|
||||
let createStartMinutes = $state(0);
|
||||
let createEndMinutes = $state(0);
|
||||
let createPreviewTop = $state(0);
|
||||
let createPreviewHeight = $state(0);
|
||||
let hasMoved = $state(false);
|
||||
|
||||
function getSnapMinutes(): number {
|
||||
return getConfig().snapMinutes ?? SNAP_INTERVAL_MINUTES;
|
||||
}
|
||||
|
||||
function getDayFromX(clientX: number): Date | null {
|
||||
const config = getConfig();
|
||||
if (!config.containerEl) return null;
|
||||
|
||||
const rect = config.containerEl.getBoundingClientRect();
|
||||
const relativeX = clientX - rect.left;
|
||||
const dayWidth = rect.width / config.days.length;
|
||||
const dayIndex = Math.floor(relativeX / dayWidth);
|
||||
|
||||
if (dayIndex >= 0 && dayIndex < config.days.length) {
|
||||
return config.days[dayIndex];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMinutesFromY(clientY: number): number {
|
||||
const config = getConfig();
|
||||
if (!config.containerEl) return 0;
|
||||
|
||||
const rect = config.containerEl.getBoundingClientRect();
|
||||
const scrollTop = config.containerEl.parentElement?.scrollTop || 0;
|
||||
const relativeY = clientY - rect.top + scrollTop;
|
||||
const visibleMinutes =
|
||||
(relativeY / (config.totalVisibleHours * config.hourHeight)) * config.totalVisibleHours * 60;
|
||||
const totalMinutes = visibleMinutes + config.firstVisibleHour * 60;
|
||||
|
||||
const snap = getSnapMinutes();
|
||||
return Math.round(totalMinutes / snap) * snap;
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const config = getConfig();
|
||||
createPreviewTop = config.minutesToPercent(createStartMinutes);
|
||||
const duration = createEndMinutes - createStartMinutes;
|
||||
createPreviewHeight = (duration / (config.totalVisibleHours * 60)) * 100;
|
||||
}
|
||||
|
||||
function startCreate(e: PointerEvent) {
|
||||
const config = getConfig();
|
||||
if (config.isOtherOperationActive()) return;
|
||||
|
||||
// Don't start creating if clicking on interactive elements
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.closest(
|
||||
'.event-card, .task-block, .all-day-event, .all-day-block-event, .overflow-indicator, .resize-handle'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const day = getDayFromX(e.clientX);
|
||||
if (!day) return;
|
||||
|
||||
const minutes = getMinutesFromY(e.clientY);
|
||||
const snap = getSnapMinutes();
|
||||
const snappedMinutes = Math.round(minutes / snap) * snap;
|
||||
|
||||
isCreating = true;
|
||||
hasMoved = false;
|
||||
createTargetDay = day;
|
||||
createStartMinutes = snappedMinutes;
|
||||
createEndMinutes = snappedMinutes + snap;
|
||||
|
||||
updatePreview();
|
||||
|
||||
document.addEventListener('pointermove', handleCreateMove);
|
||||
document.addEventListener('pointerup', handleCreateEnd);
|
||||
}
|
||||
|
||||
function handleCreateMove(e: PointerEvent) {
|
||||
if (!isCreating) return;
|
||||
|
||||
hasMoved = true;
|
||||
const config = getConfig();
|
||||
const snap = getSnapMinutes();
|
||||
|
||||
const day = getDayFromX(e.clientX);
|
||||
if (day) createTargetDay = day;
|
||||
|
||||
const minutes = getMinutesFromY(e.clientY);
|
||||
const snappedMinutes = Math.round(minutes / snap) * snap;
|
||||
|
||||
if (snappedMinutes >= createStartMinutes) {
|
||||
createEndMinutes = Math.max(snappedMinutes, createStartMinutes + snap);
|
||||
} else {
|
||||
createEndMinutes = createStartMinutes + snap;
|
||||
createStartMinutes = snappedMinutes;
|
||||
}
|
||||
|
||||
createStartMinutes = Math.max(config.firstVisibleHour * 60, createStartMinutes);
|
||||
createEndMinutes = Math.min(config.lastVisibleHour * 60, createEndMinutes);
|
||||
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function handleCreateEnd(e: PointerEvent) {
|
||||
document.removeEventListener('pointermove', handleCreateMove);
|
||||
document.removeEventListener('pointerup', handleCreateEnd);
|
||||
|
||||
if (!isCreating || !createTargetDay) {
|
||||
isCreating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = new Date(createTargetDay);
|
||||
startTime.setHours(Math.floor(createStartMinutes / 60), createStartMinutes % 60, 0, 0);
|
||||
|
||||
const endTime = new Date(createTargetDay);
|
||||
endTime.setHours(Math.floor(createEndMinutes / 60), createEndMinutes % 60, 0, 0);
|
||||
|
||||
isCreating = false;
|
||||
createTargetDay = null;
|
||||
hasMoved = false;
|
||||
|
||||
const config = getConfig();
|
||||
config.onCreateEnd?.(startTime, endTime, { x: e.clientX, y: e.clientY });
|
||||
}
|
||||
|
||||
function getCreatePreviewTime(): string {
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return `${pad(Math.floor(createStartMinutes / 60))}:${pad(createStartMinutes % 60)} - ${pad(Math.floor(createEndMinutes / 60))}:${pad(createEndMinutes % 60)}`;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (isCreating) {
|
||||
document.removeEventListener('pointermove', handleCreateMove);
|
||||
document.removeEventListener('pointerup', handleCreateEnd);
|
||||
isCreating = false;
|
||||
createTargetDay = null;
|
||||
hasMoved = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get isCreating() {
|
||||
return isCreating;
|
||||
},
|
||||
get createTargetDay() {
|
||||
return createTargetDay;
|
||||
},
|
||||
get createPreviewTop() {
|
||||
return createPreviewTop;
|
||||
},
|
||||
get createPreviewHeight() {
|
||||
return createPreviewHeight;
|
||||
},
|
||||
|
||||
startCreate,
|
||||
cancel,
|
||||
getCreatePreviewTime,
|
||||
};
|
||||
}
|
||||
|
|
@ -373,6 +373,36 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted time range during resize preview
|
||||
*/
|
||||
function getResizePreviewTime(): string {
|
||||
if (!resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) return '';
|
||||
|
||||
const config = getConfig();
|
||||
const origStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes();
|
||||
const origEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes();
|
||||
|
||||
const previewStartMinutes =
|
||||
(resizePreviewTop / 100) * config.totalVisibleHours * 60 + config.firstVisibleHour * 60;
|
||||
const previewEndMinutes =
|
||||
previewStartMinutes + (resizePreviewHeight / 100) * config.totalVisibleHours * 60;
|
||||
|
||||
let startMin: number;
|
||||
let endMin: number;
|
||||
|
||||
if (resizeEdge === 'top') {
|
||||
startMin = Math.round(previewStartMinutes);
|
||||
endMin = origEndMinutes;
|
||||
} else {
|
||||
startMin = origStartMinutes;
|
||||
endMin = Math.round(previewEndMinutes);
|
||||
}
|
||||
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return `${pad(Math.floor(startMin / 60))}:${pad(startMin % 60)} - ${pad(Math.floor(endMin / 60))}:${pad(endMin % 60)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
// Drag state (reactive getters)
|
||||
get isDragging() {
|
||||
|
|
@ -423,5 +453,6 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) {
|
|||
startResize,
|
||||
cancel,
|
||||
cleanup,
|
||||
getResizePreviewTime,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue