frappe-ui 1.0.0-beta.71 → 1.0.0-beta.72
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.api.md +20 -4
- package/experimental/Calendar/Calendar.md +93 -42
- package/experimental/Calendar/Calendar.vue +110 -44
- package/experimental/Calendar/CalendarAgenda.vue +618 -0
- package/experimental/Calendar/CalendarDaily.vue +281 -73
- package/experimental/Calendar/CalendarEventPopover.vue +75 -0
- package/experimental/Calendar/CalendarEventRow.vue +327 -0
- package/experimental/Calendar/CalendarMonthEvent.vue +68 -93
- package/experimental/Calendar/CalendarMonthly.vue +275 -55
- package/experimental/Calendar/CalendarTimeMarker.vue +15 -7
- package/experimental/Calendar/CalendarWeekDayEvent.vue +608 -168
- package/experimental/Calendar/CalendarWeekly.vue +312 -154
- package/experimental/Calendar/Icon/AgendaIcon.vue +14 -0
- package/experimental/Calendar/agendaDays.ts +206 -0
- package/experimental/Calendar/calendarUtils.ts +89 -130
- package/experimental/Calendar/composables/useCalendarData.ts +11 -5
- package/experimental/Calendar/composables/useEventModal.ts +3 -1
- package/experimental/Calendar/composables/useHourGrid.ts +71 -0
- package/experimental/Calendar/eventRow.ts +199 -0
- package/experimental/Calendar/eventSpan.ts +127 -3
- package/experimental/Calendar/index.ts +6 -0
- package/experimental/Calendar/monthStrip.ts +4 -51
- package/experimental/Calendar/style.css +90 -22
- package/experimental/Calendar/types.ts +47 -7
- package/experimental/Calendar/useEventBase.ts +63 -32
- package/experimental/TextEditor/extensions/image-group/ImageGroupNodeView.vue +2 -2
- package/experimental/TextEditor/extensions/image-group/ImageGroupUploadDialog.vue +3 -3
- package/experimental/TextEditor/extensions/toc-node/TocNodeView.vue +1 -1
- package/package.json +2 -1
- package/src/components/BottomSheet/BottomSheet.vue +6 -0
- package/src/components/PageHeader/PageHeaderMobile.vue +1 -1
- package/src/components/Tree/Tree.api.md +4 -4
- package/src/components/Tree/Tree.md +40 -14
- package/src/components/Tree/Tree.vue +92 -51
- package/src/components/Tree/index.ts +1 -0
- package/src/components/Tree/types.ts +27 -10
- package/src/components/Tree/useWarnLegacyExpanded.ts +64 -0
- package/src/molecules/editor/extensions/image-group/ImageGroupGrid.vue +1 -1
- package/src/molecules/editor/extensions/image-group/ImageGroupGridCell.vue +2 -2
- package/src/molecules/editor/extensions/image-group/ImageGroupNodeView.vue +2 -2
- package/src/molecules/editor/extensions/toc-node/TocNodeView.vue +1 -1
- package/src/utils/warnDeprecated.ts +6 -1
- package/tailwind/content.js +1 -0
- package/tailwind/iconPackPlugin.js +6 -1
- package/tailwind/preset.js +7 -0
- package/experimental/Calendar/CalendarMonthStack.vue +0 -182
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
type: 'CalendarEvent[]',
|
|
13
13
|
default: '[]'
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
name: 'loading',
|
|
17
|
+
description: 'Whether the events for the visible range are still on their way.\n\nOnly the Agenda reads it, and only to tell an empty list apart from one that\nhas not arrived: a grid with nothing in it still draws the days, where a list\nwith nothing in it is a blank panel, and saying "nothing on" of a range still\nbeing fetched is saying something that may not be true.',
|
|
18
|
+
required: false,
|
|
19
|
+
type: 'boolean'
|
|
20
|
+
},
|
|
15
21
|
{
|
|
16
22
|
name: 'config',
|
|
17
23
|
description: 'Behavior overrides, merged over the defaults.',
|
|
@@ -23,7 +29,7 @@
|
|
|
23
29
|
name: 'onClick',
|
|
24
30
|
description: 'Replaces the default single-click behavior (opening the event\npopover) with your own handler.',
|
|
25
31
|
required: false,
|
|
26
|
-
type: '((data: { e: MouseEvent; calendarEvent: CalendarEvent; }) => void)'
|
|
32
|
+
type: '((data: { e: MouseEvent | KeyboardEvent; calendarEvent: CalendarEvent; }) => void)'
|
|
27
33
|
},
|
|
28
34
|
{
|
|
29
35
|
name: 'onDblClick',
|
|
@@ -48,12 +54,22 @@
|
|
|
48
54
|
{
|
|
49
55
|
name: 'event-popover-content',
|
|
50
56
|
description: '',
|
|
51
|
-
type: '{ calendarEvent:
|
|
57
|
+
type: '{ calendarEvent: CalendarEvent; date: Date; isEditMode: boolean; close: () => void; }'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'event-description',
|
|
61
|
+
description: '',
|
|
62
|
+
type: '{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'event-suffix',
|
|
66
|
+
description: '',
|
|
67
|
+
type: '{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }'
|
|
52
68
|
},
|
|
53
69
|
{
|
|
54
|
-
name: '
|
|
70
|
+
name: 'event-participant',
|
|
55
71
|
description: '',
|
|
56
|
-
type: '{
|
|
72
|
+
type: '{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }'
|
|
57
73
|
}
|
|
58
74
|
]
|
|
59
75
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Calendar
|
|
2
2
|
|
|
3
|
-
A date and event view for schedules, with Month, Week, and
|
|
3
|
+
A date and event view for schedules, with Month, Week, Day, and Agenda modes.
|
|
4
4
|
|
|
5
5
|
> **Parked** — `Calendar` left the root export in `1.0.0` and now ships from
|
|
6
6
|
> [`frappe-ui/experimental`](/docs/experimental) with its public API unchanged.
|
|
@@ -19,10 +19,9 @@ import type { CalendarEvent, CalendarConfig } from 'frappe-ui/experimental'
|
|
|
19
19
|
## Custom Header
|
|
20
20
|
|
|
21
21
|
Pass a `#header` slot to replace the default toolbar. The slot receives the
|
|
22
|
-
current title (`currentMonthYear`), the active view (`activeView`), the
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`onMonthYearChange`).
|
|
22
|
+
current title (`currentMonthYear`), the active view (`activeView`), the enabled
|
|
23
|
+
view options (`enabledModes`), and navigation functions (`increment`,
|
|
24
|
+
`decrement`, `updateActiveView`, `setCalendarDate`, `onMonthYearChange`).
|
|
26
25
|
|
|
27
26
|
<ComponentPreview name="Calendar-CustomHeader" csr="true" />
|
|
28
27
|
|
|
@@ -45,43 +44,86 @@ Each entry in `events` is a `CalendarEvent`:
|
|
|
45
44
|
}
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
An event runs from `fromDate fromTime` to `toDate toTime`, dates inclusive.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
it
|
|
47
|
+
An event runs from `fromDate fromTime` to `toDate toTime`, dates inclusive. One
|
|
48
|
+
whose `toDate` is later than its `fromDate` spans those days: the Month view
|
|
49
|
+
draws it as a single bar across them, and the Week view puts it in the all-day
|
|
50
|
+
row. A timed event that crosses midnight but is shorter than a day (an evening
|
|
51
|
+
running late) stays in the time grid, cut at midnight into a piece per day. A
|
|
52
|
+
timed event ending at `00:00` stops as that day begins, so it does not occupy
|
|
53
|
+
it. Dragging a spanning event moves both ends together.
|
|
55
54
|
|
|
56
55
|
`isFullDay` events ignore their times and cover `fromDate`..`toDate` whole.
|
|
57
56
|
|
|
58
|
-
The calendar keeps an internal copy of `events` and refreshes it when the
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
The calendar keeps an internal copy of `events` and refreshes it when the prop
|
|
58
|
+
changes. Edits made inside the calendar (create, drag, resize, delete) mutate
|
|
59
|
+
the copy and come back through the `create`, `update`, and `delete` emits —
|
|
60
|
+
persist them and refresh your source of truth from there.
|
|
62
61
|
|
|
63
|
-
`CalendarColorMap` exports the color palette (`amber`, `violet`, `pink`,
|
|
64
|
-
`
|
|
65
|
-
|
|
62
|
+
`CalendarColorMap` exports the color palette (`amber`, `violet`, `pink`, `cyan`,
|
|
63
|
+
`blue`, `orange`, `green`) with the CSS variables used per state, for building
|
|
64
|
+
matching UI such as a color picker.
|
|
66
65
|
|
|
67
66
|
## Month view
|
|
68
67
|
|
|
69
68
|
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
|
-
|
|
69
|
+
scrolls when the rows outgrow the calendar's height. The arrows, Today, and the
|
|
70
|
+
month picker move the month and scroll the strip to their date.
|
|
72
71
|
|
|
73
72
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Below the `sm` breakpoint the
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
across the top of the row; single-day events sit beneath them in their cells
|
|
74
|
+
with the title wrapping to a second line, so every event is shown — there is no
|
|
75
|
+
"n more".
|
|
76
|
+
|
|
77
|
+
Below the `sm` breakpoint it is the same grid a size down: shorter pills, a
|
|
78
|
+
tighter lane, and a cell that shows what fits in it with a `+n` count for the
|
|
79
|
+
rest — a full week trades its last lane of bars for those counts. Clicking a
|
|
80
|
+
date number at either width opens that day in the Day view.
|
|
81
|
+
|
|
82
|
+
`rangeChange` reports the strip's full extent for the Month view — the padding
|
|
83
|
+
days of the first and last weeks included — so a data source that fetches by
|
|
84
|
+
range has events for every cell.
|
|
85
|
+
|
|
86
|
+
## Agenda view
|
|
87
|
+
|
|
88
|
+
The Agenda view is three months as a list of days, each day a card: its name,
|
|
89
|
+
its date, how much is on it once that is more than one thing, then its events as
|
|
90
|
+
rows. A day with nothing on it is not listed — an empty row says nothing the
|
|
91
|
+
dates either side of it do not, and neither does a line counting how many were
|
|
92
|
+
skipped. Cards are grouped under the week they fall in, which replaces the month
|
|
93
|
+
dividers a flat list needs. Today's card carries a dot before its name; it, the
|
|
94
|
+
day before it and the day after say so in words beside their dates, and a day
|
|
95
|
+
already spent fades its header. An event that has ended is dimmed, the way a
|
|
96
|
+
pill in the grid is once its time has passed. An event under way says so with
|
|
97
|
+
its own tag.
|
|
98
|
+
|
|
99
|
+
The window covers the month in view and the two after it, padded out to whole
|
|
100
|
+
weeks at either end the way the Month view's strip is — the list groups its days
|
|
101
|
+
under the week they fall in, and a week is either listed or it is not. A single
|
|
102
|
+
month would be 31 days on the 1st and one day on the 31st, which is why it is
|
|
103
|
+
three. The arrows step a month at a time, so each move keeps two thirds of what
|
|
104
|
+
was on screen, and `rangeChange` reports exactly the span listed, padding
|
|
105
|
+
included, so a data source fetching by range agrees with it. The header names
|
|
106
|
+
the three months themselves, not the days the padding reaches into.
|
|
107
|
+
|
|
108
|
+
Rows have the room a grid pill does not, so they carry a description line and
|
|
109
|
+
tags. `Calendar` fills in the description itself — where the event is, and which
|
|
110
|
+
day of a stay the row is (`Day 2/3`) — and marks a draft as a pill does, an
|
|
111
|
+
outline in its colour on the plain ground rather than a tinted block. Where it stands against the clock
|
|
112
|
+
reads right after that: `Now` in blue while it runs and `Soon` in amber in the
|
|
113
|
+
hour before it, and nothing beyond that: an hour count would only restate the
|
|
114
|
+
time written beside it. Who is coming (`participant`) stands at the row's far
|
|
115
|
+
end, the one edge a card aligns on other than the time column, so the counts of
|
|
116
|
+
a day's rows line up — and the `#event-description` and `#event-suffix` slots
|
|
117
|
+
let you say the rest.
|
|
118
|
+
|
|
119
|
+
Drawn narrower than 640px — a phone, or a pane a sidebar has squeezed — the list
|
|
120
|
+
stacks. A day's name becomes a band over its rows rather than a column beside
|
|
121
|
+
them, and each row takes two lines: the title, its tags and who is coming on the
|
|
122
|
+
first, the time and the description on the second. The list measures its own
|
|
123
|
+
width for this, so a calendar in a narrow pane reads the same way a phone's
|
|
124
|
+
does.
|
|
125
|
+
|
|
126
|
+
<ComponentPreview name="Calendar-Agenda" csr="true" />
|
|
85
127
|
|
|
86
128
|
## Config
|
|
87
129
|
|
|
@@ -90,7 +132,7 @@ defaults:
|
|
|
90
132
|
|
|
91
133
|
```ts
|
|
92
134
|
{
|
|
93
|
-
defaultMode: 'Month', // 'Day' | 'Week' | 'Month'
|
|
135
|
+
defaultMode: 'Month', // 'Day' | 'Week' | 'Month' | 'Agenda'
|
|
94
136
|
disableModes: [], // views removed from the view switcher
|
|
95
137
|
isEditMode: false, // create / drag / resize / delete
|
|
96
138
|
enableShortcuts: true, // keyboard shortcuts (below)
|
|
@@ -108,8 +150,8 @@ defaults:
|
|
|
108
150
|
|
|
109
151
|
## Keyboard shortcuts
|
|
110
152
|
|
|
111
|
-
With `enableShortcuts` on: `m` / `w` / `d` switch views, `t` jumps to
|
|
112
|
-
`←` / `→` navigate, and `Delete` removes the event whose popover is open
|
|
153
|
+
With `enableShortcuts` on: `m` / `w` / `d` / `a` switch views, `t` jumps to
|
|
154
|
+
today, `←` / `→` navigate, and `Delete` removes the event whose popover is open
|
|
113
155
|
(edit mode only).
|
|
114
156
|
|
|
115
157
|
They stay out of the way of whatever is on top: nothing fires while a field has
|
|
@@ -119,17 +161,26 @@ focus, or while a dialog, popover, menu or select is open anywhere on the page.
|
|
|
119
161
|
|
|
120
162
|
By default, a single click on an event opens its detail popover and a double
|
|
121
163
|
click opens the edit modal (edit mode only). Clicking an empty cell opens the
|
|
122
|
-
new-event modal in edit mode. Each behavior is replaceable with the
|
|
123
|
-
`
|
|
124
|
-
|
|
164
|
+
new-event modal in edit mode. Each behavior is replaceable with the `onClick`,
|
|
165
|
+
`onDblClick`, and `onCellClick` callback props — passing one turns the default
|
|
166
|
+
off for that interaction.
|
|
125
167
|
|
|
126
168
|
The popover's content is replaceable with the `#event-popover-content` slot,
|
|
127
169
|
which receives `{ calendarEvent, date, isEditMode, close }`.
|
|
128
170
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
171
|
+
The Agenda's rows take three more slots: `#event-description` for the line
|
|
172
|
+
under the title, `#event-suffix` for the tags beside it, and
|
|
173
|
+
`#event-participant` for the row's far end, where the event's `participant`
|
|
174
|
+
string is shown unless you fill it — with faces in front of the count, say. All
|
|
175
|
+
three receive `{ calendarEvent, date, description, timing }`, where
|
|
176
|
+
`description` and `timing` are what the calendar derived itself, so you can add
|
|
177
|
+
to them rather than work them out again. The suffix has no tags of its own to
|
|
178
|
+
hand you — it is yours to fill. Grid pills have no room for any of these and
|
|
179
|
+
ignore the slots.
|
|
180
|
+
|
|
181
|
+
`CalendarActiveEvent` exports the ref holding the id of the event whose popover
|
|
182
|
+
is open. Set it from outside to highlight an event, or clear it with an empty
|
|
183
|
+
string. The ref is module-level: every `<Calendar>` on the page shares it.
|
|
133
184
|
|
|
134
185
|
## Template ref
|
|
135
186
|
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
<!-- No overflow-hidden here: each view scrolls itself, and clipping at the
|
|
3
3
|
root only cut the focus outline off the header's buttons. -->
|
|
4
4
|
<div class="flex h-full flex-col">
|
|
5
|
+
<!-- v-if on the slot, not a fallback inside it: `renderSlot` falls back whenever a
|
|
6
|
+
slot renders nothing, so a consumer passing an empty <template #header /> — a
|
|
7
|
+
phone, which draws its own — got this header anyway and had to cover it with an
|
|
8
|
+
element that renders nothing. Asking whether the slot was passed at all is the
|
|
9
|
+
question that was being asked. -->
|
|
5
10
|
<slot
|
|
11
|
+
v-if="$slots.header"
|
|
6
12
|
name="header"
|
|
7
13
|
v-bind="{
|
|
8
14
|
currentMonthYear,
|
|
@@ -17,7 +23,8 @@
|
|
|
17
23
|
onMonthYearChange,
|
|
18
24
|
selectedMonthDate,
|
|
19
25
|
}"
|
|
20
|
-
|
|
26
|
+
/>
|
|
27
|
+
<template v-else>
|
|
21
28
|
<div class="mb-2 flex justify-between">
|
|
22
29
|
<!-- left side -->
|
|
23
30
|
<!-- Year, Month -->
|
|
@@ -63,7 +70,7 @@
|
|
|
63
70
|
/>
|
|
64
71
|
</div>
|
|
65
72
|
</div>
|
|
66
|
-
</
|
|
73
|
+
</template>
|
|
67
74
|
|
|
68
75
|
<CalendarMonthly
|
|
69
76
|
v-if="activeView === 'Month'"
|
|
@@ -96,17 +103,32 @@
|
|
|
96
103
|
:current-date="selectedDay"
|
|
97
104
|
:config="overrideConfig"
|
|
98
105
|
>
|
|
99
|
-
<template #header="{ parseDateWithDay, currentDate, fullDay }">
|
|
100
|
-
<slot
|
|
101
|
-
name="daily-header"
|
|
102
|
-
v-bind="{ parseDateWithDay, currentDate, fullDay }"
|
|
103
|
-
/>
|
|
104
|
-
</template>
|
|
105
106
|
<template #event-popover-content="slotProps">
|
|
106
107
|
<slot name="event-popover-content" v-bind="slotProps" />
|
|
107
108
|
</template>
|
|
108
109
|
</CalendarDaily>
|
|
109
110
|
|
|
111
|
+
<CalendarAgenda
|
|
112
|
+
v-else-if="activeView === 'Agenda'"
|
|
113
|
+
:events="events"
|
|
114
|
+
:anchor="agendaAnchor"
|
|
115
|
+
:jump="agendaJump"
|
|
116
|
+
:loading="loading"
|
|
117
|
+
>
|
|
118
|
+
<template #event-description="slotProps">
|
|
119
|
+
<slot name="event-description" v-bind="slotProps" />
|
|
120
|
+
</template>
|
|
121
|
+
<template #event-suffix="slotProps">
|
|
122
|
+
<slot name="event-suffix" v-bind="slotProps" />
|
|
123
|
+
</template>
|
|
124
|
+
<template #event-participant="slotProps">
|
|
125
|
+
<slot name="event-participant" v-bind="slotProps" />
|
|
126
|
+
</template>
|
|
127
|
+
<template #event-popover-content="slotProps">
|
|
128
|
+
<slot name="event-popover-content" v-bind="slotProps" />
|
|
129
|
+
</template>
|
|
130
|
+
</CalendarAgenda>
|
|
131
|
+
|
|
110
132
|
<NewEventModal
|
|
111
133
|
v-if="showEventModal"
|
|
112
134
|
v-model="showEventModal"
|
|
@@ -123,7 +145,6 @@ import {
|
|
|
123
145
|
ref,
|
|
124
146
|
watch,
|
|
125
147
|
nextTick,
|
|
126
|
-
type Component,
|
|
127
148
|
} from 'vue'
|
|
128
149
|
import { Button } from '#components/Button'
|
|
129
150
|
import { TabButtons } from '#components/TabButtons'
|
|
@@ -136,18 +157,18 @@ import {
|
|
|
136
157
|
} from './calendarUtils'
|
|
137
158
|
import { dayjs } from '#utils/dayjs'
|
|
138
159
|
import { isTargetEditable } from '#composables/useKeyboardShortcut'
|
|
139
|
-
import DayIcon from './Icon/DayIcon.vue'
|
|
140
|
-
import WeekIcon from './Icon/WeekIcon.vue'
|
|
141
|
-
import MonthIcon from './Icon/MonthIcon.vue'
|
|
142
160
|
import DatePicker from '#components/DatePicker/DatePicker.vue'
|
|
143
161
|
import CalendarMonthly from './CalendarMonthly.vue'
|
|
144
162
|
import CalendarWeekly from './CalendarWeekly.vue'
|
|
145
163
|
import CalendarDaily from './CalendarDaily.vue'
|
|
164
|
+
import CalendarAgenda from './CalendarAgenda.vue'
|
|
146
165
|
import NewEventModal from './NewEventModal.vue'
|
|
147
166
|
import useEventModal from './composables/useEventModal'
|
|
148
167
|
import { isAnyPopoverOpen } from './useEventBase'
|
|
149
168
|
import { stripPlacement } from './eventSpan'
|
|
150
169
|
import { stripRange } from './monthStrip'
|
|
170
|
+
import { activeEvent } from './composables/useCalendarData'
|
|
171
|
+
import { agendaMonths, agendaRange } from './agendaDays'
|
|
151
172
|
import {
|
|
152
173
|
ACTIVE_VIEW_KEY,
|
|
153
174
|
CALENDAR_ACTIONS_KEY,
|
|
@@ -173,6 +194,9 @@ const emit = defineEmits<{
|
|
|
173
194
|
]
|
|
174
195
|
}>()
|
|
175
196
|
|
|
197
|
+
/** Bumped by setCalendarDate; the Agenda scrolls back to its anchor on every bump. */
|
|
198
|
+
const agendaJump = ref(0)
|
|
199
|
+
|
|
176
200
|
const defaultConfig: CalendarConfig = {
|
|
177
201
|
scrollToHour: 15,
|
|
178
202
|
disableModes: [],
|
|
@@ -266,24 +290,17 @@ function handleShortcuts(e: KeyboardEvent) {
|
|
|
266
290
|
if (isOverlayOpen()) return
|
|
267
291
|
if (isTargetEditable(e)) return
|
|
268
292
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (e.key
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
if (e.key === 'ArrowLeft') {
|
|
282
|
-
decrement()
|
|
283
|
-
}
|
|
284
|
-
if (e.key === 'ArrowRight') {
|
|
285
|
-
increment()
|
|
286
|
-
}
|
|
293
|
+
const view = viewShortcuts[e.key.toLowerCase()]
|
|
294
|
+
if (view) activeView.value = view
|
|
295
|
+
if (e.key.toLowerCase() === 't') setCalendarDate()
|
|
296
|
+
if (e.key === 'ArrowLeft') decrement()
|
|
297
|
+
if (e.key === 'ArrowRight') increment()
|
|
298
|
+
}
|
|
299
|
+
const viewShortcuts: Record<string, CalendarMode> = {
|
|
300
|
+
m: 'Month',
|
|
301
|
+
w: 'Week',
|
|
302
|
+
a: 'Agenda',
|
|
303
|
+
d: 'Day',
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
provide(ACTIVE_VIEW_KEY, activeView)
|
|
@@ -388,6 +405,9 @@ function handleCellClick(
|
|
|
388
405
|
return
|
|
389
406
|
}
|
|
390
407
|
|
|
408
|
+
// Clicking the list or the grid itself is how you let go of the event.
|
|
409
|
+
activeEvent.value = ''
|
|
410
|
+
|
|
391
411
|
const data: CalendarCellClickData = {
|
|
392
412
|
e,
|
|
393
413
|
view: activeView.value,
|
|
@@ -406,14 +426,21 @@ function handleCellClick(
|
|
|
406
426
|
type CalendarActionOption = {
|
|
407
427
|
label: CalendarMode
|
|
408
428
|
value: CalendarMode
|
|
409
|
-
iconLeft:
|
|
429
|
+
iconLeft: string
|
|
410
430
|
}
|
|
411
431
|
|
|
412
|
-
// Calendar View Options
|
|
432
|
+
// Calendar View Options.
|
|
433
|
+
//
|
|
434
|
+
// Named lucide icons rather than four SVGs of the library's own: the set draws each
|
|
435
|
+
// view as the shape of what it lays out — one pane, columns, a grid, a stack of
|
|
436
|
+
// rows. Being lucide they also carry the same weight and
|
|
437
|
+
// optical size as every other icon on the page, which hand-drawn glyphs at a fixed
|
|
438
|
+
// 16px did not.
|
|
413
439
|
const actionOptions: CalendarActionOption[] = [
|
|
414
|
-
{ label: 'Day', value: 'Day', iconLeft:
|
|
415
|
-
{ label: 'Week', value: 'Week', iconLeft:
|
|
416
|
-
{ label: 'Month', value: 'Month', iconLeft:
|
|
440
|
+
{ label: 'Day', value: 'Day', iconLeft: 'lucide-square-square' },
|
|
441
|
+
{ label: 'Week', value: 'Week', iconLeft: 'lucide-columns-3' },
|
|
442
|
+
{ label: 'Month', value: 'Month', iconLeft: 'lucide-grid-3x3' },
|
|
443
|
+
{ label: 'Agenda', value: 'Agenda', iconLeft: 'lucide-rows-3' },
|
|
417
444
|
]
|
|
418
445
|
let enabledModes = actionOptions.filter(
|
|
419
446
|
(mode) => !overrideConfig.disableModes.includes(mode.value),
|
|
@@ -458,16 +485,19 @@ let date = ref(
|
|
|
458
485
|
)
|
|
459
486
|
let selectedDay = computed(() => currentMonthDates.value[date.value])
|
|
460
487
|
|
|
488
|
+
/**
|
|
489
|
+
* The day the calendar is on, whichever view it is in: the one `date` points
|
|
490
|
+
* at, which every view moves — a step of the month or the week lands it on the
|
|
491
|
+
* first day in the period, a step of the day on the next one, and a date the
|
|
492
|
+
* host sets on that date. Reported the same way in every view so a host's mini
|
|
493
|
+
* month circles the day the reader picked. It used to answer the Month view
|
|
494
|
+
* with 1 and the Week view with the week's Sunday, and a mini month handed
|
|
495
|
+
* those circled the 1st, or the Sunday, while the calendar sat on the 11th the
|
|
496
|
+
* reader had just clicked in it.
|
|
497
|
+
*/
|
|
461
498
|
function computeCurrentDay(): number | null {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return weekDates[0] ? weekDates[0].getDate() : null
|
|
465
|
-
}
|
|
466
|
-
if (activeView.value === 'Day') {
|
|
467
|
-
const day = selectedDay.value
|
|
468
|
-
return day ? new Date(day).getDate() : null
|
|
469
|
-
}
|
|
470
|
-
return 1
|
|
499
|
+
const day = selectedDay.value
|
|
500
|
+
return day ? new Date(day).getDate() : null
|
|
471
501
|
}
|
|
472
502
|
|
|
473
503
|
let currentDay = ref(computeCurrentDay())
|
|
@@ -500,8 +530,14 @@ const incrementClickEvents: Record<CalendarMode, () => void> = {
|
|
|
500
530
|
Month: incrementMonth,
|
|
501
531
|
Week: incrementWeek,
|
|
502
532
|
Day: incrementDay,
|
|
533
|
+
// The Agenda's window is anchored on a month and covers three, so its arrows
|
|
534
|
+
// step one — each move keeps two thirds of what was on screen.
|
|
535
|
+
Agenda: incrementMonth,
|
|
503
536
|
}
|
|
504
537
|
|
|
538
|
+
/** The day the Agenda is anchored on: whatever day the calendar is sitting on. */
|
|
539
|
+
const agendaAnchor = computed(() => selectedDay.value ?? new Date())
|
|
540
|
+
|
|
505
541
|
// decrementMonth lands on the month's last day, which is right for stepping
|
|
506
542
|
// back a day across a month edge but scrolls the Month strip to its bottom;
|
|
507
543
|
// the Month view's own arrow lands on the first day, as the other arrow does.
|
|
@@ -515,6 +551,9 @@ const decrementClickEvents: Record<CalendarMode, () => void> = {
|
|
|
515
551
|
Month: decrementMonthView,
|
|
516
552
|
Week: decrementWeek,
|
|
517
553
|
Day: decrementDay,
|
|
554
|
+
// Lands on the 1st rather than the last day, which is where the window's
|
|
555
|
+
// first month wants to start reading.
|
|
556
|
+
Agenda: decrementMonthView,
|
|
518
557
|
}
|
|
519
558
|
|
|
520
559
|
function incrementMonth() {
|
|
@@ -666,6 +705,18 @@ const currentMonthYear = computed(() => {
|
|
|
666
705
|
}
|
|
667
706
|
}
|
|
668
707
|
|
|
708
|
+
// The Agenda is anchored on whole months, so it names them rather than the
|
|
709
|
+
// dates its ends happen to land on — `agendaMonths`, not the listed range,
|
|
710
|
+
// which pads to whole weeks and would have the title naming a month for the
|
|
711
|
+
// sake of the two days of it the first week reaches back into.
|
|
712
|
+
if (activeView.value === 'Agenda') {
|
|
713
|
+
const { start, end } = agendaMonths(agendaAnchor.value)
|
|
714
|
+
const short = (d: Date) => monthList[d.getMonth()].slice(0, 3)
|
|
715
|
+
return start.getFullYear() === end.getFullYear()
|
|
716
|
+
? `${short(start)} – ${short(end)} ${end.getFullYear()}`
|
|
717
|
+
: `${short(start)} ${start.getFullYear()} – ${short(end)} ${end.getFullYear()}`
|
|
718
|
+
}
|
|
719
|
+
|
|
669
720
|
// Non-week views or empty week fallback
|
|
670
721
|
if (activeView.value !== 'Week')
|
|
671
722
|
return formatMonthYear(currentMonth.value, currentYear.value)
|
|
@@ -695,6 +746,10 @@ function isCurrentMonthDate(date?: Date) {
|
|
|
695
746
|
function setCalendarDate(d?: Date | string) {
|
|
696
747
|
const dt = d ? new Date(d) : new Date()
|
|
697
748
|
if (dt.toString() === 'Invalid Date') return
|
|
749
|
+
// Counted, not compared: the Agenda reads this to know it has been sent somewhere,
|
|
750
|
+
// which is not the same as its anchor changing — Today, pressed halfway down the
|
|
751
|
+
// list on the day it is already anchored on, moves nothing and means everything.
|
|
752
|
+
agendaJump.value++
|
|
698
753
|
currentYear.value = dt.getFullYear()
|
|
699
754
|
currentMonth.value = dt.getMonth()
|
|
700
755
|
currentDate.value = dt
|
|
@@ -735,6 +790,17 @@ function getVisibleRange() {
|
|
|
735
790
|
}
|
|
736
791
|
}
|
|
737
792
|
|
|
793
|
+
// The Agenda reports what it actually lists — three months from where it is
|
|
794
|
+
// anchored, less the days already spent — so a consumer's fetch window and
|
|
795
|
+
// its "new event" anchor agree with what is on screen.
|
|
796
|
+
if (activeView.value === 'Agenda') {
|
|
797
|
+
const { start, end } = agendaRange(agendaAnchor.value)
|
|
798
|
+
return {
|
|
799
|
+
startDate: dayjs(start).format('YYYY-MM-DD'),
|
|
800
|
+
endDate: dayjs(end).format('YYYY-MM-DD'),
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
738
804
|
// The Month strip's first and last weeks reach into the neighbouring months.
|
|
739
805
|
const range = stripRange(currentMonth.value, currentYear.value)
|
|
740
806
|
return {
|