@k2b/ui 0.22.0 → 0.23.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/dist/browser/content/Calendar.js +1086 -1049
- package/dist/browser/content/Calendar.js.map +1 -1
- package/dist/browser/content/CalendarDayView.js +1187 -0
- package/dist/browser/content/CalendarDayView.js.map +1 -0
- package/dist/browser/content/CalendarWeekView.js +1571 -0
- package/dist/browser/content/CalendarWeekView.js.map +1 -0
- package/dist/browser/content/ChartExplorer.js +174 -82
- package/dist/browser/content/ChartExplorer.js.map +1 -1
- package/dist/browser/content/ChartFilterControls.js +90 -33
- package/dist/browser/content/ChartFilterControls.js.map +1 -1
- package/dist/browser/content/DataTable.js +199 -171
- package/dist/browser/content/DataTable.js.map +1 -1
- package/dist/browser/content/calendar-day-scroll.js +98 -0
- package/dist/browser/content/calendar-day-scroll.js.map +1 -0
- package/dist/browser/content/calendar-days.js +408 -0
- package/dist/browser/content/calendar-days.js.map +1 -0
- package/dist/browser/content/calendar-dialogs.js +236 -0
- package/dist/browser/content/calendar-dialogs.js.map +1 -0
- package/dist/browser/content/calendar-messages.js +210 -2
- package/dist/browser/content/calendar-messages.js.map +1 -1
- package/dist/browser/content/calendar-pointer.js +15 -3
- package/dist/browser/content/calendar-pointer.js.map +1 -1
- package/dist/browser/content/calendar-shared.js +328 -0
- package/dist/browser/content/calendar-shared.js.map +1 -0
- package/dist/browser/content/calendar-time-view.js +1649 -0
- package/dist/browser/content/calendar-time-view.js.map +1 -0
- package/dist/browser/content/chart-messages.js +10 -0
- package/dist/browser/content/chart-messages.js.map +1 -1
- package/dist/browser/content/index.js.map +1 -1
- package/dist/browser/inputs/Combobox.js +0 -1
- package/dist/browser/inputs/Combobox.js.map +1 -1
- package/dist/browser/layout/AppWorkspace.js +123 -97
- package/dist/browser/layout/AppWorkspace.js.map +1 -1
- package/dist/browser/layout/PageTitle.js +4 -1
- package/dist/browser/layout/PageTitle.js.map +1 -1
- package/dist/browser/layout/app-workspace-controller.js +164 -86
- package/dist/browser/layout/app-workspace-controller.js.map +1 -1
- package/dist/browser/surfaces/StatCell.js +18 -6
- package/dist/browser/surfaces/StatCell.js.map +1 -1
- package/dist/ssr/content/Calendar.js +524 -335
- package/dist/ssr/content/Calendar.js.map +1 -1
- package/dist/ssr/content/CalendarDayView.js +800 -0
- package/dist/ssr/content/CalendarDayView.js.map +1 -0
- package/dist/ssr/content/CalendarWeekView.js +1069 -0
- package/dist/ssr/content/CalendarWeekView.js.map +1 -0
- package/dist/ssr/content/ChartExplorer.js +93 -34
- package/dist/ssr/content/ChartExplorer.js.map +1 -1
- package/dist/ssr/content/ChartFilterControls.js +49 -7
- package/dist/ssr/content/ChartFilterControls.js.map +1 -1
- package/dist/ssr/content/DataTable.js +69 -51
- package/dist/ssr/content/DataTable.js.map +1 -1
- package/dist/ssr/content/calendar-day-scroll.js +98 -0
- package/dist/ssr/content/calendar-day-scroll.js.map +1 -0
- package/dist/ssr/content/calendar-days.js +408 -0
- package/dist/ssr/content/calendar-days.js.map +1 -0
- package/dist/ssr/content/calendar-dialogs.js +159 -0
- package/dist/ssr/content/calendar-dialogs.js.map +1 -0
- package/dist/ssr/content/calendar-messages.js +210 -2
- package/dist/ssr/content/calendar-messages.js.map +1 -1
- package/dist/ssr/content/calendar-pointer.js +15 -3
- package/dist/ssr/content/calendar-pointer.js.map +1 -1
- package/dist/ssr/content/calendar-shared.js +261 -0
- package/dist/ssr/content/calendar-shared.js.map +1 -0
- package/dist/ssr/content/calendar-time-view.js +1306 -0
- package/dist/ssr/content/calendar-time-view.js.map +1 -0
- package/dist/ssr/content/chart-messages.js +10 -0
- package/dist/ssr/content/chart-messages.js.map +1 -1
- package/dist/ssr/content/index.js.map +1 -1
- package/dist/ssr/inputs/Combobox.js.map +1 -1
- package/dist/ssr/layout/AppWorkspace.js +38 -29
- package/dist/ssr/layout/AppWorkspace.js.map +1 -1
- package/dist/ssr/layout/PageTitle.js +4 -1
- package/dist/ssr/layout/PageTitle.js.map +1 -1
- package/dist/ssr/layout/app-workspace-controller.js +164 -86
- package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
- package/dist/ssr/surfaces/StatCell.js +2 -2
- package/dist/ssr/surfaces/StatCell.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/content/Calendar.d.ts +91 -0
- package/dist/types/content/CalendarDayView.d.ts +7 -0
- package/dist/types/content/CalendarWeekView.d.ts +7 -0
- package/dist/types/content/ChartExplorer.d.ts +36 -1
- package/dist/types/content/ChartFilterControls.d.ts +6 -1
- package/dist/types/content/DataTable.d.ts +5 -0
- package/dist/types/content/calendar-day-scroll.d.ts +42 -0
- package/dist/types/content/calendar-days.d.ts +192 -0
- package/dist/types/content/calendar-dialogs.d.ts +22 -0
- package/dist/types/content/calendar-messages.d.ts +99 -0
- package/dist/types/content/calendar-pointer.d.ts +9 -0
- package/dist/types/content/calendar-shared.d.ts +69 -0
- package/dist/types/content/calendar-time-view.d.ts +252 -0
- package/dist/types/content/chart-messages.d.ts +5 -0
- package/dist/types/content/index.d.ts +2 -2
- package/dist/types/layout/AppWorkspace.d.ts +25 -3
- package/dist/types/layout/PageTitle.d.ts +2 -1
- package/dist/types/layout/app-workspace-controller.d.ts +9 -1
- package/dist/types/surfaces/StatCell.d.ts +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1306 @@
|
|
|
1
|
+
import { ssrStyle as _$ssrStyle } from "solid-js/web";
|
|
2
|
+
import { memo as _$memo } from "solid-js/web";
|
|
3
|
+
import { ssrStyleProperty as _$ssrStyleProperty } from "solid-js/web";
|
|
4
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
5
|
+
import { escape as _$escape } from "solid-js/web";
|
|
6
|
+
import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
|
|
7
|
+
import { ssr as _$ssr } from "solid-js/web";
|
|
8
|
+
var _tmpl$ = "<svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><rect x=\"1.75\" y=\"1.75\" width=\"12.5\" height=\"12.5\" rx=\"3.5\"></rect><path d=\"M4.6 8.3l2.3 2.3 4.6-4.9\"></path></svg>",
|
|
9
|
+
_tmpl$2 = ["<button type=\"button\" class=\"k2b-calendar-check\" role=\"checkbox\"", ">", "</button>"],
|
|
10
|
+
_tmpl$3 = ["<span class=\"k2b-calendar-check\"", " aria-hidden=\"true\">", "</span>"],
|
|
11
|
+
_tmpl$4 = "<span class=\"k2b-calendar-band__fill\" aria-hidden=\"true\"></span>",
|
|
12
|
+
_tmpl$5 = "<span class=\"k2b-calendar-band__grip\" data-edge=\"start\" aria-hidden=\"true\"></span>",
|
|
13
|
+
_tmpl$6 = "<span class=\"k2b-calendar-band__grip\" data-edge=\"end\" aria-hidden=\"true\"></span>",
|
|
14
|
+
_tmpl$7 = ["<div class=\"k2b-calendar-band\"", " style=\"", "\">", "", "", "", "</div>"],
|
|
15
|
+
_tmpl$8 = ["<div class=\"k2b-calendar-marker\"", " style=\"", "\"><span class=\"k2b-calendar-marker__line\" aria-hidden=\"true\"></span><span class=\"k2b-calendar-marker__pill\">", "", "</span></div>"],
|
|
16
|
+
_tmpl$9 = ["<span class=\"k2b-calendar-marker__time\">", "</span>"],
|
|
17
|
+
_tmpl$0 = ["<button type=\"button\" class=\"k2b-calendar-gap__button\"", "><i class=\"ti ti-plus\" aria-hidden=\"true\"></i><span class=\"k2b-calendar-gap__text\">", "</span></button>"],
|
|
18
|
+
_tmpl$1 = ["<div class=\"k2b-calendar-gap\"", " style=\"", "\"><span class=\"k2b-calendar-gap__zone\" aria-hidden=\"true\"></span>", "</div>"],
|
|
19
|
+
_tmpl$10 = ["<span class=\"k2b-calendar-slot\" style=\"", "\" aria-hidden=\"true\"></span>"],
|
|
20
|
+
_tmpl$11 = ["<div class=\"k2b-calendar-draft\" style=\"", "\"><span class=\"k2b-calendar-draft__grip\" data-edge=\"start\" aria-hidden=\"true\"></span><button type=\"button\" class=\"k2b-calendar-draft__open\"", "><i class=\"ti ti-plus\" aria-hidden=\"true\"></i><span>", "–", " · ", "</span></button><span class=\"k2b-calendar-draft__grip\" data-edge=\"end\" aria-hidden=\"true\"></span></div>"],
|
|
21
|
+
_tmpl$12 = ["<div class=\"k2b-calendar-ghost\" data-live=\"true\"", " style=\"", "\" aria-hidden=\"true\"><span class=\"k2b-calendar-ghost__title\">", "</span><span class=\"k2b-calendar-ghost__time\">", "</span>", "</div>"],
|
|
22
|
+
_tmpl$13 = ["<span class=\"k2b-calendar-ghost__fit\"", ">", "</span>"],
|
|
23
|
+
_tmpl$14 = ["<div class=\"k2b-calendar-popover\" popover=\"auto\" role=\"dialog\"", " data-kind=\"create\">", "</div>"],
|
|
24
|
+
_tmpl$15 = ["<span class=\"k2b-calendar-allday-chip\"", " style=\"", "\">", "", "</span>"];
|
|
25
|
+
import { dates as calendar } from "@k2b/stdlib";
|
|
26
|
+
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show, untrack } from "solid-js";
|
|
27
|
+
import { announce } from "../feedback/announce";
|
|
28
|
+
import { addDayKeys, buildDays, clockAt, DAY_MINUTES, dayKeyOf, dayLanes, dayMinuteAt, dayTopTerms, dayYTerms, freeGaps, GAP_LABEL_MINUTES, GAP_LONG_MINUTES, gapProposal, instantInDay, instantsInDay, minuteInDay, readDayGeometry, resizedInDay, snapMinutes, subtractTerms, termsCss, wakeMinutes } from "./calendar-days";
|
|
29
|
+
import { useCalendarMessages } from "./calendar-messages";
|
|
30
|
+
import { startCalendarPointerSession } from "./calendar-pointer";
|
|
31
|
+
import { EventChip, eventHref, ownerDateConfig, parseDate, placeCalendarPopover, validDate } from "./calendar-shared";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shared state and parts of the day and week views with `dayHours`: the time model of the loaded days, the selection,
|
|
35
|
+
* the quick create, pointer gestures, and the pieces both views draw into a day's timeline.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** What the header and the calendar's controller ask of the active day or week view. */
|
|
39
|
+
|
|
40
|
+
const COLOR_TOKENS = {
|
|
41
|
+
blue: "#3b82f6",
|
|
42
|
+
emerald: "#10b981",
|
|
43
|
+
amber: "#f59e0b",
|
|
44
|
+
red: "#ef4444",
|
|
45
|
+
violet: "#8b5cf6",
|
|
46
|
+
cyan: "#06b6d4",
|
|
47
|
+
zinc: "#a1a1aa"
|
|
48
|
+
};
|
|
49
|
+
export const eventAccent = event => event.colorHex ?? COLOR_TOKENS[event.color ?? "blue"];
|
|
50
|
+
|
|
51
|
+
/** Minimum minutes an overlap counts for a band, about the height of its title. */
|
|
52
|
+
const LANE_MINUTES = 20;
|
|
53
|
+
export const reducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
54
|
+
|
|
55
|
+
/** Swallows the click a browser sends right after a gesture ends, never the next one. */
|
|
56
|
+
export const swallowNextClick = () => {
|
|
57
|
+
const stop = event => {
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
};
|
|
61
|
+
window.addEventListener("click", stop, {
|
|
62
|
+
capture: true,
|
|
63
|
+
once: true
|
|
64
|
+
});
|
|
65
|
+
window.setTimeout(() => window.removeEventListener("click", stop, {
|
|
66
|
+
capture: true
|
|
67
|
+
}), 120);
|
|
68
|
+
};
|
|
69
|
+
export const createTimeView = props => {
|
|
70
|
+
const messages = useCalendarMessages();
|
|
71
|
+
const dateConfig = createMemo(() => ownerDateConfig(props.owner));
|
|
72
|
+
const hours = () => props.hours;
|
|
73
|
+
const wake = createMemo(() => wakeMinutes(props.hours));
|
|
74
|
+
const keyOf = date => calendar.formatDateKey(date, dateConfig());
|
|
75
|
+
const parseKey = key => calendar.parseCalendarDate(key, dateConfig());
|
|
76
|
+
|
|
77
|
+
/** The loaded days: the host's range, or the day or the week of the date. */
|
|
78
|
+
const range = createMemo(() => {
|
|
79
|
+
const context = dateConfig();
|
|
80
|
+
const loaded = props.owner.range;
|
|
81
|
+
if (loaded) {
|
|
82
|
+
const from = parseDate(loaded.from, context);
|
|
83
|
+
const to = parseDate(loaded.to, context);
|
|
84
|
+
if (validDate(from) && validDate(to)) {
|
|
85
|
+
const first = calendar.formatDateKey(from, context);
|
|
86
|
+
const last = calendar.formatDateKey(new Date(Math.max(from.getTime(), to.getTime() - 1)), context);
|
|
87
|
+
return {
|
|
88
|
+
first,
|
|
89
|
+
last
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const key = calendar.formatDateKey(props.date, context);
|
|
94
|
+
if (props.view === "day") return {
|
|
95
|
+
first: key,
|
|
96
|
+
last: key
|
|
97
|
+
};
|
|
98
|
+
const start = calendar.formatDateKey(calendar.startOfWeek(props.date, context), context);
|
|
99
|
+
return {
|
|
100
|
+
first: start,
|
|
101
|
+
last: addDayKeys(start, 6)
|
|
102
|
+
};
|
|
103
|
+
}, undefined, {
|
|
104
|
+
equals: (left, right) => left.first === right.first && left.last === right.last
|
|
105
|
+
});
|
|
106
|
+
const anchorKey = createMemo(() => keyOf(props.date));
|
|
107
|
+
|
|
108
|
+
/** Checkbox states the reader just set, shown until the host's events agree or drop the entry. */
|
|
109
|
+
const [checks, setChecks] = createSignal({});
|
|
110
|
+
const [justChecked, setJustChecked] = createSignal(new Set());
|
|
111
|
+
createEffect(on(() => props.events, events => {
|
|
112
|
+
const current = untrack(checks);
|
|
113
|
+
const ids = Object.keys(current);
|
|
114
|
+
if (ids.length === 0) return;
|
|
115
|
+
const next = {
|
|
116
|
+
...current
|
|
117
|
+
};
|
|
118
|
+
for (const id of ids) {
|
|
119
|
+
const event = events.find(candidate => candidate.id === id);
|
|
120
|
+
if (!event || event.checked === current[id]) delete next[id];
|
|
121
|
+
}
|
|
122
|
+
setChecks(next);
|
|
123
|
+
}, {
|
|
124
|
+
defer: true
|
|
125
|
+
}));
|
|
126
|
+
const checkedOf = event => event.checked === undefined ? undefined : checks()[event.id] ?? event.checked;
|
|
127
|
+
/** What Space on an entry's chip does: check its box, where it has one the reader may change. */
|
|
128
|
+
const checkOf = event => checkedOf(event) !== undefined && props.owner.onEventCheck ? () => toggleCheck(event) : undefined;
|
|
129
|
+
const forgetJustChecked = id => setJustChecked(value => {
|
|
130
|
+
if (!value.has(id)) return value;
|
|
131
|
+
const rest = new Set(value);
|
|
132
|
+
rest.delete(id);
|
|
133
|
+
return rest;
|
|
134
|
+
});
|
|
135
|
+
/** Today's day for a moment after its last open to-do was checked off, which its heading celebrates. */
|
|
136
|
+
const [cheered, setCheered] = createSignal(null);
|
|
137
|
+
let cheerTimer = 0;
|
|
138
|
+
// The server renders the view too, and has no window.
|
|
139
|
+
onCleanup(() => clearTimeout(cheerTimer));
|
|
140
|
+
/** The entries with a checkbox on a day: its deadlines, the to-dos planned into its hours, and the ones due all day. */
|
|
141
|
+
const dueOn = key => {
|
|
142
|
+
const model = dayOf(key);
|
|
143
|
+
return [...model.markers.map(marker => marker.item), ...model.bands.map(band => band.item), ...model.spans.filter(span => span.marker && span.firstKey === key).map(span => span.item)].filter(item => item.checked !== undefined);
|
|
144
|
+
};
|
|
145
|
+
const toggleCheck = event => {
|
|
146
|
+
const current = checkedOf(event);
|
|
147
|
+
if (current === undefined || !props.owner.onEventCheck) return;
|
|
148
|
+
const next = !current;
|
|
149
|
+
setChecks(value => ({
|
|
150
|
+
...value,
|
|
151
|
+
[event.id]: next
|
|
152
|
+
}));
|
|
153
|
+
if (next) {
|
|
154
|
+
setJustChecked(value => new Set([...value, event.id]));
|
|
155
|
+
window.setTimeout(() => forgetJustChecked(event.id), 1100);
|
|
156
|
+
const today = nowKey();
|
|
157
|
+
const due = dueOn(today);
|
|
158
|
+
if (due.includes(event) && due.every(item => checkedOf(item))) {
|
|
159
|
+
// The heading shows it only for a moment, so a screen reader hears it once instead.
|
|
160
|
+
announce(messages().calendarAllDone);
|
|
161
|
+
setCheered(today);
|
|
162
|
+
window.clearTimeout(cheerTimer);
|
|
163
|
+
cheerTimer = window.setTimeout(() => setCheered(null), 2200);
|
|
164
|
+
}
|
|
165
|
+
} else setCheered(null);
|
|
166
|
+
// A change the host refuses puts the box back as it was.
|
|
167
|
+
void Promise.resolve(props.owner.onEventCheck(event, next)).catch(() => {
|
|
168
|
+
forgetJustChecked(event.id);
|
|
169
|
+
if (next) setCheered(null);
|
|
170
|
+
setChecks(value => {
|
|
171
|
+
if (value[event.id] !== next) return value;
|
|
172
|
+
const rest = {
|
|
173
|
+
...value
|
|
174
|
+
};
|
|
175
|
+
delete rest[event.id];
|
|
176
|
+
return rest;
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
const entries = createMemo(() => {
|
|
181
|
+
const context = dateConfig();
|
|
182
|
+
return props.events.flatMap(event => {
|
|
183
|
+
const start = parseDate(event.start, context);
|
|
184
|
+
if (!validDate(start)) return [];
|
|
185
|
+
const marker = event.display === "marker";
|
|
186
|
+
const parsedEnd = event.end ? parseDate(event.end, context) : null;
|
|
187
|
+
let end = parsedEnd && validDate(parsedEnd) && parsedEnd > start ? parsedEnd : null;
|
|
188
|
+
if (!end) end = event.allDay ? calendar.addDays(calendar.startOfDay(start, context), 1, context) : marker ? start : new Date(start.getTime() + 3_600_000);
|
|
189
|
+
return [{
|
|
190
|
+
item: event,
|
|
191
|
+
start,
|
|
192
|
+
end,
|
|
193
|
+
allDay: Boolean(event.allDay),
|
|
194
|
+
marker
|
|
195
|
+
}];
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
const days = createMemo(() => buildDays(range().first, range().last, entries(), props.hours, dateConfig()));
|
|
199
|
+
const emptyModel = key => ({
|
|
200
|
+
key,
|
|
201
|
+
bands: [],
|
|
202
|
+
markers: [],
|
|
203
|
+
spans: [],
|
|
204
|
+
night: false,
|
|
205
|
+
empty: true
|
|
206
|
+
});
|
|
207
|
+
const dayOf = key => days().get(key) ?? emptyModel(key);
|
|
208
|
+
const sourceOf = createMemo(() => new Map(entries().map(entry => [entry.item, entry])));
|
|
209
|
+
|
|
210
|
+
/** The day that holds now: before the start hour still the day before. */
|
|
211
|
+
const nowKey = createMemo(() => dayKeyOf(props.now, props.hours, dateConfig()));
|
|
212
|
+
const nowMinute = createMemo(() => minuteInDay(nowKey(), props.now, props.hours, dateConfig()));
|
|
213
|
+
/** Now as a minute of a day: past days lie wholly before it, coming days after. */
|
|
214
|
+
const nowIn = key => key === nowKey() ? nowMinute() : key < nowKey() ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
215
|
+
const calendarToday = createMemo(() => keyOf(props.now));
|
|
216
|
+
const clock = minute => clockAt(minute, props.hours);
|
|
217
|
+
const duration = minutes => messages().calendarDuration({
|
|
218
|
+
minutes: Math.max(1, Math.round(minutes))
|
|
219
|
+
});
|
|
220
|
+
const instant = (key, minute) => instantInDay(key, minute, props.hours, dateConfig());
|
|
221
|
+
const slot = (key, start, end) => instantsInDay(key, start, end, props.hours, dateConfig());
|
|
222
|
+
const dayStartMinute = hour => Math.max(0, (hour - props.hours[0]) * 60);
|
|
223
|
+
|
|
224
|
+
/** A day's timed piece as the event chip reads it. */
|
|
225
|
+
const normalized = (event, key) => {
|
|
226
|
+
const source = sourceOf().get(event);
|
|
227
|
+
const start = source?.start ?? parseDate(event.start, dateConfig());
|
|
228
|
+
const end = source?.end ?? start;
|
|
229
|
+
return {
|
|
230
|
+
...event,
|
|
231
|
+
startDate: start,
|
|
232
|
+
endDate: end,
|
|
233
|
+
sourceStartDate: start,
|
|
234
|
+
sourceEndDate: end,
|
|
235
|
+
dayKey: key
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
const [selection, setSelection] = createSignal(null);
|
|
239
|
+
const [touchDraft, setTouchDraft] = createSignal(null);
|
|
240
|
+
const [activeKey, setActiveKey] = createSignal(null);
|
|
241
|
+
const [ghost, setGhost] = createSignal(null);
|
|
242
|
+
const [movingId, setMovingId] = createSignal("");
|
|
243
|
+
const [dragging, setDragging] = createSignal("");
|
|
244
|
+
const [nightOverrides, setNightOverrides] = createSignal({});
|
|
245
|
+
const [quick, setQuick] = createSignal(null);
|
|
246
|
+
const rangeOfSelection = value => value.start === undefined || value.end === undefined ? {
|
|
247
|
+
start: parseKey(value.key),
|
|
248
|
+
end: calendar.addDays(parseKey(value.key), 1, dateConfig()),
|
|
249
|
+
allDay: true
|
|
250
|
+
} : {
|
|
251
|
+
...slot(value.key, value.start, value.end),
|
|
252
|
+
allDay: false
|
|
253
|
+
};
|
|
254
|
+
createEffect(on(selection, value => props.owner.onSelectionChange?.(value ? rangeOfSelection(value) : null), {
|
|
255
|
+
defer: true
|
|
256
|
+
}));
|
|
257
|
+
onCleanup(() => {
|
|
258
|
+
if (untrack(selection)) props.owner.onSelectionChange?.(null);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Body elements of the days on screen, for pointer positions.
|
|
262
|
+
const bodies = new Map();
|
|
263
|
+
const registerBody = (key, element, expanded) => {
|
|
264
|
+
bodies.set(key, {
|
|
265
|
+
element,
|
|
266
|
+
expanded
|
|
267
|
+
});
|
|
268
|
+
onCleanup(() => {
|
|
269
|
+
if (bodies.get(key)?.element === element) bodies.delete(key);
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
const bodyAt = (clientX, clientY) => {
|
|
273
|
+
for (const [key, body] of bodies) {
|
|
274
|
+
if (!body.element.isConnected) continue;
|
|
275
|
+
const rect = body.element.getBoundingClientRect();
|
|
276
|
+
if (clientX < rect.left || clientX >= rect.right || clientY < rect.top || clientY >= rect.bottom) continue;
|
|
277
|
+
const geometry = readDayGeometry(body.element, wake(), body.expanded());
|
|
278
|
+
return {
|
|
279
|
+
key,
|
|
280
|
+
element: body.element,
|
|
281
|
+
geometry,
|
|
282
|
+
minute: dayMinuteAt(geometry, clientY - rect.top)
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
};
|
|
287
|
+
const limitOf = key => bodies.get(key)?.expanded() ? DAY_MINUTES : wake();
|
|
288
|
+
const nightOpen = (key, model) => nightOverrides()[key] ?? model.night;
|
|
289
|
+
const setNight = (key, open) => setNightOverrides(value => ({
|
|
290
|
+
...value,
|
|
291
|
+
[key]: open
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
/** The gap under a minute while dragging, and whether `need` minutes fit it; the gap shows "fits" or how much is missing. */
|
|
295
|
+
const fitAt = (key, minute, need, span) => {
|
|
296
|
+
const gap = freeGaps(dayOf(key).bands, wake(), Number.NEGATIVE_INFINITY).find(candidate => minute >= candidate.start && minute < candidate.end);
|
|
297
|
+
if (!gap) return null;
|
|
298
|
+
const fits = gap.end - gap.start >= need;
|
|
299
|
+
if (fits && span && (span.start < gap.start || span.end > gap.end)) return null;
|
|
300
|
+
return {
|
|
301
|
+
gap,
|
|
302
|
+
fits,
|
|
303
|
+
text: fits ? messages().calendarFits : messages().calendarTooShort({
|
|
304
|
+
duration: duration(need - (gap.end - gap.start))
|
|
305
|
+
})
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
const [gapHit, setGapHit] = createSignal(null);
|
|
309
|
+
const overlapAt = (key, start, end, except) => dayOf(key).bands.find(band => band.item.id !== except && band.start < end && band.end > start)?.item ?? null;
|
|
310
|
+
const openQuickCreate = (key, start, end, anchor, create) => {
|
|
311
|
+
const range = rangeOfSelection({
|
|
312
|
+
key,
|
|
313
|
+
start,
|
|
314
|
+
end
|
|
315
|
+
});
|
|
316
|
+
setTouchDraft(null);
|
|
317
|
+
if (!props.owner.renderQuickCreate) {
|
|
318
|
+
props.owner.onSlotActivate?.(range);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
setSelection({
|
|
322
|
+
key,
|
|
323
|
+
start,
|
|
324
|
+
end
|
|
325
|
+
});
|
|
326
|
+
setQuick({
|
|
327
|
+
range,
|
|
328
|
+
anchor,
|
|
329
|
+
create,
|
|
330
|
+
key,
|
|
331
|
+
start,
|
|
332
|
+
end
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
const canCreate = () => Boolean(props.owner.renderQuickCreate || props.owner.onSlotActivate);
|
|
336
|
+
|
|
337
|
+
/** The time a new entry takes: the selection, today's next full hour while today shows, else 09:00 of the first day. */
|
|
338
|
+
const createTarget = visible => {
|
|
339
|
+
const current = selection();
|
|
340
|
+
if (current && current.start !== undefined && current.end !== undefined) return {
|
|
341
|
+
key: current.key,
|
|
342
|
+
start: current.start,
|
|
343
|
+
end: current.end
|
|
344
|
+
};
|
|
345
|
+
const nine = dayStartMinute(9);
|
|
346
|
+
const nextHour = key => {
|
|
347
|
+
const minute = Math.ceil((nowMinute() + 1) / 60) * 60;
|
|
348
|
+
return key === nowKey() ? minute <= wake() - 60 ? minute : null : nine;
|
|
349
|
+
};
|
|
350
|
+
let key;
|
|
351
|
+
let start;
|
|
352
|
+
if (current) {
|
|
353
|
+
key = current.key;
|
|
354
|
+
start = key === nowKey() ? nextHour(key) : nine;
|
|
355
|
+
} else if (nowKey() >= visible.first && nowKey() <= visible.last) {
|
|
356
|
+
key = nowKey();
|
|
357
|
+
start = nextHour(key);
|
|
358
|
+
} else {
|
|
359
|
+
key = visible.first;
|
|
360
|
+
start = nine;
|
|
361
|
+
}
|
|
362
|
+
if (start === null) {
|
|
363
|
+
key = addDayKeys(key, 1);
|
|
364
|
+
start = nine;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
key,
|
|
368
|
+
start,
|
|
369
|
+
end: start + 60
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Pointer gestures: moving and resizing bands and markers, drawing a new event, and dragging entries in.
|
|
374
|
+
let cancelGesture;
|
|
375
|
+
onCleanup(() => cancelGesture?.());
|
|
376
|
+
const step = event => event.shiftKey ? 5 : 15;
|
|
377
|
+
const shiftHeld = {
|
|
378
|
+
current: false
|
|
379
|
+
};
|
|
380
|
+
const trackShift = event => {
|
|
381
|
+
shiftHeld.current = event.shiftKey;
|
|
382
|
+
};
|
|
383
|
+
onMount(() => {
|
|
384
|
+
window.addEventListener("keydown", trackShift, true);
|
|
385
|
+
window.addEventListener("keyup", trackShift, true);
|
|
386
|
+
onCleanup(() => {
|
|
387
|
+
window.removeEventListener("keydown", trackShift, true);
|
|
388
|
+
window.removeEventListener("keyup", trackShift, true);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
const endGesture = () => {
|
|
392
|
+
setGhost(null);
|
|
393
|
+
setGapHit(null);
|
|
394
|
+
setMovingId("");
|
|
395
|
+
setDragging("");
|
|
396
|
+
};
|
|
397
|
+
const startMove = (pointerEvent, event, key, start, end, scroller) => {
|
|
398
|
+
const marker = event.display === "marker";
|
|
399
|
+
if (!props.owner.onEventDrop) return;
|
|
400
|
+
const body = bodies.get(key);
|
|
401
|
+
if (!body) return;
|
|
402
|
+
const rect = body.element.getBoundingClientRect();
|
|
403
|
+
const geometry = readDayGeometry(body.element, wake(), body.expanded());
|
|
404
|
+
const grab = dayMinuteAt(geometry, pointerEvent.clientY - rect.top) - start;
|
|
405
|
+
const length = end - start;
|
|
406
|
+
cancelGesture?.();
|
|
407
|
+
cancelGesture = startCalendarPointerSession({
|
|
408
|
+
event: pointerEvent,
|
|
409
|
+
scrollContainer: scroller,
|
|
410
|
+
resolve: (clientX, clientY) => {
|
|
411
|
+
const hit = bodyAt(clientX, clientY);
|
|
412
|
+
if (!hit) return null;
|
|
413
|
+
const limit = limitOf(hit.key);
|
|
414
|
+
const from = Math.max(0, Math.min(limit - Math.max(length, 15), snapMinutes(hit.minute - grab, step({
|
|
415
|
+
shiftKey: shiftHeld.current
|
|
416
|
+
}))));
|
|
417
|
+
return {
|
|
418
|
+
key: hit.key,
|
|
419
|
+
start: from,
|
|
420
|
+
end: from + length
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
onActivate: () => {
|
|
424
|
+
setMovingId(event.id);
|
|
425
|
+
setDragging("move");
|
|
426
|
+
setSelection(null);
|
|
427
|
+
},
|
|
428
|
+
onPreview: next => {
|
|
429
|
+
const clash = marker ? null : overlapAt(next.key, next.start, next.end, event.id);
|
|
430
|
+
const fit = marker ? null : fitAt(next.key, next.start + (next.end - next.start) / 2, next.end - next.start, next);
|
|
431
|
+
setGapHit(fit ? {
|
|
432
|
+
key: next.key,
|
|
433
|
+
start: fit.gap.start,
|
|
434
|
+
fits: fit.fits,
|
|
435
|
+
text: fit.text
|
|
436
|
+
} : null);
|
|
437
|
+
const time = marker ? messages().calendarDue({
|
|
438
|
+
time: clock(next.start)
|
|
439
|
+
}) : `${clock(next.start)}–${clock(next.end)} · ${duration(next.end - next.start)}`;
|
|
440
|
+
setGhost({
|
|
441
|
+
key: next.key,
|
|
442
|
+
start: next.start,
|
|
443
|
+
end: marker ? next.start + 20 : next.end,
|
|
444
|
+
title: event.title,
|
|
445
|
+
time: clash ? `${time} · ${messages().calendarOverlaps({
|
|
446
|
+
title: clash.title
|
|
447
|
+
})}` : time,
|
|
448
|
+
colorHex: eventAccent(event),
|
|
449
|
+
warn: Boolean(clash),
|
|
450
|
+
fit: fit ? {
|
|
451
|
+
fits: fit.fits,
|
|
452
|
+
text: fit.text
|
|
453
|
+
} : undefined
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
onCommit: next => {
|
|
457
|
+
endGesture();
|
|
458
|
+
swallowNextClick();
|
|
459
|
+
if (next.key === key && next.start === start && next.end === end) return;
|
|
460
|
+
const source = sourceOf().get(event);
|
|
461
|
+
// A moved entry keeps its real length, also when it lands across a clock change.
|
|
462
|
+
const startDate = instant(next.key, next.start);
|
|
463
|
+
const endDate = marker ? startDate : source ? new Date(startDate.getTime() + source.end.getTime() - source.start.getTime()) : slot(next.key, next.start, next.end).end;
|
|
464
|
+
if (source && source.start.getTime() === startDate.getTime() && source.end.getTime() === endDate.getTime()) return;
|
|
465
|
+
props.owner.onEventDrop?.(event, {
|
|
466
|
+
start: startDate,
|
|
467
|
+
end: endDate,
|
|
468
|
+
allDay: false
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
onCancel: endGesture
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
const startResize = (pointerEvent, event, key, start, end, edge, scroller) => {
|
|
475
|
+
if (!props.owner.onEventResize) return;
|
|
476
|
+
pointerEvent.preventDefault();
|
|
477
|
+
pointerEvent.stopPropagation();
|
|
478
|
+
const body = bodies.get(key);
|
|
479
|
+
if (!body) return;
|
|
480
|
+
cancelGesture?.();
|
|
481
|
+
cancelGesture = startCalendarPointerSession({
|
|
482
|
+
event: pointerEvent,
|
|
483
|
+
scrollContainer: scroller,
|
|
484
|
+
threshold: 1,
|
|
485
|
+
resolve: (_clientX, clientY) => {
|
|
486
|
+
const rect = body.element.getBoundingClientRect();
|
|
487
|
+
const geometry = readDayGeometry(body.element, wake(), body.expanded());
|
|
488
|
+
const minute = snapMinutes(dayMinuteAt(geometry, clientY - rect.top), step({
|
|
489
|
+
shiftKey: shiftHeld.current
|
|
490
|
+
}));
|
|
491
|
+
const limit = limitOf(key);
|
|
492
|
+
const size = step({
|
|
493
|
+
shiftKey: shiftHeld.current
|
|
494
|
+
});
|
|
495
|
+
return edge === "end" ? {
|
|
496
|
+
start,
|
|
497
|
+
end: Math.max(start + size, Math.min(limit, minute))
|
|
498
|
+
} : {
|
|
499
|
+
start: Math.max(0, Math.min(end - size, minute)),
|
|
500
|
+
end
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
onActivate: () => {
|
|
504
|
+
setMovingId(event.id);
|
|
505
|
+
setDragging(edge);
|
|
506
|
+
},
|
|
507
|
+
onPreview: next => setGhost({
|
|
508
|
+
key,
|
|
509
|
+
start: next.start,
|
|
510
|
+
end: next.end,
|
|
511
|
+
title: event.title,
|
|
512
|
+
time: `${clock(next.start)}–${clock(next.end)} · ${duration(next.end - next.start)}`,
|
|
513
|
+
colorHex: eventAccent(event)
|
|
514
|
+
}),
|
|
515
|
+
onCommit: next => {
|
|
516
|
+
endGesture();
|
|
517
|
+
swallowNextClick();
|
|
518
|
+
if (next.start === start && next.end === end) return;
|
|
519
|
+
const source = sourceOf().get(event);
|
|
520
|
+
const range = source ? resizedInDay(source, edge, key, next, props.hours, dateConfig()) : slot(key, next.start, next.end);
|
|
521
|
+
props.owner.onEventResize?.(event, {
|
|
522
|
+
...range,
|
|
523
|
+
allDay: false
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
onCancel: endGesture
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
/** Drawing on free time: a new event with its live length, on a 15-minute grid, with Shift on 5 minutes. */
|
|
531
|
+
const startDraw = (pointerEvent, key, scroller) => {
|
|
532
|
+
if (!canCreate() || pointerEvent.pointerType !== "mouse") return;
|
|
533
|
+
const body = bodies.get(key);
|
|
534
|
+
if (!body) return;
|
|
535
|
+
const minuteAt = clientY => {
|
|
536
|
+
const rect = body.element.getBoundingClientRect();
|
|
537
|
+
const geometry = readDayGeometry(body.element, wake(), body.expanded());
|
|
538
|
+
return Math.max(0, Math.min(limitOf(key), dayMinuteAt(geometry, clientY - rect.top)));
|
|
539
|
+
};
|
|
540
|
+
const origin = minuteAt(pointerEvent.clientY);
|
|
541
|
+
cancelGesture?.();
|
|
542
|
+
cancelGesture = startCalendarPointerSession({
|
|
543
|
+
event: pointerEvent,
|
|
544
|
+
scrollContainer: scroller,
|
|
545
|
+
resolve: (_clientX, clientY) => {
|
|
546
|
+
const size = step({
|
|
547
|
+
shiftKey: shiftHeld.current
|
|
548
|
+
});
|
|
549
|
+
const minute = minuteAt(clientY);
|
|
550
|
+
const from = Math.floor(Math.min(origin, minute) / size) * size;
|
|
551
|
+
let to = Math.ceil(Math.max(origin, minute) / size) * size;
|
|
552
|
+
if (to - from < size) to = from + size;
|
|
553
|
+
return {
|
|
554
|
+
start: from,
|
|
555
|
+
end: Math.min(to, limitOf(key))
|
|
556
|
+
};
|
|
557
|
+
},
|
|
558
|
+
onActivate: () => {
|
|
559
|
+
setDragging("draw");
|
|
560
|
+
setSelection(null);
|
|
561
|
+
setTouchDraft(null);
|
|
562
|
+
},
|
|
563
|
+
onPreview: next => setGhost({
|
|
564
|
+
key,
|
|
565
|
+
start: next.start,
|
|
566
|
+
end: next.end,
|
|
567
|
+
title: messages().calendarNewEvent,
|
|
568
|
+
time: `${clock(next.start)}–${clock(next.end)} · ${duration(next.end - next.start)}`
|
|
569
|
+
}),
|
|
570
|
+
onCommit: next => {
|
|
571
|
+
const anchor = document.querySelector(".k2b-calendar-ghost[data-live]")?.getBoundingClientRect() ?? body.element;
|
|
572
|
+
endGesture();
|
|
573
|
+
swallowNextClick();
|
|
574
|
+
openQuickCreate(key, next.start, next.end, anchor);
|
|
575
|
+
},
|
|
576
|
+
onCancel: endGesture
|
|
577
|
+
});
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
/** Follows a pointer that pressed an entry outside the calendar into its free time. */
|
|
581
|
+
const dragIn = (pointerEvent, item, onDrop, scroller) => {
|
|
582
|
+
if (pointerEvent.button !== 0 || !pointerEvent.isPrimary) return;
|
|
583
|
+
cancelGesture?.();
|
|
584
|
+
cancelGesture = undefined;
|
|
585
|
+
const length = Math.max(15, Math.round(item.minutes));
|
|
586
|
+
const color = item.colorHex ?? COLOR_TOKENS[item.color ?? "blue"];
|
|
587
|
+
let token;
|
|
588
|
+
let spring;
|
|
589
|
+
const target = pointerEvent.target instanceof HTMLElement ? pointerEvent.target.closest("li, [data-calendar-drag-source]") : null;
|
|
590
|
+
let held = false;
|
|
591
|
+
let cleaned = false;
|
|
592
|
+
const preventContextMenu = event => event.preventDefault();
|
|
593
|
+
const preventScroll = event => {
|
|
594
|
+
if (held) event.preventDefault();
|
|
595
|
+
};
|
|
596
|
+
const cleanup = () => {
|
|
597
|
+
if (cleaned) return;
|
|
598
|
+
cleaned = true;
|
|
599
|
+
document.removeEventListener("touchmove", preventScroll);
|
|
600
|
+
window.removeEventListener("contextmenu", preventContextMenu, true);
|
|
601
|
+
if (pointerEvent.pointerType === "touch") held = false;
|
|
602
|
+
if (target) delete target.dataset.lifted;
|
|
603
|
+
token?.remove();
|
|
604
|
+
token = undefined;
|
|
605
|
+
if (spring) window.clearTimeout(spring.timer);
|
|
606
|
+
spring = undefined;
|
|
607
|
+
endGesture();
|
|
608
|
+
};
|
|
609
|
+
const begin = () => {
|
|
610
|
+
cancelGesture = startCalendarPointerSession({
|
|
611
|
+
event: pointerEvent,
|
|
612
|
+
scrollContainer: scroller,
|
|
613
|
+
armAutoScroll: true,
|
|
614
|
+
threshold: held && pointerEvent.pointerType !== "mouse" ? 1 : 6,
|
|
615
|
+
resolve: (clientX, clientY) => {
|
|
616
|
+
const hit = bodyAt(clientX, clientY);
|
|
617
|
+
if (!hit || hit.minute >= limitOf(hit.key)) return {
|
|
618
|
+
key: "",
|
|
619
|
+
start: 0,
|
|
620
|
+
end: 0,
|
|
621
|
+
clash: null
|
|
622
|
+
};
|
|
623
|
+
const fit = fitAt(hit.key, hit.minute, length);
|
|
624
|
+
let start = snapMinutes(hit.minute - length / 2, 15);
|
|
625
|
+
if (fit?.fits) start = Math.max(fit.gap.start, Math.min(fit.gap.end - length, start));
|
|
626
|
+
start = Math.max(0, Math.min(limitOf(hit.key) - length, start));
|
|
627
|
+
return {
|
|
628
|
+
key: hit.key,
|
|
629
|
+
start,
|
|
630
|
+
end: start + length,
|
|
631
|
+
clash: overlapAt(hit.key, start, start + length),
|
|
632
|
+
fit
|
|
633
|
+
};
|
|
634
|
+
},
|
|
635
|
+
onActivate: () => {
|
|
636
|
+
setDragging("external");
|
|
637
|
+
setSelection(null);
|
|
638
|
+
token = document.createElement("div");
|
|
639
|
+
token.className = "k2b-calendar-token";
|
|
640
|
+
token.textContent = item.title;
|
|
641
|
+
token.style.setProperty("--k2b-calendar-accent", color);
|
|
642
|
+
token.setAttribute("aria-hidden", "true");
|
|
643
|
+
document.body.append(token);
|
|
644
|
+
},
|
|
645
|
+
onMove: (clientX, clientY) => {
|
|
646
|
+
if (token) token.style.transform = `translate(${clientX + 12}px, ${clientY + 10}px)`;
|
|
647
|
+
// A folded run of empty days opens when the entry rests on it.
|
|
648
|
+
const empty = document.elementFromPoint(clientX, clientY)?.closest("[data-calendar-unfold]");
|
|
649
|
+
if (empty && spring?.element !== empty) {
|
|
650
|
+
if (spring) window.clearTimeout(spring.timer);
|
|
651
|
+
spring = {
|
|
652
|
+
element: empty,
|
|
653
|
+
timer: window.setTimeout(() => empty.click(), 650)
|
|
654
|
+
};
|
|
655
|
+
} else if (!empty && spring) {
|
|
656
|
+
window.clearTimeout(spring.timer);
|
|
657
|
+
spring = undefined;
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
onPreview: next => {
|
|
661
|
+
if (token) token.dataset.hidden = next.key ? "true" : "false";
|
|
662
|
+
if (!next.key) {
|
|
663
|
+
setGhost(null);
|
|
664
|
+
setGapHit(null);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
setGapHit(next.fit ? {
|
|
668
|
+
key: next.key,
|
|
669
|
+
start: next.fit.gap.start,
|
|
670
|
+
fits: next.fit.fits,
|
|
671
|
+
text: next.fit.text
|
|
672
|
+
} : null);
|
|
673
|
+
const time = `${clock(next.start)}–${clock(next.end)} · ${duration(length)}`;
|
|
674
|
+
setGhost({
|
|
675
|
+
key: next.key,
|
|
676
|
+
start: next.start,
|
|
677
|
+
end: next.end,
|
|
678
|
+
title: item.title,
|
|
679
|
+
time: next.clash ? `${time} · ${messages().calendarOverlaps({
|
|
680
|
+
title: next.clash.title
|
|
681
|
+
})}` : time,
|
|
682
|
+
colorHex: color,
|
|
683
|
+
warn: Boolean(next.clash),
|
|
684
|
+
fit: next.fit ? {
|
|
685
|
+
fits: next.fit.fits,
|
|
686
|
+
text: next.fit.text
|
|
687
|
+
} : undefined
|
|
688
|
+
});
|
|
689
|
+
},
|
|
690
|
+
onCommit: next => {
|
|
691
|
+
swallowNextClick();
|
|
692
|
+
if (next.key) onDrop({
|
|
693
|
+
...slot(next.key, next.start, next.end),
|
|
694
|
+
allDay: false
|
|
695
|
+
});
|
|
696
|
+
},
|
|
697
|
+
onEnd: cleanup
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
if (pointerEvent.pointerType === "mouse") {
|
|
701
|
+
begin();
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
// A finger first holds the entry still, so a short swipe keeps scrolling the list it sits in.
|
|
705
|
+
const origin = {
|
|
706
|
+
x: pointerEvent.clientX,
|
|
707
|
+
y: pointerEvent.clientY
|
|
708
|
+
};
|
|
709
|
+
const abort = () => {
|
|
710
|
+
window.clearTimeout(timer);
|
|
711
|
+
window.removeEventListener("pointermove", watch);
|
|
712
|
+
window.removeEventListener("pointerup", abort);
|
|
713
|
+
window.removeEventListener("pointercancel", abort);
|
|
714
|
+
window.removeEventListener("contextmenu", preventContextMenu, true);
|
|
715
|
+
if (target) delete target.dataset.lifted;
|
|
716
|
+
if (cancelGesture === abort) cancelGesture = undefined;
|
|
717
|
+
};
|
|
718
|
+
const watch = event => {
|
|
719
|
+
if (event.pointerId === pointerEvent.pointerId && Math.hypot(event.clientX - origin.x, event.clientY - origin.y) > 10) abort();
|
|
720
|
+
};
|
|
721
|
+
const timer = window.setTimeout(() => {
|
|
722
|
+
abort();
|
|
723
|
+
held = true;
|
|
724
|
+
if (target) target.dataset.lifted = "true";
|
|
725
|
+
navigator.vibrate?.(8);
|
|
726
|
+
document.addEventListener("touchmove", preventScroll, {
|
|
727
|
+
passive: false
|
|
728
|
+
});
|
|
729
|
+
window.addEventListener("contextmenu", preventContextMenu, true);
|
|
730
|
+
begin();
|
|
731
|
+
}, 420);
|
|
732
|
+
window.addEventListener("pointermove", watch);
|
|
733
|
+
window.addEventListener("pointerup", abort);
|
|
734
|
+
window.addEventListener("pointercancel", abort);
|
|
735
|
+
window.addEventListener("contextmenu", preventContextMenu, true);
|
|
736
|
+
cancelGesture = abort;
|
|
737
|
+
};
|
|
738
|
+
return {
|
|
739
|
+
props,
|
|
740
|
+
messages,
|
|
741
|
+
dateConfig,
|
|
742
|
+
hours,
|
|
743
|
+
wake,
|
|
744
|
+
range,
|
|
745
|
+
anchorKey,
|
|
746
|
+
days,
|
|
747
|
+
dayOf,
|
|
748
|
+
keyOf,
|
|
749
|
+
parseKey,
|
|
750
|
+
nowKey,
|
|
751
|
+
nowMinute,
|
|
752
|
+
nowIn,
|
|
753
|
+
calendarToday,
|
|
754
|
+
clock,
|
|
755
|
+
duration,
|
|
756
|
+
instant,
|
|
757
|
+
normalized,
|
|
758
|
+
checkedOf,
|
|
759
|
+
toggleCheck,
|
|
760
|
+
checkOf,
|
|
761
|
+
justChecked,
|
|
762
|
+
cheered,
|
|
763
|
+
selection,
|
|
764
|
+
setSelection,
|
|
765
|
+
touchDraft,
|
|
766
|
+
setTouchDraft,
|
|
767
|
+
activeKey,
|
|
768
|
+
setActiveKey,
|
|
769
|
+
ghost,
|
|
770
|
+
gapHit,
|
|
771
|
+
movingId,
|
|
772
|
+
dragging,
|
|
773
|
+
nightOpen,
|
|
774
|
+
setNight,
|
|
775
|
+
quick,
|
|
776
|
+
setQuick,
|
|
777
|
+
openQuickCreate,
|
|
778
|
+
canCreate,
|
|
779
|
+
createTarget,
|
|
780
|
+
rangeOfSelection,
|
|
781
|
+
registerBody,
|
|
782
|
+
bodyAt,
|
|
783
|
+
limitOf,
|
|
784
|
+
startMove,
|
|
785
|
+
startResize,
|
|
786
|
+
startDraw,
|
|
787
|
+
dragIn,
|
|
788
|
+
dayStartMinute
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
/**
|
|
792
|
+
* German short dates read "Fr 9. Okt", without the dots of the abbreviations; with `comma`, a title such as
|
|
793
|
+
* "Fr, 9. Okt 2026" keeps the comma after the day.
|
|
794
|
+
*/
|
|
795
|
+
export const compactGermanDate = (text, locale, comma = false) => locale?.startsWith("de") ? text.replace(comma ? /\b(Mo|Di|Mi|Do|Fr|Sa|So)\./g : /\b(Mo|Di|Mi|Do|Fr|Sa|So)\.,?/g, "$1").replace(/\b(Jan|Feb|Mär|Apr|Jun|Jul|Aug|Sep|Sept|Okt|Nov|Dez)\./g, "$1") : text;
|
|
796
|
+
|
|
797
|
+
/** A short day such as "Fri 9" or "Fr 9.", in the calendar's locale. */
|
|
798
|
+
export const shortDay = (view, key, withMonth = false) => compactGermanDate(view.parseKey(key).toLocaleDateString(view.dateConfig().locale ?? "en", {
|
|
799
|
+
weekday: "short",
|
|
800
|
+
day: "numeric",
|
|
801
|
+
...(withMonth ? {
|
|
802
|
+
month: "short"
|
|
803
|
+
} : {}),
|
|
804
|
+
timeZone: view.dateConfig().timeZone
|
|
805
|
+
}), view.dateConfig().locale);
|
|
806
|
+
|
|
807
|
+
/** A long day such as "Friday, October 9", with the year when it is not this year's. */
|
|
808
|
+
export const longDay = (view, key) => view.parseKey(key).toLocaleDateString(view.dateConfig().locale ?? "en", {
|
|
809
|
+
weekday: "long",
|
|
810
|
+
month: "long",
|
|
811
|
+
day: "numeric",
|
|
812
|
+
...(key.slice(0, 4) !== view.calendarToday().slice(0, 4) ? {
|
|
813
|
+
year: "numeric"
|
|
814
|
+
} : {}),
|
|
815
|
+
timeZone: view.dateConfig().timeZone
|
|
816
|
+
});
|
|
817
|
+
const CHECK_PATH = "M4.6 8.3l2.3 2.3 4.6-4.9";
|
|
818
|
+
|
|
819
|
+
/** A to-do's checkbox: it draws its check, and the title beside it strikes through. */
|
|
820
|
+
export const TimeCheck = props => {
|
|
821
|
+
const checked = () => props.view.checkedOf(props.event) ?? false;
|
|
822
|
+
const box = _$ssr(_tmpl$);
|
|
823
|
+
return _$createComponent(Show, {
|
|
824
|
+
get when() {
|
|
825
|
+
return props.view.props.owner.onEventCheck;
|
|
826
|
+
},
|
|
827
|
+
get fallback() {
|
|
828
|
+
return _$ssr(_tmpl$3, _$ssrAttribute("data-checked", checked() ? "true" : _$escape(undefined, true), false), _$escape(box));
|
|
829
|
+
},
|
|
830
|
+
get children() {
|
|
831
|
+
return _$ssr(_tmpl$2, _$ssrAttribute("aria-checked", checked() ? "true" : "false", false) + _$ssrAttribute("aria-label", _$escape(props.view.messages().calendarDone({
|
|
832
|
+
title: props.event.title
|
|
833
|
+
}), true), false) + _$ssrAttribute("tabindex", _$escape(props.tabIndex, true), false) + _$ssrAttribute("data-checked", checked() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-just-checked", props.view.justChecked().has(props.event.id) ? "true" : _$escape(undefined, true), false), _$escape(box));
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
/** A timed entry in a day's timeline: a band as long as the entry, side by side with overlapping ones. */
|
|
839
|
+
export const TimeBand = props => {
|
|
840
|
+
const view = props.view;
|
|
841
|
+
const event = () => props.band.item;
|
|
842
|
+
const wake = () => view.wake();
|
|
843
|
+
const limit = () => props.expanded ? DAY_MINUTES : wake();
|
|
844
|
+
const visibleStart = () => Math.max(0, props.band.start);
|
|
845
|
+
const visibleEnd = () => Math.min(limit(), props.band.end);
|
|
846
|
+
const top = () => dayTopTerms(wake(), props.expanded, visibleStart());
|
|
847
|
+
const bottom = () => dayYTerms(wake(), props.expanded, visibleEnd());
|
|
848
|
+
const now = () => view.nowIn(props.dayKey);
|
|
849
|
+
const running = () => now() >= props.band.start && now() < props.band.end && Number.isFinite(now());
|
|
850
|
+
const past = () => now() >= props.band.end;
|
|
851
|
+
const length = () => props.band.end - props.band.start;
|
|
852
|
+
const pixels = () => length() / 60 * props.hourPx - 2;
|
|
853
|
+
const size = () => pixels() < 27 ? "xs" : pixels() < 40 ? "s" : "m";
|
|
854
|
+
const checked = () => view.checkedOf(event());
|
|
855
|
+
const remaining = () => running() ? view.messages().calendarRemaining({
|
|
856
|
+
duration: view.duration(props.band.end - now())
|
|
857
|
+
}) : undefined;
|
|
858
|
+
const lines = () => Math.max(1, Math.floor((pixels() - 24) / 15));
|
|
859
|
+
return _$ssr(_tmpl$7, _$ssrAttribute("data-calendar-entry", _$escape(event().id, true), false) + _$ssrAttribute("data-size", _$escape(size(), true), false) + _$ssrAttribute("data-past", past() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-running", running() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-checked", checked() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-checkbox", checked() !== undefined ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-just-checked", view.justChecked().has(event().id) ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-cut-bottom", props.band.end > limit() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-moving", view.movingId() === event().id ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-display", _$escape(event().display, true), false), _$ssrStyleProperty("--k2b-calendar-accent:", _$escape(eventAccent(event()), true)) + _$ssrStyleProperty(";--k2b-calendar-lane:", _$escape(String(props.band.lane), true)) + _$ssrStyleProperty(";--k2b-calendar-lanes:", _$escape(String(props.band.lanes), true)) + _$ssrStyleProperty(";--k2b-calendar-lines:", _$escape(String(lines()), true)) + _$ssrStyleProperty(";--k2b-calendar-progress:", running() ? `${_$escape(((now() - props.band.start) / length() * 100).toFixed(1), true)}%` : _$escape(undefined, true)) + _$ssrStyleProperty(";top:", `calc(${_$escape(termsCss(top()), true)} + 1px)`) + _$ssrStyleProperty(";height:", `max(var(--k2b-calendar-band-min), calc(${_$escape(termsCss(subtractTerms(bottom(), top())), true)} - 2px))`), _$escape(_$createComponent(Show, {
|
|
860
|
+
get when() {
|
|
861
|
+
return running();
|
|
862
|
+
},
|
|
863
|
+
get children() {
|
|
864
|
+
return _$ssr(_tmpl$4);
|
|
865
|
+
}
|
|
866
|
+
})), _$escape(_$createComponent(Show, {
|
|
867
|
+
get when() {
|
|
868
|
+
return checked() !== undefined;
|
|
869
|
+
},
|
|
870
|
+
get children() {
|
|
871
|
+
return _$createComponent(TimeCheck, {
|
|
872
|
+
view: view,
|
|
873
|
+
get event() {
|
|
874
|
+
return event();
|
|
875
|
+
},
|
|
876
|
+
get tabIndex() {
|
|
877
|
+
return props.tabIndex === undefined ? undefined : -1;
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
})), _$escape(_$createComponent(EventChip, {
|
|
882
|
+
get event() {
|
|
883
|
+
return view.normalized(event(), props.dayKey);
|
|
884
|
+
},
|
|
885
|
+
get owner() {
|
|
886
|
+
return view.props.owner;
|
|
887
|
+
},
|
|
888
|
+
get href() {
|
|
889
|
+
return eventHref(view.props.owner, event());
|
|
890
|
+
},
|
|
891
|
+
fill: true,
|
|
892
|
+
timeline: true,
|
|
893
|
+
get compact() {
|
|
894
|
+
return props.compact;
|
|
895
|
+
},
|
|
896
|
+
get timeLabel() {
|
|
897
|
+
return `${view.clock(props.band.start)}–${view.clock(props.band.end)}`;
|
|
898
|
+
},
|
|
899
|
+
get showLocation() {
|
|
900
|
+
return _$memo(() => !!(props.showLocation && size() === "m"))() && pixels() >= 64;
|
|
901
|
+
},
|
|
902
|
+
get remaining() {
|
|
903
|
+
return remaining();
|
|
904
|
+
},
|
|
905
|
+
get checkbox() {
|
|
906
|
+
return checked() !== undefined;
|
|
907
|
+
},
|
|
908
|
+
get onCheck() {
|
|
909
|
+
return view.checkOf(event());
|
|
910
|
+
},
|
|
911
|
+
get moving() {
|
|
912
|
+
return view.movingId() === event().id;
|
|
913
|
+
},
|
|
914
|
+
get tabIndex() {
|
|
915
|
+
return props.tabIndex;
|
|
916
|
+
},
|
|
917
|
+
get stop() {
|
|
918
|
+
return props.stop;
|
|
919
|
+
},
|
|
920
|
+
get onMovePointerDown() {
|
|
921
|
+
return view.props.owner.onEventDrop ? pointerEvent => view.startMove(pointerEvent, event(), props.dayKey, props.band.start, props.band.end, props.scroller?.()) : undefined;
|
|
922
|
+
}
|
|
923
|
+
})), _$escape(_$createComponent(Show, {
|
|
924
|
+
get when() {
|
|
925
|
+
return view.props.owner.onEventResize;
|
|
926
|
+
},
|
|
927
|
+
get children() {
|
|
928
|
+
return [_$ssr(_tmpl$5), _$createComponent(Show, {
|
|
929
|
+
get when() {
|
|
930
|
+
return props.band.end <= limit();
|
|
931
|
+
},
|
|
932
|
+
get children() {
|
|
933
|
+
return _$ssr(_tmpl$6);
|
|
934
|
+
}
|
|
935
|
+
})];
|
|
936
|
+
}
|
|
937
|
+
})));
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
/** A point in time such as a deadline: a dashed line across the day and a pill with its checkbox and title. */
|
|
941
|
+
export const TimeMarker = props => {
|
|
942
|
+
const view = props.view;
|
|
943
|
+
const checked = () => view.checkedOf(props.event);
|
|
944
|
+
const past = () => view.nowIn(props.dayKey) >= props.minute;
|
|
945
|
+
return _$ssr(_tmpl$8, _$ssrAttribute("data-calendar-entry", _$escape(props.event.id, true), false) + _$ssrAttribute("data-past", past() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-overdue", past() && checked() === false ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-checked", checked() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-just-checked", view.justChecked().has(props.event.id) ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-above", props.above ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-moving", view.movingId() === props.event.id ? "true" : _$escape(undefined, true), false), _$ssrStyleProperty("--k2b-calendar-accent:", _$escape(eventAccent(props.event), true)) + _$ssrStyleProperty(";--k2b-calendar-marker-lane:", _$escape(String(props.lane), true)) + _$ssrStyleProperty(";--k2b-calendar-marker-lanes:", _$escape(String(props.lanes), true)) + _$ssrStyleProperty(";top:", _$escape(termsCss(dayTopTerms(view.wake(), props.expanded, props.minute)), true)), _$escape(_$createComponent(Show, {
|
|
946
|
+
get when() {
|
|
947
|
+
return checked() !== undefined;
|
|
948
|
+
},
|
|
949
|
+
get children() {
|
|
950
|
+
return _$createComponent(TimeCheck, {
|
|
951
|
+
view: view,
|
|
952
|
+
get event() {
|
|
953
|
+
return props.event;
|
|
954
|
+
},
|
|
955
|
+
get tabIndex() {
|
|
956
|
+
return props.tabIndex === undefined ? undefined : -1;
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
})), _$escape(_$createComponent(EventChip, {
|
|
961
|
+
get event() {
|
|
962
|
+
return view.normalized(props.event, props.dayKey);
|
|
963
|
+
},
|
|
964
|
+
get owner() {
|
|
965
|
+
return view.props.owner;
|
|
966
|
+
},
|
|
967
|
+
get href() {
|
|
968
|
+
return eventHref(view.props.owner, props.event);
|
|
969
|
+
},
|
|
970
|
+
compact: true,
|
|
971
|
+
timeline: true,
|
|
972
|
+
get checkbox() {
|
|
973
|
+
return checked() !== undefined;
|
|
974
|
+
},
|
|
975
|
+
get onCheck() {
|
|
976
|
+
return view.checkOf(props.event);
|
|
977
|
+
},
|
|
978
|
+
get tabIndex() {
|
|
979
|
+
return props.tabIndex;
|
|
980
|
+
},
|
|
981
|
+
get stop() {
|
|
982
|
+
return props.stop;
|
|
983
|
+
},
|
|
984
|
+
get trailing() {
|
|
985
|
+
return _$createComponent(Show, {
|
|
986
|
+
get when() {
|
|
987
|
+
return props.showTime;
|
|
988
|
+
},
|
|
989
|
+
get children() {
|
|
990
|
+
return _$ssr(_tmpl$9, _$escape(view.clock(props.minute)));
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
},
|
|
994
|
+
get onMovePointerDown() {
|
|
995
|
+
return view.props.owner.onEventDrop ? pointerEvent => view.startMove(pointerEvent, props.event, props.dayKey, props.minute, props.minute, props.scroller?.()) : undefined;
|
|
996
|
+
}
|
|
997
|
+
})));
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
/** Free time between entries, labelled from an hour on; activating it creates there. */
|
|
1001
|
+
export const TimeGap = props => {
|
|
1002
|
+
const view = props.view;
|
|
1003
|
+
const length = () => props.gap.end - props.gap.start;
|
|
1004
|
+
const proposal = () => gapProposal(props.gap, view.hours());
|
|
1005
|
+
const label = () => view.messages().calendarFree({
|
|
1006
|
+
duration: view.duration(length())
|
|
1007
|
+
});
|
|
1008
|
+
const hit = () => {
|
|
1009
|
+
const current = view.gapHit();
|
|
1010
|
+
return current && current.key === props.dayKey && current.start === props.gap.start ? current : null;
|
|
1011
|
+
};
|
|
1012
|
+
const top = () => dayYTerms(view.wake(), props.expanded, props.gap.start);
|
|
1013
|
+
/**
|
|
1014
|
+
* Shorter free time is hidden until something is dragged into it, and past free time has no label, so neither is a
|
|
1015
|
+
* keyboard stop.
|
|
1016
|
+
*/
|
|
1017
|
+
const labelled = () => length() >= GAP_LABEL_MINUTES;
|
|
1018
|
+
const stop = () => labelled() && !props.gap.past ? props.stop : undefined;
|
|
1019
|
+
return _$ssr(_tmpl$1, _$ssrAttribute("data-short", labelled() ? _$escape(undefined, true) : "true", false) + _$ssrAttribute("data-long", length() >= GAP_LONG_MINUTES ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-past", props.gap.past ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-aside", props.aside ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-hit", hit() ? hit().fits ? "fit" : "short" : _$escape(undefined, true), false), _$ssrStyleProperty("top:", _$escape(termsCss(top()), true)) + _$ssrStyleProperty(";height:", _$escape(termsCss(subtractTerms(dayYTerms(view.wake(), props.expanded, props.gap.end), top())), true)), _$escape(_$createComponent(Show, {
|
|
1020
|
+
get when() {
|
|
1021
|
+
return view.canCreate();
|
|
1022
|
+
},
|
|
1023
|
+
get children() {
|
|
1024
|
+
return _$ssr(_tmpl$0, _$ssrAttribute("tabindex", labelled() ? _$escape(props.tabIndex ?? (props.gap.past ? -1 : 0), true) : -1, false) + _$ssrAttribute("data-stop", _$escape(stop()?.id, true), false) + _$ssrAttribute("data-col", _$escape(stop()?.column, true), false) + _$ssrAttribute("data-minute", _$escape(stop()?.minute, true), false) + _$ssrAttribute("aria-label", _$escape(view.messages().calendarFreeAction({
|
|
1025
|
+
duration: view.duration(length()),
|
|
1026
|
+
from: view.clock(props.gap.start),
|
|
1027
|
+
to: view.clock(props.gap.end),
|
|
1028
|
+
start: view.clock(proposal()[0]),
|
|
1029
|
+
end: view.clock(proposal()[1])
|
|
1030
|
+
}), true), false) + _$ssrAttribute("title", _$escape(view.messages().calendarCreateAt({
|
|
1031
|
+
start: view.clock(proposal()[0]),
|
|
1032
|
+
end: view.clock(proposal()[1])
|
|
1033
|
+
}), true), false), hit() ? _$escape(hit().text) : _$escape(label()));
|
|
1034
|
+
}
|
|
1035
|
+
})));
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
/** The preview of a drag, a draw, or an entry dragged in, and the selected slot. */
|
|
1039
|
+
export const TimeOverlays = props => {
|
|
1040
|
+
const view = props.view;
|
|
1041
|
+
const ghost = () => {
|
|
1042
|
+
const current = view.ghost();
|
|
1043
|
+
return current && current.key === props.dayKey ? current : null;
|
|
1044
|
+
};
|
|
1045
|
+
const selected = () => {
|
|
1046
|
+
const current = view.selection();
|
|
1047
|
+
return current && current.key === props.dayKey && current.start !== undefined && current.end !== undefined ? current : null;
|
|
1048
|
+
};
|
|
1049
|
+
const draft = () => {
|
|
1050
|
+
const current = view.touchDraft();
|
|
1051
|
+
return current && current.key === props.dayKey ? current : null;
|
|
1052
|
+
};
|
|
1053
|
+
const box = (start, end) => {
|
|
1054
|
+
const top = dayTopTerms(view.wake(), props.expanded, start);
|
|
1055
|
+
return {
|
|
1056
|
+
top: termsCss(top),
|
|
1057
|
+
height: termsCss(subtractTerms(dayYTerms(view.wake(), props.expanded, end), top))
|
|
1058
|
+
};
|
|
1059
|
+
};
|
|
1060
|
+
return [_$createComponent(Show, {
|
|
1061
|
+
get when() {
|
|
1062
|
+
return selected();
|
|
1063
|
+
},
|
|
1064
|
+
children: slot => _$ssr(_tmpl$10, _$ssrStyle(box(slot().start, slot().end)))
|
|
1065
|
+
}), _$createComponent(Show, {
|
|
1066
|
+
get when() {
|
|
1067
|
+
return draft();
|
|
1068
|
+
},
|
|
1069
|
+
children: slot => _$ssr(_tmpl$11, _$ssrStyle(box(slot().start, slot().end)), _$ssrAttribute("aria-label", _$escape(view.messages().calendarCreateAt({
|
|
1070
|
+
start: view.clock(slot().start),
|
|
1071
|
+
end: view.clock(slot().end)
|
|
1072
|
+
}), true), false), _$escape(view.clock(slot().start)), _$escape(view.clock(slot().end)), _$escape(view.duration(slot().end - slot().start)))
|
|
1073
|
+
}), _$createComponent(Show, {
|
|
1074
|
+
get when() {
|
|
1075
|
+
return ghost();
|
|
1076
|
+
},
|
|
1077
|
+
children: current => _$ssr(_tmpl$12, _$ssrAttribute("data-size", current().end - current().start < 40 ? "s" : "m", false) + _$ssrAttribute("data-warn", current().warn ? "true" : _$escape(undefined, true), false), _$ssrStyle({
|
|
1078
|
+
...box(current().start, current().end),
|
|
1079
|
+
"--k2b-calendar-accent": current().colorHex ?? "var(--k2b-action)"
|
|
1080
|
+
}), _$escape(current().title), _$escape(current().time), _$escape(_$createComponent(Show, {
|
|
1081
|
+
get when() {
|
|
1082
|
+
return current().fit;
|
|
1083
|
+
},
|
|
1084
|
+
children: fit => _$ssr(_tmpl$13, _$ssrAttribute("data-fit", fit().fits ? "fit" : "short", false), _$escape(fit().text))
|
|
1085
|
+
})))
|
|
1086
|
+
})];
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
/** The handles of a tapped slot change its start or end. */
|
|
1090
|
+
const startDraftResize = (view, pointerEvent, edge) => {
|
|
1091
|
+
const draft = view.touchDraft();
|
|
1092
|
+
if (!draft) return;
|
|
1093
|
+
pointerEvent.preventDefault();
|
|
1094
|
+
pointerEvent.stopPropagation();
|
|
1095
|
+
startCalendarPointerSession({
|
|
1096
|
+
event: pointerEvent,
|
|
1097
|
+
threshold: 1,
|
|
1098
|
+
resolve: (clientX, clientY) => {
|
|
1099
|
+
const hit = view.bodyAt(clientX, clientY);
|
|
1100
|
+
if (!hit || hit.key !== draft.key) return null;
|
|
1101
|
+
const minute = snapMinutes(hit.minute, 15);
|
|
1102
|
+
const current = view.touchDraft() ?? draft;
|
|
1103
|
+
return edge === "end" ? {
|
|
1104
|
+
...current,
|
|
1105
|
+
end: Math.max(current.start + 15, Math.min(view.limitOf(draft.key), minute))
|
|
1106
|
+
} : {
|
|
1107
|
+
...current,
|
|
1108
|
+
start: Math.max(0, Math.min(current.end - 15, minute))
|
|
1109
|
+
};
|
|
1110
|
+
},
|
|
1111
|
+
onPreview: next => {
|
|
1112
|
+
view.setTouchDraft(next);
|
|
1113
|
+
view.setSelection({
|
|
1114
|
+
key: next.key,
|
|
1115
|
+
start: next.start,
|
|
1116
|
+
end: next.end
|
|
1117
|
+
});
|
|
1118
|
+
},
|
|
1119
|
+
onCommit: () => swallowNextClick()
|
|
1120
|
+
});
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
/** Free time of a day with the gap of now trimmed, as both views draw it. */
|
|
1124
|
+
export const dayGaps = (view, key) => freeGaps(view.dayOf(key).bands, view.wake(), view.nowIn(key));
|
|
1125
|
+
|
|
1126
|
+
/** The bands of a day with their lanes, cut to what its timeline draws. */
|
|
1127
|
+
export const dayBands = (view, key, expanded) => {
|
|
1128
|
+
const limit = expanded ? DAY_MINUTES : view.wake();
|
|
1129
|
+
const visible = view.dayOf(key).bands.filter(band => Math.min(limit, band.end) > Math.max(0, band.start));
|
|
1130
|
+
return dayLanes(visible, LANE_MINUTES);
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
/** Entries the folded night hides, as chips with their time. */
|
|
1134
|
+
export const foldedEntries = (view, key) => {
|
|
1135
|
+
const model = view.dayOf(key);
|
|
1136
|
+
const wake = view.wake();
|
|
1137
|
+
return [...model.bands.filter(band => band.end > wake).map(band => ({
|
|
1138
|
+
event: band.item,
|
|
1139
|
+
time: band.start >= wake ? view.clock(band.start) : view.messages().calendarUntil({
|
|
1140
|
+
time: view.clock(band.end)
|
|
1141
|
+
})
|
|
1142
|
+
})), ...model.markers.filter(marker => marker.minute >= wake).map(marker => ({
|
|
1143
|
+
event: marker.item,
|
|
1144
|
+
time: view.clock(marker.minute)
|
|
1145
|
+
}))];
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
/** The quick create at a slot or gap, as in the month view. */
|
|
1149
|
+
export const TimeQuickCreate = props => {
|
|
1150
|
+
const view = props.view;
|
|
1151
|
+
let element;
|
|
1152
|
+
let returnFocus = null;
|
|
1153
|
+
createEffect(() => {
|
|
1154
|
+
const current = view.quick();
|
|
1155
|
+
if (!element) return;
|
|
1156
|
+
if (!current) {
|
|
1157
|
+
if (element.matches(":popover-open")) element.hidePopover();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1161
|
+
if (!element.matches(":popover-open")) element.showPopover();
|
|
1162
|
+
queueMicrotask(() => {
|
|
1163
|
+
if (view.quick() !== current || !element?.isConnected) return;
|
|
1164
|
+
placeCalendarPopover(current.anchor, element);
|
|
1165
|
+
element.querySelector("input,textarea,select,button,a[href],[tabindex]:not([tabindex='-1'])")?.focus();
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
const close = restore => {
|
|
1169
|
+
if (!view.quick()) return;
|
|
1170
|
+
view.setQuick(null);
|
|
1171
|
+
view.setSelection(null);
|
|
1172
|
+
if (restore && returnFocus?.isConnected) returnFocus.focus({
|
|
1173
|
+
preventScroll: true
|
|
1174
|
+
});
|
|
1175
|
+
};
|
|
1176
|
+
return _$ssr(_tmpl$14, _$ssrAttribute("aria-label", _$escape(view.messages().calendarQuickCreate, true), false), _$escape(_$createComponent(Show, {
|
|
1177
|
+
get when() {
|
|
1178
|
+
return view.quick();
|
|
1179
|
+
},
|
|
1180
|
+
keyed: true,
|
|
1181
|
+
children: value => view.props.owner.renderQuickCreate?.(value.range, {
|
|
1182
|
+
create: value.create,
|
|
1183
|
+
close: () => {
|
|
1184
|
+
if (view.quick() === value) close(true);
|
|
1185
|
+
}
|
|
1186
|
+
})
|
|
1187
|
+
})));
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
/** All-day entries of a day as chips: bars with torn ends where they go on, and points due that day. */
|
|
1191
|
+
export const AllDayChips = props => {
|
|
1192
|
+
const view = props.view;
|
|
1193
|
+
return _$createComponent(For, {
|
|
1194
|
+
get each() {
|
|
1195
|
+
return props.spans;
|
|
1196
|
+
},
|
|
1197
|
+
children: span => {
|
|
1198
|
+
const event = span.item;
|
|
1199
|
+
const checked = () => view.checkedOf(event);
|
|
1200
|
+
return _$ssr(_tmpl$15, _$ssrAttribute("data-bar", span.marker ? _$escape(undefined, true) : "true", false) + _$ssrAttribute("data-checkbox", checked() !== undefined ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-checked", checked() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-just-checked", view.justChecked().has(event.id) ? "true" : _$escape(undefined, true), false), _$ssrStyleProperty("--k2b-calendar-accent:", _$escape(eventAccent(event), true)), _$escape(_$createComponent(Show, {
|
|
1201
|
+
get when() {
|
|
1202
|
+
return checked() !== undefined;
|
|
1203
|
+
},
|
|
1204
|
+
get children() {
|
|
1205
|
+
return _$createComponent(TimeCheck, {
|
|
1206
|
+
view: view,
|
|
1207
|
+
event: event
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
})), _$escape(_$createComponent(EventChip, {
|
|
1211
|
+
get event() {
|
|
1212
|
+
return view.normalized(event, props.dayKey);
|
|
1213
|
+
},
|
|
1214
|
+
get owner() {
|
|
1215
|
+
return view.props.owner;
|
|
1216
|
+
},
|
|
1217
|
+
get href() {
|
|
1218
|
+
return eventHref(view.props.owner, event);
|
|
1219
|
+
},
|
|
1220
|
+
compact: true,
|
|
1221
|
+
timeline: true,
|
|
1222
|
+
get bar() {
|
|
1223
|
+
return !span.marker;
|
|
1224
|
+
},
|
|
1225
|
+
get checkbox() {
|
|
1226
|
+
return checked() !== undefined;
|
|
1227
|
+
},
|
|
1228
|
+
get onCheck() {
|
|
1229
|
+
return view.checkOf(event);
|
|
1230
|
+
},
|
|
1231
|
+
get continuesBefore() {
|
|
1232
|
+
return span.firstKey < props.dayKey;
|
|
1233
|
+
},
|
|
1234
|
+
get continuesAfter() {
|
|
1235
|
+
return span.lastKey > props.dayKey;
|
|
1236
|
+
}
|
|
1237
|
+
})));
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
/** Hour labels and lines of a day's timeline with the night open, and the date at midnight. */
|
|
1243
|
+
export const nightHours = wake => {
|
|
1244
|
+
const hours = [];
|
|
1245
|
+
for (let minute = wake + 60; minute < DAY_MINUTES; minute += 60) hours.push(minute);
|
|
1246
|
+
return hours;
|
|
1247
|
+
};
|
|
1248
|
+
export const isNearNow = (view, key, minute) => key === view.nowKey() && Math.abs(view.nowMinute() - minute) < 14;
|
|
1249
|
+
|
|
1250
|
+
/** Whether a marker sits right above a band's start, so its pill goes above the line. */
|
|
1251
|
+
export const markerAbove = (model, minute) => model.bands.some(band => band.start >= minute - 10 && band.start <= minute + 20);
|
|
1252
|
+
|
|
1253
|
+
/** Whether a deadline pill sits near the middle of a gap, so the gap's label moves aside. */
|
|
1254
|
+
export const gapAside = (model, gap) => {
|
|
1255
|
+
const middle = (gap.start + gap.end) / 2;
|
|
1256
|
+
return model.markers.some(marker => marker.minute > gap.start - 10 && marker.minute < gap.end + 10 && Math.abs(marker.minute - middle) < 25);
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
/** A day's counts for its header: events and all-day entries, to-dos and deadlines. */
|
|
1260
|
+
export const daySummary = (view, key) => {
|
|
1261
|
+
const model = view.dayOf(key);
|
|
1262
|
+
const isTask = event => event.display === "marker" || event.checked !== undefined;
|
|
1263
|
+
const all = [...model.bands.map(band => band.item), ...model.markers.map(marker => marker.item), ...model.spans.map(span => span.item)];
|
|
1264
|
+
const tasks = all.filter(isTask).length;
|
|
1265
|
+
return view.messages().calendarDaySummary({
|
|
1266
|
+
events: all.length - tasks,
|
|
1267
|
+
tasks
|
|
1268
|
+
});
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
/** What starts within the next hour today, such as "Planning in 12 min". */
|
|
1272
|
+
export const soonNote = (view, key) => {
|
|
1273
|
+
if (key !== view.nowKey()) return "";
|
|
1274
|
+
const now = view.nowMinute();
|
|
1275
|
+
const next = view.dayOf(key).bands.filter(band => band.start > now && band.start - now <= 60 && !view.checkedOf(band.item)).sort((left, right) => left.start - right.start)[0];
|
|
1276
|
+
return next ? view.messages().calendarSoon({
|
|
1277
|
+
title: next.item.title,
|
|
1278
|
+
duration: view.duration(next.start - now)
|
|
1279
|
+
}) : "";
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
/** The busy minutes of a day's waking hours, merged, with the color of the first entry, for the thin load line. */
|
|
1283
|
+
export const dayLoad = (view, key) => {
|
|
1284
|
+
const pieces = view.dayOf(key).bands.map(band => ({
|
|
1285
|
+
start: Math.max(0, band.start),
|
|
1286
|
+
end: Math.min(view.wake(), band.end),
|
|
1287
|
+
color: eventAccent(band.item)
|
|
1288
|
+
})).filter(piece => piece.end > piece.start).sort((left, right) => left.start - right.start);
|
|
1289
|
+
const merged = [];
|
|
1290
|
+
for (const piece of pieces) {
|
|
1291
|
+
const last = merged[merged.length - 1];
|
|
1292
|
+
if (last && piece.start <= last.end) last.end = Math.max(last.end, piece.end);else merged.push({
|
|
1293
|
+
...piece
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
return merged;
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
/** Day keys of the week that holds a day, from its first day. */
|
|
1300
|
+
export const weekKeysOf = (view, key) => {
|
|
1301
|
+
const start = view.keyOf(calendar.startOfWeek(view.parseKey(key), view.dateConfig()));
|
|
1302
|
+
return Array.from({
|
|
1303
|
+
length: 7
|
|
1304
|
+
}, (_, index) => addDayKeys(start, index));
|
|
1305
|
+
};
|
|
1306
|
+
//# sourceMappingURL=calendar-time-view.js.map
|