@nomideusz/svelte-calendar 0.8.0 → 0.10.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.
Files changed (37) hide show
  1. package/README.md +29 -8
  2. package/dist/calendar/Calendar.svelte +187 -64
  3. package/dist/calendar/Calendar.svelte.d.ts +14 -0
  4. package/dist/core/clock.svelte.d.ts +1 -1
  5. package/dist/core/clock.svelte.js +8 -4
  6. package/dist/core/locale.d.ts +15 -1
  7. package/dist/core/locale.js +9 -2
  8. package/dist/core/timezone.d.ts +12 -0
  9. package/dist/core/timezone.js +31 -0
  10. package/dist/headless/create-calendar.svelte.js +12 -6
  11. package/dist/headless/types.d.ts +2 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/theme/auto.js +27 -6
  15. package/dist/theme/presets.d.ts +4 -4
  16. package/dist/theme/presets.js +11 -7
  17. package/dist/views/agenda/AgendaDay.svelte +260 -111
  18. package/dist/views/agenda/AgendaWeek.svelte +191 -66
  19. package/dist/views/mobile/MobileDay.svelte +409 -214
  20. package/dist/views/mobile/MobileWeek.svelte +137 -75
  21. package/dist/views/mobile/swipe.d.ts +28 -0
  22. package/dist/views/mobile/swipe.js +64 -0
  23. package/dist/views/month/MonthGrid.svelte +167 -31
  24. package/dist/views/planner/PlannerDay.svelte +156 -81
  25. package/dist/views/planner/PlannerWeek.svelte +1156 -631
  26. package/dist/views/planner/PlannerWeek.svelte.d.ts +2 -0
  27. package/dist/views/shared/context.svelte.d.ts +4 -0
  28. package/dist/views/shared/context.svelte.js +14 -9
  29. package/dist/views/shared/format.d.ts +2 -1
  30. package/dist/views/shared/format.js +8 -5
  31. package/dist/widget/CalendarWidget.svelte +33 -5
  32. package/dist/widget/CalendarWidget.svelte.d.ts +16 -2
  33. package/dist/widget/widget.d.ts +9 -0
  34. package/dist/widget/widget.js +59 -13
  35. package/package.json +1 -1
  36. package/widget/widget.js +3830 -2755
  37. package/widget/svelte-calendar.css +0 -3054
@@ -10,8 +10,8 @@ import EventContent from "../shared/EventContent.svelte";
10
10
  import { createClock } from "../../core/clock.svelte.js";
11
11
  import { DAY_MS, sod, isAllDay, isMultiDay } from "../../core/time.js";
12
12
  import { startOfWeek as sowFn } from "../../core/time.js";
13
- import { fmtTime as _fmtTime, weekdayShort, getLabels } from "../../core/locale.js";
14
- const L = $derived(getLabels());
13
+ import { fmtTime as _fmtTime, weekdayShort } from "../../core/locale.js";
14
+ import { createSwipe } from "./swipe.js";
15
15
  let {
16
16
  mondayStart = true,
17
17
  locale,
@@ -20,11 +20,10 @@ let {
20
20
  style = "",
21
21
  focusDate,
22
22
  oneventclick,
23
- oneventcreate,
24
- selectedEventId = null,
25
- readOnly = false
23
+ selectedEventId = null
26
24
  } = $props();
27
25
  const ctx = useCalendarContext();
26
+ const L = $derived(ctx.labels);
28
27
  const viewState = $derived(ctx.viewState);
29
28
  const equalDays = $derived(ctx.equalDays);
30
29
  const showDates = $derived(ctx.showDates);
@@ -33,8 +32,7 @@ const autoHeight = $derived(ctx.autoHeight);
33
32
  const oneventhover = $derived(ctx.oneventhover);
34
33
  const disabledSet = $derived(ctx.disabledSet);
35
34
  const loadRangeCtx = $derived(ctx.loadRange);
36
- const minDuration = $derived(ctx.minDuration);
37
- const clock = createClock();
35
+ const clock = createClock(ctx.timezone);
38
36
  const MAX_EVENTS = 3;
39
37
  const customDays = $derived(viewState?.dayCount ?? 7);
40
38
  const todayMs = $derived(clock.today);
@@ -80,54 +78,53 @@ const dayCells = $derived.by(() => {
80
78
  }
81
79
  return result;
82
80
  });
