feat: create event with click and drag
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -22,6 +22,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
'event-click': [event: EntityObject];
|
||||
'date-click': [date: Date];
|
||||
'date-range-select': [range: { start: Date; end: Date }];
|
||||
'update:current-date': [date: Date];
|
||||
'update:days-span': [span: DaysViewSpan];
|
||||
'update:agenda-span': [span: AgendaViewSpan];
|
||||
@@ -79,6 +80,7 @@ function hidePopup() {
|
||||
:initial-span="initialDaysSpan"
|
||||
@event-click="$emit('event-click', $event)"
|
||||
@date-click="$emit('date-click', $event)"
|
||||
@date-range-select="$emit('date-range-select', $event)"
|
||||
@event-hover="handleEventHover"
|
||||
@event-hover-end="hidePopup"
|
||||
@update:span="$emit('update:days-span', $event)"
|
||||
|
||||
+160
-14
@@ -8,7 +8,7 @@ import type { CollectionObject } from '@ChronoManager/models/collection';
|
||||
import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
|
||||
import type { CalendarInstance } from '@/types/instance';
|
||||
import ViewControls from './ViewControls.vue';
|
||||
import { formatEpochTime, formatHour, formatWeekDay } from '@/utils/format';
|
||||
import { formatEpochTime, formatHour, formatTime, formatWeekDay } from '@/utils/format';
|
||||
|
||||
const ALL_DAY_EVENT_HEIGHT = 24;
|
||||
|
||||
@@ -21,6 +21,8 @@ const props = defineProps<{
|
||||
const instancesStore = useChronoInstancesStore();
|
||||
|
||||
const selectedSpan = ref<DaysViewSpan>(props.initialSpan ?? '7d');
|
||||
const hoveredTime = ref<{ day: string; minutes: number } | null>(null);
|
||||
const timeSelection = ref<{ day: string; anchor: number; current: number } | null>(null);
|
||||
const daysCount = computed(() => spanToDays(selectedSpan.value));
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -28,6 +30,7 @@ const emit = defineEmits<{
|
||||
'event-hover': [data: { event: MouseEvent; entity: EntityObject }];
|
||||
'event-hover-end': [];
|
||||
'date-click': [date: Date];
|
||||
'date-range-select': [range: { start: Date; end: Date }];
|
||||
'update:span': [span: DaysViewSpan];
|
||||
'update:current-date': [date: Date];
|
||||
}>();
|
||||
@@ -113,22 +116,99 @@ function getEventStyle(instance: CalendarInstance) {
|
||||
};
|
||||
}
|
||||
|
||||
function handleDayClick(event: MouseEvent, day: Date) {
|
||||
function pointerMinutes(event: MouseEvent, allowEndOfDay = false): number {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const clickY = event.clientY - rect.top;
|
||||
const pointerY = Math.min(Math.max(event.clientY - rect.top, 0), rect.height);
|
||||
const minutes = Math.round(((pointerY / rect.height) * 1440) / 15) * 15;
|
||||
|
||||
// Calculate the hour based on click position (60px per hour)
|
||||
const totalMinutes = Math.floor((clickY / 60) * 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = Math.round((totalMinutes % 60) / 15) * 15; // Round to nearest 15 min
|
||||
|
||||
// Create a new date with the clicked time
|
||||
const clickedDate = new Date(day);
|
||||
clickedDate.setHours(hours, minutes, 0, 0);
|
||||
|
||||
emit('date-click', clickedDate);
|
||||
return Math.min(minutes, allowEndOfDay ? 1440 : 1425);
|
||||
}
|
||||
|
||||
function handleHover(event: MouseEvent, day: Date) {
|
||||
hoveredTime.value = {
|
||||
day: day.toISOString(),
|
||||
minutes: pointerMinutes(event),
|
||||
};
|
||||
}
|
||||
|
||||
function formatHoverTime(totalMinutes: number): string {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const date = new Date(2000, 0, 1, hours, minutes);
|
||||
return formatTime(date);
|
||||
}
|
||||
|
||||
function dateAtMinutes(day: Date, totalMinutes: number): Date {
|
||||
const date = new Date(day);
|
||||
date.setHours(0, totalMinutes, 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
function selectionBounds(selection: NonNullable<typeof timeSelection.value>) {
|
||||
const start = Math.min(selection.anchor, selection.current);
|
||||
const end = selection.anchor === selection.current
|
||||
? Math.min(start + 15, 1440)
|
||||
: Math.max(selection.anchor, selection.current);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent, day: Date) {
|
||||
if (event.pointerType !== 'mouse' || event.button !== 0) return;
|
||||
|
||||
event.preventDefault();
|
||||
const minutes = pointerMinutes(event);
|
||||
hoveredTime.value = null;
|
||||
timeSelection.value = {
|
||||
day: day.toISOString(),
|
||||
anchor: minutes,
|
||||
current: minutes,
|
||||
};
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function handlePointerUp(event: PointerEvent, day: Date) {
|
||||
const selection = timeSelection.value;
|
||||
if (!selection || selection.day !== day.toISOString()) return;
|
||||
|
||||
selection.current = pointerMinutes(event, true);
|
||||
const { start, end } = selectionBounds(selection);
|
||||
timeSelection.value = null;
|
||||
|
||||
if (selection.anchor === selection.current) {
|
||||
emit('date-click', dateAtMinutes(day, start));
|
||||
return;
|
||||
}
|
||||
|
||||
emit('date-range-select', {
|
||||
start: dateAtMinutes(day, start),
|
||||
end: dateAtMinutes(day, end),
|
||||
});
|
||||
}
|
||||
|
||||
function handlePointerMove(event: PointerEvent, day: Date) {
|
||||
if (timeSelection.value?.day === day.toISOString()) {
|
||||
timeSelection.value.current = pointerMinutes(event, true);
|
||||
return;
|
||||
}
|
||||
|
||||
handleHover(event, day);
|
||||
}
|
||||
|
||||
function selectionStyle(selection: NonNullable<typeof timeSelection.value>) {
|
||||
const { start, end } = selectionBounds(selection);
|
||||
return {
|
||||
top: `${start / 1440 * 100}%`,
|
||||
height: `${(end - start) / 1440 * 100}%`,
|
||||
};
|
||||
}
|
||||
|
||||
function selectionLabel(selection: NonNullable<typeof timeSelection.value>): string {
|
||||
const { start, end } = selectionBounds(selection);
|
||||
return `${formatHoverTime(start)} – ${formatHoverTime(end)} · ${end - start} min`;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -191,12 +271,34 @@ function handleDayClick(event: MouseEvent, day: Date) {
|
||||
</div>
|
||||
<div class="days-columns">
|
||||
<div v-for="day in visibleDates" :key="day.toISOString()" class="day-column" :class="{ 'single-day': daysCount === 1 }">
|
||||
<div class="day-events" @click="handleDayClick($event, day)">
|
||||
<div
|
||||
class="day-events"
|
||||
@pointerdown="handlePointerDown($event, day)"
|
||||
@pointermove="handlePointerMove($event, day)"
|
||||
@pointerup="handlePointerUp($event, day)"
|
||||
@pointercancel="timeSelection = null"
|
||||
@mouseleave="hoveredTime = null"
|
||||
>
|
||||
<div
|
||||
v-if="hoveredTime?.day === day.toISOString()"
|
||||
class="hover-time-indicator"
|
||||
:style="{ top: `${hoveredTime.minutes / 1440 * 100}%` }"
|
||||
>
|
||||
<span>{{ formatHoverTime(hoveredTime.minutes) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="timeSelection?.day === day.toISOString()"
|
||||
class="time-selection-preview"
|
||||
:style="selectionStyle(timeSelection)"
|
||||
>
|
||||
<span>{{ selectionLabel(timeSelection) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="instance in getTimedEvents(day)"
|
||||
:key="instance.key"
|
||||
class="day-event"
|
||||
:style="getEventStyle(instance)"
|
||||
@pointerdown.stop
|
||||
@click.stop="emit('event-click', instance.entity)"
|
||||
@mouseenter="emit('event-hover', { event: $event, entity: instance.entity })"
|
||||
@mouseleave="emit('event-hover-end')"
|
||||
@@ -461,6 +563,50 @@ function handleDayClick(event: MouseEvent, day: Date) {
|
||||
background-color: rgba(var(--v-theme-primary), 0.02);
|
||||
}
|
||||
|
||||
.hover-time-indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
height: 1px;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hover-time-indicator span {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 4px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.time-selection-preview {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
right: 3px;
|
||||
z-index: 4;
|
||||
min-height: 15px;
|
||||
padding: 3px 5px;
|
||||
border: 1px solid rgb(var(--v-theme-primary));
|
||||
border-radius: 4px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.2);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.time-selection-preview span {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.day-event {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
|
||||
@@ -279,6 +279,7 @@ function selectSectionFromRoute() {
|
||||
:initial-agenda-view-span="agendaViewSpan"
|
||||
@event-click="editEvent"
|
||||
@date-click="createEventFromDate"
|
||||
@date-range-select="createEventFromDate($event.start, $event.end)"
|
||||
@update:current-date="selectDate"
|
||||
@update:days-span="handleCalendarDaysViewSpanChange"
|
||||
@update:agenda-span="handleCalendarAgendaViewSpanChange"
|
||||
|
||||
@@ -61,12 +61,12 @@ export const useChronoUiStore = defineStore('chronoUiStore', () => {
|
||||
showTaskEditor.value = type === 'task'
|
||||
}
|
||||
|
||||
function createEventFromDate(date?: Date) {
|
||||
function createEventFromDate(start?: Date, end?: Date) {
|
||||
const entity = new EntityObject()
|
||||
entity.properties = new EventObject()
|
||||
if (date) {
|
||||
entity.properties.startsOn = date.toISOString()
|
||||
entity.properties.endsOn = new Date(date.getTime() + 60 * 60 * 1000).toISOString()
|
||||
if (start) {
|
||||
entity.properties.startsOn = start.toISOString()
|
||||
entity.properties.endsOn = (end ?? new Date(start.getTime() + 60 * 60 * 1000)).toISOString()
|
||||
}
|
||||
openEntityEditor(entity, 'event', 'edit')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user