picocrank 1.14.1 → 1.15.0
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/package.json +7 -7
- package/vue/components/Calendar.vue +369 -14
- package/vue/views/CalendarExample.vue +48 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picocrank",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
|
|
5
5
|
"main": "vite.config.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"url": "https://github.com/jamesread/picocrank/issues"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@hugeicons/core-free-icons": "^4.
|
|
21
|
+
"@hugeicons/core-free-icons": "^4.1.1",
|
|
22
22
|
"@hugeicons/vue": "^1.0.5",
|
|
23
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
23
|
+
"@vitejs/plugin-vue": "^6.0.6",
|
|
24
24
|
"femtocrank": "^2.5.0",
|
|
25
|
-
"unplugin-vue-components": "^
|
|
26
|
-
"vite": "^
|
|
27
|
-
"vue": "^3.5.
|
|
28
|
-
"vue-router": "^5.0.
|
|
25
|
+
"unplugin-vue-components": "^32.0.0",
|
|
26
|
+
"vite": "^8.0.10",
|
|
27
|
+
"vue": "^3.5.33",
|
|
28
|
+
"vue-router": "^5.0.6"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"semantic-release": "^25.0.3"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
2
|
+
import { ref, computed, watch, onUnmounted } from 'vue'
|
|
3
3
|
|
|
4
4
|
export interface CalendarEvent {
|
|
5
5
|
id: string | number
|
|
@@ -10,12 +10,31 @@ export interface CalendarEvent {
|
|
|
10
10
|
[key: string]: any
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** Payload for `event-move-request`: call `respond(true)` after a successful commit, or `respond(false)` to decline (e.g. failed server update). */
|
|
14
|
+
export interface CalendarEventMoveRequest {
|
|
15
|
+
event: CalendarEvent
|
|
16
|
+
/** Calendar cell where the drag started (for multi-day events, the visible segment day). */
|
|
17
|
+
sourceDate: Date
|
|
18
|
+
/** Calendar cell where the event was dropped. */
|
|
19
|
+
targetDate: Date
|
|
20
|
+
/** Invoke exactly once with the outcome. If omitted until timeout, the move is treated as rejected. */
|
|
21
|
+
respond: (accepted: boolean) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export interface CalendarProps {
|
|
14
25
|
events: CalendarEvent[]
|
|
15
26
|
monthNames?: string[]
|
|
16
27
|
dayNames?: string[]
|
|
17
28
|
loading?: boolean
|
|
18
29
|
error?: string | null
|
|
30
|
+
/** When true, events can be dragged to another day. */
|
|
31
|
+
eventDragEnabled?: boolean
|
|
32
|
+
/**
|
|
33
|
+
* Max time (ms) to wait for `respond()` on `event-move-request`.
|
|
34
|
+
* If `respond` is not called in time, the move is rejected and a warning is logged.
|
|
35
|
+
* Set to `0` to disable the timeout (not recommended if listeners may omit `respond`).
|
|
36
|
+
*/
|
|
37
|
+
eventMoveResponseTimeoutMs?: number
|
|
19
38
|
// Formatter functions
|
|
20
39
|
getEventDate?: (event: CalendarEvent) => Date | null
|
|
21
40
|
getEventDateRange?: (event: CalendarEvent) => { start: Date | null; end: Date | null }
|
|
@@ -35,14 +54,20 @@ const props = withDefaults(defineProps<CalendarProps>(), {
|
|
|
35
54
|
dayNames: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
|
36
55
|
loading: false,
|
|
37
56
|
error: null,
|
|
57
|
+
eventDragEnabled: true,
|
|
58
|
+
eventMoveResponseTimeoutMs: 15000,
|
|
38
59
|
showNavigation: true,
|
|
39
60
|
})
|
|
40
61
|
|
|
41
62
|
const emit = defineEmits<{
|
|
42
63
|
'event-click': [event: CalendarEvent]
|
|
43
64
|
'date-click': [date: Date]
|
|
65
|
+
'date-range-select': [start: Date, end: Date]
|
|
44
66
|
'month-change': [month: number, year: number]
|
|
45
67
|
'event-context-menu': [event: CalendarEvent, mouseEvent: MouseEvent]
|
|
68
|
+
'event-move-request': [payload: CalendarEventMoveRequest]
|
|
69
|
+
'event-moved': [payload: { event: CalendarEvent; sourceDate: Date; targetDate: Date }]
|
|
70
|
+
'event-move-rejected': [payload: { event: CalendarEvent; sourceDate: Date; targetDate: Date }]
|
|
46
71
|
}>()
|
|
47
72
|
|
|
48
73
|
// Internal calendar state
|
|
@@ -316,16 +341,239 @@ function handleEventClick(event: CalendarEvent) {
|
|
|
316
341
|
emit('event-click', event)
|
|
317
342
|
}
|
|
318
343
|
|
|
319
|
-
function handleDateClick(date: Date) {
|
|
320
|
-
emit('date-click', date)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
344
|
function handleContextMenu(event: CalendarEvent, mouseEvent: MouseEvent) {
|
|
324
345
|
mouseEvent.preventDefault()
|
|
325
346
|
mouseEvent.stopPropagation()
|
|
326
347
|
emit('event-context-menu', event, mouseEvent)
|
|
327
348
|
}
|
|
328
|
-
|
|
349
|
+
|
|
350
|
+
// --- Date range selection (click / drag) ---
|
|
351
|
+
|
|
352
|
+
function startOfDay(d: Date): Date {
|
|
353
|
+
const x = new Date(d)
|
|
354
|
+
x.setHours(0, 0, 0, 0)
|
|
355
|
+
return x
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function toDateKey(d: Date): string {
|
|
359
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function dateFromKey(key: string): Date {
|
|
363
|
+
const [y, m, day] = key.split('-').map(Number)
|
|
364
|
+
return startOfDay(new Date(y, m - 1, day))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const isDraggingRange = ref(false)
|
|
368
|
+
const rangeDragAnchor = ref<Date | null>(null)
|
|
369
|
+
const rangeDragEnd = ref<Date | null>(null)
|
|
370
|
+
const selectedRangeStart = ref<Date | null>(null)
|
|
371
|
+
const selectedRangeEnd = ref<Date | null>(null)
|
|
372
|
+
|
|
373
|
+
function isEventTarget(ev: Event): boolean {
|
|
374
|
+
const t = ev.target
|
|
375
|
+
if (!(t instanceof Node)) return false
|
|
376
|
+
return Boolean((t as HTMLElement).closest?.('.calendar-event'))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function getRangeBounds(): { lo: Date; hi: Date } | null {
|
|
380
|
+
const a = rangeDragAnchor.value
|
|
381
|
+
const b = rangeDragEnd.value
|
|
382
|
+
if (!a || !b) return null
|
|
383
|
+
const sa = startOfDay(a).getTime()
|
|
384
|
+
const sb = startOfDay(b).getTime()
|
|
385
|
+
if (sa <= sb) return { lo: startOfDay(a), hi: startOfDay(b) }
|
|
386
|
+
return { lo: startOfDay(b), hi: startOfDay(a) }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function rangeHighlightForDate(date: Date): 'none' | 'single' | 'start' | 'end' | 'middle' {
|
|
390
|
+
const bounds = isDraggingRange.value
|
|
391
|
+
? getRangeBounds()
|
|
392
|
+
: selectedRangeStart.value && selectedRangeEnd.value
|
|
393
|
+
? (() => {
|
|
394
|
+
const sa = startOfDay(selectedRangeStart.value!).getTime()
|
|
395
|
+
const sb = startOfDay(selectedRangeEnd.value!).getTime()
|
|
396
|
+
const lo = sa <= sb ? startOfDay(selectedRangeStart.value!) : startOfDay(selectedRangeEnd.value!)
|
|
397
|
+
const hi = sa <= sb ? startOfDay(selectedRangeEnd.value!) : startOfDay(selectedRangeStart.value!)
|
|
398
|
+
return { lo, hi }
|
|
399
|
+
})()
|
|
400
|
+
: null
|
|
401
|
+
if (!bounds) return 'none'
|
|
402
|
+
const t = startOfDay(date).getTime()
|
|
403
|
+
if (t < bounds.lo.getTime() || t > bounds.hi.getTime()) return 'none'
|
|
404
|
+
if (bounds.lo.getTime() === bounds.hi.getTime()) return 'single'
|
|
405
|
+
if (t === bounds.lo.getTime()) return 'start'
|
|
406
|
+
if (t === bounds.hi.getTime()) return 'end'
|
|
407
|
+
return 'middle'
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function onWindowTouchMove(ev: TouchEvent): void {
|
|
411
|
+
if (!isDraggingRange.value || ev.touches.length === 0) return
|
|
412
|
+
const { clientX, clientY } = ev.touches[0]
|
|
413
|
+
const el = document.elementFromPoint(clientX, clientY)
|
|
414
|
+
if (!el) return
|
|
415
|
+
const cell = (el as HTMLElement).closest?.('[data-calendar-date]')
|
|
416
|
+
const key = cell?.dataset?.calendarDate
|
|
417
|
+
if (!key) return
|
|
418
|
+
rangeDragEnd.value = dateFromKey(key)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function detachRangePointerListeners(): void {
|
|
422
|
+
window.removeEventListener('mouseup', endRangeSelectionFromPointer)
|
|
423
|
+
window.removeEventListener('blur', endRangeSelectionFromPointer)
|
|
424
|
+
window.removeEventListener('touchmove', onWindowTouchMove)
|
|
425
|
+
window.removeEventListener('touchend', endRangeSelectionFromPointer)
|
|
426
|
+
window.removeEventListener('touchcancel', endRangeSelectionFromPointer)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function endRangeSelectionFromPointer(): void {
|
|
430
|
+
if (!isDraggingRange.value) return
|
|
431
|
+
const bounds = getRangeBounds()
|
|
432
|
+
isDraggingRange.value = false
|
|
433
|
+
rangeDragAnchor.value = null
|
|
434
|
+
rangeDragEnd.value = null
|
|
435
|
+
detachRangePointerListeners()
|
|
436
|
+
|
|
437
|
+
if (!bounds) return
|
|
438
|
+
|
|
439
|
+
selectedRangeStart.value = new Date(bounds.lo)
|
|
440
|
+
selectedRangeEnd.value = new Date(bounds.hi)
|
|
441
|
+
emit('date-range-select', new Date(bounds.lo), new Date(bounds.hi))
|
|
442
|
+
|
|
443
|
+
if (bounds.lo.getTime() === bounds.hi.getTime()) {
|
|
444
|
+
emit('date-click', new Date(bounds.lo))
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function onDayPointerDown(date: Date, ev: MouseEvent | TouchEvent): void {
|
|
449
|
+
if (isEventTarget(ev)) return
|
|
450
|
+
if (ev instanceof MouseEvent && ev.button !== 0) return
|
|
451
|
+
|
|
452
|
+
const d = startOfDay(date)
|
|
453
|
+
isDraggingRange.value = true
|
|
454
|
+
rangeDragAnchor.value = new Date(d)
|
|
455
|
+
rangeDragEnd.value = new Date(d)
|
|
456
|
+
window.addEventListener('mouseup', endRangeSelectionFromPointer)
|
|
457
|
+
window.addEventListener('blur', endRangeSelectionFromPointer)
|
|
458
|
+
window.addEventListener('touchmove', onWindowTouchMove, { passive: true })
|
|
459
|
+
window.addEventListener('touchend', endRangeSelectionFromPointer)
|
|
460
|
+
window.addEventListener('touchcancel', endRangeSelectionFromPointer)
|
|
461
|
+
if (ev instanceof MouseEvent) {
|
|
462
|
+
ev.preventDefault()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function onDayPointerEnter(date: Date): void {
|
|
467
|
+
if (!isDraggingRange.value) return
|
|
468
|
+
rangeDragEnd.value = startOfDay(date)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
onUnmounted(() => {
|
|
472
|
+
detachRangePointerListeners()
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// --- Calendar event drag and drop (HTML5) ---
|
|
476
|
+
|
|
477
|
+
const CALENDAR_EVENT_DRAG_MIME = 'application/x-picocrank-calendar-event-id'
|
|
478
|
+
|
|
479
|
+
const draggingEventId = ref<string | number | null>(null)
|
|
480
|
+
const draggingSourceDate = ref<Date | null>(null)
|
|
481
|
+
const dragOverTargetDate = ref<Date | null>(null)
|
|
482
|
+
const eventMoveLocked = ref(false)
|
|
483
|
+
|
|
484
|
+
function sameCalendarDay(a: Date | null, b: Date | null): boolean {
|
|
485
|
+
if (!a || !b) return false
|
|
486
|
+
return startOfDay(a).getTime() === startOfDay(b).getTime()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function onEventDragStart(event: CalendarEvent, sourceCellDate: Date, dragEvent: DragEvent): void {
|
|
490
|
+
if (!props.eventDragEnabled) {
|
|
491
|
+
dragEvent.preventDefault()
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
draggingEventId.value = event.id
|
|
495
|
+
draggingSourceDate.value = startOfDay(sourceCellDate)
|
|
496
|
+
dragEvent.dataTransfer?.setData(CALENDAR_EVENT_DRAG_MIME, String(event.id))
|
|
497
|
+
dragEvent.dataTransfer?.setData('text/plain', event.title)
|
|
498
|
+
if (dragEvent.dataTransfer) {
|
|
499
|
+
dragEvent.dataTransfer.effectAllowed = 'move'
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function onEventDragEnd(): void {
|
|
504
|
+
draggingEventId.value = null
|
|
505
|
+
draggingSourceDate.value = null
|
|
506
|
+
dragOverTargetDate.value = null
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function onDayDragOver(dayDate: Date, ev: DragEvent): void {
|
|
510
|
+
if (!props.eventDragEnabled || draggingEventId.value === null) return
|
|
511
|
+
ev.preventDefault()
|
|
512
|
+
if (ev.dataTransfer) {
|
|
513
|
+
ev.dataTransfer.dropEffect = 'move'
|
|
514
|
+
}
|
|
515
|
+
dragOverTargetDate.value = startOfDay(dayDate)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function onDayDrop(dayDate: Date, ev: DragEvent): void {
|
|
519
|
+
ev.preventDefault()
|
|
520
|
+
dragOverTargetDate.value = null
|
|
521
|
+
if (!props.eventDragEnabled) return
|
|
522
|
+
|
|
523
|
+
const idStr = ev.dataTransfer?.getData(CALENDAR_EVENT_DRAG_MIME)
|
|
524
|
+
if (!idStr) return
|
|
525
|
+
|
|
526
|
+
const moved = props.events.find(e => String(e.id) === idStr)
|
|
527
|
+
if (!moved) return
|
|
528
|
+
|
|
529
|
+
const sourceRaw = draggingSourceDate.value
|
|
530
|
+
if (!sourceRaw) return
|
|
531
|
+
|
|
532
|
+
const targetDate = startOfDay(dayDate)
|
|
533
|
+
const sourceDate = startOfDay(sourceRaw)
|
|
534
|
+
if (targetDate.getTime() === sourceDate.getTime()) return
|
|
535
|
+
if (eventMoveLocked.value) return
|
|
536
|
+
eventMoveLocked.value = true
|
|
537
|
+
|
|
538
|
+
const sourceClone = new Date(sourceDate)
|
|
539
|
+
const targetClone = new Date(targetDate)
|
|
540
|
+
|
|
541
|
+
let settled = false
|
|
542
|
+
let watchdog: ReturnType<typeof setTimeout> | undefined
|
|
543
|
+
|
|
544
|
+
function respond(accepted: boolean): void {
|
|
545
|
+
if (settled) return
|
|
546
|
+
settled = true
|
|
547
|
+
if (watchdog !== undefined) {
|
|
548
|
+
clearTimeout(watchdog)
|
|
549
|
+
}
|
|
550
|
+
eventMoveLocked.value = false
|
|
551
|
+
if (accepted) {
|
|
552
|
+
emit('event-moved', { event: moved, sourceDate: sourceClone, targetDate: targetClone })
|
|
553
|
+
} else {
|
|
554
|
+
emit('event-move-rejected', { event: moved, sourceDate: sourceClone, targetDate: targetClone })
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
emit('event-move-request', {
|
|
559
|
+
event: moved,
|
|
560
|
+
sourceDate: sourceClone,
|
|
561
|
+
targetDate: targetClone,
|
|
562
|
+
respond,
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const timeoutMs = props.eventMoveResponseTimeoutMs
|
|
566
|
+
if (timeoutMs > 0) {
|
|
567
|
+
watchdog = setTimeout(() => {
|
|
568
|
+
if (!settled) {
|
|
569
|
+
console.warn(
|
|
570
|
+
'[Calendar] event-move-request: respond() was not called before timeout; treating as rejected.'
|
|
571
|
+
)
|
|
572
|
+
respond(false)
|
|
573
|
+
}
|
|
574
|
+
}, timeoutMs)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
329
577
|
|
|
330
578
|
// Sync grid start when controlled month/year props change (but not during scrolling)
|
|
331
579
|
watch([currentMonth, currentYear], () => {
|
|
@@ -352,7 +600,14 @@ watch([currentMonth, currentYear], () => {
|
|
|
352
600
|
<div class="calendar-container">
|
|
353
601
|
<div v-if="error" class="calendar-error">{{ error }}</div>
|
|
354
602
|
<div v-if="loading" class="calendar-loading-overlay">Loading…</div>
|
|
355
|
-
<div
|
|
603
|
+
<div
|
|
604
|
+
class="calendar-grid"
|
|
605
|
+
:class="{
|
|
606
|
+
'is-dragging-range': isDraggingRange,
|
|
607
|
+
'is-dragging-event': draggingEventId !== null
|
|
608
|
+
}"
|
|
609
|
+
@wheel.prevent="handleWheel"
|
|
610
|
+
>
|
|
356
611
|
<!-- Day headers -->
|
|
357
612
|
<div v-for="day in dayNames" :key="day" class="day-header">{{ day }}</div>
|
|
358
613
|
|
|
@@ -361,15 +616,26 @@ watch([currentMonth, currentYear], () => {
|
|
|
361
616
|
v-for="(day, index) in calendarDays"
|
|
362
617
|
:key="index"
|
|
363
618
|
class="calendar-day"
|
|
619
|
+
:data-calendar-date="toDateKey(day.date)"
|
|
620
|
+
@mousedown="onDayPointerDown(day.date, $event)"
|
|
621
|
+
@touchstart.passive="onDayPointerDown(day.date, $event)"
|
|
622
|
+
@mouseenter="onDayPointerEnter(day.date)"
|
|
623
|
+
@dragover="onDayDragOver(day.date, $event)"
|
|
624
|
+
@drop="onDayDrop(day.date, $event)"
|
|
364
625
|
:class="{
|
|
365
|
-
'today': day
|
|
366
|
-
'weekend': day
|
|
367
|
-
'prev-month': day
|
|
368
|
-
'next-month': day
|
|
369
|
-
'past':
|
|
626
|
+
'today': day.date.toDateString() === new Date().toDateString(),
|
|
627
|
+
'weekend': day.date.getDay() === 0 || day.date.getDay() === 6,
|
|
628
|
+
'prev-month': day.date.getMonth() !== currentMonth && day.date.getMonth() !== (currentMonth + 1) % 12,
|
|
629
|
+
'next-month': day.date.getMonth() !== currentMonth && day.date.getMonth() === (currentMonth + 1) % 12,
|
|
630
|
+
'past': isPastDay(day.date),
|
|
631
|
+
'range-single': rangeHighlightForDate(day.date) === 'single',
|
|
632
|
+
'range-start': rangeHighlightForDate(day.date) === 'start',
|
|
633
|
+
'range-end': rangeHighlightForDate(day.date) === 'end',
|
|
634
|
+
'range-middle': rangeHighlightForDate(day.date) === 'middle',
|
|
635
|
+
'drag-over-target': eventDragEnabled && sameCalendarDay(dragOverTargetDate, day.date)
|
|
370
636
|
}"
|
|
371
637
|
>
|
|
372
|
-
<div
|
|
638
|
+
<div class="day-content">
|
|
373
639
|
<div class="day-number clickable">
|
|
374
640
|
{{ day.date.getDate() }}<span class="day-month">{{ getOrdinalSuffix(day.date.getDate()) }} {{ monthNames[day.date.getMonth()].substring(0, 3) }}</span>
|
|
375
641
|
</div>
|
|
@@ -388,8 +654,12 @@ watch([currentMonth, currentYear], () => {
|
|
|
388
654
|
'multi-day': isMultiDayEvent(event),
|
|
389
655
|
'multi-day-start': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'start',
|
|
390
656
|
'multi-day-middle': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'middle',
|
|
391
|
-
'multi-day-end': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'end'
|
|
657
|
+
'multi-day-end': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'end',
|
|
658
|
+
'drag-source': draggingEventId === event.id
|
|
392
659
|
}"
|
|
660
|
+
:draggable="eventDragEnabled"
|
|
661
|
+
@dragstart.stop="onEventDragStart(event, day.date, $event)"
|
|
662
|
+
@dragend.stop="onEventDragEnd"
|
|
393
663
|
@click.stop="handleEventClick(event)"
|
|
394
664
|
@contextmenu.stop="handleContextMenu(event, $event)"
|
|
395
665
|
>
|
|
@@ -482,6 +752,45 @@ watch([currentMonth, currentYear], () => {
|
|
|
482
752
|
min-height: 400px;
|
|
483
753
|
}
|
|
484
754
|
|
|
755
|
+
.calendar-grid.is-dragging-range,
|
|
756
|
+
.calendar-grid.is-dragging-event {
|
|
757
|
+
user-select: none;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.calendar-day.range-single {
|
|
761
|
+
background-color: rgba(0, 123, 255, 0.22) !important;
|
|
762
|
+
box-shadow: inset 0 0 0 2px #007bff;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.calendar-day.range-start {
|
|
766
|
+
background-color: rgba(0, 123, 255, 0.14) !important;
|
|
767
|
+
box-shadow:
|
|
768
|
+
inset 2px 0 0 0 #007bff,
|
|
769
|
+
inset 0 2px 0 0 #007bff,
|
|
770
|
+
inset 0 -2px 0 0 #007bff;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.calendar-day.range-end {
|
|
774
|
+
background-color: rgba(0, 123, 255, 0.14) !important;
|
|
775
|
+
box-shadow:
|
|
776
|
+
inset -2px 0 0 0 #007bff,
|
|
777
|
+
inset 0 2px 0 0 #007bff,
|
|
778
|
+
inset 0 -2px 0 0 #007bff;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.calendar-day.range-middle {
|
|
782
|
+
background-color: rgba(0, 123, 255, 0.14) !important;
|
|
783
|
+
box-shadow:
|
|
784
|
+
inset 0 2px 0 0 #007bff,
|
|
785
|
+
inset 0 -2px 0 0 #007bff;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.calendar-day.drag-over-target {
|
|
789
|
+
background-color: rgba(0, 123, 255, 0.1) !important;
|
|
790
|
+
outline: 2px dashed #007bff;
|
|
791
|
+
outline-offset: -3px;
|
|
792
|
+
}
|
|
793
|
+
|
|
485
794
|
.day-header {
|
|
486
795
|
padding: 1rem;
|
|
487
796
|
text-align: center;
|
|
@@ -604,6 +913,18 @@ watch([currentMonth, currentYear], () => {
|
|
|
604
913
|
padding: 0.1rem 0.1rem;
|
|
605
914
|
}
|
|
606
915
|
|
|
916
|
+
.calendar-event[draggable='true'] {
|
|
917
|
+
cursor: grab;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.calendar-event.drag-source {
|
|
921
|
+
opacity: 0.55;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.calendar-event[draggable='true']:active {
|
|
925
|
+
cursor: grabbing;
|
|
926
|
+
}
|
|
927
|
+
|
|
607
928
|
.calendar-event:hover {
|
|
608
929
|
background: #b3de6e;
|
|
609
930
|
border-color: #c4db96;
|
|
@@ -777,6 +1098,40 @@ watch([currentMonth, currentYear], () => {
|
|
|
777
1098
|
background: #374151;
|
|
778
1099
|
}
|
|
779
1100
|
|
|
1101
|
+
.calendar-day.range-single {
|
|
1102
|
+
background-color: rgba(96, 165, 250, 0.28) !important;
|
|
1103
|
+
box-shadow: inset 0 0 0 2px #60a5fa;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.calendar-day.range-start {
|
|
1107
|
+
background-color: rgba(96, 165, 250, 0.18) !important;
|
|
1108
|
+
box-shadow:
|
|
1109
|
+
inset 2px 0 0 0 #60a5fa,
|
|
1110
|
+
inset 0 2px 0 0 #60a5fa,
|
|
1111
|
+
inset 0 -2px 0 0 #60a5fa;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.calendar-day.range-end {
|
|
1115
|
+
background-color: rgba(96, 165, 250, 0.18) !important;
|
|
1116
|
+
box-shadow:
|
|
1117
|
+
inset -2px 0 0 0 #60a5fa,
|
|
1118
|
+
inset 0 2px 0 0 #60a5fa,
|
|
1119
|
+
inset 0 -2px 0 0 #60a5fa;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.calendar-day.range-middle {
|
|
1123
|
+
background-color: rgba(96, 165, 250, 0.18) !important;
|
|
1124
|
+
box-shadow:
|
|
1125
|
+
inset 0 2px 0 0 #60a5fa,
|
|
1126
|
+
inset 0 -2px 0 0 #60a5fa;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.calendar-day.drag-over-target {
|
|
1130
|
+
background-color: rgba(96, 165, 250, 0.12) !important;
|
|
1131
|
+
outline: 2px dashed #60a5fa;
|
|
1132
|
+
outline-offset: -3px;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
780
1135
|
.calendar-day.past {
|
|
781
1136
|
opacity: 0.6;
|
|
782
1137
|
}
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
:current-year="currentYear"
|
|
28
28
|
@event-click="handleEventClick"
|
|
29
29
|
@date-click="handleDateClick"
|
|
30
|
+
@date-range-select="handleDateRangeSelect"
|
|
31
|
+
@event-move-request="handleEventMoveRequest"
|
|
32
|
+
@event-moved="handleEventMoved"
|
|
33
|
+
@event-move-rejected="handleEventMoveRejected"
|
|
30
34
|
@month-change="handleMonthChange"
|
|
31
35
|
/>
|
|
32
36
|
</Section>
|
|
@@ -187,8 +191,51 @@ function handleEventClick(event) {
|
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
function handleDateClick(date) {
|
|
194
|
+
// Single-day picks also emit `date-range-select`; keep this for integrations that only listen for `date-click`.
|
|
190
195
|
console.log('Date clicked:', date)
|
|
191
|
-
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function handleDateRangeSelect(start, end) {
|
|
199
|
+
const same = start.getTime() === end.getTime()
|
|
200
|
+
const label = same
|
|
201
|
+
? start.toLocaleDateString()
|
|
202
|
+
: `${start.toLocaleDateString()} – ${end.toLocaleDateString()}`
|
|
203
|
+
console.log('Date range:', start, end)
|
|
204
|
+
alert(`Range selected: ${label}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function handleEventMoveRequest({ respond, event, sourceDate, targetDate }) {
|
|
208
|
+
window.setTimeout(() => {
|
|
209
|
+
const ok = window.confirm(
|
|
210
|
+
`Move "${event.title}" to ${targetDate.toLocaleDateString()}? (Cancel = decline)`
|
|
211
|
+
)
|
|
212
|
+
respond(ok)
|
|
213
|
+
}, 50)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function applyEventDateShift(event, deltaMs) {
|
|
217
|
+
const i = events.value.findIndex(e => String(e.id) === String(event.id))
|
|
218
|
+
if (i === -1) return
|
|
219
|
+
const ev = events.value[i]
|
|
220
|
+
const next = { ...ev }
|
|
221
|
+
if (next.startDate != null && next.endDate != null) {
|
|
222
|
+
next.startDate = new Date(new Date(next.startDate).getTime() + deltaMs)
|
|
223
|
+
next.endDate = new Date(new Date(next.endDate).getTime() + deltaMs)
|
|
224
|
+
} else if (next.date != null) {
|
|
225
|
+
next.date = new Date(new Date(next.date).getTime() + deltaMs)
|
|
226
|
+
}
|
|
227
|
+
const copy = [...events.value]
|
|
228
|
+
copy[i] = next
|
|
229
|
+
events.value = copy
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function handleEventMoved({ event, sourceDate, targetDate }) {
|
|
233
|
+
const deltaMs = targetDate.getTime() - sourceDate.getTime()
|
|
234
|
+
applyEventDateShift(event, deltaMs)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function handleEventMoveRejected(payload) {
|
|
238
|
+
console.log('event-move-rejected', payload)
|
|
192
239
|
}
|
|
193
240
|
|
|
194
241
|
// Helper function to update the date picker value
|