Major new version (#2)
Release Notes Architecture - Component refactor: removed monoliths (`Calendar.vue`, `CalendarGrid.vue`); expanded granular view + header/control components. - Dialog system introduced (`BaseDialog`, `SettingsDialog`). State & Data - Store redesigned: Map-based events + recurrence map; mutation counters. - Local persistence + undo/redo history (custom plugins). Date & Holidays - Migrated all date logic to `date-fns` (+ tz). - Added national holiday support (toggle + loading utilities). Recurrence & Events - Consolidated recurrence handling; fixes for monthly edge days (29–31), annual, multi‑day, and complex weekly repeats. - Reliable splitting/moving/resizing/deletion of repeating and multi‑day events. Interaction & UX - Double‑tap to create events; improved drag (multi‑day + position retention). - Scroll & inertial/momentum navigation; year change via numeric scroller. - Movable event dialog; live settings application. Performance - Progressive / virtual week rendering, reduced off‑screen buffer. - Targeted repaint strategy; minimized full re-renders. Plugins Added - History, undo normalization, persistence, scroll manager, virtual weeks. Styling & Layout - Responsive + compact layout refinements; header restructured. - Simplified visual elements (removed dots/overflow text); holiday styling adjustments. Reliability / Fixes - Numerous recurrence, deletion, orientation/rotation, and event indexing corrections. - Cross-browser fallback (Firefox week info). Dependencies Added - date-fns, date-fns-tz, date-holidays, pinia-plugin-persistedstate. Net Change - 28 files modified; ~4.4K insertions / ~2.2K deletions (major refactor + feature set).
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import {
|
||||
getLocalizedWeekdayNames,
|
||||
getLocaleFirstDay,
|
||||
@@ -44,7 +44,10 @@ import {
|
||||
const model = defineModel({
|
||||
type: Array,
|
||||
default: () => [false, false, false, false, false, false, false],
|
||||
})
|
||||
}) // external value consumers see
|
||||
|
||||
// Internal state preserves the user's explicit picks even if all false
|
||||
const internal = ref([false, false, false, false, false, false, false])
|
||||
|
||||
const props = defineProps({
|
||||
weekend: { type: Array, default: undefined },
|
||||
@@ -55,12 +58,11 @@ const props = defineProps({
|
||||
firstDay: { type: Number, default: null },
|
||||
})
|
||||
|
||||
// If external model provided is entirely false, keep as-is (user will see fallback styling),
|
||||
// only overwrite if null/undefined.
|
||||
if (!model.value) model.value = [...props.fallback]
|
||||
// Initialize internal from external if it has any true; else keep empty (fallback handled on emit)
|
||||
if (model.value?.some?.(Boolean)) internal.value = [...model.value]
|
||||
const labelsMondayFirst = getLocalizedWeekdayNames()
|
||||
const labels = [labelsMondayFirst[6], ...labelsMondayFirst.slice(0, 6)]
|
||||
const anySelected = computed(() => model.value.some(Boolean))
|
||||
const anySelected = computed(() => internal.value.some(Boolean))
|
||||
const localeFirst = getLocaleFirstDay()
|
||||
const localeWeekend = getLocaleWeekendDays()
|
||||
const firstDay = computed(() => (props.firstDay ?? localeFirst) % 7)
|
||||
@@ -71,10 +73,38 @@ const weekendDays = computed(() => {
|
||||
})
|
||||
|
||||
const displayLabels = computed(() => reorderByFirstDay(labels, firstDay.value))
|
||||
const displayValuesCommitted = computed(() => reorderByFirstDay(model.value, firstDay.value))
|
||||
const displayValuesCommitted = computed(() => reorderByFirstDay(internal.value, firstDay.value))
|
||||
const displayWorking = computed(() => reorderByFirstDay(weekendDays.value, firstDay.value))
|
||||
const displayDefault = computed(() => reorderByFirstDay(props.fallback, firstDay.value))
|
||||
|
||||
// Expose a normalized pattern (Sunday-first) that substitutes the fallback day if none selected.
|
||||
// This keeps UI visually showing fallback (muted) but downstream logic can opt-in by reading this.
|
||||
function computeFallbackPattern() {
|
||||
const fb = props.fallback && props.fallback.length === 7 ? props.fallback : null
|
||||
if (fb && fb.some(Boolean)) return [...fb]
|
||||
const arr = [false, false, false, false, false, false, false]
|
||||
const idx = fb ? fb.findIndex(Boolean) : -1
|
||||
if (idx >= 0) arr[idx] = true
|
||||
else arr[0] = true
|
||||
return arr
|
||||
}
|
||||
function emitExternal() {
|
||||
model.value = internal.value.some(Boolean) ? [...internal.value] : computeFallbackPattern()
|
||||
}
|
||||
emitExternal()
|
||||
watch(
|
||||
() => model.value,
|
||||
(nv) => {
|
||||
if (!nv) return
|
||||
if (!nv.some(Boolean)) return
|
||||
const fb = computeFallbackPattern()
|
||||
const isFallback = fb.every((v, i) => v === nv[i])
|
||||
// If internal is empty and model only reflects fallback, do not sync into internal
|
||||
if (isFallback && !internal.value.some(Boolean)) return
|
||||
internal.value = [...nv]
|
||||
},
|
||||
)
|
||||
|
||||
// Mapping from display index to original model index
|
||||
const orderIndices = computed(() => Array.from({ length: 7 }, (_, i) => (i + firstDay.value) % 7))
|
||||
|
||||
@@ -135,8 +165,8 @@ function isPressing(di) {
|
||||
}
|
||||
|
||||
function onPointerDown(di) {
|
||||
originalValues = [...model.value]
|
||||
dragVal.value = !model.value[(di + firstDay.value) % 7]
|
||||
originalValues = [...internal.value]
|
||||
dragVal.value = !internal.value[(di + firstDay.value) % 7]
|
||||
dragStart.value = di
|
||||
previewEnd.value = di
|
||||
dragging.value = true
|
||||
@@ -155,7 +185,8 @@ function onPointerUp() {
|
||||
// simple click: toggle single
|
||||
const next = [...originalValues]
|
||||
next[(dragStart.value + firstDay.value) % 7] = dragVal.value
|
||||
model.value = next
|
||||
internal.value = next
|
||||
emitExternal()
|
||||
cleanupDrag()
|
||||
} else {
|
||||
commitDrag()
|
||||
@@ -169,7 +200,8 @@ function commitDrag() {
|
||||
: [previewEnd.value, dragStart.value]
|
||||
const next = [...originalValues]
|
||||
for (let di = s; di <= e; di++) next[orderIndices.value[di]] = dragVal.value
|
||||
model.value = next
|
||||
internal.value = next
|
||||
emitExternal()
|
||||
cleanupDrag()
|
||||
}
|
||||
function cancelDrag() {
|
||||
@@ -185,14 +217,15 @@ function cleanupDrag() {
|
||||
function toggleWeekend(work) {
|
||||
const base = weekendDays.value
|
||||
const target = work ? base : base.map((v) => !v)
|
||||
const current = model.value
|
||||
const current = internal.value
|
||||
const allOn = current.every(Boolean)
|
||||
const isTargetActive = current.every((v, i) => v === target[i])
|
||||
if (allOn || isTargetActive) {
|
||||
model.value = [false, false, false, false, false, false, false]
|
||||
internal.value = [false, false, false, false, false, false, false]
|
||||
} else {
|
||||
model.value = [...target]
|
||||
internal.value = [...target]
|
||||
}
|
||||
emitExternal()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user