Compare commits

..

No commits in common. "43db5e75c5e82ccfb5fc28723bc3aad0d2763ef0" and "86d38a5a29fba63316bc423b65df3b4da3a9417a" have entirely different histories.

8 changed files with 134 additions and 319 deletions

View File

@ -1,36 +1,9 @@
<script setup> <script setup>
import { ref } from 'vue'
import CalendarView from './components/CalendarView.vue' import CalendarView from './components/CalendarView.vue'
import EventDialog from './components/EventDialog.vue'
const eventDialog = ref(null)
const handleCreateEvent = (eventData) => {
if (eventDialog.value) {
const selectionData = {
startDate: eventData.startDate,
dayCount: eventData.dayCount,
}
setTimeout(() => eventDialog.value.openCreateDialog(selectionData), 50)
}
}
const handleEditEvent = (eventInstanceId) => {
if (eventDialog.value) {
eventDialog.value.openEditDialog(eventInstanceId)
}
}
const handleClearSelection = () => {}
</script> </script>
<template> <template>
<CalendarView @create-event="handleCreateEvent" @edit-event="handleEditEvent" /> <CalendarView />
<EventDialog
ref="eventDialog"
:selection="{ startDate: null, dayCount: 0 }"
@clear-selection="handleClearSelection"
/>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -8,12 +8,7 @@
</div> </div>
<div class="calendar-viewport" ref="viewportEl" @scroll="handleScroll"> <div class="calendar-viewport" ref="viewportEl" @scroll="handleScroll">
<div class="calendar-content" :style="{ height: `${totalVirtualWeeks * rowHeight}px` }"> <div class="calendar-content" :style="{ height: `${totalVirtualWeeks * rowHeight}px` }">
<WeekRow <WeekRow v.for="week in visibleWeeks" :key="week.virtualWeek" :week="week" :style="{ top: `${(week.virtualWeek - minVirtualWeek) * rowHeight}px` }" />
v.for="week in visibleWeeks"
:key="week.virtualWeek"
:week="week"
:style="{ top: `${(week.virtualWeek - minVirtualWeek) * rowHeight}px` }"
/>
</div> </div>
</div> </div>
</template> </template>
@ -21,15 +16,7 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue' import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { useCalendarStore } from '@/stores/CalendarStore' import { useCalendarStore } from '@/stores/CalendarStore'
import { import { getLocalizedWeekdayNames, isoWeekInfo, fromLocalString, toLocalString, mondayIndex } from '@/utils/date'
getLocalizedWeekdayNames,
getLocaleWeekendDays,
getLocaleFirstDay,
isoWeekInfo,
fromLocalString,
toLocalString,
mondayIndex,
} from '@/utils/date'
import WeekRow from './WeekRow.vue' import WeekRow from './WeekRow.vue'
const calendarStore = useCalendarStore() const calendarStore = useCalendarStore()
@ -42,10 +29,10 @@ const visibleWeeks = ref([])
const config = { const config = {
min_year: 1900, min_year: 1900,
max_year: 2100, max_year: 2100,
weekend: getLocaleWeekendDays(), weekend: [true, false, false, false, false, false, true] // Sun, Mon, ..., Sat
} }
const baseDate = new Date(1970, 0, 4 + getLocaleFirstDay()) const baseDate = new Date(2024, 0, 1) // 2024 begins with Monday
const WEEK_MS = 7 * 86400000 const WEEK_MS = 7 * 86400000
const weekdayNames = getLocalizedWeekdayNames() const weekdayNames = getLocalizedWeekdayNames()
@ -97,16 +84,13 @@ const updateVisibleWeeks = () => {
const endIdx = Math.ceil((scrollTop + viewportH + buffer * rowHeight.value) / rowHeight.value) const endIdx = Math.ceil((scrollTop + viewportH + buffer * rowHeight.value) / rowHeight.value)
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value) const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
const endVW = Math.min( const endVW = Math.min(totalVirtualWeeks.value + minVirtualWeek.value - 1, endIdx + minVirtualWeek.value)
totalVirtualWeeks.value + minVirtualWeek.value - 1,
endIdx + minVirtualWeek.value,
)
const newVisibleWeeks = [] const newVisibleWeeks = []
for (let vw = startVW; vw <= endVW; vw++) { for (let vw = startVW; vw <= endVW; vw++) {
newVisibleWeeks.push({ newVisibleWeeks.push({
virtualWeek: vw, virtualWeek: vw,
monday: getMondayForVirtualWeek(vw), monday: getMondayForVirtualWeek(vw)
}) })
} }
visibleWeeks.value = newVisibleWeeks visibleWeeks.value = newVisibleWeeks
@ -118,7 +102,7 @@ const handleScroll = () => {
const handleWheel = (e) => { const handleWheel = (e) => {
const currentYear = calendarStore.viewYear const currentYear = calendarStore.viewYear
const delta = Math.round(e.deltaY * (1 / 3)) const delta = Math.round(e.deltaY * (1/3))
if (!delta) return if (!delta) return
const newYear = Math.max(config.min_year, Math.min(config.max_year, currentYear + delta)) const newYear = Math.max(config.min_year, Math.min(config.max_year, currentYear + delta))
if (newYear === currentYear) return if (newYear === currentYear) return
@ -181,4 +165,5 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener('goToToday', goToTodayHandler) document.removeEventListener('goToToday', goToTodayHandler)
}) })
</script> </script>