81
+ let expandedDays = $state(/* @__PURE__ */ new Set());
82
+ function toggleExpand(ms) {
83
+ const next = new Set(expandedDays);
84
+ if (next.has(ms)) next.delete(ms);
85
+ else next.add(ms);
86
+ expandedDays = next;
87
+ }
83
88
  function fmtTime(d) {
84
89
  return _fmtTime(d, locale);
85
90
  }
86
- let touchStartX = 0;
87
- let touchStartY = 0;
88
- let swiping = false;
89
- let swipeOffset = $state(0);
90
- const SWIPE_THRESHOLD = 60;
91
- function onTouchStart(e) {
92
- const t = e.touches[0];
93
- touchStartX = t.clientX;
94
- touchStartY = t.clientY;
95
- swiping = true;
96
- swipeOffset = 0;
91
+ function evTimeLabel(ev) {
92
+ if (isAllDay(ev) || isMultiDay(ev)) return L.allDay;
93
+ return `${fmtTime(ev.start)} \u2013 ${fmtTime(ev.end)}`;
97
94
  }
98
- function onTouchMove(e) {
99
- if (!swiping) return;
100
- const t = e.touches[0];
101
- const dx = t.clientX - touchStartX;
102
- const dy = t.clientY - touchStartY;
103
- if (Math.abs(dy) > Math.abs(dx) * 0.8) {
104
- swiping = false;
105
- return;
106
- }
107
- swipeOffset = dx;
95
+ function statusText(ev) {
96
+ if (ev.status === "cancelled") return ` (${L.cancelled})`;
97
+ if (ev.status === "tentative") return ` (${L.tentative})`;
98
+ if (ev.status === "full") return ` (${L.full})`;
99
+ if (ev.status === "limited") return ` (${L.limited})`;
100
+ return "";
108
101
  }
109
- function onTouchEnd() {
110
- if (!swiping) {
111
- swipeOffset = 0;
112
- return;
113
- }
114
- if (Math.abs(swipeOffset) > SWIPE_THRESHOLD) {
115
- if (swipeOffset > 0) {
116
- viewState?.prev();
102
+ let swipeOffset = $state(0);
103
+ let swipeAnimate = $state(false);
104
+ const swipe = createSwipe({
105
+ onmove: (dx) => {
106
+ swipeAnimate = false;
107
+ swipeOffset = dx;
108
+ },
109
+ onend: (dir) => {
110
+ if (dir !== 0) {
111
+ swipeAnimate = false;
112
+ swipeOffset = 0;
113
+ if (dir > 0) viewState?.prev();
114
+ else viewState?.next();
117
115
  } else {
118
- viewState?.next();
116
+ swipeAnimate = true;
117
+ swipeOffset = 0;
119
118
  }
120
119
  }
121
- swipeOffset = 0;
122
- swiping = false;
123
- }
120
+ });
124
121
  function handleDayTap(dayMs) {
125
- if (viewState) {
126
- viewState.setFocusDate(new Date(dayMs));
127
- const currentView = viewState.view;
128
- const dayView = currentView.replace("week", "day");
129
- viewState.setView(dayView);
130
- }
122
+ if (!viewState) return;
123
+ viewState.setFocusDate(new Date(dayMs));
124
+ const currentView = viewState.view;
125
+ if (!currentView.split("-").includes("week")) return;
126
+ const dayView = currentView.split("-").map((seg) => seg === "week" ? "day" : seg).join("-");
127
+ if (dayView !== currentView) viewState.setView(dayView);
131
128
  }
132
129
  function handleDayKeydown(e, dayMs) {
133
130
  if (e.key !== "Enter" && e.key !== " ") return;
@@ -143,12 +140,17 @@ function handleDayKeydown(e, dayMs) {
143
140
  style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}
144
141
  role="region"
145
142
  aria-label={L.weekAhead}
146
- ontouchstart={onTouchStart}
147
- ontouchmove={onTouchMove}
148
- ontouchend={onTouchEnd}
143
+ ontouchstart={swipe.ontouchstart}
144
+ ontouchmove={swipe.ontouchmove}
145
+ ontouchend={swipe.ontouchend}
149
146
  >
150
147
  <!-- Vertical day list -->
151
- <div class="mw-list" role="list">
148
+ <div
149
+ class="mw-list"
150
+ class:mw-list--animate={swipeAnimate}
151
+ style:transform={swipeOffset !== 0 ? `translateX(${swipeOffset}px)` : undefined}
152
+ role="list"
153
+ >
152
154
  {#each dayCells as cell (cell.ms)}
153
155
  <div
154
156
  class="mw-row"
@@ -157,13 +159,15 @@ function handleDayKeydown(e, dayMs) {
157
159
  class:mw-row--weekend={cell.isWeekend}
158
160
  class:mw-row--disabled={cell.isDisabled}
159
161
  role="listitem"
162
+ aria-current={cell.isToday ? 'date' : undefined}
160
163
  >
161
164
  <button
165
+ type="button"
162
166
  class="mw-row-target"
163
167
  disabled={cell.isDisabled}
164
168
  onclick={() => handleDayTap(cell.ms)}
165
169
  onkeydown={(e) => handleDayKeydown(e, cell.ms)}
166
- aria-label="{cell.dayName} {cell.dayNum}"
170
+ aria-label="{cell.dayName} {cell.dayNum}{cell.isToday ? `, ${L.today}` : ''}"
167
171
  ></button>
168
172
  <!-- Date column -->
169
173
  <div class="mw-date">
@@ -178,7 +182,7 @@ function handleDayKeydown(e, dayMs) {
178
182
  {#if cell.events.length === 0}
179
183
  <span class="mw-empty">{L.noEvents}</span>
180
184
  {:else}
181
- {#each cell.events.slice(0, MAX_EVENTS) as ev (ev.id)}
185
+ {#each (expandedDays.has(cell.ms) ? cell.events : cell.events.slice(0, MAX_EVENTS)) as ev (ev.id)}
182
186
  <button
183
187
  type="button"
184
188
  class="mw-ev"
@@ -193,6 +197,7 @@ function handleDayKeydown(e, dayMs) {
193
197
  onclick={(e) => { e.stopPropagation(); oneventclick?.(ev); }}
194
198
  onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
195
199
  onpointerenter={() => oneventhover?.(ev)}
200
+ aria-label="{ev.title}{statusText(ev)}, {evTimeLabel(ev)}"
196
201
  >
197
202
  <span class="mw-ev-stripe"></span>
198
203
  <div class="mw-ev-body">
@@ -208,7 +213,14 @@ function handleDayKeydown(e, dayMs) {
208
213
  </button>
209
214
  {/each}
210
215
  {#if cell.totalCount > MAX_EVENTS}
211
- <span class="mw-ev-more">{L.nMore(cell.totalCount - MAX_EVENTS)}</span>
216
+ <button
217
+ type="button"
218
+ class="mw-ev-more"
219
+ aria-expanded={expandedDays.has(cell.ms)}
220
+ onclick={() => toggleExpand(cell.ms)}
221
+ >
222
+ {expandedDays.has(cell.ms) ? L.showLess : L.nMore(cell.totalCount - MAX_EVENTS)}
223
+ </button>
212
224
  {/if}
213
225
  {/if}
214
226
  </div>
@@ -231,6 +243,7 @@ function handleDayKeydown(e, dayMs) {
231
243
  overflow: hidden;
232
244
  background: var(--dt-bg, #fff);
233
245
  -webkit-tap-highlight-color: transparent;
246
+ touch-action: pan-y;
234
247
  }
235
248
  .mw--auto { overflow: visible; }
236
249
 
@@ -238,11 +251,18 @@ function handleDayKeydown(e, dayMs) {
238
251
  .mw-list {
239
252
  flex: 1;
240
253
  overflow-y: auto;
254
+ overscroll-behavior: contain;
241
255
  -webkit-overflow-scrolling: touch;
242
256
  scrollbar-width: thin;
243
257
  scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
244
258
  }
245
259
  .mw--auto .mw-list { overflow-y: visible; }
260
+ .mw-list--animate {
261
+ transition: transform 180ms ease;
262
+ }
263
+ @media (prefers-reduced-motion: reduce) {
264
+ .mw-list--animate { transition: none; }
265
+ }
246
266
 
247
267
  /* ─── Day row ────────────────────────────────────── */
248
268
  .mw-row {
@@ -268,8 +288,13 @@ function handleDayKeydown(e, dayMs) {
268
288
  .mw-row--today {
269
289
  background: color-mix(in srgb, var(--dt-accent, #2563eb) 4%, transparent);
270
290
  }
291
+ /* Token-based dim (not subtree opacity) so past rows stay legible/tappable */
271
292
  .mw-row--past {
272
- opacity: 0.5;
293
+ background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 3%, transparent);
294
+ }
295
+ .mw-row--past :global(.mw-ev-title),
296
+ .mw-row--past .mw-day-num {
297
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
273
298
  }
274
299
  .mw-row--disabled {
275
300
  background-image: repeating-linear-gradient(
@@ -294,8 +319,8 @@ function handleDayKeydown(e, dayMs) {
294
319
  cursor: default;
295
320
  }
296
321
  .mw-row-target:focus-visible {
297
- outline: 2px solid var(--dt-accent, #2563eb);
298
- outline-offset: -2px;
322
+ outline: none;
323
+ box-shadow: inset 0 0 0 2px var(--dt-accent, #2563eb);
299
324
  }
300
325
 
301
326
  /* ─── Date column ────────────────────────────────── */
@@ -312,10 +337,10 @@ function handleDayKeydown(e, dayMs) {
312
337
  }
313
338
 
314
339
  .mw-day-name {
315
- font: 600 10px/1 var(--dt-sans, system-ui, sans-serif);
340
+ font: 600 11px/1 var(--dt-sans, system-ui, sans-serif);
316
341
  letter-spacing: 0.06em;
317
342
  text-transform: uppercase;
318
- color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
343
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
319
344
  }
320
345
  .mw-day-name--today {
321
346
  color: var(--dt-accent, #2563eb);
@@ -338,6 +363,8 @@ function handleDayKeydown(e, dayMs) {
338
363
  }
339
364
 
340
365
  /* ─── Events column ──────────────────────────────── */
366
+ /* pointer-events pass through to the full-row target underneath;
367
+ only the chips (and "+N more") re-capture them. */
341
368
  .mw-events {
342
369
  flex: 1;
343
370
  min-width: 0;
@@ -346,12 +373,12 @@ function handleDayKeydown(e, dayMs) {
346
373
  gap: 4px;
347
374
  position: relative;
348
375
  z-index: 2;
376
+ pointer-events: none;
349
377
  }
350
378
 
351
379
  .mw-empty {
352
- font: 400 12px/1 var(--dt-sans, system-ui, sans-serif);
353
- color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
354
- font-style: italic;
380
+ font: 400 13px/1 var(--dt-sans, system-ui, sans-serif);
381
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
355
382
  }
356
383
 
357
384
  /* ─── Event chip ─────────────────────────────────── */
@@ -359,6 +386,7 @@ function handleDayKeydown(e, dayMs) {
359
386
  display: flex;
360
387
  align-items: center;
361
388
  gap: 0;
389
+ min-height: 44px;
362
390
  border-radius: 6px;
363
391
  background: color-mix(in srgb, var(--ev-color) 10%, var(--dt-surface, #f9fafb));
364
392
  overflow: hidden;
@@ -368,6 +396,7 @@ function handleDayKeydown(e, dayMs) {
368
396
  border: none;
369
397
  text-align: left;
370
398
  padding: 0;
399
+ pointer-events: auto;
371
400
  }
372
401
  .mw-ev:active {
373
402
  background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #f9fafb));
@@ -381,22 +410,32 @@ function handleDayKeydown(e, dayMs) {
381
410
  .mw-ev--allday {
382
411
  background: color-mix(in srgb, var(--ev-color) 14%, var(--dt-surface, #f9fafb));
383
412
  }
413
+ /* Status treatments: token-level dims + a non-opacity signal
414
+ (strikethrough / border style) — never a bare opacity on the chip. */
384
415
  .mw-ev--cancelled {
385
- opacity: 0.5;
416
+ background: color-mix(in srgb, var(--ev-color) 5%, var(--dt-surface, #f9fafb));
386
417
  }
387
418
  .mw-ev--cancelled .mw-ev-title {
388
419
  text-decoration: line-through;
420
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
421
+ }
422
+ .mw-ev--cancelled .mw-ev-stripe {
423
+ opacity: 0.45; /* decorative bar only */
389
424
  }
390
425
  .mw-ev--tentative {
391
- opacity: 0.65;
392
- border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
426
+ background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
427
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
393
428
  }
394
429
  .mw-ev--full {
395
- opacity: 0.55;
430
+ background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #f9fafb));
431
+ border: 1px solid color-mix(in srgb, var(--ev-color) 30%, transparent);
432
+ }
433
+ .mw-ev--full .mw-ev-title {
434
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
396
435
  }
397
436
  .mw-ev--limited {
398
- opacity: 0.65;
399
- border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
437
+ background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
438
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 45%, transparent);
400
439
  }
401
440
 
402
441
  .mw-ev-stripe {
@@ -417,7 +456,7 @@ function handleDayKeydown(e, dayMs) {
417
456
  }
418
457
 
419
458
  .mw-ev-title {
420
- font: 500 12px/1.2 var(--dt-sans, system-ui, sans-serif);
459
+ font: 500 15px/1.2 var(--dt-sans, system-ui, sans-serif);
421
460
  color: var(--dt-text, rgba(0, 0, 0, 0.87));
422
461
  white-space: nowrap;
423
462
  overflow: hidden;
@@ -427,16 +466,39 @@ function handleDayKeydown(e, dayMs) {
427
466
  }
428
467
 
429
468
  .mw-ev-time {
430
- font: 400 11px/1 var(--dt-mono, ui-monospace, monospace);
431
- color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
469
+ font: 400 12px/1 var(--dt-mono, ui-monospace, monospace);
470
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
432
471
  white-space: nowrap;
433
472
  flex-shrink: 0;
434
473
  }
435
474
 
436
475
  .mw-ev-more {
437
- font: 400 11px/1 var(--dt-sans, system-ui, sans-serif);
438
- color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
439
- padding: 2px 0;
476
+ font: 500 12px/1 var(--dt-sans, system-ui, sans-serif);
477
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
478
+ padding: 2px 4px;
479
+ min-height: 32px;
480
+ border: none;
481
+ background: transparent;
482
+ cursor: pointer;
483
+ text-align: left;
484
+ align-self: flex-start;
485
+ position: relative;
486
+ pointer-events: auto;
487
+ -webkit-tap-highlight-color: transparent;
488
+ }
489
+ /* Hit-slop: 44px effective touch target */
490
+ .mw-ev-more::before {
491
+ content: '';
492
+ position: absolute;
493
+ left: 0;
494
+ right: 0;
495
+ top: 50%;
496
+ transform: translateY(-50%);
497
+ height: 44px;
498
+ }
499
+ .mw-ev-more:focus-visible {
500
+ outline: none;
501
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
440
502
  }
441
503
 
442
504
  /* ─── Chevron ────────────────────────────────────── */
@@ -450,7 +512,7 @@ function handleDayKeydown(e, dayMs) {
450
512
 
451
513
  /* ─── Focus ──────────────────────────────────────── */
452
514
  .mw-ev:focus-visible {
453
- outline: 2px solid var(--ev-color, var(--dt-accent));
454
- outline-offset: 1px;
515
+ outline: none;
516
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
455
517
  }
456
518
  </style>
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared horizontal-swipe tracker for the mobile views.
3
+ *
4
+ * Tracks touch gestures on a container and reports the live horizontal
5
+ * offset (so the view can follow the finger) plus a commit direction on
6
+ * release. Vertical-dominant movement hands the gesture back to native
7
+ * scrolling immediately.
8
+ */
9
+ /** Unified swipe commit threshold (px) across mobile day + week views. */
10
+ export declare const SWIPE_THRESHOLD = 50;
11
+ export interface SwipeCallbacks {
12
+ /** Live horizontal offset (px) while the finger is down. Reset to 0 when the gesture is abandoned. */
13
+ onmove: (dx: number) => void;
14
+ /**
15
+ * Gesture finished.
16
+ * `dir` is `1` (swiped right → previous period), `-1` (swiped left → next period),
17
+ * or `0` (below threshold — snap back).
18
+ */
19
+ onend: (dir: -1 | 0 | 1) => void;
20
+ /** Return true to ignore the gesture entirely (e.g. while a drag-create/resize is active). */
21
+ disabled?: () => boolean;
22
+ }
23
+ export interface SwipeHandlers {
24
+ ontouchstart: (e: TouchEvent) => void;
25
+ ontouchmove: (e: TouchEvent) => void;
26
+ ontouchend: () => void;
27
+ }
28
+ export declare function createSwipe(cb: SwipeCallbacks): SwipeHandlers;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared horizontal-swipe tracker for the mobile views.
3
+ *
4
+ * Tracks touch gestures on a container and reports the live horizontal
5
+ * offset (so the view can follow the finger) plus a commit direction on
6
+ * release. Vertical-dominant movement hands the gesture back to native
7
+ * scrolling immediately.
8
+ */
9
+ /** Unified swipe commit threshold (px) across mobile day + week views. */
10
+ export const SWIPE_THRESHOLD = 50;
11
+ export function createSwipe(cb) {
12
+ let startX = 0;
13
+ let startY = 0;
14
+ let tracking = false;
15
+ let dx = 0;
16
+ function abandon() {
17
+ tracking = false;
18
+ if (dx !== 0) {
19
+ dx = 0;
20
+ cb.onmove(0);
21
+ }
22
+ }
23
+ return {
24
+ ontouchstart(e) {
25
+ if (cb.disabled?.()) {
26
+ tracking = false;
27
+ return;
28
+ }
29
+ const t = e.touches[0];
30
+ startX = t.clientX;
31
+ startY = t.clientY;
32
+ tracking = true;
33
+ dx = 0;
34
+ },
35
+ ontouchmove(e) {
36
+ if (!tracking)
37
+ return;
38
+ if (cb.disabled?.()) {
39
+ abandon();
40
+ return;
41
+ }
42
+ const t = e.touches[0];
43
+ const mx = t.clientX - startX;
44
+ const my = t.clientY - startY;
45
+ // Vertical movement dominates → this is a scroll, not a swipe.
46
+ if (Math.abs(my) > Math.abs(mx) * 0.8) {
47
+ abandon();
48
+ return;
49
+ }
50
+ dx = mx;
51
+ cb.onmove(dx);
52
+ },
53
+ ontouchend() {
54
+ if (!tracking) {
55
+ cb.onend(0);
56
+ return;
57
+ }
58
+ tracking = false;
59
+ const dir = Math.abs(dx) > SWIPE_THRESHOLD ? (dx > 0 ? 1 : -1) : 0;
60
+ dx = 0;
61
+ cb.onend(dir);
62
+ },
63
+ };
64
+ }