Event search (Ctrl+F), locale/RTL handling, weekday selector workday/weekend, refactored event handling, Firefox compatibility #3

Merged
LeoVasanko merged 17 commits from vol003 into main 2025-08-27 13:41:46 +01:00
Showing only changes of commit 57305e531b - Show all commits

View File

@ -1,7 +1,13 @@
<template>
<div class="week-overlay">
<div class="week-overlay" :style="{ gridColumn: '1 / -1' }" ref="weekOverlayRef">
<div
v-for="span in eventSpans"
v-for="seg in eventSegments"
:key="'seg-' + seg.startIdx + '-' + seg.endIdx"
:class="['segment-grid', { compress: isSegmentCompressed(seg) }]"
:style="segmentStyle(seg)"
>
<div
v-for="span in seg.events"
:key="span.id"
class="event-span"
dir="auto"
@ -9,7 +15,7 @@
:data-id="span.id"
:data-n="span._recurrenceIndex != null ? span._recurrenceIndex : 0"
:style="{
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
gridColumn: `${span.startIdxRel + 1} / ${span.endIdxRel + 2}`,
gridRow: `${span.row}`,
}"
@click="handleEventClick(span)"
@ -26,9 +32,10 @@
></div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useCalendarStore } from '@/stores/CalendarStore'
import { daysInclusive, addDaysStr } from '@/utils/date'
@ -41,45 +48,123 @@ const store = useCalendarStore()
// Drag state
const dragState = ref(null)
const justDragged = ref(false)
const weekOverlayRef = ref(null)
const segmentCompression = ref({}) // key -> boolean
// Consolidate already-provided day.events into contiguous spans (no recurrence generation)
const eventSpans = computed(() => {
const weekEvents = new Map()
props.week.days.forEach((day, dayIndex) => {
// Build event segments: each segment is a contiguous day range with at least one bridging event between any adjacent days within it.
const eventSegments = computed(() => {
// Construct spans across the week
const spanMap = new Map()
props.week.days.forEach((day, di) => {
day.events.forEach((ev) => {
const key = ev.id
if (!weekEvents.has(key)) {
weekEvents.set(key, { ...ev, startIdx: dayIndex, endIdx: dayIndex })
} else {
const ref = weekEvents.get(key)
ref.endIdx = Math.max(ref.endIdx, dayIndex)
}
const k = ev.id
if (!spanMap.has(k)) spanMap.set(k, { ...ev, startIdx: di, endIdx: di })
else spanMap.get(k).endIdx = Math.max(spanMap.get(k).endIdx, di)
})
})
const arr = Array.from(weekEvents.values())
arr.sort((a, b) => {
const spanA = a.endIdx - a.startIdx
const spanB = b.endIdx - b.startIdx
if (spanA !== spanB) return spanB - spanA
const spans = Array.from(spanMap.values())
// Sort so longer multi-day first, then earlier, then id for stability
spans.sort((a, b) => {
const la = a.endIdx - a.startIdx
const lb = b.endIdx - b.startIdx
if (la !== lb) return lb - la
if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx
// For one-day events that are otherwise equal, sort by color (0 first)
if (spanA === 0 && spanB === 0 && a.startIdx === b.startIdx) {
const colorA = a.colorId || 0
const colorB = b.colorId || 0
if (colorA !== colorB) return colorA - colorB
}
const ca = a.colorId != null ? a.colorId : 0
const cb = b.colorId != null ? b.colorId : 0
if (ca !== cb) return ca - cb
return String(a.id).localeCompare(String(b.id))
})
// Assign non-overlapping rows
const rowsLastEnd = []
arr.forEach((ev) => {
let row = 0
while (row < rowsLastEnd.length && !(ev.startIdx > rowsLastEnd[row])) row++
if (row === rowsLastEnd.length) rowsLastEnd.push(-1)
rowsLastEnd[row] = ev.endIdx
ev.row = row + 1
// Identify breaks
const breaks = []
for (let d = 0; d < 6; d++) {
const bridged = spans.some((sp) => sp.startIdx <= d && sp.endIdx >= d + 1)
if (!bridged) breaks.push(d)
}
const rawSegments = []
let segStart = 0
for (const b of breaks) {
rawSegments.push([segStart, b])
segStart = b + 1
}
rawSegments.push([segStart, 6])
const segments = rawSegments.map(([s, e]) => {
const evs = spans.filter((sp) => sp.startIdx >= s && sp.endIdx <= e)
// Row packing in this segment (gap fill)
const rows = [] // each row: intervals
function fits(row, a, b) {
return row.every((iv) => b < iv.start || a > iv.end)
}
function addInterval(row, a, b) {
let inserted = false
for (let i = 0; i < row.length; i++) {
if (b < row[i].start) {
row.splice(i, 0, { start: a, end: b })
inserted = true
break
}
}
if (!inserted) row.push({ start: a, end: b })
}
evs.forEach((ev) => {
let placed = false
for (let r = 0; r < rows.length; r++) {
if (fits(rows[r], ev.startIdx, ev.endIdx)) {
addInterval(rows[r], ev.startIdx, ev.endIdx)
ev.row = r + 1
placed = true
break
}
}
if (!placed) {
rows.push([{ start: ev.startIdx, end: ev.endIdx }])
ev.row = rows.length
}
ev.startIdxRel = ev.startIdx - s
ev.endIdxRel = ev.endIdx - s
})
return arr
return { startIdx: s, endIdx: e, events: evs, rowsCount: rows.length }
})
return segments
})
function segmentStyle(seg) {
return { gridColumn: `${seg.startIdx + 1} / ${seg.endIdx + 2}` }
}
function segmentKey(seg) {
return seg.startIdx + '-' + seg.endIdx
}
function isSegmentCompressed(seg) {
return !!segmentCompression.value[segmentKey(seg)]
}
function recomputeCompression() {
const el = weekOverlayRef.value
if (!el) return
const available = el.clientHeight || 0
if (!available) return
const cs = getComputedStyle(el)
const fontSize = parseFloat(cs.fontSize) || 16
const baseRowPx = fontSize * 1.5 // desired row height (matches CSS 1.5em)
const marginTop = 0 // already applied outside height
const usable = Math.max(0, available - marginTop)
const nextMap = {}
for (const seg of eventSegments.value) {
const desired = (seg.rowsCount || 1) * baseRowPx
nextMap[segmentKey(seg)] = desired > usable
}
segmentCompression.value = nextMap
}
watch(eventSegments, () => nextTick(() => recomputeCompression()))
onMounted(() => {
nextTick(() => recomputeCompression())
window.addEventListener('resize', recomputeCompression)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', recomputeCompression)
})
function handleEventClick(span) {
@ -403,16 +488,22 @@ function applyRangeDuringDrag(st, startDate, endDate) {
.week-overlay {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 15;
display: grid;
/* Prevent content from expanding tracks beyond container width */
grid-template-columns: repeat(7, minmax(0, 1fr));
grid-auto-rows: minmax(0, 1.5em);
row-gap: 0.05em;
grid-template-columns: repeat(7, 1fr);
margin-top: 1.8em;
pointer-events: none;
}
.segment-grid {
display: grid;
gap: 2px;
align-content: start;
pointer-events: none;
overflow: hidden;
grid-auto-columns: 1fr;
grid-auto-rows: 1.5em;
}
.segment-grid.compress {
grid-auto-rows: 1fr;
}
.event-span {
@ -430,13 +521,8 @@ function applyRangeDuringDrag(st, startDate, endDate) {
align-items: center;
position: relative;
user-select: none;
height: 100%;
width: 100%;
max-width: 100%;
box-sizing: border-box;
z-index: 1;
text-align: center;
min-width: 0;
}
/* Inner title wrapper ensures proper ellipsis within flex/grid constraints */