@nomideusz/svelte-calendar 0.7.4 → 0.8.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/README.md +19 -4
- package/dist/calendar/Calendar.svelte +364 -497
- package/dist/calendar/Calendar.svelte.d.ts +6 -2
- package/dist/core/clock.svelte.js +12 -5
- package/dist/core/locale.d.ts +4 -0
- package/dist/core/locale.js +4 -0
- package/dist/engine/view-state.svelte.d.ts +2 -2
- package/dist/engine/view-state.svelte.js +20 -0
- package/dist/headless/create-calendar.svelte.js +18 -2
- package/dist/headless/types.d.ts +6 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/primitives/DayHeader.svelte +9 -25
- package/dist/primitives/EmptySlot.svelte +15 -31
- package/dist/primitives/EventBlock.svelte +33 -61
- package/dist/primitives/NowIndicator.svelte +16 -42
- package/dist/primitives/TimeGutter.svelte +9 -23
- package/dist/views/agenda/Agenda.svelte +3 -10
- package/dist/views/agenda/AgendaDay.svelte +195 -167
- package/dist/views/agenda/AgendaWeek.svelte +141 -195
- package/dist/views/mobile/Mobile.svelte +3 -10
- package/dist/views/mobile/MobileDay.svelte +422 -266
- package/dist/views/mobile/MobileWeek.svelte +131 -186
- package/dist/views/month/MonthGrid.svelte +334 -0
- package/dist/views/month/MonthGrid.svelte.d.ts +15 -0
- package/dist/views/planner/Planner.svelte +3 -10
- package/dist/views/planner/PlannerDay.svelte +645 -549
- package/dist/views/planner/PlannerWeek.svelte +299 -412
- package/dist/views/shared/EventContent.svelte +16 -0
- package/dist/views/shared/EventContent.svelte.d.ts +9 -0
- package/dist/views/shared/context.svelte.d.ts +1 -0
- package/dist/views/shared/context.svelte.js +1 -0
- package/dist/widget/CalendarWidget.svelte +79 -116
- package/package.json +3 -2
- package/widget/svelte-calendar.css +329 -13
- package/widget/widget.js +1945 -1031
|
@@ -17,495 +17,362 @@
|
|
|
17
17
|
oneventcreate={handleCreate}
|
|
18
18
|
/>
|
|
19
19
|
-->
|
|
20
|
-
<script lang="ts">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
store.load({ start: range.start, end: range.end });
|
|
377
|
-
});
|
|
378
|
-
// Eager initial load — $effect runs after paint, but for sync adapters
|
|
379
|
-
// (memory, recurring) this resolves in the same microtask.
|
|
380
|
-
untrack(() => store.load({ start: viewState.range.start, end: viewState.range.end }));
|
|
381
|
-
|
|
382
|
-
// Keep active view in sync when external view prop changes after mount.
|
|
383
|
-
$effect(() => {
|
|
384
|
-
if (activeViewId) viewState.setView(activeViewId);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// Sync controlled currentDate prop → viewState
|
|
388
|
-
$effect(() => {
|
|
389
|
-
if (currentDate) viewState.setFocusDate(currentDate);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// Sync days prop → viewState.dayCount
|
|
393
|
-
$effect(() => {
|
|
394
|
-
if (days !== undefined && viewState.dayCount !== days) viewState.setDayCount(days);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// Notify host when focusDate changes
|
|
398
|
-
$effect(() => {
|
|
399
|
-
const d = viewState.focusDate;
|
|
400
|
-
ondatechange?.(d);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// Keep view state's week-start rule in sync with incoming prop changes.
|
|
404
|
-
$effect(() => {
|
|
405
|
-
if (viewState.mondayStart !== mondayStart) {
|
|
406
|
-
viewState.setMondayStart(mondayStart);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// Notify host when active view changes (e.g. via mode toggles).
|
|
411
|
-
$effect(() => {
|
|
412
|
-
onviewchange?.(viewState.view);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// ── Resolve active view ──
|
|
417
|
-
// When mobile is active, Planner views get remapped to Mobile variants.
|
|
418
|
-
// Agenda views stay as Agenda — they're already list-based and will adapt
|
|
419
|
-
// via the 'calendar:mobile' context flag.
|
|
420
|
-
const resolvedView = $derived.by(() => {
|
|
421
|
-
const requested = views.find((v) => v.id === viewState.view) ?? views[0];
|
|
422
|
-
if (!useMobile || !requested) return requested;
|
|
423
|
-
// Already a mobile view? Keep it.
|
|
424
|
-
if (requested.id.endsWith('-mobile')) return requested;
|
|
425
|
-
// Agenda views: keep as-is (they adapt internally via mobile context).
|
|
426
|
-
if (requested.label === 'Agenda') return requested;
|
|
427
|
-
// Planner / other views: remap to mobile variant with the same mode.
|
|
428
|
-
const mobileVariant = views.find(
|
|
429
|
-
(v) => v.id === `${requested.mode}-mobile`
|
|
430
|
-
);
|
|
431
|
-
return mobileVariant ?? requested;
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// Backward-compat alias used in the template.
|
|
435
|
-
const activeView = $derived(resolvedView);
|
|
436
|
-
|
|
437
|
-
// Non-mobile views for mode pills (exclude mobile entries).
|
|
438
|
-
const desktopViews = $derived(views.filter((v) => !v.id.endsWith('-mobile')));
|
|
439
|
-
|
|
440
|
-
// ── Date label (always visible, centered over views) ──
|
|
441
|
-
const dateLabel = $derived.by(() => {
|
|
442
|
-
if (!showDates) {
|
|
443
|
-
// Template week mode: just show weekday name (day) or nothing (week)
|
|
444
|
-
if (viewState.mode === 'day') {
|
|
445
|
-
return viewState.focusDate.toLocaleDateString(locale, { weekday: 'long' });
|
|
446
|
-
}
|
|
447
|
-
return ''; // week views have their own day headers
|
|
448
|
-
}
|
|
449
|
-
if (viewState.mode === 'day') {
|
|
450
|
-
return viewState.focusDate.toLocaleDateString(locale, {
|
|
451
|
-
weekday: 'long',
|
|
452
|
-
month: 'short',
|
|
453
|
-
day: 'numeric',
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
return viewState.focusDate.toLocaleDateString(locale, {
|
|
457
|
-
month: 'long',
|
|
458
|
-
year: 'numeric',
|
|
459
|
-
});
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// Which modes are available?
|
|
463
|
-
const modes = $derived.by(() => {
|
|
464
|
-
const g = new Set(desktopViews.map((v) => v.mode));
|
|
465
|
-
return (['day', 'week'] as const).filter((key) => g.has(key));
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
const L = $derived(getLabels());
|
|
469
|
-
|
|
470
|
-
/** Switch to a different mode (day/week), preserving the current view label. */
|
|
471
|
-
function switchMode(g: 'day' | 'week') {
|
|
472
|
-
const currentLabel = desktopViews.find((v) => v.id === viewState.view)?.label
|
|
473
|
-
?? activeView?.label;
|
|
474
|
-
const match = desktopViews.find((v) => v.mode === g && v.label === currentLabel);
|
|
475
|
-
const fallback = desktopViews.find((v) => v.mode === g);
|
|
476
|
-
const target = match ?? fallback;
|
|
477
|
-
if (target) viewState.setView(target.id);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/** True when the current view range already includes today. */
|
|
481
|
-
const viewIncludesToday = $derived.by(() => {
|
|
482
|
-
const now = Date.now();
|
|
483
|
-
const { start, end } = viewState.range;
|
|
484
|
-
return now >= start.getTime() && now < end.getTime();
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
/** Header context for custom header snippet */
|
|
488
|
-
const headerCtx = $derived({
|
|
489
|
-
dateLabel,
|
|
490
|
-
mode: viewState.mode,
|
|
491
|
-
modes,
|
|
492
|
-
switchMode,
|
|
493
|
-
prev: () => viewState.prev(),
|
|
494
|
-
next: () => viewState.next(),
|
|
495
|
-
goToday: () => viewState.goToday(),
|
|
496
|
-
isViewOnToday: viewIncludesToday,
|
|
497
|
-
focusDate: viewState.focusDate,
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
/** Navigation context for custom navigation snippet */
|
|
501
|
-
const navCtx = $derived({
|
|
502
|
-
prev: () => viewState.prev(),
|
|
503
|
-
next: () => viewState.next(),
|
|
504
|
-
goToday: () => viewState.goToday(),
|
|
505
|
-
isViewOnToday: viewIncludesToday,
|
|
506
|
-
focusDate: viewState.focusDate,
|
|
507
|
-
mode: viewState.mode,
|
|
508
|
-
});
|
|
20
|
+
<script lang="ts">import { setContext, untrack } from "svelte";
|
|
21
|
+
import { createEventStore } from "../engine/event-store.svelte.js";
|
|
22
|
+
import { createViewState } from "../engine/view-state.svelte.js";
|
|
23
|
+
import { createSelection } from "../engine/selection.svelte.js";
|
|
24
|
+
import { createDragState } from "../engine/drag.svelte.js";
|
|
25
|
+
import { onMount } from "svelte";
|
|
26
|
+
import { getLabels } from "../core/locale.js";
|
|
27
|
+
import { auto } from "../theme/presets.js";
|
|
28
|
+
import { probeHostTheme, observeHostTheme } from "../theme/auto.js";
|
|
29
|
+
import Planner from "../views/planner/Planner.svelte";
|
|
30
|
+
import Agenda from "../views/agenda/Agenda.svelte";
|
|
31
|
+
import Mobile from "../views/mobile/Mobile.svelte";
|
|
32
|
+
import MonthGrid from "../views/month/MonthGrid.svelte";
|
|
33
|
+
const MOBILE_BREAKPOINT = 768;
|
|
34
|
+
const DEFAULT_VIEWS = [
|
|
35
|
+
{ id: "day-planner", label: "Planner", mode: "day", component: Planner },
|
|
36
|
+
{ id: "week-planner", label: "Planner", mode: "week", component: Planner },
|
|
37
|
+
{ id: "day-agenda", label: "Agenda", mode: "day", component: Agenda },
|
|
38
|
+
{ id: "week-agenda", label: "Agenda", mode: "week", component: Agenda },
|
|
39
|
+
{ id: "day-mobile", label: "Mobile", mode: "day", component: Mobile },
|
|
40
|
+
{ id: "week-mobile", label: "Mobile", mode: "week", component: Mobile },
|
|
41
|
+
{ id: "month-grid", label: "Month", mode: "month", component: MonthGrid }
|
|
42
|
+
];
|
|
43
|
+
let {
|
|
44
|
+
adapter,
|
|
45
|
+
views = DEFAULT_VIEWS,
|
|
46
|
+
view: activeViewId,
|
|
47
|
+
theme = auto,
|
|
48
|
+
autoTheme,
|
|
49
|
+
mondayStart = true,
|
|
50
|
+
height: heightProp = 600,
|
|
51
|
+
borderRadius = 12,
|
|
52
|
+
dir,
|
|
53
|
+
locale,
|
|
54
|
+
readOnly = false,
|
|
55
|
+
visibleHours,
|
|
56
|
+
initialDate,
|
|
57
|
+
snapInterval = 15,
|
|
58
|
+
showModePills = true,
|
|
59
|
+
showNavigation = true,
|
|
60
|
+
equalDays = false,
|
|
61
|
+
showDates = true,
|
|
62
|
+
hideDays,
|
|
63
|
+
currentDate,
|
|
64
|
+
blockedSlots,
|
|
65
|
+
days,
|
|
66
|
+
minDuration,
|
|
67
|
+
maxDuration,
|
|
68
|
+
disabledDates,
|
|
69
|
+
compact = false,
|
|
70
|
+
mobile: mobileProp = "auto",
|
|
71
|
+
event: eventSnippet,
|
|
72
|
+
empty: emptySnippet,
|
|
73
|
+
dayHeader: dayHeaderSnippet,
|
|
74
|
+
header: headerSnippet,
|
|
75
|
+
navigation: navigationSnippet,
|
|
76
|
+
oneventclick,
|
|
77
|
+
oneventcreate,
|
|
78
|
+
oneventmove,
|
|
79
|
+
onviewchange,
|
|
80
|
+
ondatechange,
|
|
81
|
+
oneventhover,
|
|
82
|
+
ondayclick,
|
|
83
|
+
onerror
|
|
84
|
+
} = $props();
|
|
85
|
+
const effectiveCreate = $derived(readOnly ? void 0 : oneventcreate);
|
|
86
|
+
const effectiveMove = $derived(readOnly ? void 0 : oneventmove);
|
|
87
|
+
function handleEventClick(ev) {
|
|
88
|
+
selection.select(ev.id);
|
|
89
|
+
oneventclick?.(ev);
|
|
90
|
+
}
|
|
91
|
+
let containerWidth = $state(0);
|
|
92
|
+
const isMobileContainer = $derived(containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT);
|
|
93
|
+
const useMobile = $derived(
|
|
94
|
+
mobileProp === "auto" ? isMobileContainer : Boolean(mobileProp)
|
|
95
|
+
);
|
|
96
|
+
let calEl = $state();
|
|
97
|
+
let probedTheme = $state("");
|
|
98
|
+
const needsProbe = $derived(theme === auto && autoTheme !== false);
|
|
99
|
+
onMount(() => {
|
|
100
|
+
if (!calEl) return;
|
|
101
|
+
containerWidth = calEl.clientWidth;
|
|
102
|
+
const ro = new ResizeObserver((entries) => {
|
|
103
|
+
containerWidth = Math.round(entries[0].contentRect.width);
|
|
104
|
+
});
|
|
105
|
+
ro.observe(calEl);
|
|
106
|
+
if (!needsProbe) return () => ro.disconnect();
|
|
107
|
+
const opts = typeof autoTheme === "object" ? autoTheme : {};
|
|
108
|
+
const stopTheme = observeHostTheme(calEl, (vars) => {
|
|
109
|
+
probedTheme = vars;
|
|
110
|
+
}, opts);
|
|
111
|
+
return () => {
|
|
112
|
+
ro.disconnect();
|
|
113
|
+
stopTheme?.();
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
const effectiveTheme = $derived(theme === auto && autoTheme !== false ? probedTheme : theme);
|
|
117
|
+
const store = $derived(createEventStore(adapter));
|
|
118
|
+
const viewState = createViewState(untrack(() => ({
|
|
119
|
+
view: activeViewId ?? views[0]?.id,
|
|
120
|
+
mondayStart,
|
|
121
|
+
initialDate,
|
|
122
|
+
dayCount: days,
|
|
123
|
+
modeForView: (viewId) => views.find((v) => v.id === viewId)?.mode
|
|
124
|
+
})));
|
|
125
|
+
const selection = createSelection();
|
|
126
|
+
const drag = createDragState();
|
|
127
|
+
async function commitDrag() {
|
|
128
|
+
if (readOnly) {
|
|
129
|
+
drag.cancel();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const mode = drag.mode;
|
|
133
|
+
const payload = drag.commit();
|
|
134
|
+
if (!payload) return;
|
|
135
|
+
let { start, end } = payload;
|
|
136
|
+
if (mode === "create" || mode === "resize-start" || mode === "resize-end") {
|
|
137
|
+
if (end.getTime() <= start.getTime()) {
|
|
138
|
+
const floorMs = Math.max(1, snapInterval) * 6e4;
|
|
139
|
+
if (mode === "resize-start") start = new Date(end.getTime() - floorMs);
|
|
140
|
+
else end = new Date(start.getTime() + floorMs);
|
|
141
|
+
}
|
|
142
|
+
const durationMs = end.getTime() - start.getTime();
|
|
143
|
+
const durationMin = durationMs / 6e4;
|
|
144
|
+
if (minDuration && durationMin < minDuration) {
|
|
145
|
+
if (mode === "resize-start") {
|
|
146
|
+
start = new Date(end.getTime() - minDuration * 6e4);
|
|
147
|
+
} else {
|
|
148
|
+
end = new Date(start.getTime() + minDuration * 6e4);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (maxDuration && durationMin > maxDuration) {
|
|
152
|
+
if (mode === "resize-start") {
|
|
153
|
+
start = new Date(end.getTime() - maxDuration * 6e4);
|
|
154
|
+
} else {
|
|
155
|
+
end = new Date(start.getTime() + maxDuration * 6e4);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (disabledDates?.length) {
|
|
160
|
+
const startDay = new Date(start);
|
|
161
|
+
startDay.setHours(0, 0, 0, 0);
|
|
162
|
+
const endDay = new Date(end.getTime() - 1);
|
|
163
|
+
endDay.setHours(0, 0, 0, 0);
|
|
164
|
+
for (const dd of disabledDates) {
|
|
165
|
+
const dt = new Date(dd);
|
|
166
|
+
dt.setHours(0, 0, 0, 0);
|
|
167
|
+
const ts = dt.getTime();
|
|
168
|
+
if (ts >= startDay.getTime() && ts <= endDay.getTime()) return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (blockedSlots?.length) {
|
|
172
|
+
const startH = start.getHours() + start.getMinutes() / 60;
|
|
173
|
+
const endH = end.getHours() + end.getMinutes() / 60 + (end.getDate() !== start.getDate() ? 24 : 0);
|
|
174
|
+
const jsDay = start.getDay();
|
|
175
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
176
|
+
for (const slot of blockedSlots) {
|
|
177
|
+
if (slot.day && slot.day !== isoDay) continue;
|
|
178
|
+
if (startH < slot.end && endH > slot.start) return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if ((mode === "move" || mode === "resize-start" || mode === "resize-end") && payload.eventId) {
|
|
182
|
+
try {
|
|
183
|
+
await store.move(payload.eventId, start, end);
|
|
184
|
+
const ev = store.byId(payload.eventId);
|
|
185
|
+
if (ev) effectiveMove?.(ev, start, end);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
const msg = e instanceof Error ? e.message : "";
|
|
188
|
+
if (msg.includes("read-only")) {
|
|
189
|
+
const ev = store.byId(payload.eventId);
|
|
190
|
+
if (ev) effectiveMove?.(ev, start, end);
|
|
191
|
+
} else if (!msg.includes("not found")) {
|
|
192
|
+
if (onerror) onerror(e instanceof Error ? e : new Error(String(e)));
|
|
193
|
+
else console.warn("[calendar] drag commit failed:", e);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else if (mode === "create") {
|
|
197
|
+
effectiveCreate?.({ start, end });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
let viewLoadRange = $state(null);
|
|
201
|
+
setContext("calendar", {
|
|
202
|
+
// Engine objects (hold $state internally)
|
|
203
|
+
get store() {
|
|
204
|
+
return store;
|
|
205
|
+
},
|
|
206
|
+
viewState,
|
|
207
|
+
selection,
|
|
208
|
+
drag,
|
|
209
|
+
commitDrag,
|
|
210
|
+
// Callbacks
|
|
211
|
+
get oneventclick() {
|
|
212
|
+
return handleEventClick;
|
|
213
|
+
},
|
|
214
|
+
get oneventcreate() {
|
|
215
|
+
return effectiveCreate;
|
|
216
|
+
},
|
|
217
|
+
get oneventmove() {
|
|
218
|
+
return effectiveMove;
|
|
219
|
+
},
|
|
220
|
+
get oneventhover() {
|
|
221
|
+
return oneventhover;
|
|
222
|
+
},
|
|
223
|
+
get ondayclick() {
|
|
224
|
+
return ondayclick;
|
|
225
|
+
},
|
|
226
|
+
// Config (reactive via getters)
|
|
227
|
+
get readOnly() {
|
|
228
|
+
return readOnly;
|
|
229
|
+
},
|
|
230
|
+
get visibleHours() {
|
|
231
|
+
return visibleHours;
|
|
232
|
+
},
|
|
233
|
+
get snapInterval() {
|
|
234
|
+
return snapInterval;
|
|
235
|
+
},
|
|
236
|
+
get eventSnippet() {
|
|
237
|
+
return eventSnippet;
|
|
238
|
+
},
|
|
239
|
+
get emptySnippet() {
|
|
240
|
+
return emptySnippet;
|
|
241
|
+
},
|
|
242
|
+
get equalDays() {
|
|
243
|
+
return equalDays;
|
|
244
|
+
},
|
|
245
|
+
get showDates() {
|
|
246
|
+
return showDates;
|
|
247
|
+
},
|
|
248
|
+
get hideDays() {
|
|
249
|
+
return hideDays;
|
|
250
|
+
},
|
|
251
|
+
get blockedSlots() {
|
|
252
|
+
return blockedSlots;
|
|
253
|
+
},
|
|
254
|
+
get dayHeaderSnippet() {
|
|
255
|
+
return dayHeaderSnippet;
|
|
256
|
+
},
|
|
257
|
+
get minDuration() {
|
|
258
|
+
return minDuration;
|
|
259
|
+
},
|
|
260
|
+
get maxDuration() {
|
|
261
|
+
return maxDuration;
|
|
262
|
+
},
|
|
263
|
+
get disabledDates() {
|
|
264
|
+
return disabledDates;
|
|
265
|
+
},
|
|
266
|
+
get mobile() {
|
|
267
|
+
return useMobile;
|
|
268
|
+
},
|
|
269
|
+
get autoHeight() {
|
|
270
|
+
return heightProp === "auto";
|
|
271
|
+
},
|
|
272
|
+
get compact() {
|
|
273
|
+
return compact;
|
|
274
|
+
},
|
|
275
|
+
// Load range (read/write)
|
|
276
|
+
get loadRange() {
|
|
277
|
+
return viewLoadRange;
|
|
278
|
+
},
|
|
279
|
+
setLoadRange(range) {
|
|
280
|
+
viewLoadRange = range;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
$effect(() => {
|
|
284
|
+
const range = viewLoadRange ?? viewState.range;
|
|
285
|
+
store.load({ start: range.start, end: range.end });
|
|
286
|
+
});
|
|
287
|
+
untrack(() => store.load({ start: viewState.range.start, end: viewState.range.end }));
|
|
288
|
+
$effect(() => {
|
|
289
|
+
if (activeViewId) viewState.setView(activeViewId);
|
|
290
|
+
});
|
|
291
|
+
$effect(() => {
|
|
292
|
+
if (currentDate) viewState.setFocusDate(currentDate);
|
|
293
|
+
});
|
|
294
|
+
$effect(() => {
|
|
295
|
+
if (days !== void 0 && viewState.dayCount !== days) viewState.setDayCount(days);
|
|
296
|
+
});
|
|
297
|
+
$effect(() => {
|
|
298
|
+
const d = viewState.focusDate;
|
|
299
|
+
ondatechange?.(d);
|
|
300
|
+
});
|
|
301
|
+
$effect(() => {
|
|
302
|
+
if (viewState.mondayStart !== mondayStart) {
|
|
303
|
+
viewState.setMondayStart(mondayStart);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
$effect(() => {
|
|
307
|
+
onviewchange?.(viewState.view);
|
|
308
|
+
});
|
|
309
|
+
$effect(() => {
|
|
310
|
+
if (store.error && onerror) onerror(new Error(store.error));
|
|
311
|
+
});
|
|
312
|
+
const resolvedView = $derived.by(() => {
|
|
313
|
+
const requested = views.find((v) => v.id === viewState.view) ?? views[0];
|
|
314
|
+
if (!useMobile || !requested) return requested;
|
|
315
|
+
if (requested.id.endsWith("-mobile")) return requested;
|
|
316
|
+
if (requested.label === "Agenda") return requested;
|
|
317
|
+
const mobileVariant = views.find(
|
|
318
|
+
(v) => v.id === `${requested.mode}-mobile`
|
|
319
|
+
);
|
|
320
|
+
return mobileVariant ?? requested;
|
|
321
|
+
});
|
|
322
|
+
const activeView = $derived(resolvedView);
|
|
323
|
+
const desktopViews = $derived(views.filter((v) => !v.id.endsWith("-mobile")));
|
|
324
|
+
const dateLabel = $derived.by(() => {
|
|
325
|
+
if (!showDates) {
|
|
326
|
+
return "";
|
|
327
|
+
}
|
|
328
|
+
if (viewState.mode === "day") {
|
|
329
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
330
|
+
weekday: "long",
|
|
331
|
+
month: "short",
|
|
332
|
+
day: "numeric"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
336
|
+
month: "long",
|
|
337
|
+
year: "numeric"
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
const modes = $derived.by(() => {
|
|
341
|
+
const g = new Set(desktopViews.map((v) => v.mode));
|
|
342
|
+
return ["day", "week", "month"].filter((key) => g.has(key));
|
|
343
|
+
});
|
|
344
|
+
const L = $derived(getLabels());
|
|
345
|
+
function switchMode(g) {
|
|
346
|
+
const currentLabel = desktopViews.find((v) => v.id === viewState.view)?.label ?? activeView?.label;
|
|
347
|
+
const match = desktopViews.find((v) => v.mode === g && v.label === currentLabel);
|
|
348
|
+
const fallback = desktopViews.find((v) => v.mode === g);
|
|
349
|
+
const target = match ?? fallback;
|
|
350
|
+
if (target) viewState.setView(target.id);
|
|
351
|
+
}
|
|
352
|
+
const viewIncludesToday = $derived.by(() => {
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
const { start, end } = viewState.range;
|
|
355
|
+
return now >= start.getTime() && now < end.getTime();
|
|
356
|
+
});
|
|
357
|
+
const headerCtx = $derived({
|
|
358
|
+
dateLabel,
|
|
359
|
+
mode: viewState.mode,
|
|
360
|
+
modes,
|
|
361
|
+
switchMode,
|
|
362
|
+
prev: () => viewState.prev(),
|
|
363
|
+
next: () => viewState.next(),
|
|
364
|
+
goToday: () => viewState.goToday(),
|
|
365
|
+
isViewOnToday: viewIncludesToday,
|
|
366
|
+
focusDate: viewState.focusDate
|
|
367
|
+
});
|
|
368
|
+
const navCtx = $derived({
|
|
369
|
+
prev: () => viewState.prev(),
|
|
370
|
+
next: () => viewState.next(),
|
|
371
|
+
goToday: () => viewState.goToday(),
|
|
372
|
+
isViewOnToday: viewIncludesToday,
|
|
373
|
+
focusDate: viewState.focusDate,
|
|
374
|
+
mode: viewState.mode
|
|
375
|
+
});
|
|
509
376
|
</script>
|
|
510
377
|
|
|
511
378
|
<div
|
|
@@ -523,13 +390,13 @@
|
|
|
523
390
|
{@render headerSnippet(headerCtx)}
|
|
524
391
|
|
|
525
392
|
<!-- ─── Mobile header (flow layout, no absolute) ─── -->
|
|
526
|
-
{:else if useMobile}
|
|
393
|
+
{:else if useMobile && (showNavigation || (showModePills && modes.length > 1) || dateLabel)}
|
|
527
394
|
<div class="cal-m-hd">
|
|
528
395
|
<div class="cal-m-left">
|
|
529
396
|
{#if navigationSnippet}
|
|
530
397
|
{@render navigationSnippet(navCtx)}
|
|
531
398
|
{:else if showNavigation}
|
|
532
|
-
<button class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : L.previousWeek}>
|
|
399
|
+
<button class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : viewState.mode === 'month' ? L.previousMonth : L.previousWeek}>
|
|
533
400
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
|
|
534
401
|
</button>
|
|
535
402
|
{/if}
|
|
@@ -543,7 +410,7 @@
|
|
|
543
410
|
aria-pressed={viewState.mode === g}
|
|
544
411
|
onclick={() => switchMode(g)}
|
|
545
412
|
>
|
|
546
|
-
{g === 'day' ? L.day : L.week}
|
|
413
|
+
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
547
414
|
</button>
|
|
548
415
|
{/each}
|
|
549
416
|
</div>
|
|
@@ -554,7 +421,7 @@
|
|
|
554
421
|
|
|
555
422
|
<div class="cal-m-right">
|
|
556
423
|
{#if !navigationSnippet && showNavigation}
|
|
557
|
-
<button class="cal-m-nav" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : L.nextWeek}>
|
|
424
|
+
<button class="cal-m-nav" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : viewState.mode === 'month' ? L.nextMonth : L.nextWeek}>
|
|
558
425
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
|
|
559
426
|
</button>
|
|
560
427
|
{/if}
|
|
@@ -582,10 +449,10 @@
|
|
|
582
449
|
{L.today}
|
|
583
450
|
</button>
|
|
584
451
|
{/if}
|
|
585
|
-
<button class="cal-hd-btn" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : L.previousWeek}>
|
|
452
|
+
<button class="cal-hd-btn" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : viewState.mode === 'month' ? L.previousMonth : L.previousWeek}>
|
|
586
453
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
|
|
587
454
|
</button>
|
|
588
|
-
<button class="cal-hd-btn" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : L.nextWeek}>
|
|
455
|
+
<button class="cal-hd-btn" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : viewState.mode === 'month' ? L.nextMonth : L.nextWeek}>
|
|
589
456
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
|
|
590
457
|
</button>
|
|
591
458
|
{/if}
|
|
@@ -601,7 +468,7 @@
|
|
|
601
468
|
aria-pressed={viewState.mode === g}
|
|
602
469
|
onclick={() => switchMode(g)}
|
|
603
470
|
>
|
|
604
|
-
{g === 'day' ? L.day : L.week}
|
|
471
|
+
{g === 'day' ? L.day : g === 'week' ? L.week : L.month}
|
|
605
472
|
</button>
|
|
606
473
|
{/each}
|
|
607
474
|
</div>
|
|
@@ -621,7 +488,7 @@
|
|
|
621
488
|
mondayStart={viewState.mondayStart}
|
|
622
489
|
{locale}
|
|
623
490
|
focusDate={viewState.focusDate}
|
|
624
|
-
oneventclick={
|
|
491
|
+
oneventclick={handleEventClick}
|
|
625
492
|
oneventcreate={effectiveCreate}
|
|
626
493
|
readOnly={readOnly}
|
|
627
494
|
visibleHours={visibleHours}
|