Creating events now requires a double tap or double tap with drag on the second tap, to prevent erratic clicks. Bugfixes on event handling and some unrelated comment changes.
This commit is contained in:
parent
898ec2df00
commit
9a4d1c7196
@ -1,12 +1,14 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
day: Object,
|
||||
dragging: { type: Boolean, default: false },
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="cell"
|
||||
:style="props.dragging ? 'touch-action:none;' : 'touch-action:pan-y;'"
|
||||
:class="[
|
||||
props.day.monthClass,
|
||||
{
|
||||
@ -37,7 +39,6 @@ const props = defineProps({
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
|
@ -47,10 +47,33 @@ const selection = ref({ startDate: null, dayCount: 0 })
|
||||
const isDragging = ref(false)
|
||||
const dragAnchor = ref(null)
|
||||
|
||||
// Double-tap state
|
||||
const lastTapTime = ref(0)
|
||||
const lastTapDate = ref(null)
|
||||
const DOUBLE_TAP_DELAY = 300 // milliseconds
|
||||
const DOUBLE_TAP_DELAY = 300
|
||||
const pendingTap = ref({ date: null, time: 0, type: null })
|
||||
const suppressMouseUntil = ref(0)
|
||||
|
||||
function normalizeDate(val) {
|
||||
if (typeof val === 'string') return val
|
||||
if (val && typeof val === 'object') {
|
||||
if (val.date) return String(val.date)
|
||||
if (val.startDate) return String(val.startDate)
|
||||
}
|
||||
return String(val)
|
||||
}
|
||||
|
||||
function registerTap(rawDate, type) {
|
||||
const dateStr = normalizeDate(rawDate)
|
||||
const now = Date.now()
|
||||
const prev = pendingTap.value
|
||||
const delta = now - prev.time
|
||||
const isDouble =
|
||||
prev.date === dateStr && prev.type === type && delta <= DOUBLE_TAP_DELAY && delta >= 35
|
||||
if (isDouble) {
|
||||
pendingTap.value = { date: null, time: 0, type: null }
|
||||
return true
|
||||
}
|
||||
pendingTap.value = { date: dateStr, time: now, type }
|
||||
return false
|
||||
}
|
||||
|
||||
const minVirtualWeek = computed(() => {
|
||||
const date = new Date(calendarStore.minYear, 0, 1)
|
||||
@ -153,29 +176,24 @@ function createWeek(virtualWeek) {
|
||||
const dateStr = toLocalString(cur, DEFAULT_TZ)
|
||||
const storedEvents = []
|
||||
|
||||
// Find all non-repeating events that occur on this date
|
||||
for (const ev of calendarStore.events.values()) {
|
||||
if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
||||
storedEvents.push(ev)
|
||||
}
|
||||
}
|
||||
// Build day events starting with stored (base/spanning) then virtual occurrences
|
||||
const dayEvents = [...storedEvents]
|
||||
// Expand repeating events into per-day occurrences (including virtual spans) when they cover this date.
|
||||
for (const base of repeatingBases) {
|
||||
// If the current date falls within the base event's original span, include the base
|
||||
// event itself as occurrence index 0. Previously this was skipped which caused the
|
||||
// first (n=0) occurrence of repeating events to be missing from the calendar.
|
||||
// Base event's original span: include it directly as occurrence index 0.
|
||||
if (dateStr >= base.startDate && dateStr <= base.endDate) {
|
||||
dayEvents.push({
|
||||
...base,
|
||||
// Mark explicit recurrence index for consistency with virtual occurrences
|
||||
_recurrenceIndex: 0,
|
||||
_baseId: base.id,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if any virtual occurrence spans this date
|
||||
const baseStart = fromLocalString(base.startDate, DEFAULT_TZ)
|
||||
const baseEnd = fromLocalString(base.endDate, DEFAULT_TZ)
|
||||
const spanDays = Math.max(0, differenceInCalendarDays(baseEnd, baseStart))
|
||||
@ -183,19 +201,16 @@ function createWeek(virtualWeek) {
|
||||
|
||||
let occurrenceFound = false
|
||||
|
||||
// Walk backwards within span to find occurrence start
|
||||
// Walk backwards within the base span to locate a matching virtual occurrence start.
|
||||
for (let offset = 0; offset <= spanDays && !occurrenceFound; offset++) {
|
||||
const candidateStart = addDays(currentDate, -offset)
|
||||
const candidateStartStr = toLocalString(candidateStart, DEFAULT_TZ)
|
||||
|
||||
const occurrenceIndex = getOccurrenceIndex(base, candidateStartStr, DEFAULT_TZ)
|
||||
if (occurrenceIndex !== null) {
|
||||
// Calculate the end date of this occurrence
|
||||
const virtualEndDate = getVirtualOccurrenceEndDate(base, candidateStartStr, DEFAULT_TZ)
|
||||
|
||||
// Check if this occurrence spans through the current date
|
||||
if (dateStr >= candidateStartStr && dateStr <= virtualEndDate) {
|
||||
// Create virtual occurrence (if not already created)
|
||||
const virtualId = base.id + '_v_' + candidateStartStr
|
||||
const alreadyExists = dayEvents.some((ev) => ev.id === virtualId)
|
||||
|
||||
@ -232,8 +247,6 @@ function createWeek(virtualWeek) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get holiday info once per day
|
||||
// Ensure holidays initialized lazily
|
||||
let holiday = null
|
||||
if (calendarStore.config.holidays.enabled) {
|
||||
calendarStore._ensureHolidaysInitialized?.()
|
||||
@ -308,21 +321,13 @@ function clearSelection() {
|
||||
selection.value = { startDate: null, dayCount: 0 }
|
||||
}
|
||||
|
||||
function isDoubleTap(dateStr) {
|
||||
const now = Date.now()
|
||||
const isDouble = lastTapDate.value === dateStr && now - lastTapTime.value <= DOUBLE_TAP_DELAY
|
||||
|
||||
lastTapTime.value = now
|
||||
lastTapDate.value = dateStr
|
||||
|
||||
return isDouble
|
||||
}
|
||||
|
||||
function startDrag(dateStr) {
|
||||
dateStr = normalizeDate(dateStr)
|
||||
if (calendarStore.config.select_days === 0) return
|
||||
isDragging.value = true
|
||||
dragAnchor.value = dateStr
|
||||
selection.value = { startDate: dateStr, dayCount: 1 }
|
||||
addGlobalTouchListeners()
|
||||
}
|
||||
|
||||
function updateDrag(dateStr) {
|
||||
@ -338,6 +343,88 @@ function endDrag(dateStr) {
|
||||
selection.value = { startDate, dayCount }
|
||||
}
|
||||
|
||||
function finalizeDragAndCreate() {
|
||||
if (!isDragging.value) return
|
||||
isDragging.value = false
|
||||
const eventData = createEventFromSelection()
|
||||
if (eventData) {
|
||||
clearSelection()
|
||||
emit('create-event', eventData)
|
||||
}
|
||||
removeGlobalTouchListeners()
|
||||
}
|
||||
|
||||
function getDateUnderPoint(x, y) {
|
||||
const el = document.elementFromPoint(x, y)
|
||||
let cur = el
|
||||
while (cur) {
|
||||
if (cur.dataset && cur.dataset.date) return cur.dataset.date
|
||||
cur = cur.parentElement
|
||||
}
|
||||
return getDateFromCoordinates(x, y)
|
||||
}
|
||||
|
||||
function onGlobalTouchMove(e) {
|
||||
if (!isDragging.value) return
|
||||
const t = e.touches && e.touches[0]
|
||||
if (!t) return
|
||||
e.preventDefault()
|
||||
const dateStr = getDateUnderPoint(t.clientX, t.clientY)
|
||||
if (dateStr) updateDrag(dateStr)
|
||||
}
|
||||
|
||||
function onGlobalTouchEnd(e) {
|
||||
if (!isDragging.value) {
|
||||
removeGlobalTouchListeners()
|
||||
return
|
||||
}
|
||||
const t = (e.changedTouches && e.changedTouches[0]) || (e.touches && e.touches[0])
|
||||
if (t) {
|
||||
const dateStr = getDateUnderPoint(t.clientX, t.clientY)
|
||||
if (dateStr) {
|
||||
const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr)
|
||||
selection.value = { startDate, dayCount }
|
||||
}
|
||||
}
|
||||
finalizeDragAndCreate()
|
||||
}
|
||||
|
||||
function addGlobalTouchListeners() {
|
||||
window.addEventListener('touchmove', onGlobalTouchMove, { passive: false })
|
||||
window.addEventListener('touchend', onGlobalTouchEnd, { passive: false })
|
||||
window.addEventListener('touchcancel', onGlobalTouchEnd, { passive: false })
|
||||
}
|
||||
|
||||
function removeGlobalTouchListeners() {
|
||||
window.removeEventListener('touchmove', onGlobalTouchMove)
|
||||
window.removeEventListener('touchend', onGlobalTouchEnd)
|
||||
window.removeEventListener('touchcancel', onGlobalTouchEnd)
|
||||
}
|
||||
|
||||
// Fallback hit-test if elementFromPoint doesn't find a day cell (e.g., moving between rows).
|
||||
function getDateFromCoordinates(clientX, clientY) {
|
||||
if (!viewport.value) return null
|
||||
const vpRect = viewport.value.getBoundingClientRect()
|
||||
const yOffset = clientY - vpRect.top + viewport.value.scrollTop
|
||||
if (yOffset < 0) return null
|
||||
const rowIndex = Math.floor(yOffset / rowHeight.value)
|
||||
const virtualWeek = minVirtualWeek.value + rowIndex
|
||||
if (virtualWeek < minVirtualWeek.value || virtualWeek > maxVirtualWeek.value) return null
|
||||
const sampleWeek = viewport.value.querySelector('.week-row')
|
||||
if (!sampleWeek) return null
|
||||
const labelEl = sampleWeek.querySelector('.week-label')
|
||||
const jogwheelWidth = 48
|
||||
const wrRect = sampleWeek.getBoundingClientRect()
|
||||
const labelRight = labelEl ? labelEl.getBoundingClientRect().right : wrRect.left
|
||||
const daysAreaRight = wrRect.right - jogwheelWidth
|
||||
const daysWidth = daysAreaRight - labelRight
|
||||
if (clientX < labelRight || clientX > daysAreaRight) return null
|
||||
const col = Math.min(6, Math.max(0, Math.floor(((clientX - labelRight) / daysWidth) * 7)))
|
||||
const firstDay = getFirstDayForVirtualWeek(virtualWeek)
|
||||
const targetDate = addDays(firstDay, col)
|
||||
return toLocalString(targetDate, DEFAULT_TZ)
|
||||
}
|
||||
|
||||
function calculateSelection(anchorStr, otherStr) {
|
||||
const limit = calendarStore.config.select_days
|
||||
const anchorDate = fromLocalString(anchorStr, DEFAULT_TZ)
|
||||
@ -395,59 +482,33 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const handleDayMouseDown = (dateStr) => {
|
||||
// Check for double tap - only start drag on second tap
|
||||
if (isDoubleTap(dateStr)) {
|
||||
startDrag(dateStr)
|
||||
const handleDayMouseDown = (d) => {
|
||||
d = normalizeDate(d)
|
||||
if (Date.now() < suppressMouseUntil.value) return
|
||||
if (registerTap(d, 'mouse')) startDrag(d)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDayMouseEnter = (dateStr) => {
|
||||
if (isDragging.value) {
|
||||
updateDrag(dateStr)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDayMouseUp = (dateStr) => {
|
||||
if (isDragging.value) {
|
||||
endDrag(dateStr)
|
||||
const eventData = createEventFromSelection()
|
||||
if (eventData) {
|
||||
const handleDayMouseEnter = (d) => updateDrag(normalizeDate(d))
|
||||
const handleDayMouseUp = (d) => {
|
||||
d = normalizeDate(d)
|
||||
if (Date.now() < suppressMouseUntil.value && !isDragging.value) return
|
||||
if (!isDragging.value) return
|
||||
endDrag(d)
|
||||
const ev = createEventFromSelection()
|
||||
if (ev) {
|
||||
clearSelection()
|
||||
emit('create-event', eventData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDayTouchStart = (dateStr) => {
|
||||
// Check for double tap - only start drag on second tap
|
||||
if (isDoubleTap(dateStr)) {
|
||||
startDrag(dateStr)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDayTouchMove = (dateStr) => {
|
||||
if (isDragging.value) {
|
||||
updateDrag(dateStr)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDayTouchEnd = (dateStr) => {
|
||||
if (isDragging.value) {
|
||||
endDrag(dateStr)
|
||||
const eventData = createEventFromSelection()
|
||||
if (eventData) {
|
||||
clearSelection()
|
||||
emit('create-event', eventData)
|
||||
emit('create-event', ev)
|
||||
}
|
||||
}
|
||||
const handleDayTouchStart = (d) => {
|
||||
d = normalizeDate(d)
|
||||
suppressMouseUntil.value = Date.now() + 800
|
||||
if (registerTap(d, 'touch')) startDrag(d)
|
||||
}
|
||||
|
||||
const handleEventClick = (payload) => {
|
||||
emit('edit-event', payload)
|
||||
}
|
||||
|
||||
// Handle year change emitted from CalendarHeader: scroll to computed target position
|
||||
const handleHeaderYearChange = ({ scrollTop: st }) => {
|
||||
const maxScroll = contentHeight.value - viewportHeight.value
|
||||
const clamped = Math.max(0, Math.min(st, isFinite(maxScroll) ? maxScroll : st))
|
||||
@ -458,7 +519,7 @@ const handleHeaderYearChange = ({ scrollTop: st }) => {
|
||||
function openSettings() {
|
||||
settingsDialog.value?.open()
|
||||
}
|
||||
// Preserve approximate top visible date when first_day changes
|
||||
// Keep roughly same visible date when first_day setting changes.
|
||||
watch(
|
||||
() => calendarStore.config.first_day,
|
||||
() => {
|
||||
@ -504,13 +565,12 @@ watch(
|
||||
v-for="week in visibleWeeks"
|
||||
:key="week.virtualWeek"
|
||||
:week="week"
|
||||
:dragging="isDragging"
|
||||
:style="{ top: week.top + 'px' }"
|
||||
@day-mousedown="handleDayMouseDown"
|
||||
@day-mouseenter="handleDayMouseEnter"
|
||||
@day-mouseup="handleDayMouseUp"
|
||||
@day-touchstart="handleDayTouchStart"
|
||||
@day-touchmove="handleDayTouchMove"
|
||||
@day-touchend="handleDayTouchEnd"
|
||||
@event-click="handleEventClick"
|
||||
/>
|
||||
<!-- Month labels positioned absolutely -->
|
||||
|
@ -2,17 +2,13 @@
|
||||
import CalendarDay from './CalendarDay.vue'
|
||||
import EventOverlay from './EventOverlay.vue'
|
||||
|
||||
const props = defineProps({
|
||||
week: Object,
|
||||
})
|
||||
const props = defineProps({ week: Object, dragging: { type: Boolean, default: false } })
|
||||
|
||||
const emit = defineEmits([
|
||||
'day-mousedown',
|
||||
'day-mouseenter',
|
||||
'day-mouseup',
|
||||
'day-touchstart',
|
||||
'day-touchmove',
|
||||
'day-touchend',
|
||||
'event-click',
|
||||
])
|
||||
|
||||
@ -32,13 +28,7 @@ const handleDayTouchStart = (dateStr) => {
|
||||
emit('day-touchstart', dateStr)
|
||||
}
|
||||
|
||||
const handleDayTouchMove = (dateStr) => {
|
||||
emit('day-touchmove', dateStr)
|
||||
}
|
||||
|
||||
const handleDayTouchEnd = (dateStr) => {
|
||||
emit('day-touchend', dateStr)
|
||||
}
|
||||
// touchmove & touchend handled globally in CalendarView
|
||||
|
||||
const handleEventClick = (payload) => {
|
||||
emit('event-click', payload)
|
||||
@ -53,12 +43,11 @@ const handleEventClick = (payload) => {
|
||||
v-for="day in props.week.days"
|
||||
:key="day.date"
|
||||
:day="day"
|
||||
:dragging="props.dragging"
|
||||
@mousedown="handleDayMouseDown(day.date)"
|
||||
@mouseenter="handleDayMouseEnter(day.date)"
|
||||
@mouseup="handleDayMouseUp(day.date)"
|
||||
@touchstart="handleDayTouchStart(day.date)"
|
||||
@touchmove="handleDayTouchMove(day.date)"
|
||||
@touchend="handleDayTouchEnd(day.date)"
|
||||
/>
|
||||
<EventOverlay :week="props.week" @event-click="handleEventClick" />
|
||||
</div>
|
||||
|
@ -165,8 +165,10 @@ function startLocalDrag(init, evt) {
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default to avoid text selection and other interference
|
||||
// Prevent default for mouse/pen to avoid text selection. For touch we skip so the user can still scroll.
|
||||
if (!(evt.pointerType === 'touch')) {
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
window.addEventListener('pointermove', onDragPointerMove, { passive: false })
|
||||
window.addEventListener('pointerup', onDragPointerUp, { passive: false })
|
||||
|
Loading…
x
Reference in New Issue
Block a user