View File

@ -1,12 +1,12 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useCalendarStore } from '@/stores/CalendarStore' import { useCalendarStore } from '@/stores/CalendarStore'
import { getLocalizedWeekdayNames, reorderByFirstDay, isoWeekInfo } from '@/utils/date' import { getLocalizedWeekdayNames, isoWeekInfo, mondayIndex } from '@/utils/date'
const props = defineProps({ const props = defineProps({
scrollTop: { type: Number, default: 0 }, scrollTop: { type: Number, default: 0 },
rowHeight: { type: Number, default: 64 }, rowHeight: { type: Number, default: 64 },
minVirtualWeek: { type: Number, default: 0 }, minVirtualWeek: { type: Number, default: 0 }
}) })
const calendarStore = useCalendarStore() const calendarStore = useCalendarStore()
@ -14,22 +14,17 @@ const calendarStore = useCalendarStore()
const yearLabel = computed(() => { const yearLabel = computed(() => {
const topDisplayIndex = Math.floor(props.scrollTop / props.rowHeight) const topDisplayIndex = Math.floor(props.scrollTop / props.rowHeight)
const topVW = topDisplayIndex + props.minVirtualWeek const topVW = topDisplayIndex + props.minVirtualWeek
const baseDate = new Date(1970, 0, 4 + calendarStore.config.first_day) const baseDate = new Date(2024, 0, 1) // Monday
const firstDay = new Date(baseDate) const monday = new Date(baseDate)
firstDay.setDate(firstDay.getDate() + topVW * 7) monday.setDate(monday.getDate() + topVW * 7)
return isoWeekInfo(firstDay).year return isoWeekInfo(monday).year
}) })
const weekdayNames = computed(() => { const weekdayNames = computed(() => {
// Get Monday-first names, then reorder by first day, then add weekend info const names = getLocalizedWeekdayNames()
const mondayFirstNames = getLocalizedWeekdayNames() return names.map((name, i) => ({
const sundayFirstNames = [mondayFirstNames[6], ...mondayFirstNames.slice(0, 6)]
const reorderedNames = reorderByFirstDay(sundayFirstNames, calendarStore.config.first_day)
const reorderedWeekend = reorderByFirstDay(calendarStore.weekend, calendarStore.config.first_day)
return reorderedNames.map((name, i) => ({
name, name,
isWeekend: reorderedWeekend[i], isWeekend: calendarStore.weekend[(i + 1) % 7]
})) }))
}) })
</script> </script>
@ -37,14 +32,7 @@ const weekdayNames = computed(() => {
<template> <template>
<div class="calendar-header"> <div class="calendar-header">
<div class="year-label">{{ yearLabel }}</div> <div class="year-label">{{ yearLabel }}</div>
<div <div v-for="day in weekdayNames" :key="day.name" class="dow" :class="{ weekend: day.isWeekend }">{{ day.name }}</div>
v-for="day in weekdayNames"
:key="day.name"
class="dow"
:class="{ workday: !day.isWeekend, weekend: day.isWeekend }"
>
{{ day.name }}
</div>
<div class="overlay-header-spacer"></div> <div class="overlay-header-spacer"></div>
</div> </div>
</template> </template>
@ -74,19 +62,14 @@ const weekdayNames = computed(() => {
font-size: 1.2em; font-size: 1.2em;
padding: 0.5rem; padding: 0.5rem;
} }
.dow { .dow {
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
padding: 0.5rem; padding: 0.5rem;
font-weight: 500; font-weight: 500;
} }
.dow.weekend {
color: var(--weekend);
}
.dow.workday {
color: var(--workday);
}
.overlay-header-spacer { .overlay-header-spacer {
grid-area: auto; /* Empty spacer for the month label column */
} }
</style> </style>

View File

@ -4,39 +4,22 @@ import { useCalendarStore } from '@/stores/CalendarStore'
import CalendarHeader from '@/components/CalendarHeader.vue' import CalendarHeader from '@/components/CalendarHeader.vue'
import CalendarWeek from '@/components/CalendarWeek.vue' import CalendarWeek from '@/components/CalendarWeek.vue'
import Jogwheel from '@/components/Jogwheel.vue' import Jogwheel from '@/components/Jogwheel.vue'
import { import EventDialog from '@/components/EventDialog.vue'
isoWeekInfo, import { isoWeekInfo, getLocalizedMonthName, monthAbbr, lunarPhaseSymbol, pad, mondayIndex, daysInclusive, addDaysStr, formatDateRange } from '@/utils/date'
getLocalizedMonthName,
monthAbbr,
lunarPhaseSymbol,
pad,
daysInclusive,
addDaysStr,
formatDateRange,
} from '@/utils/date'
import { toLocalString, fromLocalString } from '@/utils/date' import { toLocalString, fromLocalString } from '@/utils/date'
const calendarStore = useCalendarStore() const calendarStore = useCalendarStore()
const viewport = ref(null) const viewport = ref(null)
const eventDialog = ref(null)
const emit = defineEmits(['create-event', 'edit-event']) // UI state moved from store
function createEventFromSelection() {
if (!selection.value.startDate || selection.value.dayCount === 0) return null
return {
startDate: selection.value.startDate,
dayCount: selection.value.dayCount,
endDate: addDaysStr(selection.value.startDate, selection.value.dayCount - 1),
}
}
const scrollTop = ref(0) const scrollTop = ref(0)
const viewportHeight = ref(600) const viewportHeight = ref(600)
const rowHeight = ref(64) const rowHeight = ref(64)
const baseDate = new Date(1970, 0, 4 + calendarStore.config.first_day) const baseDate = new Date(2024, 0, 1) // Monday
const selection = ref({ startDate: null, dayCount: 0 }) // Selection state moved from store
const selection = ref({ start: null, end: null })
const isDragging = ref(false) const isDragging = ref(false)
const dragAnchor = ref(null) const dragAnchor = ref(null)
@ -44,18 +27,16 @@ const WEEK_MS = 7 * 24 * 60 * 60 * 1000
const minVirtualWeek = computed(() => { const minVirtualWeek = computed(() => {
const date = new Date(calendarStore.minYear, 0, 1) const date = new Date(calendarStore.minYear, 0, 1)
const firstDayOfWeek = new Date(date) const monday = new Date(date)
const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7 monday.setDate(date.getDate() - ((date.getDay() + 6) % 7))
firstDayOfWeek.setDate(date.getDate() - dayOffset) return Math.floor((monday - baseDate) / WEEK_MS)
return Math.floor((firstDayOfWeek - baseDate) / WEEK_MS)
}) })
const maxVirtualWeek = computed(() => { const maxVirtualWeek = computed(() => {
const date = new Date(calendarStore.maxYear, 11, 31) const date = new Date(calendarStore.maxYear, 11, 31)
const firstDayOfWeek = new Date(date) const monday = new Date(date)
const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7 monday.setDate(date.getDate() - ((date.getDay() + 6) % 7))
firstDayOfWeek.setDate(date.getDate() - dayOffset) return Math.floor((monday - baseDate) / WEEK_MS)
return Math.floor((firstDayOfWeek - baseDate) / WEEK_MS)
}) })
const totalVirtualWeeks = computed(() => { const totalVirtualWeeks = computed(() => {
@ -69,25 +50,18 @@ const initialScrollTop = computed(() => {
const selectedDateRange = computed(() => { const selectedDateRange = computed(() => {
if (!selection.value.start || !selection.value.end) return '' if (!selection.value.start || !selection.value.end) return ''
return formatDateRange( return formatDateRange(fromLocalString(selection.value.start), fromLocalString(selection.value.end))
fromLocalString(selection.value.start),
fromLocalString(selection.value.end),
)
}) })
const todayString = computed(() => { const todayString = computed(() => {
const t = calendarStore.now const t = calendarStore.now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric'}).replace(/,? /, "\n")
.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })
.replace(/,? /, '\n')
return t.charAt(0).toUpperCase() + t.slice(1) return t.charAt(0).toUpperCase() + t.slice(1)
}) })
const visibleWeeks = computed(() => { const visibleWeeks = computed(() => {
const buffer = 10 const buffer = 10
const startIdx = Math.floor((scrollTop.value - buffer * rowHeight.value) / rowHeight.value) const startIdx = Math.floor((scrollTop.value - buffer * rowHeight.value) / rowHeight.value)
const endIdx = Math.ceil( const endIdx = Math.ceil((scrollTop.value + viewportHeight.value + buffer * rowHeight.value) / rowHeight.value)
(scrollTop.value + viewportHeight.value + buffer * rowHeight.value) / rowHeight.value,
)
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value) const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value) const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value)
@ -103,6 +77,7 @@ const contentHeight = computed(() => {
return totalVirtualWeeks.value * rowHeight.value return totalVirtualWeeks.value * rowHeight.value
}) })
// Functions moved from store
function computeRowHeight() { function computeRowHeight() {
const el = document.createElement('div') const el = document.createElement('div')
el.style.position = 'absolute' el.style.position = 'absolute'
@ -116,23 +91,22 @@ function computeRowHeight() {
} }
function getWeekIndex(date) { function getWeekIndex(date) {
const firstDayOfWeek = new Date(date) const monday = new Date(date)
const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7 monday.setDate(date.getDate() - mondayIndex(date))
firstDayOfWeek.setDate(date.getDate() - dayOffset) return Math.floor((monday - baseDate) / WEEK_MS)
return Math.floor((firstDayOfWeek - baseDate) / WEEK_MS)
} }
function getFirstDayForVirtualWeek(virtualWeek) { function getMondayForVirtualWeek(virtualWeek) {
const firstDay = new Date(baseDate) const monday = new Date(baseDate)
firstDay.setDate(firstDay.getDate() + virtualWeek * 7) monday.setDate(monday.getDate() + virtualWeek * 7)
return firstDay return monday
} }
function createWeek(virtualWeek) { function createWeek(virtualWeek) {
const firstDay = getFirstDayForVirtualWeek(virtualWeek) const monday = getMondayForVirtualWeek(virtualWeek)
const weekNumber = isoWeekInfo(firstDay).week const weekNumber = isoWeekInfo(monday).week
const days = [] const days = []
const cur = new Date(firstDay) const cur = new Date(monday)
let hasFirst = false let hasFirst = false
let monthToLabel = null let monthToLabel = null
let labelYear = null let labelYear = null
@ -154,7 +128,7 @@ function createWeek(virtualWeek) {
if (cur.getMonth() === 0) { if (cur.getMonth() === 0) {
displayText = cur.getFullYear() displayText = cur.getFullYear()
} else { } else {
displayText = monthAbbr[cur.getMonth()].slice(0, 3).toUpperCase() displayText = monthAbbr[cur.getMonth()].slice(0,3).toUpperCase()
} }
} }
@ -167,12 +141,8 @@ function createWeek(virtualWeek) {
isWeekend: calendarStore.weekend[dow], isWeekend: calendarStore.weekend[dow],
isFirstDay: isFirst, isFirstDay: isFirst,
lunarPhase: lunarPhaseSymbol(cur), lunarPhase: lunarPhaseSymbol(cur),
isSelected: isSelected: selection.value.start && selection.value.end && dateStr >= selection.value.start && dateStr <= selection.value.end,
selection.value.startDate && events: eventsForDay
selection.value.dayCount > 0 &&
dateStr >= selection.value.startDate &&
dateStr <= addDaysStr(selection.value.startDate, selection.value.dayCount - 1),
events: eventsForDay,
}) })
cur.setDate(cur.getDate() + 1) cur.setDate(cur.getDate() + 1)
} }
@ -180,9 +150,10 @@ function createWeek(virtualWeek) {
let monthLabel = null let monthLabel = null
if (hasFirst && monthToLabel !== null) { if (hasFirst && monthToLabel !== null) {
if (labelYear && labelYear <= calendarStore.config.max_year) { if (labelYear && labelYear <= calendarStore.config.max_year) {
// Calculate how many weeks this month spans
let weeksSpan = 0 let weeksSpan = 0
const d = new Date(cur) const d = new Date(cur)
d.setDate(cur.getDate() - 1) d.setDate(cur.getDate() - 1) // Go back to last day of the week we just processed
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
d.setDate(cur.getDate() - 1 + i * 7) d.setDate(cur.getDate() - 1 + i * 7)
@ -197,7 +168,7 @@ function createWeek(virtualWeek) {
text: `${getLocalizedMonthName(monthToLabel)} '${year}`, text: `${getLocalizedMonthName(monthToLabel)} '${year}`,
month: monthToLabel, month: monthToLabel,
weeksSpan: weeksSpan, weeksSpan: weeksSpan,
height: weeksSpan * rowHeight.value, height: weeksSpan * rowHeight.value
} }
} }
} }
@ -207,7 +178,7 @@ function createWeek(virtualWeek) {
weekNumber: pad(weekNumber), weekNumber: pad(weekNumber),
days, days,
monthLabel, monthLabel,
top: (virtualWeek - minVirtualWeek.value) * rowHeight.value, top: (virtualWeek - minVirtualWeek.value) * rowHeight.value
} }
} }
@ -222,47 +193,39 @@ function goToToday() {
} }
function clearSelection() { function clearSelection() {
selection.value = { startDate: null, dayCount: 0 } selection.value = { start: null, end: null }
} }
function startDrag(dateStr) { function startDrag(dateStr) {
if (calendarStore.config.select_days === 0) return if (calendarStore.config.select_days === 0) return
isDragging.value = true isDragging.value = true
dragAnchor.value = dateStr dragAnchor.value = dateStr
selection.value = { startDate: dateStr, dayCount: 1 } selection.value = { start: dateStr, end: dateStr }
} }
function updateDrag(dateStr) { function updateDrag(dateStr) {
if (!isDragging.value) return if (!isDragging.value) return
const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr) const [start, end] = clampRange(dragAnchor.value, dateStr)
selection.value = { startDate, dayCount } selection.value = { start, end }
} }
function endDrag(dateStr) { function endDrag(dateStr) {
if (!isDragging.value) return if (!isDragging.value) return
isDragging.value = false isDragging.value = false
const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr) const [start, end] = clampRange(dragAnchor.value, dateStr)
selection.value = { startDate, dayCount } selection.value = { start, end }
} }
function calculateSelection(anchorStr, otherStr) { function clampRange(anchorStr, otherStr) {
const limit = calendarStore.config.select_days const limit = calendarStore.config.select_days
const anchorDate = fromLocalString(anchorStr) const forward = fromLocalString(otherStr) >= fromLocalString(anchorStr)
const otherDate = fromLocalString(otherStr)
const forward = otherDate >= anchorDate
const span = daysInclusive(anchorStr, otherStr) const span = daysInclusive(anchorStr, otherStr)
if (span <= limit) { if (span <= limit) {
const startDate = forward ? anchorStr : otherStr const a = [anchorStr, otherStr].sort()
return { startDate, dayCount: span } return [a[0], a[1]]
}
if (forward) {
return { startDate: anchorStr, dayCount: limit }
} else {
const startDate = addDaysStr(anchorStr, -(limit - 1))
return { startDate, dayCount: limit }
} }
if (forward) return [anchorStr, addDaysStr(anchorStr, limit - 1)]
return [addDaysStr(anchorStr, -(limit - 1)), anchorStr]
} }
const onScroll = () => { const onScroll = () => {
@ -278,6 +241,7 @@ const handleJogwheelScrollTo = (newScrollTop) => {
} }
onMounted(() => { onMounted(() => {
// Compute row height and initialize
computeRowHeight() computeRowHeight()
calendarStore.updateCurrentDate() calendarStore.updateCurrentDate()
@ -287,9 +251,10 @@ onMounted(() => {
viewport.value.addEventListener('scroll', onScroll) viewport.value.addEventListener('scroll', onScroll)
} }
// Update time periodically
const timer = setInterval(() => { const timer = setInterval(() => {
calendarStore.updateCurrentDate() calendarStore.updateCurrentDate()
}, 60000) }, 60000) // Update every minute
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearInterval(timer) clearInterval(timer)
@ -315,14 +280,14 @@ const handleDayMouseEnter = (dateStr) => {
const handleDayMouseUp = (dateStr) => { const handleDayMouseUp = (dateStr) => {
if (isDragging.value) { if (isDragging.value) {
endDrag(dateStr) endDrag(dateStr)
const eventData = createEventFromSelection() // Show event dialog if we have a selection
if (eventData) { if (selection.value.start && selection.value.end && eventDialog.value) {
clearSelection() setTimeout(() => eventDialog.value.openCreateDialog(), 50)
emit('create-event', eventData)
} }
} }
} }
// Touch event handlers
const handleDayTouchStart = (dateStr) => { const handleDayTouchStart = (dateStr) => {
startDrag(dateStr) startDrag(dateStr)
} }
@ -336,16 +301,17 @@ const handleDayTouchMove = (dateStr) => {
const handleDayTouchEnd = (dateStr) => { const handleDayTouchEnd = (dateStr) => {
if (isDragging.value) { if (isDragging.value) {
endDrag(dateStr) endDrag(dateStr)
const eventData = createEventFromSelection() // Show event dialog if we have a selection
if (eventData) { if (selection.value.start && selection.value.end && eventDialog.value) {
clearSelection() setTimeout(() => eventDialog.value.openCreateDialog(), 50)
emit('create-event', eventData)
} }
} }
} }
const handleEventClick = (eventInstanceId) => { const handleEventClick = (eventInstanceId) => {
emit('edit-event', eventInstanceId) if (eventDialog.value) {
eventDialog.value.openEditDialog(eventInstanceId)
}
} }
</script> </script>
@ -386,7 +352,7 @@ const handleEventClick = (eventInstanceId) => {
class="month-name-label" class="month-name-label"
:style="{ :style="{
top: week.top + 'px', top: week.top + 'px',
height: week.monthLabel?.height + 'px', height: week.monthLabel?.height + 'px'
}" }"
> >
<span>{{ week.monthLabel?.text }}</span> <span>{{ week.monthLabel?.text }}</span>
@ -402,6 +368,11 @@ const handleEventClick = (eventInstanceId) => {
@scroll-to="handleJogwheelScrollTo" @scroll-to="handleJogwheelScrollTo"
/> />
</div> </div>
<EventDialog
ref="eventDialog"
:selection="selection"
@clear-selection="clearSelection"
/>
</div> </div>
</template> </template>

View File

@ -3,10 +3,9 @@ import { useCalendarStore } from '@/stores/CalendarStore'
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import WeekdaySelector from './WeekdaySelector.vue' import WeekdaySelector from './WeekdaySelector.vue'
import Numeric from './Numeric.vue' import Numeric from './Numeric.vue'
import { addDaysStr } from '@/utils/date'
const props = defineProps({ const props = defineProps({
selection: { type: Object, default: () => ({ startDate: null, dayCount: 0 }) }, selection: { type: Object, default: () => ({ start: null, end: null }) },
}) })
const emit = defineEmits(['clear-selection']) const emit = defineEmits(['clear-selection'])
@ -28,10 +27,9 @@ const eventSaved = ref(false)
const titleInput = ref(null) const titleInput = ref(null)
// Helper to get starting weekday (Sunday-first index) // Helper to get starting weekday (Sunday-first index)
function getStartingWeekday(selectionData = null) { function getStartingWeekday() {
const currentSelection = selectionData || props.selection if (!props.selection.start) return 0 // Default to Sunday
if (!currentSelection.start) return 0 // Default to Sunday const date = new Date(props.selection.start + 'T00:00:00')
const date = new Date(currentSelection.start + 'T00:00:00')
const dayOfWeek = date.getDay() // 0=Sunday, 1=Monday, ... const dayOfWeek = date.getDay() // 0=Sunday, 1=Monday, ...
return dayOfWeek // Keep Sunday-first (0=Sunday, 6=Saturday) return dayOfWeek // Keep Sunday-first (0=Sunday, 6=Saturday)
} }
@ -93,23 +91,7 @@ const selectedColor = computed({
}, },
}) })
function openCreateDialog(selectionData = null) { function openCreateDialog() {
const currentSelection = selectionData || props.selection
// Convert new format to start/end for compatibility with existing logic
let start, end
if (currentSelection.startDate && currentSelection.dayCount) {
start = currentSelection.startDate
end = addDaysStr(currentSelection.startDate, currentSelection.dayCount - 1)
} else if (currentSelection.start && currentSelection.end) {
// Fallback for old format
start = currentSelection.start
end = currentSelection.end
} else {
start = null
end = null
}
occurrenceContext.value = null occurrenceContext.value = null
dialogMode.value = 'create' dialogMode.value = 'create'
title.value = '' title.value = ''
@ -118,16 +100,18 @@ function openCreateDialog(selectionData = null) {
recurrenceFrequency.value = 'weeks' recurrenceFrequency.value = 'weeks'
recurrenceWeekdays.value = [false, false, false, false, false, false, false] recurrenceWeekdays.value = [false, false, false, false, false, false, false]
recurrenceOccurrences.value = 0 recurrenceOccurrences.value = 0
colorId.value = calendarStore.selectEventColorId(start, end) colorId.value = calendarStore.selectEventColorId(props.selection.start, props.selection.end)
eventSaved.value = false eventSaved.value = false
const startingDay = getStartingWeekday({ start, end }) // Auto-select starting day for weekly recurrence
const startingDay = getStartingWeekday()
recurrenceWeekdays.value[startingDay] = true recurrenceWeekdays.value[startingDay] = true
// Create the event immediately in the store
editingEventId.value = calendarStore.createEvent({ editingEventId.value = calendarStore.createEvent({
title: '', title: '',
startDate: start, startDate: props.selection.start,
endDate: end, endDate: props.selection.end,
colorId: colorId.value, colorId: colorId.value,
repeat: repeat.value, repeat: repeat.value,
repeatInterval: recurrenceInterval.value, repeatInterval: recurrenceInterval.value,

View File

@ -34,12 +34,7 @@
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { import { getLocalizedWeekdayNames } from '@/utils/date'
getLocalizedWeekdayNames,
getLocaleFirstDay,
getLocaleWeekendDays,
reorderByFirstDay,
} from '@/utils/date'
const model = defineModel({ const model = defineModel({
type: Array, type: Array,
@ -61,19 +56,21 @@ if (!model.value) model.value = [...props.fallback]
const labelsMondayFirst = getLocalizedWeekdayNames() const labelsMondayFirst = getLocalizedWeekdayNames()
const labels = [labelsMondayFirst[6], ...labelsMondayFirst.slice(0, 6)] const labels = [labelsMondayFirst[6], ...labelsMondayFirst.slice(0, 6)]
const anySelected = computed(() => model.value.some(Boolean)) const anySelected = computed(() => model.value.some(Boolean))
const localeFirst = getLocaleFirstDay() const localeFirst = new Intl.Locale(navigator.language).weekInfo.firstDay % 7
const localeWeekend = getLocaleWeekendDays() const localeWeekend = new Intl.Locale(navigator.language).weekInfo.weekend
const firstDay = computed(() => (props.firstDay ?? localeFirst) % 7) const firstDay = computed(() => (props.firstDay ?? localeFirst) % 7)
const weekendDays = computed(() => { const weekendDays = computed(() => {
if (props.weekend && props.weekend.length === 7) return props.weekend if (props.weekend && props.weekend.length === 7) return props.weekend
return localeWeekend const dayidx = new Set(localeWeekend)
return Array.from({ length: 7 }, (_, i) => dayidx.has(i || 7))
}) })
const displayLabels = computed(() => reorderByFirstDay(labels, firstDay.value)) const reorder = (days) => Array.from({ length: 7 }, (_, i) => days[(i + firstDay.value) % 7])
const displayValuesCommitted = computed(() => reorderByFirstDay(model.value, firstDay.value)) const displayLabels = computed(() => reorder(labels))
const displayWorking = computed(() => reorderByFirstDay(weekendDays.value, firstDay.value)) const displayValuesCommitted = computed(() => reorder(model.value))
const displayDefault = computed(() => reorderByFirstDay(props.fallback, firstDay.value)) const displayWorking = computed(() => reorder(weekendDays.value))
const displayDefault = computed(() => reorder(props.fallback))
// Mapping from display index to original model index // Mapping from display index to original model index
const orderIndices = computed(() => Array.from({ length: 7 }, (_, i) => (i + firstDay.value) % 7)) const orderIndices = computed(() => Array.from({ length: 7 }, (_, i) => (i + firstDay.value) % 7))

View File

@ -1,58 +1,19 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { import { toLocalString, fromLocalString } from '@/utils/date'
toLocalString,
fromLocalString,
getLocaleFirstDay,
getLocaleWeekendDays,
} from '@/utils/date'
/**
* Calendar configuration can be overridden via window.calendarConfig:
*
* window.calendarConfig = {
* firstDay: 0, // 0=Sunday, 1=Monday, etc. (default: 1)
* firstDay: 'auto', // Use locale detection
* weekendDays: [true, false, false, false, false, false, true], // Custom weekend
* weekendDays: 'auto' // Use locale detection (default)
* }
*/
const MIN_YEAR = 1900 const MIN_YEAR = 1900
const MAX_YEAR = 2100 const MAX_YEAR = 2100
// Helper function to determine first day with config override support
function getConfiguredFirstDay() {
// Check for environment variable or global config
const configOverride = window?.calendarConfig?.firstDay
if (configOverride !== undefined) {
return configOverride === 'auto' ? getLocaleFirstDay() : Number(configOverride)
}
// Default to Monday (1) instead of locale
return 1
}
// Helper function to determine weekend days with config override support
function getConfiguredWeekendDays() {
// Check for environment variable or global config
const configOverride = window?.calendarConfig?.weekendDays
if (configOverride !== undefined) {
return configOverride === 'auto' ? getLocaleWeekendDays() : configOverride
}
// Default to locale-based weekend days
return getLocaleWeekendDays()
}
export const useCalendarStore = defineStore('calendar', { export const useCalendarStore = defineStore('calendar', {
state: () => ({ state: () => ({
today: toLocalString(new Date()), today: toLocalString(new Date()),
now: new Date(), now: new Date(),
events: new Map(), // Map of date strings to arrays of events events: new Map(), // Map of date strings to arrays of events
weekend: getConfiguredWeekendDays(), weekend: [true, false, false, false, false, false, true], // Sunday to Saturday
config: { config: {
select_days: 1000, select_days: 1000,
min_year: MIN_YEAR, min_year: MIN_YEAR,
max_year: MAX_YEAR, max_year: MAX_YEAR,
first_day: getConfiguredFirstDay(),
}, },
}), }),

View File

@ -106,42 +106,6 @@ function getLocalizedWeekdayNames() {
return res return res
} }
/**
* Get the locale's first day of the week (0=Sunday, 1=Monday, etc.)
* @returns {number} First day of the week (0-6)
*/
function getLocaleFirstDay() {
try {
return new Intl.Locale(navigator.language).weekInfo.firstDay % 7
} catch {
return 1 // Default to Monday if locale info not available
}
}
/**
* Get the locale's weekend days as an array of booleans (Sunday=index 0)
* @returns {Array<boolean>} Array where true indicates a weekend day
*/
function getLocaleWeekendDays() {
try {
const localeWeekend = new Intl.Locale(navigator.language).weekInfo.weekend
const dayidx = new Set(localeWeekend)
return Array.from({ length: 7 }, (_, i) => dayidx.has(i || 7))
} catch {
return [true, false, false, false, false, false, true] // Default to Saturday/Sunday weekend
}
}
/**
* Reorder a 7-element array based on the first day of the week
* @param {Array} days - Array of 7 elements (Sunday=index 0)
* @param {number} firstDay - First day of the week (0=Sunday, 1=Monday, etc.)
* @returns {Array} Reordered array
*/
function reorderByFirstDay(days, firstDay) {
return Array.from({ length: 7 }, (_, i) => days[(i + firstDay) % 7])
}
/** /**
* Get localized month name * Get localized month name
* @param {number} idx - Month index (0-11) * @param {number} idx - Month index (0-11)
@ -212,9 +176,6 @@ export {
daysInclusive, daysInclusive,
addDaysStr, addDaysStr,
getLocalizedWeekdayNames, getLocalizedWeekdayNames,
getLocaleFirstDay,
getLocaleWeekendDays,
reorderByFirstDay,
getLocalizedMonthName, getLocalizedMonthName,
formatDateRange, formatDateRange,
lunarPhaseSymbol, lunarPhaseSymbol,