@@ -0,0 +1,620 @@
|
||||
<template>
|
||||
<div class="week-overlay">
|
||||
<div
|
||||
v-for="span in eventSpans"
|
||||
:key="span.id"
|
||||
class="event-span"
|
||||
:class="[`event-color-${span.colorId}`]"
|
||||
:style="{
|
||||
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useCalendarStore } from '@/stores/CalendarStore'
|
||||
import { toLocalString, fromLocalString, daysInclusive, addDaysStr } from '@/utils/date'
|
||||
|
||||
const props = defineProps({
|
||||
week: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['event-click'])
|
||||
const store = useCalendarStore()
|
||||
|
||||
// Local drag state
|
||||
const dragState = ref(null)
|
||||
const justDragged = ref(false)
|
||||
|
||||
// Generate repeat occurrences for a specific date
|
||||
function generateRepeatOccurrencesForDate(targetDateStr) {
|
||||
const occurrences = []
|
||||
|
||||
// Get all events from the store and check for repeating ones
|
||||
for (const [, eventList] of store.events) {
|
||||
for (const baseEvent of eventList) {
|
||||
if (!baseEvent.isRepeating || baseEvent.repeat === 'none') {
|
||||
continue
|
||||
}
|
||||
|
||||
const targetDate = new Date(fromLocalString(targetDateStr))
|
||||
const baseStartDate = new Date(fromLocalString(baseEvent.startDate))
|
||||
const baseEndDate = new Date(fromLocalString(baseEvent.endDate))
|
||||
const spanDays = Math.floor((baseEndDate - baseStartDate) / (24 * 60 * 60 * 1000))
|
||||
|
||||
if (baseEvent.repeat === 'weeks') {
|
||||
const repeatWeekdays = baseEvent.repeatWeekdays
|
||||
if (targetDate < baseStartDate) continue
|
||||
const maxOccurrences =
|
||||
baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
|
||||
if (maxOccurrences === 0) continue
|
||||
const interval = baseEvent.repeatInterval || 1
|
||||
const msPerDay = 24 * 60 * 60 * 1000
|
||||
|
||||
// Determine if targetDate lies within some occurrence span. We look backwards up to spanDays to find a start day.
|
||||
let occStart = null
|
||||
for (let back = 0; back <= spanDays; back++) {
|
||||
const cand = new Date(targetDate)
|
||||
cand.setDate(cand.getDate() - back)
|
||||
if (cand < baseStartDate) break
|
||||
const daysDiff = Math.floor((cand - baseStartDate) / msPerDay)
|
||||
const weeksDiff = Math.floor(daysDiff / 7)
|
||||
if (weeksDiff % interval !== 0) continue
|
||||
if (repeatWeekdays[cand.getDay()]) {
|
||||
// candidate start must produce span covering targetDate
|
||||
const candEnd = new Date(cand)
|
||||
candEnd.setDate(candEnd.getDate() + spanDays)
|
||||
if (targetDate <= candEnd) {
|
||||
occStart = cand
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!occStart) continue
|
||||
// Skip base occurrence if this is within its span (base already physically stored)
|
||||
if (occStart.getTime() === baseStartDate.getTime()) continue
|
||||
// Compute occurrence index (number of previous start days)
|
||||
let occIdx = 0
|
||||
const cursor = new Date(baseStartDate)
|
||||
while (cursor < occStart && occIdx < maxOccurrences) {
|
||||
const cDaysDiff = Math.floor((cursor - baseStartDate) / msPerDay)
|
||||
const cWeeksDiff = Math.floor(cDaysDiff / 7)
|
||||
if (cWeeksDiff % interval === 0 && repeatWeekdays[cursor.getDay()]) occIdx++
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
}
|
||||
if (occIdx >= maxOccurrences) continue
|
||||
const occEnd = new Date(occStart)
|
||||
occEnd.setDate(occStart.getDate() + spanDays)
|
||||
const occStartStr = toLocalString(occStart)
|
||||
const occEndStr = toLocalString(occEnd)
|
||||
occurrences.push({
|
||||
...baseEvent,
|
||||
id: `${baseEvent.id}_repeat_${occIdx}_${occStart.getDay()}`,
|
||||
startDate: occStartStr,
|
||||
endDate: occEndStr,
|
||||
isRepeatOccurrence: true,
|
||||
repeatIndex: occIdx,
|
||||
})
|
||||
continue
|
||||
} else {
|
||||
// Handle other repeat types (months)
|
||||
let intervalsPassed = 0
|
||||
const timeDiff = targetDate - baseStartDate
|
||||
if (baseEvent.repeat === 'months') {
|
||||
intervalsPassed =
|
||||
(targetDate.getFullYear() - baseStartDate.getFullYear()) * 12 +
|
||||
(targetDate.getMonth() - baseStartDate.getMonth())
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
const interval = baseEvent.repeatInterval || 1
|
||||
if (intervalsPassed < 0 || intervalsPassed % interval !== 0) continue
|
||||
|
||||
// Check a few occurrences around the target date
|
||||
const maxOccurrences =
|
||||
baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
|
||||
if (maxOccurrences === 0) continue
|
||||
const i = intervalsPassed
|
||||
if (i >= maxOccurrences) continue
|
||||
const currentStart = new Date(baseStartDate)
|
||||
currentStart.setMonth(baseStartDate.getMonth() + i)
|
||||
const currentEnd = new Date(currentStart)
|
||||
currentEnd.setDate(currentStart.getDate() + spanDays)
|
||||
// If target day lies within base (i===0) we skip because base is stored already
|
||||
if (i === 0) {
|
||||
// only skip if targetDate within base span
|
||||
if (targetDate >= baseStartDate && targetDate <= baseEndDate) continue
|
||||
}
|
||||
const currentStartStr = toLocalString(currentStart)
|
||||
const currentEndStr = toLocalString(currentEnd)
|
||||
if (currentStartStr <= targetDateStr && targetDateStr <= currentEndStr) {
|
||||
occurrences.push({
|
||||
...baseEvent,
|
||||
id: `${baseEvent.id}_repeat_${i}`,
|
||||
startDate: currentStartStr,
|
||||
endDate: currentEndStr,
|
||||
isRepeatOccurrence: true,
|
||||
repeatIndex: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return occurrences
|
||||
}
|
||||
|
||||
// Extract original event ID from repeat occurrence ID
|
||||
function getOriginalEventId(eventId) {
|
||||
if (typeof eventId === 'string' && eventId.includes('_repeat_')) {
|
||||
return eventId.split('_repeat_')[0]
|
||||
}
|
||||
return eventId
|
||||
}
|
||||
|
||||
// Handle event click
|
||||
function handleEventClick(span) {
|
||||
if (justDragged.value) return
|
||||
// Emit the actual span id (may include repeat suffix) so edit dialog knows occurrence context
|
||||
emit('event-click', span.id)
|
||||
}
|
||||
|
||||
// Handle event pointer down for dragging
|
||||
function handleEventPointerDown(span, event) {
|
||||
// Don't start drag if clicking on resize handle
|
||||
if (event.target.classList.contains('resize-handle')) return
|
||||
|
||||
event.stopPropagation()
|
||||
// Do not preventDefault here to allow click unless drag threshold is passed
|
||||
|
||||
// Get the date under the pointer
|
||||
const hit = getDateUnderPointer(event.clientX, event.clientY, event.currentTarget)
|
||||
const anchorDate = hit ? hit.date : span.startDate
|
||||
|
||||
startLocalDrag(
|
||||
{
|
||||
id: span.id,
|
||||
mode: 'move',
|
||||
pointerStartX: event.clientX,
|
||||
pointerStartY: event.clientY,
|
||||
anchorDate,
|
||||
startDate: span.startDate,
|
||||
endDate: span.endDate,
|
||||
},
|
||||
event,
|
||||
)
|
||||
}
|
||||
|
||||
// Handle resize handle pointer down
|
||||
function handleResizePointerDown(span, mode, event) {
|
||||
event.stopPropagation()
|
||||
// Start drag from the current edge; anchorDate not needed for resize
|
||||
startLocalDrag(
|
||||
{
|
||||
id: span.id,
|
||||
mode,
|
||||
pointerStartX: event.clientX,
|
||||
pointerStartY: event.clientY,
|
||||
anchorDate: null,
|
||||
startDate: span.startDate,
|
||||
endDate: span.endDate,
|
||||
},
|
||||
event,
|
||||
)
|
||||
}
|
||||
|
||||
// Get date under pointer coordinates
|
||||
function getDateUnderPointer(clientX, clientY, targetEl) {
|
||||
// First try to find a day cell directly under the pointer
|
||||
let element = document.elementFromPoint(clientX, clientY)
|
||||
|
||||
// If we hit an event element, temporarily hide it and try again
|
||||
const hiddenElements = []
|
||||
while (element && element.classList.contains('event-span')) {
|
||||
element.style.pointerEvents = 'none'
|
||||
hiddenElements.push(element)
|
||||
element = document.elementFromPoint(clientX, clientY)
|
||||
}
|
||||
|
||||
// Restore pointer events for hidden elements
|
||||
hiddenElements.forEach((el) => (el.style.pointerEvents = 'auto'))
|
||||
|
||||
if (element) {
|
||||
// Look for a day cell with data-date attribute
|
||||
const dayElement = element.closest('[data-date]')
|
||||
if (dayElement && dayElement.dataset.date) {
|
||||
return { date: dayElement.dataset.date }
|
||||
}
|
||||
|
||||
// Also check if we're over a week element and can calculate position
|
||||
const weekElement = element.closest('.week-row')
|
||||
if (weekElement) {
|
||||
const rect = weekElement.getBoundingClientRect()
|
||||
const relativeX = clientX - rect.left
|
||||
const dayWidth = rect.width / 7
|
||||
const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth)))
|
||||
|
||||
const daysGrid = weekElement.querySelector('.days-grid')
|
||||
if (daysGrid && daysGrid.children[dayIndex]) {
|
||||
const dayEl = daysGrid.children[dayIndex]
|
||||
const date = dayEl?.dataset?.date
|
||||
if (date) return { date }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to find the week overlay and calculate position
|
||||
const overlayEl = targetEl?.closest('.week-overlay')
|
||||
const weekElement = overlayEl ? overlayEl.parentElement : null
|
||||
if (!weekElement) {
|
||||
// If we're outside this week, try to find any week element under the pointer
|
||||
const allWeekElements = document.querySelectorAll('.week-row')
|
||||
let bestWeek = null
|
||||
let bestDistance = Infinity
|
||||
|
||||
for (const week of allWeekElements) {
|
||||
const rect = week.getBoundingClientRect()
|
||||
if (clientY >= rect.top && clientY <= rect.bottom) {
|
||||
const distance = Math.abs(clientY - (rect.top + rect.height / 2))
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance
|
||||
bestWeek = week
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestWeek) {
|
||||
const rect = bestWeek.getBoundingClientRect()
|
||||
const relativeX = clientX - rect.left
|
||||
const dayWidth = rect.width / 7
|
||||
const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth)))
|
||||
|
||||
const daysGrid = bestWeek.querySelector('.days-grid')
|
||||
if (daysGrid && daysGrid.children[dayIndex]) {
|
||||
const dayEl = daysGrid.children[dayIndex]
|
||||
const date = dayEl?.dataset?.date
|
||||
if (date) return { date }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const rect = weekElement.getBoundingClientRect()
|
||||
const relativeX = clientX - rect.left
|
||||
const dayWidth = rect.width / 7
|
||||
const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth)))
|
||||
|
||||
if (props.week.days[dayIndex]) {
|
||||
return { date: props.week.days[dayIndex].date }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
dragState.value = {
|
||||
...init,
|
||||
anchorOffset,
|
||||
originSpanDays: spanDays,
|
||||
eventMoved: false,
|
||||
}
|
||||
|
||||
// Capture pointer events globally
|
||||
if (evt.currentTarget && evt.pointerId !== undefined) {
|
||||
try {
|
||||
evt.currentTarget.setPointerCapture(evt.pointerId)
|
||||
} catch (e) {
|
||||
console.warn('Could not set pointer capture:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default to avoid text selection and other interference
|
||||
evt.preventDefault()
|
||||
|
||||
window.addEventListener('pointermove', onDragPointerMove, { passive: false })
|
||||
window.addEventListener('pointerup', onDragPointerUp, { passive: false })
|
||||
window.addEventListener('pointercancel', onDragPointerUp, { passive: false })
|
||||
}
|
||||
|
||||
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 we can't find a date, don't update the range but keep the drag active
|
||||
if (!hit || !hit.date) return
|
||||
|
||||
const [ns, ne] = computeTentativeRangeFromPointer(st, hit.date)
|
||||
if (!ns || !ne) return
|
||||
applyRangeDuringDrag(st, ns, ne)
|
||||
}
|
||||
|
||||
function onDragPointerUp(e) {
|
||||
const st = dragState.value
|
||||
if (!st) return
|
||||
|
||||
// Release pointer capture if it was set
|
||||
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
|
||||
dragState.value = null
|
||||
|
||||
window.removeEventListener('pointermove', onDragPointerMove)
|
||||
window.removeEventListener('pointerup', onDragPointerUp)
|
||||
window.removeEventListener('pointercancel', onDragPointerUp)
|
||||
|
||||
if (moved) {
|
||||
justDragged.value = true
|
||||
setTimeout(() => {
|
||||
justDragged.value = false
|
||||
}, 120)
|
||||
}
|
||||
}
|
||||
|
||||
function computeTentativeRangeFromPointer(st, dropDateStr) {
|
||||
const anchorOffset = st.anchorOffset || 0
|
||||
const spanDays = st.originSpanDays || daysInclusive(st.startDate, st.endDate)
|
||||
let startStr = st.startDate
|
||||
let endStr = st.endDate
|
||||
if (st.mode === 'move') {
|
||||
startStr = addDaysStr(dropDateStr, -anchorOffset)
|
||||
endStr = addDaysStr(startStr, spanDays - 1)
|
||||
} else if (st.mode === 'resize-left') {
|
||||
startStr = dropDateStr
|
||||
endStr = st.endDate
|
||||
} else if (st.mode === 'resize-right') {
|
||||
startStr = st.startDate
|
||||
endStr = dropDateStr
|
||||
}
|
||||
return normalizeDateOrder(startStr, endStr)
|
||||
}
|
||||
|
||||
function normalizeDateOrder(aStr, bStr) {
|
||||
if (!aStr) return [bStr, bStr]
|
||||
if (!bStr) return [aStr, aStr]
|
||||
return aStr <= bStr ? [aStr, bStr] : [bStr, aStr]
|
||||
}
|
||||
|
||||
function applyRangeDuringDrag(st, startDate, endDate) {
|
||||
let ev = store.getEventById(st.id)
|
||||
let isRepeatOccurrence = false
|
||||
let baseId = st.id
|
||||
let repeatIndex = 0
|
||||
let grabbedWeekday = null
|
||||
|
||||
// If not found (repeat occurrences aren't stored) parse synthetic id
|
||||
if (!ev && typeof st.id === 'string' && st.id.includes('_repeat_')) {
|
||||
const [bid, suffix] = st.id.split('_repeat_')
|
||||
baseId = bid
|
||||
ev = store.getEventById(baseId)
|
||||
if (ev) {
|
||||
const parts = suffix.split('_')
|
||||
repeatIndex = parseInt(parts[0], 10) || 0
|
||||
grabbedWeekday = parts.length > 1 ? parseInt(parts[1], 10) : null
|
||||
isRepeatOccurrence = repeatIndex >= 0
|
||||
}
|
||||
}
|
||||
|
||||
if (!ev) return
|
||||
|
||||
const mode = st.mode === 'resize-left' || st.mode === 'resize-right' ? st.mode : 'move'
|
||||
if (isRepeatOccurrence) {
|
||||
if (repeatIndex === 0) {
|
||||
store.setEventRange(baseId, startDate, endDate, { mode })
|
||||
} else {
|
||||
if (!st.splitNewBaseId) {
|
||||
const newId = store.splitRepeatSeries(
|
||||
baseId,
|
||||
repeatIndex,
|
||||
startDate,
|
||||
endDate,
|
||||
grabbedWeekday,
|
||||
)
|
||||
if (newId) {
|
||||
st.splitNewBaseId = newId
|
||||
st.id = newId
|
||||
st.startDate = startDate
|
||||
st.endDate = endDate
|
||||
}
|
||||
} else {
|
||||
store.setEventRange(st.splitNewBaseId, startDate, endDate, { mode })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
store.setEventRange(st.id, startDate, endDate, { mode })
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate event spans for this week
|
||||
const eventSpans = computed(() => {
|
||||
const spans = []
|
||||
const weekEvents = new Map()
|
||||
|
||||
// Collect events from all days in this week, including repeat occurrences
|
||||
props.week.days.forEach((day, dayIndex) => {
|
||||
// Get base events for this day
|
||||
day.events.forEach((event) => {
|
||||
if (!weekEvents.has(event.id)) {
|
||||
weekEvents.set(event.id, {
|
||||
...event,
|
||||
startIdx: dayIndex,
|
||||
endIdx: dayIndex,
|
||||
})
|
||||
} else {
|
||||
const existing = weekEvents.get(event.id)
|
||||
existing.endIdx = dayIndex
|
||||
}
|
||||
})
|
||||
|
||||
// Generate repeat occurrences for this day
|
||||
const repeatOccurrences = generateRepeatOccurrencesForDate(day.date)
|
||||
repeatOccurrences.forEach((event) => {
|
||||
if (!weekEvents.has(event.id)) {
|
||||
weekEvents.set(event.id, {
|
||||
...event,
|
||||
startIdx: dayIndex,
|
||||
endIdx: dayIndex,
|
||||
})
|
||||
} else {
|
||||
const existing = weekEvents.get(event.id)
|
||||
existing.endIdx = dayIndex
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Convert to array and sort
|
||||
const eventArray = Array.from(weekEvents.values())
|
||||
eventArray.sort((a, b) => {
|
||||
// Sort by span length (longer first)
|
||||
const spanA = a.endIdx - a.startIdx
|
||||
const spanB = b.endIdx - b.startIdx
|
||||
if (spanA !== spanB) return spanB - spanA
|
||||
|
||||
// Then by start position
|
||||
if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx
|
||||
|
||||
// Then by start time if available
|
||||
const timeA = a.startTime ? timeToMinutes(a.startTime) : 0
|
||||
const timeB = b.startTime ? timeToMinutes(b.startTime) : 0
|
||||
if (timeA !== timeB) return timeA - timeB
|
||||
|
||||
// Fallback to ID
|
||||
return String(a.id).localeCompare(String(b.id))
|
||||
})
|
||||
|
||||
// Assign rows to avoid overlaps
|
||||
const rowsLastEnd = []
|
||||
eventArray.forEach((event) => {
|
||||
let placedRow = 0
|
||||
while (placedRow < rowsLastEnd.length && !(event.startIdx > rowsLastEnd[placedRow])) {
|
||||
placedRow++
|
||||
}
|
||||
if (placedRow === rowsLastEnd.length) {
|
||||
rowsLastEnd.push(-1)
|
||||
}
|
||||
rowsLastEnd[placedRow] = event.endIdx
|
||||
event.row = placedRow + 1
|
||||
})
|
||||
|
||||
return eventArray
|
||||
})
|
||||
|
||||
function timeToMinutes(timeStr) {
|
||||
if (!timeStr) return 0
|
||||
const [hours, minutes] = timeStr.split(':').map(Number)
|
||||
return hours * 60 + minutes
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
margin-top: 1.8em;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.event-span {
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.2em;
|
||||
font-size: clamp(0.45em, 1.8vh, 0.75em);
|
||||
font-weight: 600;
|
||||
cursor: grab;
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
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 */
|
||||
.event-title {
|
||||
display: block;
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Resize handles */
|
||||
.event-span .resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
background: transparent;
|
||||
z-index: 2;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.event-span .resize-handle.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.event-span .resize-handle.right {
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user