Improved event compression on tracks of the overlay.
This commit is contained in:
parent
85ce3678ed
commit
57305e531b
@ -1,34 +1,41 @@
|
||||
<template>
|
||||
<div class="week-overlay">
|
||||
<div class="week-overlay" :style="{ gridColumn: '1 / -1' }" ref="weekOverlayRef">
|
||||
<div
|
||||
v-for="span in eventSpans"
|
||||
:key="span.id"
|
||||
class="event-span"
|
||||
dir="auto"
|
||||
:class="[`event-color-${span.colorId}`]"
|
||||
:data-id="span.id"
|
||||
:data-n="span._recurrenceIndex != null ? span._recurrenceIndex : 0"
|
||||
:style="{
|
||||
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
|
||||
gridRow: `${span.row}`,
|
||||
}"
|
||||
@click="handleEventClick(span)"
|
||||
@pointerdown="handleEventPointerDown(span, $event)"
|
||||
v-for="seg in eventSegments"
|
||||
:key="'seg-' + seg.startIdx + '-' + seg.endIdx"
|
||||
:class="['segment-grid', { compress: isSegmentCompressed(seg) }]"
|
||||
:style="segmentStyle(seg)"
|
||||
>
|
||||
<span class="event-title">{{ span.title }}</span>
|
||||
<div
|
||||
class="resize-handle left"
|
||||
@pointerdown="handleResizePointerDown(span, 'resize-left', $event)"
|
||||
></div>
|
||||
<div
|
||||
class="resize-handle right"
|
||||
@pointerdown="handleResizePointerDown(span, 'resize-right', $event)"
|
||||
></div>
|
||||
v-for="span in seg.events"
|
||||
:key="span.id"
|
||||
class="event-span"
|
||||
dir="auto"
|
||||
:class="[`event-color-${span.colorId}`]"
|
||||
:data-id="span.id"
|
||||
:data-n="span._recurrenceIndex != null ? span._recurrenceIndex : 0"
|
||||
:style="{
|
||||
gridColumn: `${span.startIdxRel + 1} / ${span.endIdxRel + 2}`,
|
||||
gridRow: `${span.row}`,
|
||||
}"
|
||||
@click="handleEventClick(span)"
|
||||
@pointerdown="handleEventPointerDown(span, $event)"
|
||||
>
|
||||
<span class="event-title">{{ span.title }}</span>
|
||||
<div
|
||||
class="resize-handle left"
|
||||
@pointerdown="handleResizePointerDown(span, 'resize-left', $event)"
|
||||
></div>
|
||||
<div
|
||||
class="resize-handle right"
|
||||
@pointerdown="handleResizePointerDown(span, 'resize-right', $event)"
|
||||
></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 { startIdx: s, endIdx: e, events: evs, rowsCount: rows.length }
|
||||
})
|
||||
return arr
|
||||
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 */
|
||||
|
Loading…
x
Reference in New Issue
Block a user