Improved event compression on tracks of the overlay.
This commit is contained in:
parent
85ce3678ed
commit
57305e531b
@ -1,7 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="week-overlay">
|
<div class="week-overlay" :style="{ gridColumn: '1 / -1' }" ref="weekOverlayRef">
|
||||||
<div
|
<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"
|
:key="span.id"
|
||||||
class="event-span"
|
class="event-span"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
@ -9,7 +15,7 @@
|
|||||||
:data-id="span.id"
|
:data-id="span.id"
|
||||||
:data-n="span._recurrenceIndex != null ? span._recurrenceIndex : 0"
|
:data-n="span._recurrenceIndex != null ? span._recurrenceIndex : 0"
|
||||||
:style="{
|
:style="{
|
||||||
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
|
gridColumn: `${span.startIdxRel + 1} / ${span.endIdxRel + 2}`,
|
||||||
gridRow: `${span.row}`,
|
gridRow: `${span.row}`,
|
||||||
}"
|
}"
|
||||||
@click="handleEventClick(span)"
|
@click="handleEventClick(span)"
|
||||||
@ -26,9 +32,10 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
import { useCalendarStore } from '@/stores/CalendarStore'
|
import { useCalendarStore } from '@/stores/CalendarStore'
|
||||||
import { daysInclusive, addDaysStr } from '@/utils/date'
|
import { daysInclusive, addDaysStr } from '@/utils/date'
|
||||||
|
|
||||||
@ -41,45 +48,123 @@ const store = useCalendarStore()
|
|||||||
// Drag state
|
// Drag state
|
||||||
const dragState = ref(null)
|
const dragState = ref(null)
|
||||||
const justDragged = ref(false)
|
const justDragged = ref(false)
|
||||||
|
const weekOverlayRef = ref(null)
|
||||||
|
const segmentCompression = ref({}) // key -> boolean
|
||||||
|
|
||||||
// Consolidate already-provided day.events into contiguous spans (no recurrence generation)
|
// Build event segments: each segment is a contiguous day range with at least one bridging event between any adjacent days within it.
|
||||||
const eventSpans = computed(() => {
|
const eventSegments = computed(() => {
|
||||||
const weekEvents = new Map()
|
// Construct spans across the week
|
||||||
props.week.days.forEach((day, dayIndex) => {
|
const spanMap = new Map()
|
||||||
|
props.week.days.forEach((day, di) => {
|
||||||
day.events.forEach((ev) => {
|
day.events.forEach((ev) => {
|
||||||
const key = ev.id
|
const k = ev.id
|
||||||
if (!weekEvents.has(key)) {
|
if (!spanMap.has(k)) spanMap.set(k, { ...ev, startIdx: di, endIdx: di })
|
||||||
weekEvents.set(key, { ...ev, startIdx: dayIndex, endIdx: dayIndex })
|
else spanMap.get(k).endIdx = Math.max(spanMap.get(k).endIdx, di)
|
||||||
} else {
|
|
||||||
const ref = weekEvents.get(key)
|
|
||||||
ref.endIdx = Math.max(ref.endIdx, dayIndex)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const arr = Array.from(weekEvents.values())
|
const spans = Array.from(spanMap.values())
|
||||||
arr.sort((a, b) => {
|
// Sort so longer multi-day first, then earlier, then id for stability
|
||||||
const spanA = a.endIdx - a.startIdx
|
spans.sort((a, b) => {
|
||||||
const spanB = b.endIdx - b.startIdx
|
const la = a.endIdx - a.startIdx
|
||||||
if (spanA !== spanB) return spanB - spanA
|
const lb = b.endIdx - b.startIdx
|
||||||
|
if (la !== lb) return lb - la
|
||||||
if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx
|
if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx
|
||||||
// For one-day events that are otherwise equal, sort by color (0 first)
|
const ca = a.colorId != null ? a.colorId : 0
|
||||||
if (spanA === 0 && spanB === 0 && a.startIdx === b.startIdx) {
|
const cb = b.colorId != null ? b.colorId : 0
|
||||||
const colorA = a.colorId || 0
|
if (ca !== cb) return ca - cb
|
||||||
const colorB = b.colorId || 0
|
|
||||||
if (colorA !== colorB) return colorA - colorB
|
|
||||||
}
|
|
||||||
return String(a.id).localeCompare(String(b.id))
|
return String(a.id).localeCompare(String(b.id))
|
||||||
})
|
})
|
||||||
// Assign non-overlapping rows
|
// Identify breaks
|
||||||
const rowsLastEnd = []
|
const breaks = []
|
||||||
arr.forEach((ev) => {
|
for (let d = 0; d < 6; d++) {
|
||||||
let row = 0
|
const bridged = spans.some((sp) => sp.startIdx <= d && sp.endIdx >= d + 1)
|
||||||
while (row < rowsLastEnd.length && !(ev.startIdx > rowsLastEnd[row])) row++
|
if (!bridged) breaks.push(d)
|
||||||
if (row === rowsLastEnd.length) rowsLastEnd.push(-1)
|
}
|
||||||
rowsLastEnd[row] = ev.endIdx
|
const rawSegments = []
|
||||||
ev.row = row + 1
|
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) {
|
function handleEventClick(span) {
|
||||||
@ -403,16 +488,22 @@ function applyRangeDuringDrag(st, startDate, endDate) {
|
|||||||
.week-overlay {
|
.week-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
|
||||||
z-index: 15;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
/* Prevent content from expanding tracks beyond container width */
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
||||||
grid-auto-rows: minmax(0, 1.5em);
|
|
||||||
|
|
||||||
row-gap: 0.05em;
|
|
||||||
margin-top: 1.8em;
|
margin-top: 1.8em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.segment-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
align-content: start;
|
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 {
|
.event-span {
|
||||||
@ -430,13 +521,8 @@ function applyRangeDuringDrag(st, startDate, endDate) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inner title wrapper ensures proper ellipsis within flex/grid constraints */
|
/* Inner title wrapper ensures proper ellipsis within flex/grid constraints */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user