@nomideusz/svelte-calendar 0.13.1 → 0.14.1
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/README.md +2 -0
- package/dist/calendar/Calendar.svelte +9 -5
- package/dist/calendar/Calendar.svelte.d.ts +7 -0
- package/dist/engine/event-store.svelte.d.ts +7 -1
- package/dist/engine/event-store.svelte.js +35 -11
- package/dist/views/agenda/AgendaWeek.svelte +78 -4
- package/dist/views/planner/PlannerWeek.svelte +61 -14
- package/dist/views/shared/context.svelte.d.ts +5 -0
- package/dist/views/shared/context.svelte.js +2 -0
- package/package.json +1 -1
- package/widget/widget.js +182 -57
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @nomideusz/svelte-calendar
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@nomideusz/svelte-calendar) [](https://github.com/nomideusz/svelte-calendar/blob/main/LICENSE)
|
|
4
|
+
|
|
3
5
|
A themeable **Svelte 5** calendar with **Day/Week Planner & Agenda** views, touch-first mobile views, and a smart auto-theme that adapts to any page.
|
|
4
6
|
|
|
5
7
|
**[Live demo → svelte-calendar.xyz](https://svelte-calendar.xyz/)**
|
|
@@ -57,6 +57,7 @@ let {
|
|
|
57
57
|
visibleHours,
|
|
58
58
|
initialDate,
|
|
59
59
|
snapInterval = 15,
|
|
60
|
+
minColumnWidth = 110,
|
|
60
61
|
showModePills = true,
|
|
61
62
|
showNavigation = true,
|
|
62
63
|
equalDays = false,
|
|
@@ -132,7 +133,7 @@ const effectiveTheme = $derived(theme === auto && autoTheme !== false ? probedTh
|
|
|
132
133
|
const effectiveAdapter = $derived(
|
|
133
134
|
timezone ? wrapAdapterWithTimezone(adapter, timezone) : adapter
|
|
134
135
|
);
|
|
135
|
-
const store =
|
|
136
|
+
const store = createEventStore(() => effectiveAdapter);
|
|
136
137
|
const viewState = createViewState(untrack(() => ({
|
|
137
138
|
view: activeViewId ?? views[0]?.id,
|
|
138
139
|
mondayStart,
|
|
@@ -218,6 +219,10 @@ async function commitDrag() {
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
let viewLoadRange = $state(null);
|
|
222
|
+
const mergedLabels = $derived(
|
|
223
|
+
labelsProp ? { ...getLabels(), ...labelsProp } : getLabels()
|
|
224
|
+
);
|
|
225
|
+
const L = $derived(mergedLabels);
|
|
221
226
|
setContext("calendar", {
|
|
222
227
|
// Engine objects (hold $state internally)
|
|
223
228
|
get store() {
|
|
@@ -256,6 +261,9 @@ setContext("calendar", {
|
|
|
256
261
|
get snapInterval() {
|
|
257
262
|
return snapInterval;
|
|
258
263
|
},
|
|
264
|
+
get minColumnWidth() {
|
|
265
|
+
return minColumnWidth;
|
|
266
|
+
},
|
|
259
267
|
get eventSnippet() {
|
|
260
268
|
return eventSnippet;
|
|
261
269
|
},
|
|
@@ -377,10 +385,6 @@ const modes = $derived.by(() => {
|
|
|
377
385
|
const g = new Set(desktopViews.map((v) => v.mode));
|
|
378
386
|
return ["day", "week", "month"].filter((key) => g.has(key));
|
|
379
387
|
});
|
|
380
|
-
const mergedLabels = $derived(
|
|
381
|
-
labelsProp ? { ...getLabels(), ...labelsProp } : getLabels()
|
|
382
|
-
);
|
|
383
|
-
const L = $derived(mergedLabels);
|
|
384
388
|
let lastViewLabel = $state(void 0);
|
|
385
389
|
$effect(() => {
|
|
386
390
|
const current = views.find((v) => v.id === viewState.view);
|
|
@@ -57,6 +57,13 @@ interface Props {
|
|
|
57
57
|
initialDate?: Date;
|
|
58
58
|
/** Drag snap interval in minutes (default: 15) */
|
|
59
59
|
snapInterval?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Minimum width (px) of a day column in planner views (default: 110).
|
|
62
|
+
* Below the resulting total the grid scrolls horizontally. Lower it when
|
|
63
|
+
* the calendar shares its row with a sidebar and the whole week must stay
|
|
64
|
+
* visible without scrolling.
|
|
65
|
+
*/
|
|
66
|
+
minColumnWidth?: number;
|
|
60
67
|
/** Show the Day/Week mode pills (default: true) */
|
|
61
68
|
showModePills?: boolean;
|
|
62
69
|
/** Show prev/next/today navigation controls (default: true) */
|
|
@@ -26,5 +26,11 @@ export interface EventStore {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Create a reactive event store backed by a CalendarAdapter.
|
|
29
|
+
*
|
|
30
|
+
* Pass a getter instead of an adapter when the adapter identity can change
|
|
31
|
+
* (a `$derived` adapter, a host that rebuilds it to force a refetch): the
|
|
32
|
+
* store then stays alive across the swap and reloads, instead of being torn
|
|
33
|
+
* down and rebuilt with an empty event map — which reads as every event
|
|
34
|
+
* blinking out and reappearing.
|
|
29
35
|
*/
|
|
30
|
-
export declare function createEventStore(adapter: CalendarAdapter): EventStore;
|
|
36
|
+
export declare function createEventStore(adapter: CalendarAdapter | (() => CalendarAdapter)): EventStore;
|
|
@@ -19,11 +19,20 @@ import { SvelteMap } from 'svelte/reactivity';
|
|
|
19
19
|
import { sod, DAY_MS } from '../core/time.js';
|
|
20
20
|
/**
|
|
21
21
|
* Create a reactive event store backed by a CalendarAdapter.
|
|
22
|
+
*
|
|
23
|
+
* Pass a getter instead of an adapter when the adapter identity can change
|
|
24
|
+
* (a `$derived` adapter, a host that rebuilds it to force a refetch): the
|
|
25
|
+
* store then stays alive across the swap and reloads, instead of being torn
|
|
26
|
+
* down and rebuilt with an empty event map — which reads as every event
|
|
27
|
+
* blinking out and reappearing.
|
|
22
28
|
*/
|
|
23
29
|
export function createEventStore(adapter) {
|
|
30
|
+
const getAdapter = typeof adapter === 'function' ? adapter : () => adapter;
|
|
24
31
|
let eventMap = new SvelteMap();
|
|
25
32
|
let loading = $state(false);
|
|
26
33
|
let error = $state(null);
|
|
34
|
+
/** Guards against an older in-flight load pruning a newer one's result */
|
|
35
|
+
let loadSeq = 0;
|
|
27
36
|
// Derived array view of the map — consumers read this.
|
|
28
37
|
const eventArray = $derived([...eventMap.values()]);
|
|
29
38
|
// ── Internal helpers ──
|
|
@@ -48,11 +57,21 @@ export function createEventStore(adapter) {
|
|
|
48
57
|
return error;
|
|
49
58
|
},
|
|
50
59
|
async load(range) {
|
|
60
|
+
const seq = ++loadSeq;
|
|
51
61
|
loading = true;
|
|
52
62
|
error = null;
|
|
53
63
|
try {
|
|
54
|
-
const fetched = await
|
|
55
|
-
|
|
64
|
+
const fetched = await getAdapter().fetchEvents(range);
|
|
65
|
+
if (seq !== loadSeq)
|
|
66
|
+
return; // superseded by a newer load
|
|
67
|
+
// Merge: upsert fetched, don't blow away events outside this range.
|
|
68
|
+
// Inside the range the adapter is authoritative — drop what it no
|
|
69
|
+
// longer returns, or deleted/moved events linger until remount.
|
|
70
|
+
const keep = new Set(fetched.map((ev) => ev.id));
|
|
71
|
+
for (const ev of [...eventMap.values()]) {
|
|
72
|
+
if (!keep.has(ev.id) && overlaps(ev, range.start, range.end))
|
|
73
|
+
removeEvent(ev.id);
|
|
74
|
+
}
|
|
56
75
|
for (const ev of fetched) {
|
|
57
76
|
upsertEvent(ev);
|
|
58
77
|
}
|
|
@@ -61,7 +80,8 @@ export function createEventStore(adapter) {
|
|
|
61
80
|
error = e instanceof Error ? e.message : String(e);
|
|
62
81
|
}
|
|
63
82
|
finally {
|
|
64
|
-
|
|
83
|
+
if (seq === loadSeq)
|
|
84
|
+
loading = false;
|
|
65
85
|
}
|
|
66
86
|
},
|
|
67
87
|
forRange(start, end) {
|
|
@@ -76,12 +96,12 @@ export function createEventStore(adapter) {
|
|
|
76
96
|
return eventMap.get(id);
|
|
77
97
|
},
|
|
78
98
|
async add(eventData) {
|
|
79
|
-
if (!
|
|
99
|
+
if (!getAdapter().createEvent)
|
|
80
100
|
throw new Error('Adapter is read-only: createEvent not implemented');
|
|
81
101
|
loading = true;
|
|
82
102
|
error = null;
|
|
83
103
|
try {
|
|
84
|
-
const created = await
|
|
104
|
+
const created = await getAdapter().createEvent(eventData);
|
|
85
105
|
upsertEvent(created);
|
|
86
106
|
return created;
|
|
87
107
|
}
|
|
@@ -94,12 +114,12 @@ export function createEventStore(adapter) {
|
|
|
94
114
|
}
|
|
95
115
|
},
|
|
96
116
|
async update(id, patch) {
|
|
97
|
-
if (!
|
|
117
|
+
if (!getAdapter().updateEvent)
|
|
98
118
|
throw new Error('Adapter is read-only: updateEvent not implemented');
|
|
99
119
|
loading = true;
|
|
100
120
|
error = null;
|
|
101
121
|
try {
|
|
102
|
-
const updated = await
|
|
122
|
+
const updated = await getAdapter().updateEvent(id, patch);
|
|
103
123
|
upsertEvent(updated);
|
|
104
124
|
}
|
|
105
125
|
catch (e) {
|
|
@@ -111,12 +131,12 @@ export function createEventStore(adapter) {
|
|
|
111
131
|
}
|
|
112
132
|
},
|
|
113
133
|
async remove(id) {
|
|
114
|
-
if (!
|
|
134
|
+
if (!getAdapter().deleteEvent)
|
|
115
135
|
throw new Error('Adapter is read-only: deleteEvent not implemented');
|
|
116
136
|
loading = true;
|
|
117
137
|
error = null;
|
|
118
138
|
try {
|
|
119
|
-
await
|
|
139
|
+
await getAdapter().deleteEvent(id);
|
|
120
140
|
removeEvent(id);
|
|
121
141
|
}
|
|
122
142
|
catch (e) {
|
|
@@ -138,8 +158,12 @@ export function createEventStore(adapter) {
|
|
|
138
158
|
await this.update(id, { start: newStart, end: newEnd });
|
|
139
159
|
}
|
|
140
160
|
catch (e) {
|
|
141
|
-
// Revert optimistic update on failure
|
|
142
|
-
|
|
161
|
+
// Revert optimistic update on failure — except for a read-only
|
|
162
|
+
// adapter, where the HOST owns persistence (Calendar forwards to
|
|
163
|
+
// oneventmove). Reverting there snaps the block back to its old
|
|
164
|
+
// slot for the length of the host's round-trip.
|
|
165
|
+
const msg = e instanceof Error ? e.message : '';
|
|
166
|
+
if (existing && !msg.includes('read-only'))
|
|
143
167
|
upsertEvent(existing);
|
|
144
168
|
throw e;
|
|
145
169
|
}
|
|
@@ -27,7 +27,36 @@ const compact = $derived(ctx.compact);
|
|
|
27
27
|
const cols = $derived(ctx.columns && !isMobile);
|
|
28
28
|
const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
|
|
29
29
|
const oneventhover = $derived(ctx.oneventhover);
|
|
30
|
+
const ondayclick = $derived(ctx.ondayclick);
|
|
30
31
|
const disabledSet = $derived(ctx.disabledSet);
|
|
32
|
+
function clickDay(ms) {
|
|
33
|
+
ondayclick?.(new Date(ms));
|
|
34
|
+
}
|
|
35
|
+
const oneventmove = $derived(ctx.oneventmove);
|
|
36
|
+
let dragId = $state(null);
|
|
37
|
+
let dropDay = $state(null);
|
|
38
|
+
const canDrag = (ev) => cols && !!oneventmove && !ev.data?.readOnly && !isAllDay(ev) && !isMultiDay(ev);
|
|
39
|
+
const isDropDay = (day) => dragId !== null && day.tier !== "past" && !disabledSet.has(day.ms);
|
|
40
|
+
function onCardDragStart(e, ev) {
|
|
41
|
+
dragId = String(ev.id);
|
|
42
|
+
if (e.dataTransfer) {
|
|
43
|
+
e.dataTransfer.effectAllowed = "move";
|
|
44
|
+
e.dataTransfer.setData("text/plain", String(ev.id));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function onDayDrop(e, day) {
|
|
48
|
+
if (!isDropDay(day)) return;
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
const ev = events.find((x) => String(x.id) === dragId);
|
|
52
|
+
dragId = null;
|
|
53
|
+
dropDay = null;
|
|
54
|
+
if (!ev) return;
|
|
55
|
+
if (sod(ev.start.getTime()) === day.ms) return;
|
|
56
|
+
const start = new Date(day.ms + (ev.start.getTime() - sod(ev.start.getTime())));
|
|
57
|
+
const end = new Date(start.getTime() + (ev.end.getTime() - ev.start.getTime()));
|
|
58
|
+
oneventmove?.(ev, start, end);
|
|
59
|
+
}
|
|
31
60
|
let swipeStartX = 0;
|
|
32
61
|
let swipeStartY = 0;
|
|
33
62
|
let swipeActive = false;
|
|
@@ -143,6 +172,10 @@ const weekDays = $derived.by(() => {
|
|
|
143
172
|
class:ag-card--limited={ev.status === 'limited'}
|
|
144
173
|
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
145
174
|
aria-label="{ev.title}{ev.status === 'cancelled' ? ' (cancelled)' : ''}{ev.status === 'tentative' ? ' (tentative)' : ''}{ev.status === 'full' ? ' (full)' : ''}{ev.status === 'limited' ? ' (limited)' : ''}, {fmt(ev.start)} to {fmt(ev.end)}, {duration(ev)}"
|
|
175
|
+
class:ag-card--drag={dragId === String(ev.id)}
|
|
176
|
+
draggable={canDrag(ev)}
|
|
177
|
+
ondragstart={(e) => onCardDragStart(e, ev)}
|
|
178
|
+
ondragend={() => ((dragId = null), (dropDay = null))}
|
|
146
179
|
onclick={() => handleClick(ev)}
|
|
147
180
|
onpointerenter={() => oneventhover?.(ev)}
|
|
148
181
|
>
|
|
@@ -252,8 +285,13 @@ const weekDays = $derived.by(() => {
|
|
|
252
285
|
{@const expanded = day.tier === 'today' || day.tier === 'tomorrow'}
|
|
253
286
|
{#if day.tier === 'past'}
|
|
254
287
|
<!-- Past day: single collapsed line -->
|
|
255
|
-
<div class="ag-wday ag-wday--past" class:ag-wday--disabled={disabledSet.has(day.ms)} role="listitem">
|
|
256
|
-
|
|
288
|
+
<div class="ag-wday ag-wday--past" class:ag-wday--disabled={disabledSet.has(day.ms)} data-day={day.ms} role="listitem">
|
|
289
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
290
|
+
<div
|
|
291
|
+
class="ag-wday-head"
|
|
292
|
+
class:ag-wday-head--click={!!ondayclick}
|
|
293
|
+
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
294
|
+
>
|
|
257
295
|
<div class="ag-wday-head-left">
|
|
258
296
|
<span class="ag-wday-name">{day.dayName}</span>
|
|
259
297
|
{#if showDates}<span class="ag-wday-date">{day.dateLabel}</span>{/if}
|
|
@@ -278,16 +316,34 @@ const weekDays = $derived.by(() => {
|
|
|
278
316
|
{/if}
|
|
279
317
|
</div>
|
|
280
318
|
{:else}
|
|
319
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
281
320
|
<div
|
|
282
321
|
class="ag-wday"
|
|
283
322
|
class:ag-wday--today={day.tier === 'today'}
|
|
284
323
|
class:ag-wday--tomorrow={day.tier === 'tomorrow'}
|
|
285
324
|
class:ag-wday--equal={equalDays}
|
|
286
325
|
class:ag-wday--disabled={disabledSet.has(day.ms)}
|
|
326
|
+
class:ag-wday--drop={dropDay === day.ms && isDropDay(day)}
|
|
327
|
+
data-day={day.ms}
|
|
287
328
|
role="listitem"
|
|
329
|
+
ondragover={(e) => {
|
|
330
|
+
if (!isDropDay(day)) return;
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
dropDay = day.ms;
|
|
333
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
334
|
+
}}
|
|
335
|
+
ondragleave={() => {
|
|
336
|
+
if (dropDay === day.ms) dropDay = null;
|
|
337
|
+
}}
|
|
338
|
+
ondrop={(e) => onDayDrop(e, day)}
|
|
288
339
|
>
|
|
289
340
|
<!-- Day header -->
|
|
290
|
-
|
|
341
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
342
|
+
<div
|
|
343
|
+
class="ag-wday-head"
|
|
344
|
+
class:ag-wday-head--click={!!ondayclick}
|
|
345
|
+
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
346
|
+
>
|
|
291
347
|
<div class="ag-wday-head-left">
|
|
292
348
|
{#if day.isToday}
|
|
293
349
|
<span class="ag-wday-badge">{L.today}</span>
|
|
@@ -329,7 +385,12 @@ const weekDays = $derived.by(() => {
|
|
|
329
385
|
{/if}
|
|
330
386
|
|
|
331
387
|
{#if day.events.length === 0}
|
|
332
|
-
|
|
388
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
389
|
+
<div
|
|
390
|
+
class="ag-wday-empty"
|
|
391
|
+
class:ag-wday-empty--click={!!ondayclick}
|
|
392
|
+
onclick={ondayclick ? () => clickDay(day.ms) : undefined}
|
|
393
|
+
>{L.noEvents}</div>
|
|
333
394
|
{:else if compact && !cols}
|
|
334
395
|
<!-- Compact: minimal dot + time + title rows for all days.
|
|
335
396
|
Ignored in columns mode — single-line rows truncate to
|
|
@@ -692,6 +753,10 @@ const weekDays = $derived.by(() => {
|
|
|
692
753
|
padding: 2px 0 4px;
|
|
693
754
|
}
|
|
694
755
|
|
|
756
|
+
.ag-wday-head--click,
|
|
757
|
+
.ag-wday-empty--click {
|
|
758
|
+
cursor: pointer;
|
|
759
|
+
}
|
|
695
760
|
.ag-wday-head {
|
|
696
761
|
display: flex;
|
|
697
762
|
justify-content: space-between;
|
|
@@ -984,6 +1049,15 @@ const weekDays = $derived.by(() => {
|
|
|
984
1049
|
border-radius: 4px;
|
|
985
1050
|
}
|
|
986
1051
|
|
|
1052
|
+
/* ═══ Day-drop drag ═══ */
|
|
1053
|
+
.ag-card--drag {
|
|
1054
|
+
opacity: 0.4;
|
|
1055
|
+
}
|
|
1056
|
+
.ag-wday--drop {
|
|
1057
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 6%, transparent);
|
|
1058
|
+
box-shadow: inset 0 0 0 1px var(--dt-accent, #2563eb);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
987
1061
|
/* ═══ Timetable columns (desktop) ═══ */
|
|
988
1062
|
.ag--cols .ag-body {
|
|
989
1063
|
display: grid;
|
|
@@ -53,7 +53,7 @@ const disabledSet = $derived(ctx.disabledSet);
|
|
|
53
53
|
const SNAP_MS = $derived(ctx.snapInterval * 6e4);
|
|
54
54
|
const HOUR_H = 48;
|
|
55
55
|
const GUTTER_W = 48;
|
|
56
|
-
const MIN_COL_W =
|
|
56
|
+
const MIN_COL_W = $derived(ctx.minColumnWidth);
|
|
57
57
|
const ALLDAY_MAX = 3;
|
|
58
58
|
const startHour = $derived(visibleHours?.[0] ?? 0);
|
|
59
59
|
const endHour = $derived(visibleHours?.[1] ?? 24);
|
|
@@ -135,7 +135,6 @@ const layoutByDay = $derived.by(() => {
|
|
|
135
135
|
const infos = [];
|
|
136
136
|
for (const ev of events) {
|
|
137
137
|
if (isAllDay(ev) || isMultiDay(ev)) continue;
|
|
138
|
-
if (ev.id === movingId) continue;
|
|
139
138
|
const isResizing = rsP?.eventId === ev.id;
|
|
140
139
|
const s0 = isResizing ? rsP.start.getTime() : ev.start.getTime();
|
|
141
140
|
const e0 = isResizing ? rsP.end.getTime() : ev.end.getTime();
|
|
@@ -143,7 +142,7 @@ const layoutByDay = $derived.by(() => {
|
|
|
143
142
|
const sMs = Math.max(s0, bandStart);
|
|
144
143
|
const eMs = Math.min(e0, bandEnd);
|
|
145
144
|
if (eMs <= sMs) continue;
|
|
146
|
-
infos.push({ ev, startMs: sMs, endMs: eMs, isResizing, col: 0, totalCols: 1 });
|
|
145
|
+
infos.push({ ev, startMs: sMs, endMs: eMs, isResizing, isMoving: ev.id === movingId, col: 0, totalCols: 1 });
|
|
147
146
|
}
|
|
148
147
|
infos.sort((a, b) => a.startMs - b.startMs || b.endMs - a.endMs);
|
|
149
148
|
const par = infos.map((_, i) => i);
|
|
@@ -185,7 +184,8 @@ const layoutByDay = $derived.by(() => {
|
|
|
185
184
|
height: Math.max(24, (inf.endMs - inf.startMs) / HOUR_MS * HOUR_H),
|
|
186
185
|
col: inf.col,
|
|
187
186
|
totalCols: inf.totalCols,
|
|
188
|
-
isResizing: inf.isResizing
|
|
187
|
+
isResizing: inf.isResizing,
|
|
188
|
+
isMoving: inf.isMoving
|
|
189
189
|
}))
|
|
190
190
|
);
|
|
191
191
|
}
|
|
@@ -222,8 +222,41 @@ $effect(() => {
|
|
|
222
222
|
el.scrollTop = (targetHour - startHour) * HOUR_H;
|
|
223
223
|
});
|
|
224
224
|
});
|
|
225
|
+
let rafId = 0;
|
|
226
|
+
let rafRun = null;
|
|
227
|
+
let rectCache = null;
|
|
228
|
+
function perFrame(handler) {
|
|
229
|
+
return (e) => {
|
|
230
|
+
rafRun = () => {
|
|
231
|
+
rectCache = colsEl?.getBoundingClientRect() ?? null;
|
|
232
|
+
try {
|
|
233
|
+
handler(e);
|
|
234
|
+
} finally {
|
|
235
|
+
rectCache = null;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
if (rafId) return;
|
|
239
|
+
rafId = requestAnimationFrame(flushFrame);
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function flushFrame() {
|
|
243
|
+
if (rafId) {
|
|
244
|
+
cancelAnimationFrame(rafId);
|
|
245
|
+
rafId = 0;
|
|
246
|
+
}
|
|
247
|
+
const run = rafRun;
|
|
248
|
+
rafRun = null;
|
|
249
|
+
run?.();
|
|
250
|
+
}
|
|
251
|
+
function cancelFrame() {
|
|
252
|
+
if (rafId) {
|
|
253
|
+
cancelAnimationFrame(rafId);
|
|
254
|
+
rafId = 0;
|
|
255
|
+
}
|
|
256
|
+
rafRun = null;
|
|
257
|
+
}
|
|
225
258
|
function colsRect() {
|
|
226
|
-
return colsEl.getBoundingClientRect();
|
|
259
|
+
return rectCache ?? colsEl.getBoundingClientRect();
|
|
227
260
|
}
|
|
228
261
|
function pointerDayIndex(clientX) {
|
|
229
262
|
const r = colsRect();
|
|
@@ -339,7 +372,7 @@ function onColsPointerDown(e) {
|
|
|
339
372
|
window.addEventListener("pointerup", onColsCreateUp, { once: true });
|
|
340
373
|
window.addEventListener("pointercancel", onColsCreateCancel, { once: true });
|
|
341
374
|
}
|
|
342
|
-
|
|
375
|
+
const onColsCreateMove = perFrame((e) => {
|
|
343
376
|
if (!drag) return;
|
|
344
377
|
if (!crStarted) {
|
|
345
378
|
if (longPressTimer !== null) {
|
|
@@ -357,8 +390,9 @@ function onColsCreateMove(e) {
|
|
|
357
390
|
new Date(Math.min(crAnchorMs, snapped)),
|
|
358
391
|
new Date(Math.max(crAnchorMs + SNAP_MS, snapped))
|
|
359
392
|
);
|
|
360
|
-
}
|
|
393
|
+
});
|
|
361
394
|
function cleanupColsCreate() {
|
|
395
|
+
cancelFrame();
|
|
362
396
|
clearLongPress();
|
|
363
397
|
removeTouchScrollBlock();
|
|
364
398
|
window.removeEventListener("pointermove", onColsCreateMove);
|
|
@@ -367,6 +401,7 @@ function cleanupColsCreate() {
|
|
|
367
401
|
crStarted = false;
|
|
368
402
|
}
|
|
369
403
|
function onColsCreateUp() {
|
|
404
|
+
if (crStarted) flushFrame();
|
|
370
405
|
if (drag && crStarted) {
|
|
371
406
|
suppressColsClick = true;
|
|
372
407
|
commitDragCtx?.();
|
|
@@ -421,7 +456,7 @@ function onEventPointerDown(e, ev) {
|
|
|
421
456
|
window.addEventListener("pointerup", onEvUp, { once: true });
|
|
422
457
|
window.addEventListener("pointercancel", onEvCancel, { once: true });
|
|
423
458
|
}
|
|
424
|
-
|
|
459
|
+
const onEvMove = perFrame((e) => {
|
|
425
460
|
const ev = evDragEvent;
|
|
426
461
|
if (!drag || !ev || !evDragMovable) return;
|
|
427
462
|
if (!evDragStarted) {
|
|
@@ -434,8 +469,9 @@ function onEvMove(e) {
|
|
|
434
469
|
const raw = pointerTimeMs(e.clientX, e.clientY) - evGrabOffsetMs;
|
|
435
470
|
const snapped = Math.round(raw / SNAP_MS) * SNAP_MS;
|
|
436
471
|
drag.updatePointer(new Date(snapped), new Date(snapped + duration));
|
|
437
|
-
}
|
|
472
|
+
});
|
|
438
473
|
function cleanupEvDrag() {
|
|
474
|
+
cancelFrame();
|
|
439
475
|
window.removeEventListener("pointermove", onEvMove);
|
|
440
476
|
window.removeEventListener("pointerup", onEvUp);
|
|
441
477
|
window.removeEventListener("pointercancel", onEvCancel);
|
|
@@ -444,6 +480,7 @@ function cleanupEvDrag() {
|
|
|
444
480
|
evDragEvent = null;
|
|
445
481
|
}
|
|
446
482
|
function onEvUp() {
|
|
483
|
+
if (evDragStarted) flushFrame();
|
|
447
484
|
if (!evDragStarted && evDragEvent) {
|
|
448
485
|
oneventclick?.(evDragEvent);
|
|
449
486
|
} else if (evDragStarted && drag) {
|
|
@@ -474,7 +511,7 @@ function onResizePointerDown(e, ev, edge) {
|
|
|
474
511
|
window.addEventListener("pointerup", onResizeUp, { once: true });
|
|
475
512
|
window.addEventListener("pointercancel", onResizeCancel, { once: true });
|
|
476
513
|
}
|
|
477
|
-
|
|
514
|
+
const onResizeMove = perFrame((e) => {
|
|
478
515
|
const ev = rsEvent;
|
|
479
516
|
if (!drag || !ev) return;
|
|
480
517
|
if (!rsStarted) {
|
|
@@ -493,8 +530,9 @@ function onResizeMove(e) {
|
|
|
493
530
|
const start = Math.min(snapped, ev.end.getTime() - SNAP_MS);
|
|
494
531
|
drag.updatePointer(new Date(start), ev.end);
|
|
495
532
|
}
|
|
496
|
-
}
|
|
533
|
+
});
|
|
497
534
|
function cleanupResize() {
|
|
535
|
+
cancelFrame();
|
|
498
536
|
removeTouchScrollBlock();
|
|
499
537
|
window.removeEventListener("pointermove", onResizeMove);
|
|
500
538
|
window.removeEventListener("pointerup", onResizeUp);
|
|
@@ -503,6 +541,7 @@ function cleanupResize() {
|
|
|
503
541
|
rsEvent = null;
|
|
504
542
|
}
|
|
505
543
|
function onResizeUp() {
|
|
544
|
+
if (rsStarted) flushFrame();
|
|
506
545
|
if (drag && rsStarted) {
|
|
507
546
|
suppressColsClick = true;
|
|
508
547
|
commitDragCtx?.();
|
|
@@ -568,6 +607,7 @@ function onWindowKeydown(e) {
|
|
|
568
607
|
class="tw"
|
|
569
608
|
class:tw--auto={autoHeight}
|
|
570
609
|
style={style || undefined}
|
|
610
|
+
style:--tw-col-min="{MIN_COL_W}px"
|
|
571
611
|
style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}
|
|
572
612
|
role="region"
|
|
573
613
|
aria-label={L.weekAhead}
|
|
@@ -703,6 +743,7 @@ function onWindowKeydown(e) {
|
|
|
703
743
|
class:tw-ev--selected={selectedEventId === p.ev.id}
|
|
704
744
|
class:tw-ev--current={isCurrent}
|
|
705
745
|
class:tw-ev--resizing={p.isResizing}
|
|
746
|
+
class:tw-ev--moving={p.isMoving}
|
|
706
747
|
class:tw-ev--readonly={p.ev.data?.readOnly}
|
|
707
748
|
class:tw-ev--cancelled={p.ev.status === 'cancelled'}
|
|
708
749
|
class:tw-ev--tentative={p.ev.status === 'tentative'}
|
|
@@ -857,7 +898,7 @@ function onWindowKeydown(e) {
|
|
|
857
898
|
|
|
858
899
|
.tw-hd {
|
|
859
900
|
flex: 1 1 0;
|
|
860
|
-
min-width: 110px;
|
|
901
|
+
min-width: var(--tw-col-min, 110px);
|
|
861
902
|
display: flex;
|
|
862
903
|
flex-direction: column;
|
|
863
904
|
align-items: center;
|
|
@@ -923,7 +964,7 @@ function onWindowKeydown(e) {
|
|
|
923
964
|
|
|
924
965
|
.tw-ad-cell {
|
|
925
966
|
flex: 1 1 0;
|
|
926
|
-
min-width: 110px;
|
|
967
|
+
min-width: var(--tw-col-min, 110px);
|
|
927
968
|
display: flex;
|
|
928
969
|
flex-direction: column;
|
|
929
970
|
gap: 2px;
|
|
@@ -1075,7 +1116,7 @@ function onWindowKeydown(e) {
|
|
|
1075
1116
|
/* ─── Day column ─────────────────────────────────── */
|
|
1076
1117
|
.tw-col {
|
|
1077
1118
|
flex: 1 1 0;
|
|
1078
|
-
min-width: 110px;
|
|
1119
|
+
min-width: var(--tw-col-min, 110px);
|
|
1079
1120
|
position: relative;
|
|
1080
1121
|
border-left: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
1081
1122
|
box-sizing: border-box;
|
|
@@ -1197,6 +1238,12 @@ function onWindowKeydown(e) {
|
|
|
1197
1238
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
|
1198
1239
|
cursor: ns-resize;
|
|
1199
1240
|
}
|
|
1241
|
+
/* Origin of an in-flight move: still there, clearly no longer the subject. */
|
|
1242
|
+
.tw-ev--moving {
|
|
1243
|
+
opacity: 0.3;
|
|
1244
|
+
pointer-events: none;
|
|
1245
|
+
box-shadow: none;
|
|
1246
|
+
}
|
|
1200
1247
|
/* Status treatments: token-level dims + a non-opacity signal
|
|
1201
1248
|
(strikethrough / border style) — consistent with the other views. */
|
|
1202
1249
|
.tw-ev--cancelled {
|
|
@@ -8,6 +8,7 @@ export interface CalendarContext {
|
|
|
8
8
|
readonly drag: DragState | undefined;
|
|
9
9
|
readonly commitDrag: (() => void) | undefined;
|
|
10
10
|
readonly snapInterval: number;
|
|
11
|
+
readonly minColumnWidth: number;
|
|
11
12
|
readonly equalDays: boolean;
|
|
12
13
|
readonly showDates: boolean;
|
|
13
14
|
readonly hideDays: number[] | undefined;
|
|
@@ -25,6 +26,10 @@ export interface CalendarContext {
|
|
|
25
26
|
readonly minDuration: number | undefined;
|
|
26
27
|
readonly maxDuration: number | undefined;
|
|
27
28
|
readonly oneventhover: ((event: TimelineEvent) => void) | undefined;
|
|
29
|
+
/** Move callback (already readOnly-gated and timezone-unzoning at the
|
|
30
|
+
* Calendar layer) — lets non-planner views offer coarser drags, e.g.
|
|
31
|
+
* AgendaWeek's day-to-day card drop. */
|
|
32
|
+
readonly oneventmove: ((event: TimelineEvent, newStart: Date, newEnd: Date) => void) | undefined;
|
|
28
33
|
readonly ondayclick: ((date: Date) => void) | undefined;
|
|
29
34
|
readonly timezone: string | undefined;
|
|
30
35
|
readonly disabledDates: Date[] | undefined;
|
|
@@ -28,6 +28,7 @@ export function useCalendarContext() {
|
|
|
28
28
|
get drag() { return raw?.drag; },
|
|
29
29
|
get commitDrag() { return raw?.commitDrag; },
|
|
30
30
|
get snapInterval() { return raw?.snapInterval ?? 15; },
|
|
31
|
+
get minColumnWidth() { return raw?.minColumnWidth ?? 110; },
|
|
31
32
|
get equalDays() { return raw?.equalDays ?? false; },
|
|
32
33
|
get showDates() { return raw?.showDates ?? true; },
|
|
33
34
|
get hideDays() { return raw?.hideDays; },
|
|
@@ -41,6 +42,7 @@ export function useCalendarContext() {
|
|
|
41
42
|
get minDuration() { return raw?.minDuration; },
|
|
42
43
|
get maxDuration() { return raw?.maxDuration; },
|
|
43
44
|
get oneventhover() { return raw?.oneventhover; },
|
|
45
|
+
get oneventmove() { return raw?.oneventmove; },
|
|
44
46
|
get ondayclick() { return raw?.ondayclick; },
|
|
45
47
|
get timezone() { return raw?.timezone; },
|
|
46
48
|
get disabledDates() { return raw?.disabledDates; },
|