vue #1
29
src/App.vue
29
src/App.vue
@ -1,9 +1,36 @@
|
|||||||
<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 />
|
<CalendarView @create-event="handleCreateEvent" @edit-event="handleEditEvent" />
|
||||||
|
<EventDialog
|
||||||
|
ref="eventDialog"
|
||||||
|
:selection="{ startDate: null, dayCount: 0 }"
|
||||||
|
@clear-selection="handleClearSelection"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -8,7 +8,12 @@
|
|||||||
</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 v.for="week in visibleWeeks" :key="week.virtualWeek" :week="week" :style="{ top: `${(week.virtualWeek - minVirtualWeek) * rowHeight}px` }" />
|
<WeekRow
|
||||||
|
v.for="week in visibleWeeks"
|
||||||
|
:key="week.virtualWeek"
|
||||||
|
:week="week"
|
||||||
|
:style="{ top: `${(week.virtualWeek - minVirtualWeek) * rowHeight}px` }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -16,7 +21,15 @@
|
|||||||
<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 { getLocalizedWeekdayNames, isoWeekInfo, fromLocalString, toLocalString, mondayIndex } from '@/utils/date'
|
import {
|
||||||
|
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()
|
||||||
@ -29,10 +42,10 @@ const visibleWeeks = ref([])
|
|||||||
const config = {
|
const config = {
|
||||||
min_year: 1900,
|
min_year: 1900,
|
||||||
max_year: 2100,
|
max_year: 2100,
|
||||||
weekend: [true, false, false, false, false, false, true] // Sun, Mon, ..., Sat
|
weekend: getLocaleWeekendDays(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseDate = new Date(2024, 0, 1) // 2024 begins with Monday
|
const baseDate = new Date(1970, 0, 4 + getLocaleFirstDay())
|
||||||
const WEEK_MS = 7 * 86400000
|
const WEEK_MS = 7 * 86400000
|
||||||
|
|
||||||
const weekdayNames = getLocalizedWeekdayNames()
|
const weekdayNames = getLocalizedWeekdayNames()
|
||||||
@ -84,13 +97,16 @@ 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(totalVirtualWeeks.value + minVirtualWeek.value - 1, endIdx + minVirtualWeek.value)
|
const endVW = Math.min(
|
||||||
|
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
|
||||||
@ -102,7 +118,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
|
||||||
@ -165,5 +181,4 @@ onMounted(() => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('goToToday', goToTodayHandler)
|
document.removeEventListener('goToToday', goToTodayHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -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, isoWeekInfo, mondayIndex } from '@/utils/date'
|
import { getLocalizedWeekdayNames, reorderByFirstDay, isoWeekInfo } 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,17 +14,22 @@ 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(2024, 0, 1) // Monday
|
const baseDate = new Date(1970, 0, 4 + calendarStore.config.first_day)
|
||||||
const monday = new Date(baseDate)
|
const firstDay = new Date(baseDate)
|
||||||
monday.setDate(monday.getDate() + topVW * 7)
|
firstDay.setDate(firstDay.getDate() + topVW * 7)
|
||||||
return isoWeekInfo(monday).year
|
return isoWeekInfo(firstDay).year
|
||||||
})
|
})
|
||||||
|
|
||||||
const weekdayNames = computed(() => {
|
const weekdayNames = computed(() => {
|
||||||
const names = getLocalizedWeekdayNames()
|
// Get Monday-first names, then reorder by first day, then add weekend info
|
||||||
return names.map((name, i) => ({
|
const mondayFirstNames = getLocalizedWeekdayNames()
|
||||||
|
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: calendarStore.weekend[(i + 1) % 7]
|
isWeekend: reorderedWeekend[i],
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -32,7 +37,14 @@ 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 v-for="day in weekdayNames" :key="day.name" class="dow" :class="{ weekend: day.isWeekend }">{{ day.name }}</div>
|
<div
|
||||||
|
v-for="day in weekdayNames"
|
||||||
|
:key="day.name"
|
||||||
|
class="dow"
|
||||||
|
:class="{ weekend: day.isWeekend }"
|
||||||
|
>
|
||||||
|
{{ day.name }}
|
||||||
|
</div>
|
||||||
<div class="overlay-header-spacer"></div>
|
<div class="overlay-header-spacer"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -70,6 +82,6 @@ const weekdayNames = computed(() => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.overlay-header-spacer {
|
.overlay-header-spacer {
|
||||||
/* Empty spacer for the month label column */
|
grid-area: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -4,22 +4,39 @@ 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 EventDialog from '@/components/EventDialog.vue'
|
import {
|
||||||
import { isoWeekInfo, getLocalizedMonthName, monthAbbr, lunarPhaseSymbol, pad, mondayIndex, daysInclusive, addDaysStr, formatDateRange } from '@/utils/date'
|
isoWeekInfo,
|
||||||
|
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)
|
|
||||||
|
|
||||||
// UI state moved from store
|
const emit = defineEmits(['create-event', 'edit-event'])
|
||||||
|
|
||||||
|
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(2024, 0, 1) // Monday
|
const baseDate = new Date(1970, 0, 4 + calendarStore.config.first_day)
|
||||||
|
|
||||||
// Selection state moved from store
|
const selection = ref({ startDate: null, dayCount: 0 })
|
||||||
const selection = ref({ start: null, end: null })
|
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragAnchor = ref(null)
|
const dragAnchor = ref(null)
|
||||||
|
|
||||||
@ -27,16 +44,18 @@ 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 monday = new Date(date)
|
const firstDayOfWeek = new Date(date)
|
||||||
monday.setDate(date.getDate() - ((date.getDay() + 6) % 7))
|
const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7
|
||||||
return Math.floor((monday - baseDate) / WEEK_MS)
|
firstDayOfWeek.setDate(date.getDate() - dayOffset)
|
||||||
|
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 monday = new Date(date)
|
const firstDayOfWeek = new Date(date)
|
||||||
monday.setDate(date.getDate() - ((date.getDay() + 6) % 7))
|
const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7
|
||||||
return Math.floor((monday - baseDate) / WEEK_MS)
|
firstDayOfWeek.setDate(date.getDate() - dayOffset)
|
||||||
|
return Math.floor((firstDayOfWeek - baseDate) / WEEK_MS)
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalVirtualWeeks = computed(() => {
|
const totalVirtualWeeks = computed(() => {
|
||||||
@ -50,18 +69,25 @@ 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(fromLocalString(selection.value.start), fromLocalString(selection.value.end))
|
return formatDateRange(
|
||||||
|
fromLocalString(selection.value.start),
|
||||||
|
fromLocalString(selection.value.end),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const todayString = computed(() => {
|
const todayString = computed(() => {
|
||||||
const t = calendarStore.now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric'}).replace(/,? /, "\n")
|
const t = calendarStore.now
|
||||||
|
.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((scrollTop.value + viewportHeight.value + buffer * rowHeight.value) / rowHeight.value)
|
const endIdx = Math.ceil(
|
||||||
|
(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)
|
||||||
@ -77,7 +103,6 @@ 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'
|
||||||
@ -91,22 +116,23 @@ function computeRowHeight() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWeekIndex(date) {
|
function getWeekIndex(date) {
|
||||||
const monday = new Date(date)
|
const firstDayOfWeek = new Date(date)
|
||||||
monday.setDate(date.getDate() - mondayIndex(date))
|
const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7
|
||||||
return Math.floor((monday - baseDate) / WEEK_MS)
|
firstDayOfWeek.setDate(date.getDate() - dayOffset)
|
||||||
|
return Math.floor((firstDayOfWeek - baseDate) / WEEK_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMondayForVirtualWeek(virtualWeek) {
|
function getFirstDayForVirtualWeek(virtualWeek) {
|
||||||
const monday = new Date(baseDate)
|
const firstDay = new Date(baseDate)
|
||||||
monday.setDate(monday.getDate() + virtualWeek * 7)
|
firstDay.setDate(firstDay.getDate() + virtualWeek * 7)
|
||||||
return monday
|
return firstDay
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWeek(virtualWeek) {
|
function createWeek(virtualWeek) {
|
||||||
const monday = getMondayForVirtualWeek(virtualWeek)
|
const firstDay = getFirstDayForVirtualWeek(virtualWeek)
|
||||||
const weekNumber = isoWeekInfo(monday).week
|
const weekNumber = isoWeekInfo(firstDay).week
|
||||||
const days = []
|
const days = []
|
||||||
const cur = new Date(monday)
|
const cur = new Date(firstDay)
|
||||||
let hasFirst = false
|
let hasFirst = false
|
||||||
let monthToLabel = null
|
let monthToLabel = null
|
||||||
let labelYear = null
|
let labelYear = null
|
||||||
@ -128,7 +154,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,8 +167,12 @@ function createWeek(virtualWeek) {
|
|||||||
isWeekend: calendarStore.weekend[dow],
|
isWeekend: calendarStore.weekend[dow],
|
||||||
isFirstDay: isFirst,
|
isFirstDay: isFirst,
|
||||||
lunarPhase: lunarPhaseSymbol(cur),
|
lunarPhase: lunarPhaseSymbol(cur),
|
||||||
isSelected: selection.value.start && selection.value.end && dateStr >= selection.value.start && dateStr <= selection.value.end,
|
isSelected:
|
||||||
events: eventsForDay
|
selection.value.startDate &&
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -150,10 +180,9 @@ 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) // Go back to last day of the week we just processed
|
d.setDate(cur.getDate() - 1)
|
||||||
|
|
||||||
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)
|
||||||
@ -168,7 +197,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,7 +207,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,39 +222,47 @@ function goToToday() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
selection.value = { start: null, end: null }
|
selection.value = { startDate: null, dayCount: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = { start: dateStr, end: dateStr }
|
selection.value = { startDate: dateStr, dayCount: 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDrag(dateStr) {
|
function updateDrag(dateStr) {
|
||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
const [start, end] = clampRange(dragAnchor.value, dateStr)
|
const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr)
|
||||||
selection.value = { start, end }
|
selection.value = { startDate, dayCount }
|
||||||
}
|
}
|
||||||
|
|
||||||
function endDrag(dateStr) {
|
function endDrag(dateStr) {
|
||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
const [start, end] = clampRange(dragAnchor.value, dateStr)
|
const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr)
|
||||||
selection.value = { start, end }
|
selection.value = { startDate, dayCount }
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampRange(anchorStr, otherStr) {
|
function calculateSelection(anchorStr, otherStr) {
|
||||||
const limit = calendarStore.config.select_days
|
const limit = calendarStore.config.select_days
|
||||||
const forward = fromLocalString(otherStr) >= fromLocalString(anchorStr)
|
const anchorDate = 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 a = [anchorStr, otherStr].sort()
|
const startDate = forward ? anchorStr : otherStr
|
||||||
return [a[0], a[1]]
|
return { startDate, dayCount: span }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
@ -241,7 +278,6 @@ const handleJogwheelScrollTo = (newScrollTop) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Compute row height and initialize
|
|
||||||
computeRowHeight()
|
computeRowHeight()
|
||||||
calendarStore.updateCurrentDate()
|
calendarStore.updateCurrentDate()
|
||||||
|
|
||||||
@ -251,10 +287,9 @@ 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) // Update every minute
|
}, 60000)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
@ -280,14 +315,14 @@ const handleDayMouseEnter = (dateStr) => {
|
|||||||
const handleDayMouseUp = (dateStr) => {
|
const handleDayMouseUp = (dateStr) => {
|
||||||
if (isDragging.value) {
|
if (isDragging.value) {
|
||||||
endDrag(dateStr)
|
endDrag(dateStr)
|
||||||
// Show event dialog if we have a selection
|
const eventData = createEventFromSelection()
|
||||||
if (selection.value.start && selection.value.end && eventDialog.value) {
|
if (eventData) {
|
||||||
setTimeout(() => eventDialog.value.openCreateDialog(), 50)
|
clearSelection()
|
||||||
|
emit('create-event', eventData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch event handlers
|
|
||||||
const handleDayTouchStart = (dateStr) => {
|
const handleDayTouchStart = (dateStr) => {
|
||||||
startDrag(dateStr)
|
startDrag(dateStr)
|
||||||
}
|
}
|
||||||
@ -301,17 +336,16 @@ const handleDayTouchMove = (dateStr) => {
|
|||||||
const handleDayTouchEnd = (dateStr) => {
|
const handleDayTouchEnd = (dateStr) => {
|
||||||
if (isDragging.value) {
|
if (isDragging.value) {
|
||||||
endDrag(dateStr)
|
endDrag(dateStr)
|
||||||
// Show event dialog if we have a selection
|
const eventData = createEventFromSelection()
|
||||||
if (selection.value.start && selection.value.end && eventDialog.value) {
|
if (eventData) {
|
||||||
setTimeout(() => eventDialog.value.openCreateDialog(), 50)
|
clearSelection()
|
||||||
|
emit('create-event', eventData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEventClick = (eventInstanceId) => {
|
const handleEventClick = (eventInstanceId) => {
|
||||||
if (eventDialog.value) {
|
emit('edit-event', eventInstanceId)
|
||||||
eventDialog.value.openEditDialog(eventInstanceId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -352,7 +386,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>
|
||||||
@ -368,11 +402,6 @@ const handleEventClick = (eventInstanceId) => {
|
|||||||
@scroll-to="handleJogwheelScrollTo"
|
@scroll-to="handleJogwheelScrollTo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<EventDialog
|
|
||||||
ref="eventDialog"
|
|
||||||
:selection="selection"
|
|
||||||
@clear-selection="clearSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -3,9 +3,10 @@ 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: () => ({ start: null, end: null }) },
|
selection: { type: Object, default: () => ({ startDate: null, dayCount: 0 }) },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['clear-selection'])
|
const emit = defineEmits(['clear-selection'])
|
||||||
@ -27,9 +28,10 @@ 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() {
|
function getStartingWeekday(selectionData = null) {
|
||||||
if (!props.selection.start) return 0 // Default to Sunday
|
const currentSelection = selectionData || props.selection
|
||||||
const date = new Date(props.selection.start + 'T00:00:00')
|
if (!currentSelection.start) return 0 // Default to Sunday
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -91,7 +93,23 @@ const selectedColor = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function openCreateDialog() {
|
function openCreateDialog(selectionData = null) {
|
||||||
|
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 = ''
|
||||||
@ -100,18 +118,16 @@ function openCreateDialog() {
|
|||||||
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(props.selection.start, props.selection.end)
|
colorId.value = calendarStore.selectEventColorId(start, end)
|
||||||
eventSaved.value = false
|
eventSaved.value = false
|
||||||
|
|
||||||
// Auto-select starting day for weekly recurrence
|
const startingDay = getStartingWeekday({ start, end })
|
||||||
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: props.selection.start,
|
startDate: start,
|
||||||
endDate: props.selection.end,
|
endDate: end,
|
||||||
colorId: colorId.value,
|
colorId: colorId.value,
|
||||||
repeat: repeat.value,
|
repeat: repeat.value,
|
||||||
repeatInterval: recurrenceInterval.value,
|
repeatInterval: recurrenceInterval.value,
|
||||||
|
@ -34,7 +34,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { getLocalizedWeekdayNames } from '@/utils/date'
|
import {
|
||||||
|
getLocalizedWeekdayNames,
|
||||||
|
getLocaleFirstDay,
|
||||||
|
getLocaleWeekendDays,
|
||||||
|
reorderByFirstDay,
|
||||||
|
} from '@/utils/date'
|
||||||
|
|
||||||
const model = defineModel({
|
const model = defineModel({
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -56,21 +61,19 @@ 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 = new Intl.Locale(navigator.language).weekInfo.firstDay % 7
|
const localeFirst = getLocaleFirstDay()
|
||||||
const localeWeekend = new Intl.Locale(navigator.language).weekInfo.weekend
|
const localeWeekend = getLocaleWeekendDays()
|
||||||
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
|
||||||
const dayidx = new Set(localeWeekend)
|
return localeWeekend
|
||||||
return Array.from({ length: 7 }, (_, i) => dayidx.has(i || 7))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const reorder = (days) => Array.from({ length: 7 }, (_, i) => days[(i + firstDay.value) % 7])
|
const displayLabels = computed(() => reorderByFirstDay(labels, firstDay.value))
|
||||||
const displayLabels = computed(() => reorder(labels))
|
const displayValuesCommitted = computed(() => reorderByFirstDay(model.value, firstDay.value))
|
||||||
const displayValuesCommitted = computed(() => reorder(model.value))
|
const displayWorking = computed(() => reorderByFirstDay(weekendDays.value, firstDay.value))
|
||||||
const displayWorking = computed(() => reorder(weekendDays.value))
|
const displayDefault = computed(() => reorderByFirstDay(props.fallback, firstDay.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))
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { toLocalString, fromLocalString } from '@/utils/date'
|
import {
|
||||||
|
toLocalString,
|
||||||
|
fromLocalString,
|
||||||
|
getLocaleFirstDay,
|
||||||
|
getLocaleWeekendDays,
|
||||||
|
} from '@/utils/date'
|
||||||
|
|
||||||
const MIN_YEAR = 1900
|
const MIN_YEAR = 1900
|
||||||
const MAX_YEAR = 2100
|
const MAX_YEAR = 2100
|
||||||
@ -9,11 +14,12 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
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: [true, false, false, false, false, false, true], // Sunday to Saturday
|
weekend: getLocaleWeekendDays(),
|
||||||
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: getLocaleFirstDay(),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -106,6 +106,42 @@ 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)
|
||||||
@ -176,6 +212,9 @@ export {
|
|||||||
daysInclusive,
|
daysInclusive,
|
||||||
addDaysStr,
|
addDaysStr,
|
||||||
getLocalizedWeekdayNames,
|
getLocalizedWeekdayNames,
|
||||||
|
getLocaleFirstDay,
|
||||||
|
getLocaleWeekendDays,
|
||||||
|
reorderByFirstDay,
|
||||||
getLocalizedMonthName,
|
getLocalizedMonthName,
|
||||||
formatDateRange,
|
formatDateRange,
|
||||||
lunarPhaseSymbol,
|
lunarPhaseSymbol,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user