frappe-ui 1.0.0-beta.55 → 1.0.0-beta.56
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/experimental/Calendar/Calendar.cy.ts +162 -0
- package/experimental/Calendar/Calendar.md +32 -0
- package/experimental/Calendar/Calendar.vue +64 -38
- package/experimental/Calendar/CalendarDaily.vue +11 -5
- package/experimental/Calendar/CalendarMonthEvent.vue +37 -12
- package/experimental/Calendar/CalendarMonthStack.vue +182 -0
- package/experimental/Calendar/CalendarMonthly.vue +219 -127
- package/experimental/Calendar/CalendarWeekDayEvent.vue +196 -127
- package/experimental/Calendar/CalendarWeekly.vue +82 -117
- package/experimental/Calendar/calendarUtils.ts +20 -9
- package/experimental/Calendar/composables/useCalendarData.ts +26 -57
- package/experimental/Calendar/composables/useStripScroll.ts +129 -0
- package/experimental/Calendar/eventSpan.spec.ts +337 -0
- package/experimental/Calendar/eventSpan.ts +259 -0
- package/experimental/Calendar/monthStrip.spec.ts +111 -0
- package/experimental/Calendar/monthStrip.ts +89 -0
- package/experimental/Calendar/stories/Examples.vue +32 -0
- package/experimental/Calendar/style.css +10 -3
- package/experimental/Calendar/types.ts +45 -0
- package/experimental/Calendar/useEventBase.ts +4 -7
- package/experimental/FloatingWindow/useFloatingWindow.ts +9 -4
- package/{src/components → experimental}/ThemeSwitcher/ThemePreview.vue +1 -1
- package/{src/components → experimental}/ThemeSwitcher/ThemeSwitcher.cy.ts +1 -1
- package/{src/components → experimental}/ThemeSwitcher/ThemeSwitcher.md +13 -0
- package/{src/components → experimental}/ThemeSwitcher/ThemeSwitcher.vue +2 -2
- package/{src/components → experimental}/ThemeSwitcher/stories/Branded.vue +1 -1
- package/{src/components → experimental}/ThemeSwitcher/stories/Default.vue +1 -1
- package/{src/components → experimental}/ThemeSwitcher/types.ts +1 -1
- package/experimental.ts +5 -0
- package/package.json +1 -1
- package/src/components/Button/Button.vue +1 -6
- package/src/components/Combobox/Combobox.api.md +1 -1
- package/src/components/Combobox/stories/MemberPicker.vue +2 -2
- package/src/components/Combobox/types.ts +2 -1
- package/src/components/DatePicker/index.ts +0 -1
- package/src/components/Dropdown/Dropdown.api.md +1 -1
- package/src/components/Dropdown/types.ts +2 -1
- package/src/components/HoverCard/HoverCard.api.md +3 -3
- package/src/components/HoverCard/types.ts +5 -4
- package/src/components/Menu/Menu.api.md +1 -1
- package/src/components/Menu/types.ts +2 -1
- package/src/components/MultiSelect/MultiSelect.api.md +1 -1
- package/src/components/MultiSelect/types.ts +2 -1
- package/src/components/Popover/Popover.api.md +1 -1
- package/src/components/Popover/types.ts +2 -1
- package/src/components/Select/Select.api.md +1 -1
- package/src/components/Select/types.ts +2 -1
- package/src/components/Sidebar/SidebarHeader.vue +9 -8
- package/src/components/TabButtons/TabButtons.api.md +2 -2
- package/src/components/TabButtons/TabButtons.cy.ts +23 -0
- package/src/components/TabButtons/TabButtons.md +25 -0
- package/src/components/TabButtons/TabButtons.vue +1 -2
- package/src/components/TabButtons/index.ts +0 -1
- package/src/components/TabButtons/types.ts +0 -3
- package/src/components/Toast/Toast.cy.ts +4 -4
- package/src/components/Toast/Toast.sanitize.test.ts +59 -1
- package/src/components/Toast/Toast.test.ts +31 -44
- package/src/components/Toast/toast.ts +33 -126
- package/src/index.ts +2 -3
- package/experimental/Calendar/ShowMoreCalendarEvent.vue +0 -35
- /package/{src/components → experimental}/ThemeSwitcher/ThemeSwitcher.api.md +0 -0
- /package/{src/components → experimental}/ThemeSwitcher/index.ts +0 -0
- /package/{src/components → experimental}/ThemeSwitcher/previews/PreviewWindow.vue +0 -0
- /package/{src/components → experimental}/ThemeSwitcher/stories/Menu.vue +0 -0
- /package/{src/components → experimental}/ThemeSwitcher/stories/Toggle.vue +0 -0
|
@@ -13,6 +13,16 @@ function monthYear(offsetDays = 0) {
|
|
|
13
13
|
|
|
14
14
|
const today = monthYear()
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A weekday (0 = Sunday) of the current week. Weeks run Sunday to Saturday,
|
|
18
|
+
* so a span over Monday to Wednesday sits inside one week row whatever day
|
|
19
|
+
* the suite runs on; the month grid pads with neighbouring months, so it
|
|
20
|
+
* shows those days whichever month they fall in.
|
|
21
|
+
*/
|
|
22
|
+
function thisWeek(weekday: number) {
|
|
23
|
+
return monthYear(weekday - new Date().getDay())
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
const events: CalendarEvent[] = [
|
|
17
27
|
{
|
|
18
28
|
id: 'EV-001',
|
|
@@ -40,6 +50,10 @@ const events: CalendarEvent[] = [
|
|
|
40
50
|
]
|
|
41
51
|
|
|
42
52
|
describe('Calendar', () => {
|
|
53
|
+
// Below the `sm` breakpoint the Month view stacks days instead of drawing
|
|
54
|
+
// week rows; the desktop specs want the rows.
|
|
55
|
+
beforeEach(() => cy.viewport(1024, 768))
|
|
56
|
+
|
|
43
57
|
// Behavior 1: renders with default props
|
|
44
58
|
it('renders the month view with the default header', () => {
|
|
45
59
|
cy.mount(Calendar, { props: { events: [] } })
|
|
@@ -75,6 +89,134 @@ describe('Calendar', () => {
|
|
|
75
89
|
cy.contains('Design review').should('not.exist')
|
|
76
90
|
})
|
|
77
91
|
|
|
92
|
+
it('draws a multi-day event as one bar in the month view', () => {
|
|
93
|
+
cy.mount(Calendar, {
|
|
94
|
+
props: {
|
|
95
|
+
events: [
|
|
96
|
+
{
|
|
97
|
+
id: 'EV-STAY',
|
|
98
|
+
title: 'Offsite',
|
|
99
|
+
fromDate: thisWeek(1),
|
|
100
|
+
toDate: thisWeek(3),
|
|
101
|
+
isFullDay: true,
|
|
102
|
+
color: 'cyan',
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// One bar across the stay's three days, never a copy per day.
|
|
109
|
+
cy.get('.event')
|
|
110
|
+
.filter(':contains("Offsite")')
|
|
111
|
+
.should('have.length', 1)
|
|
112
|
+
.then(($bar) => {
|
|
113
|
+
const cell = $bar.closest('[data-week-row]')
|
|
114
|
+
const cellWidth = cell.width()! / 7
|
|
115
|
+
expect($bar.width()!).to.be.greaterThan(cellWidth * 1.5)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('sizes a month row to its day and shows every event in it', () => {
|
|
120
|
+
const titles = [
|
|
121
|
+
'Standup',
|
|
122
|
+
'Design review',
|
|
123
|
+
'Interview',
|
|
124
|
+
'Team lunch',
|
|
125
|
+
'Retro',
|
|
126
|
+
]
|
|
127
|
+
cy.mount(Calendar, {
|
|
128
|
+
props: {
|
|
129
|
+
events: titles.map((title, i) => ({
|
|
130
|
+
id: `EV-${i}`,
|
|
131
|
+
title,
|
|
132
|
+
fromDate: today,
|
|
133
|
+
toDate: today,
|
|
134
|
+
fromTime: `${9 + i}:00`,
|
|
135
|
+
toTime: `${10 + i}:00`,
|
|
136
|
+
})),
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Five events on one day: all five rendered, nothing folded behind a
|
|
141
|
+
// count.
|
|
142
|
+
for (const title of titles)
|
|
143
|
+
cy.contains('.event', title).should('be.visible')
|
|
144
|
+
cy.contains('more').should('not.exist')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('names the month on its first day', () => {
|
|
148
|
+
// Pinned mid-month: today's own number is a bare pill, so on the 1st the
|
|
149
|
+
// label would not be there to find.
|
|
150
|
+
cy.clock(new Date(2026, 7, 15), ['Date'])
|
|
151
|
+
cy.mount(Calendar, { props: { events: [] } })
|
|
152
|
+
|
|
153
|
+
cy.get('[data-strip-date]').first().contains('Aug 1').should('exist')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('stacks the days on a narrow screen', () => {
|
|
157
|
+
cy.viewport(390, 800)
|
|
158
|
+
cy.mount(Calendar, {
|
|
159
|
+
props: {
|
|
160
|
+
events: [
|
|
161
|
+
{
|
|
162
|
+
id: 'EV-STAY',
|
|
163
|
+
title: 'Offsite',
|
|
164
|
+
fromDate: thisWeek(1),
|
|
165
|
+
toDate: thisWeek(3),
|
|
166
|
+
isFullDay: true,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// No week-row grid; a row per day, with a stay saying which day it is on.
|
|
173
|
+
cy.get('[data-week-row]').should('not.exist')
|
|
174
|
+
cy.contains('.event', 'Offsite').should('have.length.at.least', 1)
|
|
175
|
+
cy.contains('Day 1 of 3').should('exist')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('puts a multi-day event in the all-day row and splits an overnight one', () => {
|
|
179
|
+
cy.mount(Calendar, {
|
|
180
|
+
props: {
|
|
181
|
+
events: [
|
|
182
|
+
{
|
|
183
|
+
id: 'EV-STAY',
|
|
184
|
+
title: 'Offsite',
|
|
185
|
+
fromDate: thisWeek(1),
|
|
186
|
+
toDate: thisWeek(2),
|
|
187
|
+
isFullDay: true,
|
|
188
|
+
color: 'cyan',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'EV-NIGHT',
|
|
192
|
+
title: 'Release night',
|
|
193
|
+
fromDate: thisWeek(1),
|
|
194
|
+
toDate: thisWeek(2),
|
|
195
|
+
fromTime: '22:00',
|
|
196
|
+
toTime: '02:00',
|
|
197
|
+
color: 'violet',
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
config: { defaultMode: 'Week' },
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// The all-day row holds the stay as a bar spanning its days.
|
|
205
|
+
cy.get('[data-day-columns]')
|
|
206
|
+
.first()
|
|
207
|
+
.within(() => {
|
|
208
|
+
cy.contains('.event', 'Offsite').should('exist')
|
|
209
|
+
})
|
|
210
|
+
// The overnight event shows once per day in the time grid, each piece
|
|
211
|
+
// still labelled with the whole event's times.
|
|
212
|
+
cy.get('[data-time-grid] .event')
|
|
213
|
+
.filter(':contains("Release night")')
|
|
214
|
+
.should('have.length', 2)
|
|
215
|
+
.each(($piece) => {
|
|
216
|
+
expect($piece.text()).to.contain('10 pm - 2 am')
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
78
220
|
it('emits rangeChange with the visible range on mount', () => {
|
|
79
221
|
cy.mount(Calendar, {
|
|
80
222
|
props: { events: [], onRangeChange: cy.spy().as('onRangeChange') },
|
|
@@ -122,6 +264,26 @@ describe('Calendar', () => {
|
|
|
122
264
|
cy.contains('All day').should('not.exist')
|
|
123
265
|
})
|
|
124
266
|
|
|
267
|
+
// Anything layered over the calendar owns the keyboard. The shortcuts are bare
|
|
268
|
+
// letters, so without this they reached straight through an open dialog and
|
|
269
|
+
// switched the view behind it.
|
|
270
|
+
it('ignores shortcuts while an overlay is open', () => {
|
|
271
|
+
cy.mount(Calendar, { props: { events: [] } })
|
|
272
|
+
|
|
273
|
+
// The month/year button is the header's first control; its picker is a dialog.
|
|
274
|
+
cy.get('button').first().click()
|
|
275
|
+
cy.get('[role=dialog]').should('exist')
|
|
276
|
+
|
|
277
|
+
cy.get('body').type('w')
|
|
278
|
+
cy.contains('All day').should('not.exist')
|
|
279
|
+
|
|
280
|
+
// Closed again, the same key does what it always did.
|
|
281
|
+
cy.get('body').type('{esc}')
|
|
282
|
+
cy.get('[role=dialog]').should('not.exist')
|
|
283
|
+
cy.get('body').type('w')
|
|
284
|
+
cy.contains('All day').should('exist')
|
|
285
|
+
})
|
|
286
|
+
|
|
125
287
|
// Behavior 5: every documented slot renders
|
|
126
288
|
it('renders the #header slot with its props', () => {
|
|
127
289
|
cy.mount(Calendar, {
|
|
@@ -45,6 +45,16 @@ Each entry in `events` is a `CalendarEvent`:
|
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
An event runs from `fromDate fromTime` to `toDate toTime`, dates inclusive.
|
|
49
|
+
One whose `toDate` is later than its `fromDate` spans those days: the Month
|
|
50
|
+
view draws it as a single bar across them, and the Week view puts it in the
|
|
51
|
+
all-day row. A timed event that crosses midnight but is shorter than a day
|
|
52
|
+
(an evening running late) stays in the time grid, cut at midnight into a
|
|
53
|
+
piece per day. A timed event ending at `00:00` stops as that day begins, so
|
|
54
|
+
it does not occupy it. Dragging a spanning event moves both ends together.
|
|
55
|
+
|
|
56
|
+
`isFullDay` events ignore their times and cover `fromDate`..`toDate` whole.
|
|
57
|
+
|
|
48
58
|
The calendar keeps an internal copy of `events` and refreshes it when the
|
|
49
59
|
prop changes. Edits made inside the calendar (create, drag, resize, delete)
|
|
50
60
|
mutate the copy and come back through the `create`, `update`, and `delete`
|
|
@@ -54,6 +64,25 @@ emits — persist them and refresh your source of truth from there.
|
|
|
54
64
|
`cyan`, `blue`, `orange`, `green`) with the CSS variables used per state, for
|
|
55
65
|
building matching UI such as a color picker.
|
|
56
66
|
|
|
67
|
+
## Month view
|
|
68
|
+
|
|
69
|
+
The Month view is a strip of week rows covering the month in view, and it
|
|
70
|
+
scrolls when the rows outgrow the calendar's height. The arrows, Today, and
|
|
71
|
+
the month picker move the month and scroll the strip to their date.
|
|
72
|
+
|
|
73
|
+
Each row is as tall as its busiest day needs. Multi-day events run as bars
|
|
74
|
+
across the top of the row; single-day events sit beneath them in their
|
|
75
|
+
cells with the title wrapping to a second line, so every event is shown —
|
|
76
|
+
there is no "n more".
|
|
77
|
+
|
|
78
|
+
Below the `sm` breakpoint the days stack instead: a row per day, a heading
|
|
79
|
+
where each month begins, and a week strip above to keep your place. Clicking a date number in either layout
|
|
80
|
+
opens that day in the Day view.
|
|
81
|
+
|
|
82
|
+
`rangeChange` reports the strip's full extent for the Month view — the
|
|
83
|
+
padding days of the first and last weeks included — so a data source that
|
|
84
|
+
fetches by range has events for every cell.
|
|
85
|
+
|
|
57
86
|
## Config
|
|
58
87
|
|
|
59
88
|
The `config` prop takes a partial `CalendarConfig`; unset keys use these
|
|
@@ -83,6 +112,9 @@ With `enableShortcuts` on: `m` / `w` / `d` switch views, `t` jumps to today,
|
|
|
83
112
|
`←` / `→` navigate, and `Delete` removes the event whose popover is open
|
|
84
113
|
(edit mode only).
|
|
85
114
|
|
|
115
|
+
They stay out of the way of whatever is on top: nothing fires while a field has
|
|
116
|
+
focus, or while a dialog, popover, menu or select is open anywhere on the page.
|
|
117
|
+
|
|
86
118
|
## Click handling
|
|
87
119
|
|
|
88
120
|
By default, a single click on an event opens its detail popover and a double
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<!-- No overflow-hidden here: each view scrolls itself, and clipping at the
|
|
3
|
+
root only cut the focus outline off the header's buttons. -->
|
|
4
|
+
<div class="flex h-full flex-col">
|
|
3
5
|
<slot
|
|
4
6
|
name="header"
|
|
5
7
|
v-bind="{
|
|
@@ -67,9 +69,10 @@
|
|
|
67
69
|
v-if="activeView === 'Month'"
|
|
68
70
|
:events="events"
|
|
69
71
|
:currentMonth="currentMonth"
|
|
70
|
-
:
|
|
72
|
+
:currentYear="currentYear"
|
|
73
|
+
:currentDate="selectedDay"
|
|
74
|
+
:jumpDate="currentDate"
|
|
71
75
|
:config="overrideConfig"
|
|
72
|
-
@setCurrentDate="(d) => updateCurrentDate(d)"
|
|
73
76
|
>
|
|
74
77
|
<template #event-popover-content="slotProps">
|
|
75
78
|
<slot name="event-popover-content" v-bind="slotProps" />
|
|
@@ -142,6 +145,8 @@ import CalendarDaily from './CalendarDaily.vue'
|
|
|
142
145
|
import NewEventModal from './NewEventModal.vue'
|
|
143
146
|
import useEventModal from './composables/useEventModal'
|
|
144
147
|
import { isAnyPopoverOpen } from './useEventBase'
|
|
148
|
+
import { stripPlacement } from './eventSpan'
|
|
149
|
+
import { stripRange } from './monthStrip'
|
|
145
150
|
import {
|
|
146
151
|
ACTIVE_VIEW_KEY,
|
|
147
152
|
CALENDAR_ACTIONS_KEY,
|
|
@@ -227,6 +232,27 @@ function syncSelectedMonth(year: number, month: number) {
|
|
|
227
232
|
}
|
|
228
233
|
}
|
|
229
234
|
|
|
235
|
+
// Anything layered over the calendar owns the keyboard while it is up — a modal,
|
|
236
|
+
// a dropdown, an event's own popover, the header's month picker. The shortcuts are
|
|
237
|
+
// bare letters, so a press meant for the thing on top switched the view behind it,
|
|
238
|
+
// and an arrow key moved the month under an open picker.
|
|
239
|
+
//
|
|
240
|
+
// Asked of the document rather than of the event's ancestors: an overlay is
|
|
241
|
+
// portalled to <body>, a sibling of the calendar rather than a descendant, and a
|
|
242
|
+
// press with nothing focused reports <body> itself as the target — `closest()`
|
|
243
|
+
// from there would see neither.
|
|
244
|
+
//
|
|
245
|
+
// By role, which is what tells an overlay apart from the rest of the page: reka
|
|
246
|
+
// gives dialogs and popovers `dialog`, menus `menu`, selects `listbox`, and each
|
|
247
|
+
// is in the DOM only while open. Tooltips are `tooltip` and so are left out — one
|
|
248
|
+
// showing under the pointer is not something the keyboard is talking to.
|
|
249
|
+
const OVERLAY_SELECTOR =
|
|
250
|
+
'[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]'
|
|
251
|
+
|
|
252
|
+
function isOverlayOpen() {
|
|
253
|
+
return !!document.querySelector(OVERLAY_SELECTOR)
|
|
254
|
+
}
|
|
255
|
+
|
|
230
256
|
// shortcuts for changing the active view and navigating through the calendar
|
|
231
257
|
onMounted(() => {
|
|
232
258
|
if (!overrideConfig.enableShortcuts) return
|
|
@@ -236,6 +262,8 @@ onUnmounted(() => {
|
|
|
236
262
|
window.removeEventListener('keydown', handleShortcuts)
|
|
237
263
|
})
|
|
238
264
|
function handleShortcuts(e: KeyboardEvent) {
|
|
265
|
+
if (isOverlayOpen()) return
|
|
266
|
+
|
|
239
267
|
const target = e.target as HTMLElement | null
|
|
240
268
|
if (
|
|
241
269
|
target?.tagName === 'INPUT' ||
|
|
@@ -274,28 +302,31 @@ watch(activeView, (value) => {
|
|
|
274
302
|
}
|
|
275
303
|
})
|
|
276
304
|
|
|
277
|
-
|
|
305
|
+
/**
|
|
306
|
+
* A fresh internal copy of the events prop. A function rather than a computed
|
|
307
|
+
* on purpose: the views edit the copy in place (a drag shifts its dates), and
|
|
308
|
+
* `reloadEvents` must hand back the prop's values, not the edited objects a
|
|
309
|
+
* cached computed would return again.
|
|
310
|
+
*/
|
|
311
|
+
function parseEvents(): CalendarEvent[] {
|
|
278
312
|
return (
|
|
279
313
|
props.events?.map((event) => {
|
|
280
314
|
const { fromDate, toDate, fromTime, toTime, ...rest } = event
|
|
281
|
-
const
|
|
282
|
-
const fromDateTime = fromDate + ' ' + fromTime
|
|
283
|
-
const toDateTime = toDate + ' ' + toTime
|
|
284
|
-
|
|
315
|
+
const timed = !!(fromTime && toTime)
|
|
285
316
|
return {
|
|
286
317
|
...rest,
|
|
287
|
-
date,
|
|
288
|
-
fromDateTime,
|
|
289
|
-
toDateTime,
|
|
318
|
+
date: fromDate,
|
|
319
|
+
fromDateTime: fromDate + ' ' + fromTime,
|
|
320
|
+
toDateTime: toDate + ' ' + toTime,
|
|
290
321
|
fromDate,
|
|
291
322
|
toDate,
|
|
292
|
-
fromTime,
|
|
293
|
-
toTime,
|
|
323
|
+
fromTime: timed ? handleSeconds(fromTime) : fromTime,
|
|
324
|
+
toTime: timed ? handleSeconds(toTime) : toTime,
|
|
294
325
|
}
|
|
295
326
|
}) || []
|
|
296
327
|
)
|
|
297
|
-
}
|
|
298
|
-
const events = ref<CalendarEvent[]>(parseEvents
|
|
328
|
+
}
|
|
329
|
+
const events = ref<CalendarEvent[]>(parseEvents())
|
|
299
330
|
|
|
300
331
|
watch(
|
|
301
332
|
() => props.events,
|
|
@@ -304,16 +335,9 @@ watch(
|
|
|
304
335
|
)
|
|
305
336
|
|
|
306
337
|
function reloadEvents() {
|
|
307
|
-
events.value = parseEvents
|
|
338
|
+
events.value = parseEvents()
|
|
308
339
|
}
|
|
309
340
|
|
|
310
|
-
events.value.forEach((event) => {
|
|
311
|
-
if (!event.fromTime || !event.toTime) return
|
|
312
|
-
|
|
313
|
-
event.fromTime = handleSeconds(event.fromTime)
|
|
314
|
-
event.toTime = handleSeconds(event.toTime)
|
|
315
|
-
})
|
|
316
|
-
|
|
317
341
|
const { showEventModal, newEvent, openNewEventModal } = useEventModal()
|
|
318
342
|
|
|
319
343
|
provide(CALENDAR_ACTIONS_KEY, {
|
|
@@ -322,6 +346,7 @@ provide(CALENDAR_ACTIONS_KEY, {
|
|
|
322
346
|
deleteEvent,
|
|
323
347
|
handleCellClick,
|
|
324
348
|
updateActiveView,
|
|
349
|
+
setCalendarDate,
|
|
325
350
|
props,
|
|
326
351
|
})
|
|
327
352
|
|
|
@@ -334,6 +359,8 @@ function createNewEvent(event: CalendarEvent) {
|
|
|
334
359
|
}
|
|
335
360
|
|
|
336
361
|
function updateEventState(event: CalendarEvent) {
|
|
362
|
+
// A view may hand back one day's piece of the event; store the whole one.
|
|
363
|
+
event = stripPlacement(event)
|
|
337
364
|
const eventID = event.id
|
|
338
365
|
let eventIndex = events.value.findIndex((e) => e.id === eventID)
|
|
339
366
|
event.fromDateTime = event.fromDate + ' ' + event.fromTime
|
|
@@ -466,12 +493,6 @@ watch(currentDay, (newVal) => {
|
|
|
466
493
|
setCalendarDate(target)
|
|
467
494
|
})
|
|
468
495
|
|
|
469
|
-
function updateCurrentDate(d: Date) {
|
|
470
|
-
activeView.value = 'Day'
|
|
471
|
-
date.value = findIndexOfDate(d)
|
|
472
|
-
week.value = findCurrentWeek(d)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
496
|
function increment() {
|
|
476
497
|
incrementClickEvents[activeView.value]()
|
|
477
498
|
syncSelectedMonth(currentYear.value, currentMonth.value)
|
|
@@ -488,8 +509,17 @@ const incrementClickEvents: Record<CalendarMode, () => void> = {
|
|
|
488
509
|
Day: incrementDay,
|
|
489
510
|
}
|
|
490
511
|
|
|
512
|
+
// decrementMonth lands on the month's last day, which is right for stepping
|
|
513
|
+
// back a day across a month edge but scrolls the Month strip to its bottom;
|
|
514
|
+
// the Month view's own arrow lands on the first day, as the other arrow does.
|
|
515
|
+
function decrementMonthView() {
|
|
516
|
+
decrementMonth()
|
|
517
|
+
date.value = findFirstDateOfMonth(currentMonth.value, currentYear.value)
|
|
518
|
+
week.value = findCurrentWeek(currentMonthDates.value[date.value])
|
|
519
|
+
}
|
|
520
|
+
|
|
491
521
|
const decrementClickEvents: Record<CalendarMode, () => void> = {
|
|
492
|
-
Month:
|
|
522
|
+
Month: decrementMonthView,
|
|
493
523
|
Week: decrementWeek,
|
|
494
524
|
Day: decrementDay,
|
|
495
525
|
}
|
|
@@ -712,15 +742,11 @@ function getVisibleRange() {
|
|
|
712
742
|
}
|
|
713
743
|
}
|
|
714
744
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
).startOf('day')
|
|
718
|
-
const end = dayjs(
|
|
719
|
-
new Date(currentYear.value, currentMonth.value + 1, 0),
|
|
720
|
-
).endOf('day')
|
|
745
|
+
// The Month strip's first and last weeks reach into the neighbouring months.
|
|
746
|
+
const range = stripRange(currentMonth.value, currentYear.value)
|
|
721
747
|
return {
|
|
722
|
-
startDate: start.format('YYYY-MM-DD'),
|
|
723
|
-
endDate: end.format('YYYY-MM-DD'),
|
|
748
|
+
startDate: dayjs(range.start).format('YYYY-MM-DD'),
|
|
749
|
+
endDate: dayjs(range.end).format('YYYY-MM-DD'),
|
|
724
750
|
}
|
|
725
751
|
}
|
|
726
752
|
|
|
@@ -126,6 +126,7 @@ import {
|
|
|
126
126
|
twentyFourHoursFormat,
|
|
127
127
|
} from './calendarUtils'
|
|
128
128
|
import useCalendarData from './composables/useCalendarData'
|
|
129
|
+
import { eventDays } from './eventSpan'
|
|
129
130
|
import CalendarWeekDayEvent from './CalendarWeekDayEvent.vue'
|
|
130
131
|
import {
|
|
131
132
|
CALENDAR_ACTIONS_KEY,
|
|
@@ -141,8 +142,8 @@ const props = defineProps<{
|
|
|
141
142
|
const timedEvents = computed(
|
|
142
143
|
() => useCalendarData(props.events).timedEvents.value,
|
|
143
144
|
)
|
|
144
|
-
const
|
|
145
|
-
() => useCalendarData(props.events).
|
|
145
|
+
const allDayEvents = computed(
|
|
146
|
+
() => useCalendarData(props.events).allDayEvents.value,
|
|
146
147
|
)
|
|
147
148
|
const gridRef = ref<HTMLElement | null>(null)
|
|
148
149
|
const hourHeight = props.config.hourHeight
|
|
@@ -150,9 +151,14 @@ const minuteHeight = hourHeight / 60
|
|
|
150
151
|
|
|
151
152
|
const showCollapsable = ref(false)
|
|
152
153
|
const isCollapsed = ref(true)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
154
|
+
// Every all-day-row event covering this day, a multi-day one included.
|
|
155
|
+
const dayFullDayEvents = computed(() => {
|
|
156
|
+
const day = parseDate(props.currentDate)
|
|
157
|
+
return allDayEvents.value.filter((event) => {
|
|
158
|
+
const { start, end } = eventDays(event)
|
|
159
|
+
return start <= day && day <= end
|
|
160
|
+
})
|
|
161
|
+
})
|
|
156
162
|
|
|
157
163
|
function updateFullDayEventsState() {
|
|
158
164
|
// Show collapsible if more than 4 events
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
<template #trigger>
|
|
17
17
|
<div
|
|
18
18
|
v-bind="$attrs"
|
|
19
|
-
class="event flex gap-1.5 min-h-6
|
|
19
|
+
class="event flex gap-1.5 min-h-6 rounded-4 p-[5px] transition-all duration-75 overflow-hidden"
|
|
20
20
|
:class="{
|
|
21
21
|
active: activeEvent == (props.event?.id || props.event?.name),
|
|
22
|
+
'rounded-l-none': bar && !bar.isStart,
|
|
23
|
+
'rounded-r-none': bar && !bar.isEnd,
|
|
24
|
+
'event-draft': !!props.event.isDraft,
|
|
22
25
|
}"
|
|
23
26
|
:style="eventBgStyle"
|
|
24
27
|
@click.stop="
|
|
@@ -31,22 +34,32 @@
|
|
|
31
34
|
@dblclick.prevent="handleEventEdit($event)"
|
|
32
35
|
>
|
|
33
36
|
<div
|
|
34
|
-
v-if="props.event.fromTime"
|
|
37
|
+
v-if="props.event.fromTime && !props.event.isDraft"
|
|
35
38
|
class="event-border w-[2px] rounded-4 shrink-0"
|
|
36
|
-
:style="eventBorderStyle"
|
|
37
39
|
/>
|
|
38
40
|
<div
|
|
39
|
-
class="relative flex h-full select-none items-start gap-2 overflow-hidden"
|
|
41
|
+
class="relative flex h-full min-w-0 select-none items-start gap-2 overflow-hidden"
|
|
40
42
|
>
|
|
41
43
|
<div v-if="config.showIcon && eventIcon">
|
|
42
44
|
<component :is="eventIcon" class="h-4 w-4 text-ink-gray-8" />
|
|
43
45
|
</div>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
<div class="min-w-0">
|
|
47
|
+
<p
|
|
48
|
+
class="event-title text-sm-medium"
|
|
49
|
+
:class="[
|
|
50
|
+
wrap ? 'line-clamp-2 break-words' : 'truncate',
|
|
51
|
+
props.event.isDeclined
|
|
52
|
+
? 'line-through text-ink-gray-5'
|
|
53
|
+
: 'text-ink-gray-8',
|
|
54
|
+
{ italic: !props.event.title },
|
|
55
|
+
]"
|
|
56
|
+
>
|
|
57
|
+
{{ props.event.title || '[No title]' }}
|
|
58
|
+
</p>
|
|
59
|
+
<p v-if="subtitle" class="event-subtitle mt-0.5 truncate text-xs">
|
|
60
|
+
{{ subtitle }}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
50
63
|
</div>
|
|
51
64
|
</div>
|
|
52
65
|
</template>
|
|
@@ -92,13 +105,26 @@ import NewEventModal from './NewEventModal.vue'
|
|
|
92
105
|
import Popover from '#components/Popover/Popover.vue'
|
|
93
106
|
import { useEventBase } from './useEventBase'
|
|
94
107
|
import { computed, ref } from 'vue'
|
|
95
|
-
import type { CalendarEvent } from './types'
|
|
108
|
+
import type { CalendarEvent, CalendarRowBar } from './types'
|
|
96
109
|
|
|
97
110
|
const isPopoverOpen = ref(false)
|
|
98
111
|
|
|
99
112
|
const props = defineProps<{
|
|
100
113
|
event: CalendarEvent
|
|
101
114
|
date: Date
|
|
115
|
+
/**
|
|
116
|
+
* The bar this pill draws in a week row. A side the event runs past is
|
|
117
|
+
* squared off, so a stay that began last week reads as continuing.
|
|
118
|
+
*/
|
|
119
|
+
bar?: CalendarRowBar
|
|
120
|
+
/**
|
|
121
|
+
* Lets the title run to a second line instead of cutting it off. The
|
|
122
|
+
* Month strip sizes its rows to their content, so a pill there can afford
|
|
123
|
+
* to show its whole title.
|
|
124
|
+
*/
|
|
125
|
+
wrap?: boolean
|
|
126
|
+
/** A second, quieter line under the title — a time, or "Day 2 of 3". */
|
|
127
|
+
subtitle?: string
|
|
102
128
|
}>()
|
|
103
129
|
|
|
104
130
|
defineOptions({ inheritAttrs: false })
|
|
@@ -111,7 +137,6 @@ const {
|
|
|
111
137
|
eventIcons,
|
|
112
138
|
showEventModal,
|
|
113
139
|
eventBgStyle,
|
|
114
|
-
eventBorderStyle,
|
|
115
140
|
handleEventClick,
|
|
116
141
|
handleEventEdit,
|
|
117
142
|
handleEventDelete,
|