Major new version #2

Merged
LeoVasanko merged 86 commits from vol002 into main 2025-08-26 05:58:24 +01:00
4 changed files with 144 additions and 92 deletions
Showing only changes of commit 9a4d1c7196 - Show all commits

View File

@ -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;

View File

@ -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 = (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', ev)
}
}
const handleDayMouseEnter = (dateStr) => {
if (isDragging.value) {
updateDrag(dateStr)
}
}
const handleDayMouseUp = (dateStr) => {
if (isDragging.value) {
endDrag(dateStr)
const eventData = createEventFromSelection()
if (eventData) {
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)
}
}
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 -->

View File

@ -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>

View File

@ -165,8 +165,10 @@ function startLocalDrag(init, evt) {
}
}
// Prevent default to avoid text selection and other interference
evt.preventDefault()
// 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 })