Event search (Ctrl+F), locale/RTL handling, weekday selector workday/weekend, refactored event handling, Firefox compatibility #3
@ -21,13 +21,10 @@ const props = defineProps({
|
|||||||
]"
|
]"
|
||||||
:data-date="props.day.date"
|
:data-date="props.day.date"
|
||||||
>
|
>
|
||||||
<h1>{{ props.day.displayText }}</h1>
|
<h1 class="day-number">{{ props.day.displayText }}</h1>
|
||||||
<span v-if="props.day.lunarPhase" class="lunar-phase">{{ props.day.lunarPhase }}</span>
|
<span v-if="props.day.lunarPhase" class="lunar-phase">{{ props.day.lunarPhase }}</span>
|
||||||
|
<div v-if="props.day.holiday" class="holiday-info" dir="auto" :title="props.day.holiday.name">
|
||||||
<div v-if="props.day.holiday" class="holiday-info">
|
{{ props.day.holiday.name }}
|
||||||
<span class="holiday-name" :title="props.day.holiday.name">
|
|
||||||
{{ props.day.holiday.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -38,19 +35,27 @@ const props = defineProps({
|
|||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: row;
|
/* 3 columns: day number, flexible space, lunar phase */
|
||||||
align-items: flex-start;
|
grid-template-columns: min-content 1fr min-content;
|
||||||
justify-content: flex-start;
|
/* 3 rows: header, flexible filler, holiday label */
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
/* Named grid areas (only ones actually used) */
|
||||||
|
grid-template-areas:
|
||||||
|
'day-number . lunar-phase'
|
||||||
|
'day-number . lunar-phase'
|
||||||
|
'holiday-info holiday-info holiday-info';
|
||||||
|
/* Explicit areas mainly for clarity */
|
||||||
|
grid-auto-flow: row;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--row-h);
|
height: var(--row-h);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
.cell h1.day-number {
|
||||||
.cell h1 {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 1.5em;
|
min-width: 1.5em;
|
||||||
@ -58,15 +63,16 @@ const props = defineProps({
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
|
grid-area: day-number;
|
||||||
}
|
}
|
||||||
.cell.weekend h1 {
|
.cell.weekend h1.day-number {
|
||||||
color: var(--weekend);
|
color: var(--weekend);
|
||||||
}
|
}
|
||||||
.cell.firstday h1 {
|
.cell.firstday h1.day-number {
|
||||||
color: var(--firstday);
|
color: var(--firstday);
|
||||||
text-shadow: 0 0 0.1em var(--strong);
|
text-shadow: 0 0 0.1em var(--strong);
|
||||||
}
|
}
|
||||||
.cell.today h1 {
|
.cell.today h1.day-number {
|
||||||
border-radius: 2em;
|
border-radius: 2em;
|
||||||
background: var(--today);
|
background: var(--today);
|
||||||
border: 0.2em solid var(--today);
|
border: 0.2em solid var(--today);
|
||||||
@ -77,16 +83,9 @@ const props = defineProps({
|
|||||||
.cell.selected {
|
.cell.selected {
|
||||||
filter: hue-rotate(180deg);
|
filter: hue-rotate(180deg);
|
||||||
}
|
}
|
||||||
.cell.selected h1 {
|
.cell.selected h1.day-number {
|
||||||
color: var(--strong);
|
color: var(--strong);
|
||||||
}
|
}
|
||||||
.lunar-phase {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5em;
|
|
||||||
right: 0.2em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.cell.holiday {
|
.cell.holiday {
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
@ -103,27 +102,32 @@ const props = defineProps({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.cell.holiday h1 {
|
.cell.holiday h1.day-number {
|
||||||
/* Slight emphasis without forcing a specific hue */
|
/* Slight emphasis without forcing a specific hue */
|
||||||
color: var(--holiday);
|
color: var(--holiday);
|
||||||
text-shadow: 0 0 0.3em rgba(255, 255, 255, 0.4);
|
text-shadow: 0 0 0.3em rgba(255, 255, 255, 0.4);
|
||||||
}
|
}
|
||||||
.holiday-info {
|
.lunar-phase {
|
||||||
position: absolute;
|
grid-area: lunar-phase;
|
||||||
bottom: 0.1em;
|
align-self: start;
|
||||||
left: 0.1em;
|
justify-self: end;
|
||||||
right: 0.1em;
|
margin-top: 0.5em;
|
||||||
line-height: 1;
|
margin-inline-end: 0.2em;
|
||||||
overflow: hidden;
|
font-size: 0.8em;
|
||||||
font-size: clamp(1.2vw, 0.6em, 1em);
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.holiday-name {
|
.holiday-info {
|
||||||
display: block;
|
grid-area: holiday-info;
|
||||||
color: var(--holiday-label);
|
align-self: end;
|
||||||
padding: 0.15em 0.35em 0.15em 0.25em;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
color: var(--holiday-label);
|
||||||
|
font-size: clamp(1.2vw, 0.6em, 1em);
|
||||||
|
line-height: 1;
|
||||||
|
padding-inline: 0.15em;
|
||||||
|
padding-block-end: 0.05em;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -87,7 +87,7 @@ const vwm = createVirtualWeekManager({
|
|||||||
contentHeight,
|
contentHeight,
|
||||||
})
|
})
|
||||||
const visibleWeeks = vwm.visibleWeeks
|
const visibleWeeks = vwm.visibleWeeks
|
||||||
const { scheduleWindowUpdate, resetWeeks, refreshEvents } = vwm
|
const { scheduleWindowUpdate, resetWeeks, refreshEvents, refreshHolidays } = vwm
|
||||||
|
|
||||||
// Scroll managers (after scheduleWindowUpdate available)
|
// Scroll managers (after scheduleWindowUpdate available)
|
||||||
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
|
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
|
||||||
@ -98,8 +98,7 @@ const weekColumnScrollManager = createWeekColumnScrollManager({
|
|||||||
contentHeight,
|
contentHeight,
|
||||||
setScrollTop,
|
setScrollTop,
|
||||||
})
|
})
|
||||||
const { isWeekColDragging, handleWeekColMouseDown, handlePointerLockChange } =
|
const { handleWeekColMouseDown, handlePointerLockChange } = weekColumnScrollManager
|
||||||
weekColumnScrollManager
|
|
||||||
const monthScrollManager = createMonthScrollManager({
|
const monthScrollManager = createMonthScrollManager({
|
||||||
viewport,
|
viewport,
|
||||||
viewportHeight,
|
viewportHeight,
|
||||||
@ -160,6 +159,25 @@ function clearSelection() {
|
|||||||
selection.value = { startDate: null, dayCount: 0 }
|
selection.value = { startDate: null, dayCount: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// React to holiday config changes: rebuild or refresh holidays
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
calendarStore.config.holidays.enabled,
|
||||||
|
calendarStore.config.holidays.country,
|
||||||
|
calendarStore.config.holidays.state,
|
||||||
|
calendarStore.config.holidays.region,
|
||||||
|
],
|
||||||
|
(_newVals, _oldVals) => {
|
||||||
|
// If weeks already built, just refresh holiday info
|
||||||
|
if (visibleWeeks.value.length) {
|
||||||
|
refreshHolidays('config-change')
|
||||||
|
} else {
|
||||||
|
resetWeeks('holiday-config-change')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: false },
|
||||||
|
)
|
||||||
|
|
||||||
function startDrag(dateStr) {
|
function startDrag(dateStr) {
|
||||||
dateStr = normalizeDate(dateStr)
|
dateStr = normalizeDate(dateStr)
|
||||||
if (calendarStore.config.select_days === 0) return
|
if (calendarStore.config.select_days === 0) return
|
||||||
@ -294,15 +312,6 @@ function calculateSelection(anchorStr, otherStr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Week label column drag scrolling ----------------
|
|
||||||
function getWeekLabelRect() {
|
|
||||||
// Prefer header year label width as stable reference
|
|
||||||
const headerYear = document.querySelector('.calendar-header .year-label')
|
|
||||||
if (headerYear) return headerYear.getBoundingClientRect()
|
|
||||||
const weekLabel = viewport.value?.querySelector('.week-row .week-label')
|
|
||||||
return weekLabel ? weekLabel.getBoundingClientRect() : null
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
computeRowHeight()
|
computeRowHeight()
|
||||||
calendarStore.updateCurrentDate()
|
calendarStore.updateCurrentDate()
|
||||||
@ -376,8 +385,6 @@ const handleEventClick = (payload) => {
|
|||||||
emit('edit-event', payload)
|
emit('edit-event', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// header year change delegated to manager
|
|
||||||
|
|
||||||
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
|
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
|
||||||
// We explicitly avoid locale detection; rely solely on characters present.
|
// We explicitly avoid locale detection; rely solely on characters present.
|
||||||
// Disable rotation if any CJK Unified Ideograph or Compatibility Ideograph appears.
|
// Disable rotation if any CJK Unified Ideograph or Compatibility Ideograph appears.
|
||||||
@ -402,7 +409,7 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Watch lightweight mutation counter only (not deep events map) and rebuild lazily
|
// Event changes
|
||||||
watch(
|
watch(
|
||||||
() => calendarStore.events,
|
() => calendarStore.events,
|
||||||
() => {
|
() => {
|
||||||
|
@ -101,12 +101,12 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 1.5rem;
|
margin-inline-end: 2rem;
|
||||||
}
|
}
|
||||||
.toggle-btn {
|
.toggle-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
inset-inline-end: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@ -157,7 +157,6 @@ onBeforeUnmount(() => {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-right: 0.6rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
@ -371,6 +371,28 @@ export function createVirtualWeekManager({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh holiday data for currently visible weeks without rebuilding structure
|
||||||
|
function refreshHolidays(reason = 'holidays-refresh') {
|
||||||
|
if (!visibleWeeks.value.length) return
|
||||||
|
const enabled = calendarStore.config.holidays.enabled
|
||||||
|
if (enabled) calendarStore._ensureHolidaysInitialized?.()
|
||||||
|
for (const week of visibleWeeks.value) {
|
||||||
|
for (const day of week.days) {
|
||||||
|
if (enabled) {
|
||||||
|
const holiday = getHolidayForDate(day.date)
|
||||||
|
;((day.holiday = holiday), (day.isHoliday = holiday !== null))
|
||||||
|
} else {
|
||||||
|
day.holiday = null
|
||||||
|
day.isHoliday = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug('[VirtualWeeks] refreshHolidays', reason, { weeks: visibleWeeks.value.length })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function goToToday() {
|
function goToToday() {
|
||||||
const top = addDays(new Date(calendarStore.now), -21)
|
const top = addDays(new Date(calendarStore.now), -21)
|
||||||
const targetWeekIndex = getWeekIndex(top)
|
const targetWeekIndex = getWeekIndex(top)
|
||||||
@ -391,6 +413,7 @@ export function createVirtualWeekManager({
|
|||||||
resetWeeks,
|
resetWeeks,
|
||||||
updateVisibleWeeks,
|
updateVisibleWeeks,
|
||||||
refreshEvents,
|
refreshEvents,
|
||||||
|
refreshHolidays,
|
||||||
getWeekIndex,
|
getWeekIndex,
|
||||||
getFirstDayForVirtualWeek,
|
getFirstDayForVirtualWeek,
|
||||||
goToToday,
|
goToToday,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user