654 lines
20 KiB
Vue
654 lines
20 KiB
Vue
<template>
|
|
<div class="week-overlay" :style="{ gridColumn: '1 / -1' }" ref="weekOverlayRef">
|
|
<div
|
|
v-for="seg in eventSegments"
|
|
:key="'seg-' + seg.startIdx + '-' + seg.endIdx"
|
|
class="segment-grid"
|
|
:style="{
|
|
...segmentStyle(seg),
|
|
'--segment-row-height': getSegmentRowHeight(seg),
|
|
height: getSegmentTotalHeight(seg)
|
|
}"
|
|
>
|
|
<div
|
|
v-for="span in seg.events"
|
|
:key="span.id + '-' + (span.n != null ? span.n : 0)"
|
|
class="event-span"
|
|
dir="auto"
|
|
:class="[
|
|
`event-color-${span.colorId}`,
|
|
{ 'cont-prev': span.hasPrevWeek, 'cont-next': span.hasNextWeek },
|
|
]"
|
|
:data-id="span.id"
|
|
:data-n="span.n != null ? span.n : 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
|
|
v-if="!span.hasPrevWeek"
|
|
class="resize-handle left"
|
|
@pointerdown="handleResizePointerDown(span, 'resize-left', $event)"
|
|
></div>
|
|
<div
|
|
v-if="!span.hasNextWeek"
|
|
class="resize-handle right"
|
|
@pointerdown="handleResizePointerDown(span, 'resize-right', $event)"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script setup>
|
|
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
import { useCalendarStore } from '@/stores/CalendarStore'
|
|
import { daysInclusive, addDaysStr } from '@/utils/date'
|
|
|
|
const props = defineProps({
|
|
week: { type: Object, required: true },
|
|
})
|
|
const emit = defineEmits(['event-click'])
|
|
const store = useCalendarStore()
|
|
|
|
// Drag state
|
|
const dragState = ref(null)
|
|
const justDragged = ref(false)
|
|
const weekOverlayRef = ref(null)
|
|
const segmentCompression = ref({}) // key -> boolean
|
|
|
|
// 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 + '|' + (ev.n ?? 0)
|
|
if (!spanMap.has(key)) {
|
|
// Track min/max nDay (offset inside the occurrence) to know if the span is clipped by week boundaries
|
|
spanMap.set(key, {
|
|
...ev,
|
|
startIdx: di,
|
|
endIdx: di,
|
|
minNDay: ev.nDay,
|
|
maxNDay: ev.nDay,
|
|
})
|
|
} else {
|
|
const sp = spanMap.get(key)
|
|
sp.endIdx = Math.max(sp.endIdx, di)
|
|
if (ev.nDay < sp.minNDay) sp.minNDay = ev.nDay
|
|
if (ev.nDay > sp.maxNDay) sp.maxNDay = ev.nDay
|
|
}
|
|
})
|
|
})
|
|
const spans = Array.from(spanMap.values())
|
|
// Derive span start/end date strings from week day indices (removes need for per-day stored endDate)
|
|
spans.forEach((sp) => {
|
|
sp.startDate = props.week.days[sp.startIdx].date
|
|
sp.endDate = props.week.days[sp.endIdx].date
|
|
// Determine if this span actually continues beyond the visible week on either side.
|
|
// If it begins on the first day column but its first occurrence day offset (minNDay) > 0, the event started earlier.
|
|
sp.hasPrevWeek = sp.startIdx === 0 && sp.minNDay > 0
|
|
// If it ends on the last day column but we have not yet reached the final day of the occurrence (maxNDay < days-1), it continues.
|
|
if (sp.days != null) {
|
|
sp.hasNextWeek = sp.endIdx === 6 && sp.maxNDay < sp.days - 1
|
|
} else {
|
|
sp.hasNextWeek = false
|
|
}
|
|
// Compute full occurrence start/end (may extend beyond visible week)
|
|
if (sp.minNDay != null) {
|
|
sp.occurrenceStartDate = addDaysStr(sp.startDate, -sp.minNDay)
|
|
if (sp.days != null) {
|
|
sp.occurrenceEndDate = addDaysStr(sp.occurrenceStartDate, sp.days - 1)
|
|
} else {
|
|
// Fallback: approximate using maxNDay if days unknown
|
|
const total = (sp.maxNDay || 0) + 1
|
|
sp.occurrenceEndDate = addDaysStr(sp.occurrenceStartDate, total - 1)
|
|
}
|
|
}
|
|
})
|
|
// 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
|
|
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))
|
|
})
|
|
// 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 segments
|
|
})
|
|
|
|
function segmentStyle(seg) {
|
|
return { gridColumn: `${seg.startIdx + 1} / ${seg.endIdx + 2}` }
|
|
}
|
|
|
|
function segmentKey(seg) {
|
|
return seg.startIdx + '-' + seg.endIdx
|
|
}
|
|
|
|
function getSegmentRowHeight(seg) {
|
|
const data = segmentCompression.value[segmentKey(seg)]
|
|
return data && typeof data.rowHeight === 'number' ? `${data.rowHeight}px` : '1.5em'
|
|
}
|
|
|
|
function getSegmentTotalHeight(seg) {
|
|
const data = segmentCompression.value[segmentKey(seg)]
|
|
return data && typeof data.totalHeight === 'number' ? `${data.totalHeight}px` : 'auto'
|
|
}
|
|
|
|
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 // 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 rowCount = seg.rowsCount || 1
|
|
const desired = rowCount * baseRowPx
|
|
const needsScaling = desired > usable
|
|
|
|
// Row height may be reduced to fit segment within available vertical space
|
|
let finalRowHeight = baseRowPx
|
|
if (needsScaling) {
|
|
const scaledRowHeight = usable / rowCount
|
|
finalRowHeight = Math.min(scaledRowHeight, baseRowPx)
|
|
}
|
|
|
|
// Event-level scaling not applied for horizontal fitting in this task
|
|
const segmentData = {
|
|
rowHeight: finalRowHeight,
|
|
totalHeight: needsScaling ? usable : desired,
|
|
events: {}
|
|
}
|
|
|
|
// Populate per-event map (reserved for future use)
|
|
for (const event of seg.events) {
|
|
segmentData.events[event.id + '-' + (event.n || 0)] = {}
|
|
}
|
|
|
|
nextMap[segmentKey(seg)] = segmentData
|
|
}
|
|
segmentCompression.value = nextMap
|
|
}
|
|
|
|
watch(eventSegments, () => nextTick(() => recomputeCompression()))
|
|
onMounted(() => {
|
|
nextTick(() => recomputeCompression())
|
|
window.addEventListener('resize', recomputeCompression)
|
|
})
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', recomputeCompression)
|
|
})
|
|
|
|
function handleEventClick(span) {
|
|
if (justDragged.value) return
|
|
emit('event-click', { id: span.id, n: span.n != null ? span.n : 0 })
|
|
}
|
|
|
|
function handleEventPointerDown(span, event) {
|
|
if (event.target.classList.contains('resize-handle')) return
|
|
event.stopPropagation()
|
|
const baseId = span.id
|
|
// Use full occurrence boundaries for drag logic (not clipped week portion)
|
|
const fullStart = span.occurrenceStartDate || span.startDate
|
|
const fullEnd = span.occurrenceEndDate || span.endDate
|
|
let anchorDate = fullStart
|
|
try {
|
|
const spanDays = daysInclusive(span.startDate, span.endDate)
|
|
const targetEl = event.currentTarget
|
|
if (targetEl && spanDays > 0) {
|
|
const rect = targetEl.getBoundingClientRect()
|
|
const relX = event.clientX - rect.left
|
|
const dayWidth = rect.width / spanDays
|
|
let dayIndex = Math.floor(relX / dayWidth)
|
|
if (!isFinite(dayIndex)) dayIndex = 0
|
|
if (dayIndex < 0) dayIndex = 0
|
|
if (dayIndex >= spanDays) dayIndex = spanDays - 1
|
|
const absoluteOffset = (span.minNDay || 0) + dayIndex
|
|
anchorDate = addDaysStr(fullStart, absoluteOffset)
|
|
}
|
|
} catch (e) {}
|
|
startLocalDrag(
|
|
{
|
|
id: baseId,
|
|
originalId: span.id,
|
|
mode: 'move',
|
|
pointerStartX: event.clientX,
|
|
pointerStartY: event.clientY,
|
|
anchorDate,
|
|
startDate: fullStart,
|
|
endDate: fullEnd,
|
|
n: span.n,
|
|
},
|
|
event,
|
|
)
|
|
}
|
|
|
|
function handleResizePointerDown(span, mode, event) {
|
|
event.stopPropagation()
|
|
const baseId = span.id
|
|
const fullStart = span.occurrenceStartDate || span.startDate
|
|
const fullEnd = span.occurrenceEndDate || span.endDate
|
|
startLocalDrag(
|
|
{
|
|
id: baseId,
|
|
originalId: span.id,
|
|
mode,
|
|
pointerStartX: event.clientX,
|
|
pointerStartY: event.clientY,
|
|
anchorDate: null,
|
|
startDate: fullStart,
|
|
endDate: fullEnd,
|
|
n: span.n,
|
|
},
|
|
event,
|
|
)
|
|
}
|
|
|
|
// Local drag handling
|
|
function startLocalDrag(init, evt) {
|
|
const spanDays = daysInclusive(init.startDate, init.endDate)
|
|
let anchorOffset = 0
|
|
if (init.mode === 'move' && init.anchorDate) {
|
|
if (init.anchorDate < init.startDate) anchorOffset = 0
|
|
else if (init.anchorDate > init.endDate) anchorOffset = spanDays - 1
|
|
else anchorOffset = daysInclusive(init.startDate, init.anchorDate) - 1
|
|
}
|
|
|
|
let originalWeekday = null
|
|
let originalPattern = null
|
|
if (init.mode === 'move') {
|
|
originalWeekday = new Date(init.startDate + 'T00:00:00').getDay()
|
|
const baseEv = store.getEventById(init.id)
|
|
if (
|
|
baseEv &&
|
|
baseEv.recur &&
|
|
baseEv.recur.freq === 'weeks' &&
|
|
Array.isArray(baseEv.recur.weekdays)
|
|
) {
|
|
originalPattern = [...baseEv.recur.weekdays]
|
|
}
|
|
}
|
|
|
|
dragState.value = {
|
|
...init,
|
|
anchorOffset,
|
|
originSpanDays: spanDays,
|
|
eventMoved: false,
|
|
tentativeStart: init.startDate,
|
|
tentativeEnd: init.endDate,
|
|
originalWeekday,
|
|
originalPattern,
|
|
realizedId: null,
|
|
}
|
|
|
|
// If history is empty (no baseline), create a baseline snapshot BEFORE any movement mutations
|
|
try {
|
|
const isResize = init.mode === 'resize-left' || init.mode === 'resize-right'
|
|
// Move: only baseline if history empty. Resize: force baseline (so undo returns to pre-resize) but only once.
|
|
store.$history?._baselineIfNeeded?.(isResize)
|
|
const evs = []
|
|
if (store.events instanceof Map) {
|
|
for (const [id, ev] of store.events) {
|
|
evs.push({
|
|
id,
|
|
start: ev.startDate,
|
|
days: ev.days,
|
|
title: ev.title,
|
|
color: ev.colorId,
|
|
recur: ev.recur
|
|
? {
|
|
f: ev.recur.freq,
|
|
i: ev.recur.interval,
|
|
c: ev.recur.count,
|
|
w: Array.isArray(ev.recur.weekdays) ? ev.recur.weekdays.join('') : null,
|
|
}
|
|
: null,
|
|
})
|
|
}
|
|
}
|
|
console.debug(
|
|
isResize ? '[history] pre-resize baseline snapshot' : '[history] pre-drag baseline snapshot',
|
|
{
|
|
mode: init.mode,
|
|
events: evs,
|
|
weekend: store.weekend,
|
|
firstDay: store.config?.first_day,
|
|
},
|
|
)
|
|
} catch {}
|
|
|
|
// Enter drag suppression (prevent intermediate pushes)
|
|
try {
|
|
store.$history?._beginDrag?.()
|
|
} catch {}
|
|
|
|
if (evt.currentTarget && evt.pointerId !== undefined) {
|
|
try {
|
|
evt.currentTarget.setPointerCapture(evt.pointerId)
|
|
} catch (e) {
|
|
console.warn('Could not set pointer capture:', e)
|
|
}
|
|
}
|
|
|
|
if (evt.cancelable) evt.preventDefault()
|
|
|
|
window.addEventListener('pointermove', onDragPointerMove, { passive: false })
|
|
window.addEventListener('pointerup', onDragPointerUp, { passive: false })
|
|
window.addEventListener('pointercancel', onDragPointerUp, { passive: false })
|
|
}
|
|
|
|
// Determine date under pointer: traverse DOM to find day cell carrying data-date attribute
|
|
function getDateUnderPointer(x, y, el) {
|
|
for (let cur = el; cur; cur = cur.parentElement)
|
|
if (cur.dataset?.date) return { date: cur.dataset.date }
|
|
const overlayEl = weekOverlayRef.value
|
|
const container = overlayEl?.parentElement // .days-grid
|
|
if (container) {
|
|
for (const d of container.querySelectorAll('[data-date]')) {
|
|
const { left, right, top, bottom } = d.getBoundingClientRect()
|
|
if (y >= top && y <= bottom && x >= left && x <= right) return { date: d.dataset.date }
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
function onDragPointerMove(e) {
|
|
const st = dragState.value
|
|
if (!st) return
|
|
const dx = e.clientX - st.pointerStartX
|
|
const dy = e.clientY - st.pointerStartY
|
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
if (!st.eventMoved && distance < 5) return
|
|
st.eventMoved = true
|
|
|
|
const hitEl = document.elementFromPoint(e.clientX, e.clientY)
|
|
const hit = getDateUnderPointer(e.clientX, e.clientY, hitEl)
|
|
|
|
if (!hit || !hit.date) return
|
|
|
|
const [ns, ne] = computeTentativeRangeFromPointer(st, hit.date)
|
|
if (!ns || !ne) return
|
|
// Only proceed if changed
|
|
if (ns === st.tentativeStart && ne === st.tentativeEnd) return
|
|
st.tentativeStart = ns
|
|
st.tentativeEnd = ne
|
|
if (st.mode === 'move') {
|
|
if (st.n && st.n > 0) {
|
|
if (!st.realizedId) {
|
|
const newId = store.splitMoveVirtualOccurrence(st.id, ns, ne, st.n)
|
|
if (newId) {
|
|
st.realizedId = newId
|
|
st.id = newId
|
|
// converted to standalone event
|
|
} else {
|
|
return
|
|
}
|
|
} else {
|
|
store.setEventRange(st.id, ns, ne, { mode: 'move', rotatePattern: false })
|
|
}
|
|
} else {
|
|
store.setEventRange(st.id, ns, ne, { mode: 'move', rotatePattern: false })
|
|
}
|
|
if (st.originalPattern && st.originalWeekday != null) {
|
|
try {
|
|
const currentWeekday = new Date(ns + 'T00:00:00').getDay()
|
|
const shift = currentWeekday - st.originalWeekday
|
|
const rotated = store._rotateWeekdayPattern([...st.originalPattern], shift)
|
|
const ev = store.getEventById(st.id)
|
|
if (ev && ev.recur && ev.recur.freq === 'weeks') {
|
|
ev.recur.weekdays = rotated
|
|
store.touchEvents()
|
|
}
|
|
} catch {}
|
|
}
|
|
} else if (!(st.n && st.n > 0)) {
|
|
applyRangeDuringDrag({ id: st.id, mode: st.mode, startDate: ns, endDate: ne }, ns, ne)
|
|
} else if (st.n && st.n > 0 && (st.mode === 'resize-left' || st.mode === 'resize-right')) {
|
|
if (!st.realizedId) {
|
|
const initialStart = ns
|
|
const initialEnd = ne
|
|
const newId = store.splitMoveVirtualOccurrence(st.id, initialStart, initialEnd, st.n)
|
|
if (newId) {
|
|
st.realizedId = newId
|
|
st.id = newId
|
|
// converted
|
|
} else return
|
|
}
|
|
const rotate = st.mode === 'resize-left'
|
|
store.setEventRange(st.id, ns, ne, { mode: st.mode, rotatePattern: rotate })
|
|
}
|
|
}
|
|
|
|
function onDragPointerUp(e) {
|
|
const st = dragState.value
|
|
if (!st) return
|
|
|
|
if (e.target && e.pointerId !== undefined) {
|
|
try {
|
|
e.target.releasePointerCapture(e.pointerId)
|
|
} catch (err) {
|
|
// Ignore errors - capture might not have been set
|
|
}
|
|
}
|
|
|
|
const moved = !!st.eventMoved
|
|
const finalStart = st.tentativeStart
|
|
const finalEnd = st.tentativeEnd
|
|
dragState.value = null
|
|
|
|
window.removeEventListener('pointermove', onDragPointerMove)
|
|
window.removeEventListener('pointerup', onDragPointerUp)
|
|
window.removeEventListener('pointercancel', onDragPointerUp)
|
|
|
|
if (moved) {
|
|
// Apply final mutation if virtual (we deferred) or if non-virtual no further change (rare)
|
|
if (st.n && st.n > 0) {
|
|
applyRangeDuringDrag(
|
|
{
|
|
id: st.id,
|
|
mode: st.mode,
|
|
startDate: finalStart,
|
|
endDate: finalEnd,
|
|
},
|
|
finalStart,
|
|
finalEnd,
|
|
)
|
|
}
|
|
justDragged.value = true
|
|
setTimeout(() => {
|
|
justDragged.value = false
|
|
}, 120)
|
|
}
|
|
// End drag suppression regardless; no post snapshot (pre-only model)
|
|
try {
|
|
store.$history?._endDrag?.()
|
|
} catch {}
|
|
}
|
|
|
|
const min = (a, b) => (a < b ? a : b)
|
|
const max = (a, b) => (a > b ? a : b)
|
|
|
|
function computeTentativeRangeFromPointer(st, current) {
|
|
const anchorOffset = st.anchorOffset || 0
|
|
const spanDays = st.originSpanDays || daysInclusive(st.startDate, st.endDate)
|
|
if (st.mode === 'move') {
|
|
const ns = addDaysStr(current, -anchorOffset)
|
|
const ne = addDaysStr(ns, spanDays - 1)
|
|
return [ns, ne]
|
|
}
|
|
if (st.mode === 'resize-left') return [min(st.endDate, current), st.endDate]
|
|
if (st.mode === 'resize-right') return [st.startDate, max(st.startDate, current)]
|
|
|
|
return [st.startDate, st.endDate]
|
|
}
|
|
|
|
function applyRangeDuringDrag(st, startDate, endDate) {
|
|
if (st.n && st.n > 0) {
|
|
if (st.mode !== 'move') return // no resize for virtual occurrence
|
|
// Split-move: occurrence being dragged treated as first of new series
|
|
store.splitMoveVirtualOccurrence(st.id, startDate, endDate, st.n)
|
|
return
|
|
}
|
|
store.setEventRange(st.id, startDate, endDate, { mode: st.mode })
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.week-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
margin-top: 1.0rem;
|
|
pointer-events: none;
|
|
}
|
|
.segment-grid {
|
|
display: grid;
|
|
align-content: start;
|
|
pointer-events: none;
|
|
overflow: hidden;
|
|
grid-auto-columns: 1fr;
|
|
grid-auto-rows: var(--segment-row-height);
|
|
}
|
|
|
|
.event-span {
|
|
padding: 0;
|
|
border-radius: 1rem;
|
|
/* Font-size so that ascender+descender exactly fills the row height:
|
|
given total = asc+desc at 1em (hardcoded 1.15), font-size = rowHeight / total */
|
|
font-size: calc(var(--segment-row-height, 1.5em) / 1.15);
|
|
font-weight: 500;
|
|
cursor: grab;
|
|
pointer-events: auto;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
/* Use unitless 1 so line box = font-size; combined with computed font-size above,
|
|
this makes the text box (asc+desc) fill the available row height */
|
|
line-height: 1;
|
|
display: flex;
|
|
/* Vertically anchor to top so baselines align across rows; we'll center text vertically by
|
|
using cap/descender metrics inside the child */
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
position: relative;
|
|
user-select: none;
|
|
z-index: 10;
|
|
text-align: center;
|
|
touch-action: none;
|
|
backdrop-filter: blur(.05rem);
|
|
max-width: 100%;
|
|
}
|
|
|
|
.event-span.cont-prev {
|
|
border-top-left-radius: 0;
|
|
border-bottom-left-radius: 0;
|
|
}
|
|
|
|
.event-span.cont-next {
|
|
border-top-right-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
|
|
.event-title {
|
|
display: block;
|
|
flex: 0 1 auto;
|
|
min-width: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
text-align: center;
|
|
pointer-events: none;
|
|
position: relative;
|
|
z-index: 1;
|
|
max-width: 100%;
|
|
line-height: inherit;
|
|
}
|
|
|
|
/* Resize handles */
|
|
.event-span .resize-handle {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 1rem;
|
|
background: transparent;
|
|
z-index: 2;
|
|
cursor: ew-resize;
|
|
touch-action: none; /* Allow touch resizing without scroll */
|
|
}
|
|
|
|
.event-span .resize-handle.left {
|
|
inset-inline-start: 0;
|
|
}
|
|
|
|
.event-span .resize-handle.right {
|
|
inset-inline-end: 0;
|
|
}
|
|
</style>
|