@bagelink/vue 1.2.15 → 1.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/calendar/CalendarTypes.d.ts +13 -0
- package/dist/components/calendar/CalendarTypes.d.ts.map +1 -0
- package/dist/components/calendar/Index.vue.d.ts +39 -507
- package/dist/components/calendar/Index.vue.d.ts.map +1 -1
- package/dist/components/calendar/utils.d.ts +31 -0
- package/dist/components/calendar/utils.d.ts.map +1 -0
- package/dist/components/calendar/views/AgendaView.vue.d.ts +16 -0
- package/dist/components/calendar/views/AgendaView.vue.d.ts.map +1 -0
- package/dist/components/calendar/views/DayView.vue.d.ts +50 -0
- package/dist/components/calendar/views/DayView.vue.d.ts.map +1 -0
- package/dist/components/calendar/views/MonthView.vue.d.ts +20 -0
- package/dist/components/calendar/views/MonthView.vue.d.ts.map +1 -0
- package/dist/components/calendar/views/WeekView.vue.d.ts +33 -0
- package/dist/components/calendar/views/WeekView.vue.d.ts.map +1 -0
- package/dist/components/form/BglMultiStepForm.vue.d.ts +63 -0
- package/dist/components/form/BglMultiStepForm.vue.d.ts.map +1 -0
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/DateInput.vue.d.ts +3 -3
- package/dist/components/form/inputs/DateInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/DatePicker.vue.d.ts +3 -3
- package/dist/components/form/inputs/DatePicker.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/index.cjs +2241 -3891
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2242 -3892
- package/dist/style.css +567 -633
- package/dist/utils/BagelFormUtils.d.ts +4 -2
- package/dist/utils/BagelFormUtils.d.ts.map +1 -1
- package/dist/utils/calendar/EDate.d.ts +2 -0
- package/dist/utils/calendar/EDate.d.ts.map +1 -0
- package/dist/utils/calendar/Helpers.d.ts +19 -0
- package/dist/utils/calendar/Helpers.d.ts.map +1 -0
- package/dist/utils/calendar/constants.d.ts +3 -0
- package/dist/utils/calendar/constants.d.ts.map +1 -0
- package/dist/utils/calendar/dateUtils.d.ts +30 -0
- package/dist/utils/calendar/dateUtils.d.ts.map +1 -0
- package/dist/utils/calendar/event.interface.d.ts +32 -0
- package/dist/utils/calendar/event.interface.d.ts.map +1 -0
- package/dist/utils/calendar/time.d.ts +117 -0
- package/dist/utils/calendar/time.d.ts.map +1 -0
- package/dist/utils/calendar/types.d.ts +27 -0
- package/dist/utils/calendar/types.d.ts.map +1 -0
- package/dist/utils/calendar/typings.d.ts +87 -0
- package/dist/utils/calendar/typings.d.ts.map +1 -0
- package/dist/utils/calendar/week.d.ts +117 -0
- package/dist/utils/calendar/week.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/calendar/CalendarTypes.ts +13 -0
- package/src/components/calendar/Index.vue +124 -389
- package/src/components/calendar/utils.ts +70 -0
- package/src/components/calendar/views/AgendaView.vue +263 -0
- package/src/components/calendar/views/DayView.vue +373 -0
- package/src/components/calendar/views/MonthView.vue +313 -0
- package/src/components/calendar/views/WeekView.vue +431 -0
- package/src/components/form/BglMultiStepForm.vue +383 -69
- package/src/components/form/index.ts +1 -0
- package/src/components/form/inputs/CodeEditor/Index.vue +11 -0
- package/src/components/form/inputs/DateInput.vue +3 -3
- package/src/components/form/inputs/DatePicker.vue +35 -30
- package/src/components/form/inputs/SelectInput.vue +2 -0
- package/src/components/form/inputs/Upload/upload.types.d.ts +0 -1
- package/src/index.ts +2 -2
- package/src/styles/inputs.css +138 -137
- package/src/styles/layout.css +3 -2
- package/src/styles/mobilLayout.css +4 -2
- package/src/utils/BagelFormUtils.ts +6 -2
- package/src/utils/calendar/EDate.ts +0 -0
- package/src/{components/calendar/helpers → utils/calendar}/Helpers.ts +6 -6
- package/src/utils/calendar/constants.ts +2 -0
- package/src/utils/{timeAgo.ts → calendar/dateUtils.ts} +38 -1
- package/src/utils/calendar/event.interface.ts +33 -0
- package/src/{components/calendar/helpers/Time.ts → utils/calendar/time.ts} +15 -15
- package/src/utils/calendar/types.ts +27 -0
- package/src/{components/calendar/typings/config.interface.ts → utils/calendar/typings.ts} +13 -6
- package/src/utils/calendar/week.ts +588 -0
- package/src/components/calendar/assets/base.css +0 -60
- package/src/components/calendar/components/header/Header.vue +0 -153
- package/src/components/calendar/components/month/AgendaEventTile.vue +0 -135
- package/src/components/calendar/components/month/AgendaEvents.vue +0 -72
- package/src/components/calendar/components/month/Day.vue +0 -256
- package/src/components/calendar/components/month/Event.vue +0 -164
- package/src/components/calendar/components/month/Month.vue +0 -241
- package/src/components/calendar/components/month/WeekDay.vue +0 -15
- package/src/components/calendar/components/partials/EventFlyout.vue +0 -430
- package/src/components/calendar/components/week/Day.vue +0 -198
- package/src/components/calendar/components/week/DayEvent.vue +0 -584
- package/src/components/calendar/components/week/DayTimeline.vue +0 -80
- package/src/components/calendar/components/week/FullDayEvent.vue +0 -121
- package/src/components/calendar/components/week/Week.vue +0 -414
- package/src/components/calendar/components/week/WeekTimeline.vue +0 -101
- package/src/components/calendar/constants.ts +0 -13
- package/src/components/calendar/helpers/DayIntervals.ts +0 -48
- package/src/components/calendar/helpers/EDate.ts +0 -18
- package/src/components/calendar/helpers/Errors.ts +0 -69
- package/src/components/calendar/helpers/EventChange.ts +0 -88
- package/src/components/calendar/helpers/EventConcurrency.ts +0 -69
- package/src/components/calendar/helpers/EventFlyoutPosition.ts +0 -96
- package/src/components/calendar/helpers/EventPosition.ts +0 -154
- package/src/components/calendar/helpers/EventsFilter.ts +0 -50
- package/src/components/calendar/helpers/Week.ts +0 -37
- package/src/components/calendar/language/index.ts +0 -41
- package/src/components/calendar/language/keys.ts +0 -99
- package/src/components/calendar/models/Event.ts +0 -112
- package/src/components/calendar/styles/_mixins.css +0 -21
- package/src/components/calendar/styles/_variables.css +0 -47
- package/src/components/calendar/typings/interfaces/day.interface.ts +0 -10
- package/src/components/calendar/typings/interfaces/event.interface.ts +0 -32
- package/src/components/calendar/typings/interfaces/full-day-events-week.type.ts +0 -8
- package/src/components/calendar/typings/interfaces/period.interface.ts +0 -5
- package/src/components/calendar/typings/interfaces/time-modes.ts +0 -11
- package/src/components/calendar/typings/types.ts +0 -27
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CalendarEvent } from '../CalendarTypes'
|
|
3
|
+
import { formatDate } from '@bagelink/vue'
|
|
4
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
5
|
+
|
|
6
|
+
interface AgendaViewEvent extends CalendarEvent {
|
|
7
|
+
dayLabel: string
|
|
8
|
+
isToday: boolean
|
|
9
|
+
height: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
events: CalendarEvent[]
|
|
14
|
+
startDate: Date
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
(e: 'eventClick', event: CalendarEvent): void
|
|
19
|
+
(e: 'dateChange', date: Date): void
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
// UI references
|
|
23
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
24
|
+
const currentDate = ref<Date | null>(null)
|
|
25
|
+
|
|
26
|
+
// Process events for agenda view
|
|
27
|
+
const processedEvents = computed(() => {
|
|
28
|
+
const events: AgendaViewEvent[] = []
|
|
29
|
+
const today = new Date().toDateString()
|
|
30
|
+
|
|
31
|
+
// Sort events by start time
|
|
32
|
+
const sortedEvents = [...props.events].sort(
|
|
33
|
+
(a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// Group events by date
|
|
37
|
+
const eventsByDate = sortedEvents.reduce((acc, event) => {
|
|
38
|
+
const date = new Date(event.start_time).toDateString()
|
|
39
|
+
if (!acc[date]) acc[date] = []
|
|
40
|
+
acc[date].push(event)
|
|
41
|
+
return acc
|
|
42
|
+
}, {} as { [key: string]: CalendarEvent[] })
|
|
43
|
+
|
|
44
|
+
// Process each date's events
|
|
45
|
+
Object.entries(eventsByDate).forEach(([dateStr, dateEvents]) => {
|
|
46
|
+
const date = new Date(dateStr)
|
|
47
|
+
const isToday = date.toDateString() === today
|
|
48
|
+
|
|
49
|
+
dateEvents.forEach((event) => {
|
|
50
|
+
const duration = (new Date(event.end_time).getTime() - new Date(event.start_time).getTime()) / (1000 * 60)
|
|
51
|
+
// Limit height: 60px per 30 minutes, max 200px
|
|
52
|
+
const height = Math.min(duration / 30 * 60, 200)
|
|
53
|
+
|
|
54
|
+
events.push({
|
|
55
|
+
...event,
|
|
56
|
+
dayLabel: date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }),
|
|
57
|
+
isToday,
|
|
58
|
+
height
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
return events
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Handle scroll to determine visible date
|
|
67
|
+
function handleScroll() {
|
|
68
|
+
if (!containerRef.value) return
|
|
69
|
+
|
|
70
|
+
const container = containerRef.value
|
|
71
|
+
const eventElements = container.querySelectorAll('.event')
|
|
72
|
+
let mostVisibleEvent: Element | null = null
|
|
73
|
+
let maxVisibility = 0
|
|
74
|
+
|
|
75
|
+
// Find the event that's most visible in the viewport
|
|
76
|
+
eventElements.forEach((element) => {
|
|
77
|
+
const rect = element.getBoundingClientRect()
|
|
78
|
+
const containerRect = container.getBoundingClientRect()
|
|
79
|
+
|
|
80
|
+
// Calculate how much of the element is visible
|
|
81
|
+
const visibleTop = Math.max(rect.top, containerRect.top)
|
|
82
|
+
const visibleBottom = Math.min(rect.bottom, containerRect.bottom)
|
|
83
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop)
|
|
84
|
+
const visibility = visibleHeight / rect.height
|
|
85
|
+
|
|
86
|
+
if (visibility > maxVisibility) {
|
|
87
|
+
maxVisibility = visibility
|
|
88
|
+
mostVisibleEvent = element
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Update current date if a new date is most visible
|
|
93
|
+
if (mostVisibleEvent) {
|
|
94
|
+
const element = mostVisibleEvent as HTMLElement
|
|
95
|
+
const dateStr = element.dataset.date || element.getAttribute('data-date')
|
|
96
|
+
if (dateStr) {
|
|
97
|
+
const date = new Date(dateStr)
|
|
98
|
+
if (!currentDate.value || date.toDateString() !== currentDate.value.toDateString()) {
|
|
99
|
+
currentDate.value = date
|
|
100
|
+
emit('dateChange', date)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Lifecycle hooks
|
|
107
|
+
onMounted(() => {
|
|
108
|
+
if (containerRef.value) {
|
|
109
|
+
containerRef.value.addEventListener('scroll', handleScroll)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
onUnmounted(() => {
|
|
114
|
+
if (containerRef.value) {
|
|
115
|
+
containerRef.value.removeEventListener('scroll', handleScroll)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<template>
|
|
121
|
+
<div class="agenda-view">
|
|
122
|
+
<div class="agenda-header">
|
|
123
|
+
<div class="time-column">
|
|
124
|
+
Time
|
|
125
|
+
</div>
|
|
126
|
+
<div class="event-column">
|
|
127
|
+
Event
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div ref="containerRef" class="agenda-content">
|
|
131
|
+
<div
|
|
132
|
+
v-for="event in processedEvents"
|
|
133
|
+
:key="event.id"
|
|
134
|
+
class="event"
|
|
135
|
+
:style="{
|
|
136
|
+
height: `${event.height}px`,
|
|
137
|
+
backgroundColor: event.color || 'var(--bgl-primary)',
|
|
138
|
+
}"
|
|
139
|
+
:data-date="event.start_time"
|
|
140
|
+
@click.stop="emit('eventClick', event)"
|
|
141
|
+
>
|
|
142
|
+
<div class="event-content">
|
|
143
|
+
<div class="event-time">
|
|
144
|
+
{{ formatDate(event.start_time, 'HH:mm') }}
|
|
145
|
+
</div>
|
|
146
|
+
<div class="event-details">
|
|
147
|
+
<div class="event-title">
|
|
148
|
+
{{ event.title }}
|
|
149
|
+
</div>
|
|
150
|
+
<div class="event-day" :class="{ today: event.isToday }">
|
|
151
|
+
{{ event.dayLabel }}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
159
|
+
|
|
160
|
+
<style scoped>
|
|
161
|
+
.agenda-view {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
height: 100%;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.agenda-header {
|
|
169
|
+
display: flex;
|
|
170
|
+
padding: 0.5rem;
|
|
171
|
+
border-bottom: 1px solid var(--border-color);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.time-column {
|
|
175
|
+
width: 100px;
|
|
176
|
+
flex-shrink: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.event-column {
|
|
180
|
+
flex-grow: 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.agenda-content {
|
|
184
|
+
flex-grow: 1;
|
|
185
|
+
overflow-y: auto;
|
|
186
|
+
padding: 1rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.event {
|
|
190
|
+
margin-bottom: 1rem;
|
|
191
|
+
border-radius: 4px;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
cursor: pointer;
|
|
194
|
+
transition: all 0.2s ease;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.event:hover {
|
|
198
|
+
transform: scale(1.01);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.event-content {
|
|
202
|
+
display: flex;
|
|
203
|
+
height: 100%;
|
|
204
|
+
color: white;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.event-time {
|
|
208
|
+
width: 100px;
|
|
209
|
+
flex-shrink: 0;
|
|
210
|
+
padding: 0.5rem;
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
font-size: 0.9rem;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.event-details {
|
|
217
|
+
flex-grow: 1;
|
|
218
|
+
padding: 0.5rem;
|
|
219
|
+
border-left: 1px solid rgba(255, 255, 255, 0.2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.event-title {
|
|
223
|
+
font-size: 1rem;
|
|
224
|
+
margin-bottom: 0.25rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.event-day {
|
|
228
|
+
font-size: 0.8rem;
|
|
229
|
+
opacity: 0.8;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.event-day.today {
|
|
233
|
+
color: var(--bgl-primary);
|
|
234
|
+
opacity: 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@media (max-width: 768px) {
|
|
238
|
+
.agenda-header {
|
|
239
|
+
padding: 0.5rem;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.time-column {
|
|
243
|
+
width: 80px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.event-content {
|
|
247
|
+
font-size: 0.9rem;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.event-time {
|
|
251
|
+
width: 80px;
|
|
252
|
+
font-size: 0.8rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.event-title {
|
|
256
|
+
font-size: 0.9rem;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.event-day {
|
|
260
|
+
font-size: 0.7rem;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
</style>
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CalendarEvent } from '../CalendarTypes'
|
|
3
|
+
import type { PopoverState } from '../utils'
|
|
4
|
+
import { formatDate } from '@bagelink/vue'
|
|
5
|
+
import { ref, computed, onMounted, onUnmounted, useSlots } from 'vue'
|
|
6
|
+
import {
|
|
7
|
+
openPopover as openPopoverUtil,
|
|
8
|
+
closePopover as closePopoverUtil,
|
|
9
|
+
} from '../utils'
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
events: CalendarEvent[]
|
|
13
|
+
startDate: Date
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
(e: 'eventClick', event: CalendarEvent): void
|
|
18
|
+
(e: 'eventCreate', event: { start_time: Date, end_time: Date }): void
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const slots = useSlots()
|
|
22
|
+
|
|
23
|
+
// Configuration constants
|
|
24
|
+
const slotHeight = 60
|
|
25
|
+
const slotDuration = 60
|
|
26
|
+
const timeRange = { start: 0, end: 24 }
|
|
27
|
+
|
|
28
|
+
// Drag state
|
|
29
|
+
const dragState = ref({
|
|
30
|
+
isDragging: false,
|
|
31
|
+
start: null as { x: number, y: number } | null,
|
|
32
|
+
end: null as { x: number, y: number } | null,
|
|
33
|
+
startTime: null as Date | null,
|
|
34
|
+
endTime: null as Date | null
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Time indicators
|
|
38
|
+
const currentTimeTop = ref(0)
|
|
39
|
+
const isToday = ref(false)
|
|
40
|
+
const currentTimeInterval = ref(null as any)
|
|
41
|
+
|
|
42
|
+
// Popover state
|
|
43
|
+
const activeEvent = ref<CalendarEvent | null>(null)
|
|
44
|
+
const showPopover = ref(false)
|
|
45
|
+
const popoverPosition = ref({ top: 0, left: 0, width: 0, height: 0 })
|
|
46
|
+
const popoverRef = ref<HTMLElement | null>(null)
|
|
47
|
+
|
|
48
|
+
const popoverState: PopoverState = {
|
|
49
|
+
activeEvent,
|
|
50
|
+
showPopover,
|
|
51
|
+
popoverPosition,
|
|
52
|
+
popoverRef
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Generate hourly time slots
|
|
56
|
+
const timeSlots = computed(() => {
|
|
57
|
+
const slots = []
|
|
58
|
+
for (let hour = timeRange.start; hour < timeRange.end; hour++) {
|
|
59
|
+
for (let minute = 0; minute < 60; minute += slotDuration) {
|
|
60
|
+
slots.push({
|
|
61
|
+
hour,
|
|
62
|
+
minute,
|
|
63
|
+
time: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return slots
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Process events for the current day
|
|
71
|
+
const processedEvents = computed(() => {
|
|
72
|
+
return props.events
|
|
73
|
+
.filter((event) => {
|
|
74
|
+
const eventDate = new Date(event.start_time)
|
|
75
|
+
return eventDate.toDateString() === props.startDate.toDateString()
|
|
76
|
+
})
|
|
77
|
+
.map((event) => {
|
|
78
|
+
const startDate = new Date(event.start_time)
|
|
79
|
+
const endDate = new Date(event.end_time)
|
|
80
|
+
|
|
81
|
+
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes()
|
|
82
|
+
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes()
|
|
83
|
+
|
|
84
|
+
const top = (startMinutes / slotDuration) * slotHeight
|
|
85
|
+
const height = Math.max(((endMinutes - startMinutes) / slotDuration) * slotHeight, 30)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...event,
|
|
89
|
+
top,
|
|
90
|
+
height
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Updates the current time indicator
|
|
96
|
+
function updateCurrentTimeIndicator() {
|
|
97
|
+
const now = new Date()
|
|
98
|
+
|
|
99
|
+
// Check if today is the selected day
|
|
100
|
+
isToday.value = now.toDateString() === props.startDate.toDateString()
|
|
101
|
+
|
|
102
|
+
// Calculate the position for the current time line
|
|
103
|
+
const minutes = now.getHours() * 60 + now.getMinutes()
|
|
104
|
+
currentTimeTop.value = (minutes / slotDuration) * slotHeight
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Event handlers for drag-to-create functionality
|
|
108
|
+
function handleMouseDown(e: MouseEvent) {
|
|
109
|
+
const target = e.target as HTMLElement
|
|
110
|
+
if (!target.closest('.time-slot')) return
|
|
111
|
+
|
|
112
|
+
dragState.value.isDragging = true
|
|
113
|
+
dragState.value.start = { x: e.clientX, y: e.clientY }
|
|
114
|
+
dragState.value.startTime = getTimeFromPosition(e.clientY)
|
|
115
|
+
|
|
116
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
117
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleMouseMove(e: MouseEvent) {
|
|
121
|
+
if (!dragState.value.isDragging) return
|
|
122
|
+
|
|
123
|
+
dragState.value.end = { x: e.clientX, y: e.clientY }
|
|
124
|
+
dragState.value.endTime = getTimeFromPosition(e.clientY)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleMouseUp() {
|
|
128
|
+
if (!dragState.value.isDragging || !dragState.value.startTime || !dragState.value.endTime) return
|
|
129
|
+
|
|
130
|
+
let start = new Date(dragState.value.startTime)
|
|
131
|
+
let end = new Date(dragState.value.endTime)
|
|
132
|
+
|
|
133
|
+
// Ensure end time is after start time
|
|
134
|
+
if (end < start) [start, end] = [end, start]
|
|
135
|
+
|
|
136
|
+
emit('eventCreate', { start_time: start, end_time: end })
|
|
137
|
+
|
|
138
|
+
// Reset drag state
|
|
139
|
+
dragState.value = {
|
|
140
|
+
isDragging: false,
|
|
141
|
+
start: null,
|
|
142
|
+
end: null,
|
|
143
|
+
startTime: null,
|
|
144
|
+
endTime: null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
148
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getTimeFromPosition(y: number): Date {
|
|
152
|
+
const rect = document.querySelector('.time-slots')?.getBoundingClientRect()
|
|
153
|
+
if (!rect) return new Date()
|
|
154
|
+
|
|
155
|
+
const minutes = Math.floor((y - rect.top) / slotHeight) * slotDuration
|
|
156
|
+
const date = new Date(props.startDate)
|
|
157
|
+
date.setHours(Math.floor(minutes / 60))
|
|
158
|
+
date.setMinutes(minutes % 60)
|
|
159
|
+
|
|
160
|
+
return date
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Popover handling functions
|
|
164
|
+
function openPopover(event: CalendarEvent, e: MouseEvent) {
|
|
165
|
+
openPopoverUtil(
|
|
166
|
+
event,
|
|
167
|
+
e,
|
|
168
|
+
popoverState,
|
|
169
|
+
!!slots.eventContent,
|
|
170
|
+
(evt) => { emit('eventClick', evt) }
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function closePopover() {
|
|
175
|
+
closePopoverUtil(popoverState)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Lifecycle hooks
|
|
179
|
+
onMounted(() => {
|
|
180
|
+
document.addEventListener('mousedown', handleMouseDown)
|
|
181
|
+
|
|
182
|
+
// Initialize current time indicator and set up interval to update it
|
|
183
|
+
updateCurrentTimeIndicator()
|
|
184
|
+
currentTimeInterval.value = setInterval(updateCurrentTimeIndicator, 60000)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
onUnmounted(() => {
|
|
188
|
+
document.removeEventListener('mousedown', handleMouseDown)
|
|
189
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
190
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
191
|
+
|
|
192
|
+
if (currentTimeInterval.value) {
|
|
193
|
+
clearInterval(currentTimeInterval.value)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<template>
|
|
199
|
+
<div class="w-100p overflow-hidden m_overflow h-100p grid">
|
|
200
|
+
<div class="border-bottom me-1 txt-center p-05 dayGrid">
|
|
201
|
+
<div />
|
|
202
|
+
<div>
|
|
203
|
+
{{ formatDate(startDate) }}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="overflow h-100p pe-05">
|
|
208
|
+
<div class="time-slots dayGrid border-end relative" @mousedown="handleMouseDown">
|
|
209
|
+
<div class="time-column">
|
|
210
|
+
<div
|
|
211
|
+
v-for="slot in timeSlots" :key="slot.time"
|
|
212
|
+
class="time-slot txt-light txt-12 color-gray bg-gray-light border-start"
|
|
213
|
+
:style="{ height: `${slotHeight}px` }"
|
|
214
|
+
>
|
|
215
|
+
{{ slot.time }}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="events-column">
|
|
219
|
+
<!-- Current time indicator -->
|
|
220
|
+
<div v-if="isToday" class=" absolute end w-100p z-2 flex pointer-events-none" :style="{ top: `${currentTimeTop}px` }">
|
|
221
|
+
<div class="current-time-dot h-10px aspect-ratio-1 round bg-primary" />
|
|
222
|
+
<div class="current-time-line w-100p bg-primary" />
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div
|
|
226
|
+
v-for="event in processedEvents" :key="event.id"
|
|
227
|
+
class="event absolute radius-05 overflow-hidden pointer transition z-1"
|
|
228
|
+
:style="{
|
|
229
|
+
top: `${event.top}px`,
|
|
230
|
+
height: `${event.height}px`,
|
|
231
|
+
backgroundColor: event.color || 'var(--bgl-primary)',
|
|
232
|
+
}"
|
|
233
|
+
@click.stop="slots.eventContent ? openPopover(event, $event) : emit('eventClick', event)"
|
|
234
|
+
>
|
|
235
|
+
<div class="event-content">
|
|
236
|
+
<div class=" white-space ellipsis-1">
|
|
237
|
+
{{ event.title }}
|
|
238
|
+
</div>
|
|
239
|
+
<div class="event-time opacity-8">
|
|
240
|
+
{{ formatDate(new Date(event.start_time)) }}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<!-- Drag selection preview -->
|
|
247
|
+
<div
|
|
248
|
+
v-if="dragState.isDragging && dragState.start && dragState.end"
|
|
249
|
+
class="drag-preview absolute bg-primary pointer-events-none"
|
|
250
|
+
:style="{
|
|
251
|
+
top: `${Math.min(dragState.start.y, dragState.end.y)}px`,
|
|
252
|
+
height: `${Math.abs(dragState.end.y - dragState.start.y)}px`,
|
|
253
|
+
}"
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<!-- Custom Popover -->
|
|
259
|
+
<div
|
|
260
|
+
v-if="showPopover && activeEvent"
|
|
261
|
+
ref="popoverRef"
|
|
262
|
+
v-click-outside="closePopover"
|
|
263
|
+
class="custom-popover"
|
|
264
|
+
:style="{
|
|
265
|
+
top: `${popoverPosition.top}px`,
|
|
266
|
+
left: `${popoverPosition.left}px`,
|
|
267
|
+
maxWidth: `${popoverPosition.width}px`,
|
|
268
|
+
}"
|
|
269
|
+
>
|
|
270
|
+
<slot name="eventContent" :event="activeEvent" />
|
|
271
|
+
</div>
|
|
272
|
+
<div class="border-bottom me-1" />
|
|
273
|
+
</div>
|
|
274
|
+
</template>
|
|
275
|
+
|
|
276
|
+
<style scoped>
|
|
277
|
+
.dayGrid{
|
|
278
|
+
display: grid;
|
|
279
|
+
grid-template-columns: 5rem 1fr;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.time-column {
|
|
283
|
+
width: 80px;
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.events-column {
|
|
288
|
+
flex-grow: 1;
|
|
289
|
+
position: relative;
|
|
290
|
+
border-inline-start: 1px solid var(--border-color);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.date-header {
|
|
294
|
+
padding: 0.5rem;
|
|
295
|
+
text-align: center;
|
|
296
|
+
}
|
|
297
|
+
.day-header .events-column {
|
|
298
|
+
border-inline-start: 1px solid transparent;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.time-slot {
|
|
302
|
+
height: 30px;
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.time-slots {
|
|
309
|
+
flex-grow: 1;
|
|
310
|
+
display: flex;
|
|
311
|
+
overflow: auto;
|
|
312
|
+
position: relative;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.event {
|
|
316
|
+
left: 10px;
|
|
317
|
+
right: 10px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.event:hover {
|
|
321
|
+
z-index: 2;
|
|
322
|
+
transform: scale(1.02);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.event-content {
|
|
326
|
+
padding: 0.5rem;
|
|
327
|
+
color: white;
|
|
328
|
+
font-size: 0.9rem;
|
|
329
|
+
height: 100%;
|
|
330
|
+
overflow: hidden;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.event-title {
|
|
334
|
+
white-space: nowrap;
|
|
335
|
+
overflow: hidden;
|
|
336
|
+
text-overflow: ellipsis;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.event-time {
|
|
340
|
+
font-size: 0.8rem;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.drag-preview {
|
|
344
|
+
background-color: rgba(var(--bgl-primary-rgb), 0.1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.current-time-line {
|
|
348
|
+
height: 2px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.custom-popover {
|
|
352
|
+
position: fixed;
|
|
353
|
+
z-index: 1000;
|
|
354
|
+
min-width: 250px;
|
|
355
|
+
max-width: 350px;
|
|
356
|
+
background-color: white;
|
|
357
|
+
border-radius: 4px;
|
|
358
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
359
|
+
animation: fadeIn 0.2s ease;
|
|
360
|
+
transform-origin: center left;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@keyframes fadeIn {
|
|
364
|
+
from {
|
|
365
|
+
opacity: 0;
|
|
366
|
+
transform: scale(0.95);
|
|
367
|
+
}
|
|
368
|
+
to {
|
|
369
|
+
opacity: 1;
|
|
370
|
+
transform: scale(1);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
</style>
|