@mintplayer/scheduler-wc 1.0.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.
@@ -0,0 +1,982 @@
1
+ import { dateService, generateEventId, } from '@mintplayer/scheduler-core';
2
+ import { SchedulerStateManager } from '../state/scheduler-state';
3
+ import { YearView } from '../views/year-view';
4
+ import { MonthView } from '../views/month-view';
5
+ import { WeekView } from '../views/week-view';
6
+ import { DayView } from '../views/day-view';
7
+ import { TimelineView } from '../views/timeline-view';
8
+ import { schedulerStyles } from '../styles/scheduler.styles';
9
+ /**
10
+ * MpScheduler Web Component
11
+ *
12
+ * A fully-featured scheduler/calendar component
13
+ */
14
+ export class MpScheduler extends HTMLElement {
15
+ constructor() {
16
+ super();
17
+ this.currentView = null;
18
+ this.contentContainer = null;
19
+ // Track previous state for change detection
20
+ this.previousView = null;
21
+ this.previousDate = null;
22
+ this.previousSelectedEventId = null;
23
+ // Pending drag state (before actual drag starts)
24
+ this.pendingDrag = null;
25
+ this.DRAG_THRESHOLD = 5; // pixels before drag starts
26
+ // Touch state
27
+ this.touchHoldTimer = null;
28
+ this.touchStartPosition = null;
29
+ this.isTouchDragMode = false;
30
+ this.touchHoldTarget = null;
31
+ this.TOUCH_HOLD_DURATION = 500; // ms before touch drag activates
32
+ this.TOUCH_MOVE_THRESHOLD = 10; // pixels of movement before canceling hold
33
+ this.shadow = this.attachShadow({ mode: 'open' });
34
+ this.stateManager = new SchedulerStateManager();
35
+ // Bind event handlers
36
+ this.boundHandleMouseDown = this.handleMouseDown.bind(this);
37
+ this.boundHandleMouseMove = this.handleMouseMove.bind(this);
38
+ this.boundHandleMouseUp = this.handleMouseUp.bind(this);
39
+ this.boundHandleClick = this.handleClick.bind(this);
40
+ this.boundHandleDblClick = this.handleDblClick.bind(this);
41
+ this.boundHandleKeyDown = this.handleKeyDown.bind(this);
42
+ this.boundHandleTouchStart = this.handleTouchStart.bind(this);
43
+ this.boundHandleTouchMove = this.handleTouchMove.bind(this);
44
+ this.boundHandleTouchEnd = this.handleTouchEnd.bind(this);
45
+ this.boundHandleTouchCancel = this.handleTouchCancel.bind(this);
46
+ // Subscribe to state changes
47
+ this.stateManager.subscribe((state) => this.onStateChange(state));
48
+ }
49
+ connectedCallback() {
50
+ this.render();
51
+ this.attachEventListeners();
52
+ }
53
+ disconnectedCallback() {
54
+ var _a;
55
+ this.detachEventListeners();
56
+ (_a = this.currentView) === null || _a === void 0 ? void 0 : _a.destroy();
57
+ }
58
+ attributeChangedCallback(name, oldValue, newValue) {
59
+ if (oldValue === newValue)
60
+ return;
61
+ switch (name) {
62
+ case 'view':
63
+ if (newValue && ['year', 'month', 'week', 'day', 'timeline'].includes(newValue)) {
64
+ this.stateManager.setView(newValue);
65
+ }
66
+ break;
67
+ case 'date':
68
+ if (newValue) {
69
+ this.stateManager.setDate(new Date(newValue));
70
+ }
71
+ break;
72
+ case 'locale':
73
+ if (newValue) {
74
+ this.stateManager.setOptions({ locale: newValue });
75
+ }
76
+ break;
77
+ case 'first-day-of-week':
78
+ if (newValue) {
79
+ const day = parseInt(newValue, 10);
80
+ if (day >= 0 && day <= 6) {
81
+ this.stateManager.setOptions({ firstDayOfWeek: day });
82
+ }
83
+ }
84
+ break;
85
+ case 'slot-duration':
86
+ if (newValue) {
87
+ this.stateManager.setOptions({ slotDuration: parseInt(newValue, 10) });
88
+ }
89
+ break;
90
+ case 'time-format':
91
+ if (newValue && (newValue === '12h' || newValue === '24h')) {
92
+ this.stateManager.setOptions({ timeFormat: newValue });
93
+ }
94
+ break;
95
+ case 'editable':
96
+ this.stateManager.setOptions({ editable: newValue !== 'false' });
97
+ break;
98
+ case 'selectable':
99
+ this.stateManager.setOptions({ selectable: newValue !== 'false' });
100
+ break;
101
+ }
102
+ }
103
+ // Public API
104
+ get view() {
105
+ return this.stateManager.getState().view;
106
+ }
107
+ set view(value) {
108
+ this.stateManager.setView(value);
109
+ }
110
+ get date() {
111
+ return this.stateManager.getState().date;
112
+ }
113
+ set date(value) {
114
+ this.stateManager.setDate(value);
115
+ }
116
+ get events() {
117
+ return this.stateManager.getState().events;
118
+ }
119
+ set events(value) {
120
+ this.stateManager.setEvents(value);
121
+ }
122
+ get resources() {
123
+ return this.stateManager.getState().resources;
124
+ }
125
+ set resources(value) {
126
+ this.stateManager.setResources(value);
127
+ }
128
+ get options() {
129
+ return this.stateManager.getState().options;
130
+ }
131
+ set options(value) {
132
+ this.stateManager.setOptions(value);
133
+ }
134
+ get selectedEvent() {
135
+ return this.stateManager.getState().selectedEvent;
136
+ }
137
+ set selectedEvent(value) {
138
+ this.stateManager.setSelectedEvent(value);
139
+ }
140
+ get selectedRange() {
141
+ const state = this.stateManager.getState();
142
+ if (state.previewEvent) {
143
+ return { start: state.previewEvent.start, end: state.previewEvent.end };
144
+ }
145
+ return null;
146
+ }
147
+ next() {
148
+ this.stateManager.next();
149
+ }
150
+ prev() {
151
+ this.stateManager.prev();
152
+ }
153
+ today() {
154
+ this.stateManager.today();
155
+ }
156
+ gotoDate(date) {
157
+ this.stateManager.gotoDate(date);
158
+ }
159
+ changeView(view) {
160
+ this.stateManager.setView(view);
161
+ }
162
+ addEvent(event) {
163
+ this.stateManager.addEvent(event);
164
+ }
165
+ updateEvent(event) {
166
+ this.stateManager.updateEvent(event);
167
+ }
168
+ removeEvent(eventId) {
169
+ this.stateManager.removeEvent(eventId);
170
+ }
171
+ getEventById(eventId) {
172
+ var _a;
173
+ return (_a = this.events.find((e) => e.id === eventId)) !== null && _a !== void 0 ? _a : null;
174
+ }
175
+ refetchEvents() {
176
+ var _a;
177
+ // Trigger re-render
178
+ (_a = this.currentView) === null || _a === void 0 ? void 0 : _a.update(this.stateManager.getState());
179
+ }
180
+ // Private methods
181
+ render() {
182
+ // Add styles
183
+ const style = document.createElement('style');
184
+ style.textContent = schedulerStyles;
185
+ this.shadow.appendChild(style);
186
+ // Create container
187
+ const container = document.createElement('div');
188
+ container.className = 'scheduler-container';
189
+ // Header
190
+ const header = this.createHeader();
191
+ container.appendChild(header);
192
+ // Content
193
+ this.contentContainer = document.createElement('div');
194
+ this.contentContainer.className = 'scheduler-content';
195
+ container.appendChild(this.contentContainer);
196
+ this.shadow.appendChild(container);
197
+ // Initial view render
198
+ this.renderView();
199
+ }
200
+ createHeader() {
201
+ const header = document.createElement('header');
202
+ header.className = 'scheduler-header';
203
+ // Navigation
204
+ const nav = document.createElement('nav');
205
+ nav.className = 'scheduler-nav';
206
+ const prevBtn = document.createElement('button');
207
+ prevBtn.textContent = '‹';
208
+ prevBtn.title = 'Previous';
209
+ prevBtn.addEventListener('click', () => this.prev());
210
+ const nextBtn = document.createElement('button');
211
+ nextBtn.textContent = '›';
212
+ nextBtn.title = 'Next';
213
+ nextBtn.addEventListener('click', () => this.next());
214
+ const todayBtn = document.createElement('button');
215
+ todayBtn.textContent = 'Today';
216
+ todayBtn.addEventListener('click', () => this.today());
217
+ nav.appendChild(prevBtn);
218
+ nav.appendChild(nextBtn);
219
+ nav.appendChild(todayBtn);
220
+ // Title
221
+ const title = document.createElement('div');
222
+ title.className = 'scheduler-title';
223
+ this.updateTitle(title);
224
+ // View switcher
225
+ const viewSwitcher = document.createElement('div');
226
+ viewSwitcher.className = 'scheduler-view-switcher';
227
+ const views = [
228
+ { key: 'year', label: 'Year' },
229
+ { key: 'month', label: 'Month' },
230
+ { key: 'week', label: 'Week' },
231
+ { key: 'day', label: 'Day' },
232
+ { key: 'timeline', label: 'Timeline' },
233
+ ];
234
+ for (const { key, label } of views) {
235
+ const btn = document.createElement('button');
236
+ btn.textContent = label;
237
+ btn.dataset['view'] = key;
238
+ if (key === this.view) {
239
+ btn.classList.add('active');
240
+ }
241
+ btn.addEventListener('click', () => this.changeView(key));
242
+ viewSwitcher.appendChild(btn);
243
+ }
244
+ header.appendChild(nav);
245
+ header.appendChild(title);
246
+ header.appendChild(viewSwitcher);
247
+ return header;
248
+ }
249
+ updateTitle(titleEl) {
250
+ const title = titleEl !== null && titleEl !== void 0 ? titleEl : this.shadow.querySelector('.scheduler-title');
251
+ if (!title)
252
+ return;
253
+ const state = this.stateManager.getState();
254
+ const { date, view, options } = state;
255
+ let titleText = '';
256
+ switch (view) {
257
+ case 'year':
258
+ titleText = date.getFullYear().toString();
259
+ break;
260
+ case 'month':
261
+ titleText = dateService.formatDate(date, options.locale, {
262
+ month: 'long',
263
+ year: 'numeric',
264
+ });
265
+ break;
266
+ case 'week':
267
+ case 'timeline': {
268
+ const weekStart = dateService.getWeekStart(date, options.firstDayOfWeek);
269
+ const weekEnd = dateService.addDays(weekStart, 6);
270
+ titleText = `${dateService.formatDate(weekStart, options.locale, { month: 'short', day: 'numeric' })} - ${dateService.formatDate(weekEnd, options.locale, { month: 'short', day: 'numeric', year: 'numeric' })}`;
271
+ break;
272
+ }
273
+ case 'day':
274
+ titleText = dateService.formatDate(date, options.locale, {
275
+ weekday: 'long',
276
+ month: 'long',
277
+ day: 'numeric',
278
+ year: 'numeric',
279
+ });
280
+ break;
281
+ }
282
+ title.textContent = titleText;
283
+ }
284
+ renderView() {
285
+ var _a, _b;
286
+ if (!this.contentContainer)
287
+ return;
288
+ // Destroy previous view
289
+ (_a = this.currentView) === null || _a === void 0 ? void 0 : _a.destroy();
290
+ const state = this.stateManager.getState();
291
+ // Create new view
292
+ switch (state.view) {
293
+ case 'year':
294
+ this.currentView = new YearView(this.contentContainer, state);
295
+ break;
296
+ case 'month':
297
+ this.currentView = new MonthView(this.contentContainer, state);
298
+ break;
299
+ case 'week':
300
+ this.currentView = new WeekView(this.contentContainer, state);
301
+ break;
302
+ case 'day':
303
+ this.currentView = new DayView(this.contentContainer, state);
304
+ break;
305
+ case 'timeline':
306
+ this.currentView = new TimelineView(this.contentContainer, state);
307
+ break;
308
+ }
309
+ (_b = this.currentView) === null || _b === void 0 ? void 0 : _b.render();
310
+ }
311
+ onStateChange(state) {
312
+ var _a, _b;
313
+ // Detect view/date changes and dispatch events
314
+ const viewChanged = this.previousView !== null && this.previousView !== state.view;
315
+ const dateChanged = this.previousDate !== null && this.previousDate.getTime() !== state.date.getTime();
316
+ const selectedEventId = (_b = (_a = state.selectedEvent) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
317
+ const selectionChanged = this.previousSelectedEventId !== null && this.previousSelectedEventId !== selectedEventId;
318
+ // Dispatch view-change event if view or date changed (but not on initial render)
319
+ if (viewChanged || dateChanged) {
320
+ this.dispatchEvent(new CustomEvent('view-change', {
321
+ detail: { view: state.view, date: state.date },
322
+ bubbles: true,
323
+ }));
324
+ }
325
+ // Dispatch selection-change event if selected event changed (but not on initial render)
326
+ if (selectionChanged) {
327
+ this.dispatchEvent(new CustomEvent('selection-change', {
328
+ detail: { selectedEvent: state.selectedEvent },
329
+ bubbles: true,
330
+ }));
331
+ }
332
+ // Update previous state tracking
333
+ this.previousView = state.view;
334
+ this.previousDate = new Date(state.date);
335
+ this.previousSelectedEventId = selectedEventId;
336
+ // Update title
337
+ this.updateTitle();
338
+ // Update view switcher active state
339
+ const buttons = this.shadow.querySelectorAll('.scheduler-view-switcher button');
340
+ buttons.forEach((btn) => {
341
+ const btnEl = btn;
342
+ btnEl.classList.toggle('active', btnEl.dataset['view'] === state.view);
343
+ });
344
+ // Update or re-render view
345
+ if (this.currentView) {
346
+ // Check if view type changed
347
+ const viewTypeChanged = this.viewTypeChanged(state.view);
348
+ if (viewTypeChanged) {
349
+ this.renderView();
350
+ }
351
+ else {
352
+ this.currentView.update(state);
353
+ }
354
+ }
355
+ }
356
+ viewTypeChanged(newView) {
357
+ if (!this.currentView)
358
+ return true;
359
+ const viewClass = this.currentView.constructor.name.toLowerCase();
360
+ return !viewClass.includes(newView);
361
+ }
362
+ attachEventListeners() {
363
+ const root = this.shadow;
364
+ root.addEventListener('mousedown', this.boundHandleMouseDown);
365
+ document.addEventListener('mousemove', this.boundHandleMouseMove);
366
+ document.addEventListener('mouseup', this.boundHandleMouseUp);
367
+ root.addEventListener('click', this.boundHandleClick);
368
+ root.addEventListener('dblclick', this.boundHandleDblClick);
369
+ this.addEventListener('keydown', this.boundHandleKeyDown);
370
+ // Touch events
371
+ root.addEventListener('touchstart', this.boundHandleTouchStart, { passive: false });
372
+ root.addEventListener('touchmove', this.boundHandleTouchMove, { passive: false });
373
+ root.addEventListener('touchend', this.boundHandleTouchEnd);
374
+ root.addEventListener('touchcancel', this.boundHandleTouchCancel);
375
+ }
376
+ detachEventListeners() {
377
+ const root = this.shadow;
378
+ root.removeEventListener('mousedown', this.boundHandleMouseDown);
379
+ document.removeEventListener('mousemove', this.boundHandleMouseMove);
380
+ document.removeEventListener('mouseup', this.boundHandleMouseUp);
381
+ root.removeEventListener('click', this.boundHandleClick);
382
+ root.removeEventListener('dblclick', this.boundHandleDblClick);
383
+ this.removeEventListener('keydown', this.boundHandleKeyDown);
384
+ // Touch events
385
+ root.removeEventListener('touchstart', this.boundHandleTouchStart);
386
+ root.removeEventListener('touchmove', this.boundHandleTouchMove);
387
+ root.removeEventListener('touchend', this.boundHandleTouchEnd);
388
+ root.removeEventListener('touchcancel', this.boundHandleTouchCancel);
389
+ this.cancelTouchHold();
390
+ }
391
+ handleMouseDown(e) {
392
+ const state = this.stateManager.getState();
393
+ if (!state.options.editable)
394
+ return;
395
+ const target = e.target;
396
+ // Check for resize handle - start drag immediately (no click behavior)
397
+ const resizeHandle = target.closest('.resize-handle');
398
+ if (resizeHandle) {
399
+ const eventEl = resizeHandle.closest('.scheduler-event');
400
+ const eventId = eventEl === null || eventEl === void 0 ? void 0 : eventEl.dataset['eventId'];
401
+ const event = eventId ? this.getEventById(eventId) : null;
402
+ if (event) {
403
+ const handleType = resizeHandle.dataset['handle'];
404
+ this.pendingDrag = {
405
+ type: ('resize-' + handleType),
406
+ event,
407
+ startX: e.clientX,
408
+ startY: e.clientY,
409
+ };
410
+ e.preventDefault();
411
+ return;
412
+ }
413
+ }
414
+ // Check for event - set up pending drag (actual drag starts on mouse move)
415
+ const eventEl = target.closest('.scheduler-event:not(.preview)');
416
+ if (eventEl && !eventEl.classList.contains('preview')) {
417
+ const eventId = eventEl.dataset['eventId'];
418
+ const event = eventId ? this.getEventById(eventId) : null;
419
+ if (event && event.draggable !== false) {
420
+ this.pendingDrag = {
421
+ type: 'move',
422
+ event,
423
+ startX: e.clientX,
424
+ startY: e.clientY,
425
+ };
426
+ e.preventDefault();
427
+ return;
428
+ }
429
+ }
430
+ // Check for slot - set up pending drag for create
431
+ const slotEl = target.closest('.scheduler-time-slot, .scheduler-timeline-slot');
432
+ if (slotEl && state.options.selectable) {
433
+ this.pendingDrag = {
434
+ type: 'create',
435
+ event: null,
436
+ startX: e.clientX,
437
+ startY: e.clientY,
438
+ slotEl,
439
+ };
440
+ e.preventDefault();
441
+ }
442
+ }
443
+ handleMouseMove(e) {
444
+ // Check if we have a pending drag that should start
445
+ if (this.pendingDrag) {
446
+ const dx = e.clientX - this.pendingDrag.startX;
447
+ const dy = e.clientY - this.pendingDrag.startY;
448
+ const distance = Math.sqrt(dx * dx + dy * dy);
449
+ if (distance >= this.DRAG_THRESHOLD) {
450
+ // Start the actual drag
451
+ this.startDrag(this.pendingDrag.type, this.pendingDrag.event, e, this.pendingDrag.slotEl);
452
+ this.pendingDrag = null;
453
+ }
454
+ return;
455
+ }
456
+ const state = this.stateManager.getState();
457
+ if (!state.dragState)
458
+ return;
459
+ const slot = this.getSlotAtPosition(e.clientX, e.clientY);
460
+ if (!slot)
461
+ return;
462
+ const preview = this.calculatePreview(state.dragState.type, state.dragState, slot);
463
+ if (preview) {
464
+ this.stateManager.updateDrag(slot, preview);
465
+ }
466
+ }
467
+ handleMouseUp(e) {
468
+ // If we had a pending drag that never started, it's a click
469
+ if (this.pendingDrag) {
470
+ const { type, event } = this.pendingDrag;
471
+ this.pendingDrag = null;
472
+ // If it was on an event, select it
473
+ if (type === 'move' && event) {
474
+ this.stateManager.setSelectedEvent(event);
475
+ this.dispatchEvent(new CustomEvent('event-click', {
476
+ detail: { event, originalEvent: e },
477
+ bubbles: true,
478
+ }));
479
+ }
480
+ return;
481
+ }
482
+ const state = this.stateManager.getState();
483
+ if (!state.dragState)
484
+ return;
485
+ const { type, event, preview } = state.dragState;
486
+ // Finalize the drag operation
487
+ if (preview) {
488
+ if (type === 'create') {
489
+ // Create new event
490
+ const newEvent = {
491
+ id: generateEventId(),
492
+ title: 'New Event',
493
+ start: preview.start,
494
+ end: preview.end,
495
+ color: '#3788d8',
496
+ };
497
+ this.stateManager.addEvent(newEvent);
498
+ this.dispatchEvent(new CustomEvent('event-create', {
499
+ detail: { event: newEvent, originalEvent: e },
500
+ bubbles: true,
501
+ }));
502
+ }
503
+ else if (type === 'move' && event) {
504
+ // Move event
505
+ const oldEvent = Object.assign({}, event);
506
+ const updatedEvent = Object.assign(Object.assign({}, event), { start: preview.start, end: preview.end });
507
+ this.stateManager.updateEvent(updatedEvent);
508
+ this.dispatchEvent(new CustomEvent('event-update', {
509
+ detail: { event: updatedEvent, oldEvent, originalEvent: e },
510
+ bubbles: true,
511
+ }));
512
+ }
513
+ else if ((type === 'resize-start' || type === 'resize-end') && event) {
514
+ // Resize event
515
+ const oldEvent = Object.assign({}, event);
516
+ const updatedEvent = Object.assign(Object.assign({}, event), { start: preview.start, end: preview.end });
517
+ this.stateManager.updateEvent(updatedEvent);
518
+ this.dispatchEvent(new CustomEvent('event-update', {
519
+ detail: { event: updatedEvent, oldEvent, originalEvent: e },
520
+ bubbles: true,
521
+ }));
522
+ }
523
+ }
524
+ this.stateManager.endDrag();
525
+ }
526
+ handleClick(e) {
527
+ const target = e.target;
528
+ // Group toggle
529
+ const toggle = target.closest('.expand-toggle');
530
+ if (toggle) {
531
+ const groupId = toggle.dataset['groupId'];
532
+ if (groupId) {
533
+ this.stateManager.toggleGroupCollapse(groupId);
534
+ this.renderView();
535
+ return;
536
+ }
537
+ }
538
+ // Event clicks are handled in handleMouseUp (via pendingDrag)
539
+ // Skip event click handling here to avoid duplicates
540
+ const eventEl = target.closest('.scheduler-event');
541
+ if (eventEl) {
542
+ return;
543
+ }
544
+ // Date click
545
+ const dayEl = target.closest('[data-date]');
546
+ if (dayEl) {
547
+ const dateStr = dayEl.dataset['date'];
548
+ if (dateStr) {
549
+ this.dispatchEvent(new CustomEvent('date-click', {
550
+ detail: { date: new Date(dateStr), originalEvent: e },
551
+ bubbles: true,
552
+ }));
553
+ }
554
+ }
555
+ // Month click in year view
556
+ const monthHeader = target.closest('.scheduler-year-month-header');
557
+ if (monthHeader) {
558
+ const monthStr = monthHeader.dataset['month'];
559
+ if (monthStr) {
560
+ this.stateManager.setDate(new Date(monthStr));
561
+ this.stateManager.setView('month');
562
+ }
563
+ }
564
+ // More link click
565
+ const moreLink = target.closest('.scheduler-more-link');
566
+ if (moreLink) {
567
+ const dateStr = moreLink.dataset['date'];
568
+ if (dateStr) {
569
+ this.stateManager.setDate(new Date(dateStr));
570
+ this.stateManager.setView('day');
571
+ }
572
+ }
573
+ }
574
+ handleDblClick(e) {
575
+ const target = e.target;
576
+ const eventEl = target.closest('.scheduler-event');
577
+ if (eventEl) {
578
+ const eventId = eventEl.dataset['eventId'];
579
+ const event = eventId ? this.getEventById(eventId) : null;
580
+ if (event) {
581
+ this.dispatchEvent(new CustomEvent('event-dblclick', {
582
+ detail: { event, originalEvent: e },
583
+ bubbles: true,
584
+ }));
585
+ }
586
+ }
587
+ }
588
+ handleKeyDown(e) {
589
+ const state = this.stateManager.getState();
590
+ switch (e.key) {
591
+ case 'ArrowLeft':
592
+ this.prev();
593
+ e.preventDefault();
594
+ break;
595
+ case 'ArrowRight':
596
+ this.next();
597
+ e.preventDefault();
598
+ break;
599
+ case 't':
600
+ case 'T':
601
+ this.today();
602
+ e.preventDefault();
603
+ break;
604
+ case 'y':
605
+ case 'Y':
606
+ this.changeView('year');
607
+ e.preventDefault();
608
+ break;
609
+ case 'm':
610
+ case 'M':
611
+ this.changeView('month');
612
+ e.preventDefault();
613
+ break;
614
+ case 'w':
615
+ case 'W':
616
+ this.changeView('week');
617
+ e.preventDefault();
618
+ break;
619
+ case 'd':
620
+ case 'D':
621
+ this.changeView('day');
622
+ e.preventDefault();
623
+ break;
624
+ case 'Delete':
625
+ case 'Backspace':
626
+ if (state.selectedEvent) {
627
+ this.dispatchEvent(new CustomEvent('event-delete', {
628
+ detail: { event: state.selectedEvent },
629
+ bubbles: true,
630
+ }));
631
+ }
632
+ break;
633
+ case 'Escape':
634
+ if (state.dragState) {
635
+ this.stateManager.endDrag();
636
+ }
637
+ break;
638
+ }
639
+ }
640
+ startDrag(type, event, mouseEvent, slotEl) {
641
+ const slot = slotEl
642
+ ? this.getSlotFromElement(slotEl)
643
+ : this.getSlotAtPosition(mouseEvent.clientX, mouseEvent.clientY);
644
+ if (!slot)
645
+ return;
646
+ let preview;
647
+ if (type === 'create') {
648
+ preview = {
649
+ start: slot.start,
650
+ end: slot.end,
651
+ };
652
+ }
653
+ else if (event) {
654
+ preview = {
655
+ start: event.start,
656
+ end: event.end,
657
+ };
658
+ }
659
+ else {
660
+ return;
661
+ }
662
+ this.stateManager.startDrag({
663
+ type,
664
+ event,
665
+ startSlot: slot,
666
+ currentSlot: slot,
667
+ preview,
668
+ originalEvent: event ? Object.assign({}, event) : undefined,
669
+ meta: type.startsWith('resize-')
670
+ ? { resizeHandle: type.replace('resize-', '') }
671
+ : undefined,
672
+ });
673
+ }
674
+ calculatePreview(type, dragState, currentSlot) {
675
+ const { startSlot, event, originalEvent } = dragState;
676
+ if (type === 'create') {
677
+ // Extend selection from start slot to current slot
678
+ const start = new Date(Math.min(startSlot.start.getTime(), currentSlot.start.getTime()));
679
+ const end = new Date(Math.max(startSlot.end.getTime(), currentSlot.end.getTime()));
680
+ return { start, end };
681
+ }
682
+ if (type === 'move' && originalEvent) {
683
+ // Calculate offset and apply to event
684
+ const offsetMs = currentSlot.start.getTime() - startSlot.start.getTime();
685
+ const duration = originalEvent.end.getTime() - originalEvent.start.getTime();
686
+ const newStart = new Date(originalEvent.start.getTime() + offsetMs);
687
+ const newEnd = new Date(newStart.getTime() + duration);
688
+ return { start: newStart, end: newEnd };
689
+ }
690
+ if (type === 'resize-start' && originalEvent) {
691
+ // Move start, keep end fixed
692
+ const newStart = new Date(Math.min(currentSlot.start.getTime(), originalEvent.end.getTime() - 1800000));
693
+ return { start: newStart, end: originalEvent.end };
694
+ }
695
+ if (type === 'resize-end' && originalEvent) {
696
+ // Move end, keep start fixed
697
+ const newEnd = new Date(Math.max(currentSlot.end.getTime(), originalEvent.start.getTime() + 1800000));
698
+ return { start: originalEvent.start, end: newEnd };
699
+ }
700
+ return null;
701
+ }
702
+ getSlotAtPosition(clientX, clientY) {
703
+ const slotEl = this.shadow.elementsFromPoint(clientX, clientY)
704
+ .find((el) => el.matches('.scheduler-time-slot, .scheduler-timeline-slot'));
705
+ return slotEl ? this.getSlotFromElement(slotEl) : null;
706
+ }
707
+ getSlotFromElement(el) {
708
+ const startStr = el.dataset['start'];
709
+ const endStr = el.dataset['end'];
710
+ if (!startStr || !endStr)
711
+ return null;
712
+ return {
713
+ start: new Date(startStr),
714
+ end: new Date(endStr),
715
+ };
716
+ }
717
+ // Touch event handlers
718
+ handleTouchStart(e) {
719
+ const state = this.stateManager.getState();
720
+ if (!state.options.editable)
721
+ return;
722
+ // Only handle single touch
723
+ if (e.touches.length !== 1) {
724
+ this.cancelTouchHold();
725
+ return;
726
+ }
727
+ const touch = e.touches[0];
728
+ const target = touch.target;
729
+ this.touchStartPosition = { x: touch.clientX, y: touch.clientY };
730
+ // Check if touching an event or slot that could be dragged
731
+ const eventEl = target.closest('.scheduler-event:not(.preview)');
732
+ const resizeHandle = target.closest('.resize-handle');
733
+ const slotEl = target.closest('.scheduler-time-slot, .scheduler-timeline-slot');
734
+ if (!eventEl && !slotEl) {
735
+ // Not touching a draggable element
736
+ return;
737
+ }
738
+ // Store the target for the hold callback
739
+ this.touchHoldTarget = eventEl || slotEl;
740
+ // Add visual feedback class
741
+ if (eventEl) {
742
+ eventEl.classList.add('touch-hold-pending');
743
+ }
744
+ else if (slotEl) {
745
+ slotEl.classList.add('touch-hold-pending');
746
+ }
747
+ // Start the hold timer
748
+ this.touchHoldTimer = setTimeout(() => {
749
+ this.activateTouchDragMode(touch, resizeHandle);
750
+ }, this.TOUCH_HOLD_DURATION);
751
+ }
752
+ activateTouchDragMode(touch, resizeHandle) {
753
+ const state = this.stateManager.getState();
754
+ const target = this.touchHoldTarget;
755
+ if (!target)
756
+ return;
757
+ // Trigger haptic feedback if available
758
+ this.triggerHapticFeedback();
759
+ // Enter touch drag mode
760
+ this.isTouchDragMode = true;
761
+ // Add visual feedback
762
+ const container = this.shadow.querySelector('.scheduler-container');
763
+ container === null || container === void 0 ? void 0 : container.classList.add('touch-drag-mode');
764
+ // Remove pending class, add active class
765
+ target.classList.remove('touch-hold-pending');
766
+ target.classList.add('touch-hold-active');
767
+ // Determine the drag type and start the drag
768
+ const eventEl = target.closest('.scheduler-event:not(.preview)');
769
+ if (resizeHandle) {
770
+ // Resize operation
771
+ const parentEventEl = resizeHandle.closest('.scheduler-event');
772
+ const eventId = parentEventEl === null || parentEventEl === void 0 ? void 0 : parentEventEl.dataset['eventId'];
773
+ const event = eventId ? this.getEventById(eventId) : null;
774
+ if (event) {
775
+ const handleType = resizeHandle.dataset['handle'];
776
+ this.startDragFromTouch(('resize-' + handleType), event, touch.clientX, touch.clientY);
777
+ }
778
+ }
779
+ else if (eventEl) {
780
+ // Move operation
781
+ const eventId = eventEl.dataset['eventId'];
782
+ const event = eventId ? this.getEventById(eventId) : null;
783
+ if (event && event.draggable !== false) {
784
+ this.startDragFromTouch('move', event, touch.clientX, touch.clientY);
785
+ }
786
+ }
787
+ else if (target.matches('.scheduler-time-slot, .scheduler-timeline-slot') && state.options.selectable) {
788
+ // Create operation
789
+ this.startDragFromTouch('create', null, touch.clientX, touch.clientY, target);
790
+ }
791
+ }
792
+ handleTouchMove(e) {
793
+ if (e.touches.length !== 1) {
794
+ this.cancelTouchHold();
795
+ return;
796
+ }
797
+ const touch = e.touches[0];
798
+ // If we have a pending touch hold, check if user moved too much
799
+ if (this.touchHoldTimer && this.touchStartPosition) {
800
+ const dx = touch.clientX - this.touchStartPosition.x;
801
+ const dy = touch.clientY - this.touchStartPosition.y;
802
+ const distance = Math.sqrt(dx * dx + dy * dy);
803
+ if (distance > this.TOUCH_MOVE_THRESHOLD) {
804
+ // User moved too much, cancel the hold and allow normal scrolling
805
+ this.cancelTouchHold();
806
+ return;
807
+ }
808
+ // Still waiting for hold, prevent default to avoid scroll during hold detection
809
+ // But only if we're close to the threshold
810
+ if (distance < this.TOUCH_MOVE_THRESHOLD / 2) {
811
+ // Don't prevent default yet to allow small movements
812
+ }
813
+ return;
814
+ }
815
+ // If in touch drag mode, handle the drag
816
+ if (this.isTouchDragMode) {
817
+ e.preventDefault(); // Prevent scrolling while dragging
818
+ const state = this.stateManager.getState();
819
+ if (!state.dragState)
820
+ return;
821
+ const slot = this.getSlotAtPosition(touch.clientX, touch.clientY);
822
+ if (!slot)
823
+ return;
824
+ const preview = this.calculatePreview(state.dragState.type, state.dragState, slot);
825
+ if (preview) {
826
+ this.stateManager.updateDrag(slot, preview);
827
+ }
828
+ }
829
+ }
830
+ handleTouchEnd(e) {
831
+ // If we had a pending hold that never activated, treat as a tap
832
+ if (this.touchHoldTimer) {
833
+ this.cancelTouchHold();
834
+ // Handle as a tap/click on the target
835
+ if (this.touchHoldTarget) {
836
+ const eventEl = this.touchHoldTarget.closest('.scheduler-event:not(.preview)');
837
+ if (eventEl) {
838
+ const eventId = eventEl.dataset['eventId'];
839
+ const event = eventId ? this.getEventById(eventId) : null;
840
+ if (event) {
841
+ this.stateManager.setSelectedEvent(event);
842
+ this.dispatchEvent(new CustomEvent('event-click', {
843
+ detail: { event, originalEvent: e },
844
+ bubbles: true,
845
+ }));
846
+ }
847
+ }
848
+ }
849
+ this.touchHoldTarget = null;
850
+ return;
851
+ }
852
+ // If in touch drag mode, finalize the drag
853
+ if (this.isTouchDragMode) {
854
+ const state = this.stateManager.getState();
855
+ if (state.dragState) {
856
+ const { type, event, preview } = state.dragState;
857
+ if (preview) {
858
+ if (type === 'create') {
859
+ const newEvent = {
860
+ id: generateEventId(),
861
+ title: 'New Event',
862
+ start: preview.start,
863
+ end: preview.end,
864
+ color: '#3788d8',
865
+ };
866
+ this.stateManager.addEvent(newEvent);
867
+ this.dispatchEvent(new CustomEvent('event-create', {
868
+ detail: { event: newEvent, originalEvent: e },
869
+ bubbles: true,
870
+ }));
871
+ }
872
+ else if (type === 'move' && event) {
873
+ const oldEvent = Object.assign({}, event);
874
+ const updatedEvent = Object.assign(Object.assign({}, event), { start: preview.start, end: preview.end });
875
+ this.stateManager.updateEvent(updatedEvent);
876
+ this.dispatchEvent(new CustomEvent('event-update', {
877
+ detail: { event: updatedEvent, oldEvent, originalEvent: e },
878
+ bubbles: true,
879
+ }));
880
+ }
881
+ else if ((type === 'resize-start' || type === 'resize-end') && event) {
882
+ const oldEvent = Object.assign({}, event);
883
+ const updatedEvent = Object.assign(Object.assign({}, event), { start: preview.start, end: preview.end });
884
+ this.stateManager.updateEvent(updatedEvent);
885
+ this.dispatchEvent(new CustomEvent('event-update', {
886
+ detail: { event: updatedEvent, oldEvent, originalEvent: e },
887
+ bubbles: true,
888
+ }));
889
+ }
890
+ }
891
+ this.stateManager.endDrag();
892
+ }
893
+ this.exitTouchDragMode();
894
+ }
895
+ }
896
+ handleTouchCancel(_e) {
897
+ this.cancelTouchHold();
898
+ if (this.isTouchDragMode) {
899
+ this.stateManager.endDrag();
900
+ this.exitTouchDragMode();
901
+ }
902
+ }
903
+ cancelTouchHold() {
904
+ if (this.touchHoldTimer) {
905
+ clearTimeout(this.touchHoldTimer);
906
+ this.touchHoldTimer = null;
907
+ }
908
+ // Remove pending visual feedback
909
+ if (this.touchHoldTarget) {
910
+ this.touchHoldTarget.classList.remove('touch-hold-pending');
911
+ this.touchHoldTarget.classList.remove('touch-hold-active');
912
+ }
913
+ this.touchStartPosition = null;
914
+ }
915
+ exitTouchDragMode() {
916
+ this.isTouchDragMode = false;
917
+ this.touchStartPosition = null;
918
+ this.touchHoldTarget = null;
919
+ // Remove visual feedback
920
+ const container = this.shadow.querySelector('.scheduler-container');
921
+ container === null || container === void 0 ? void 0 : container.classList.remove('touch-drag-mode');
922
+ // Remove active classes from all elements
923
+ this.shadow.querySelectorAll('.touch-hold-active, .touch-hold-pending').forEach((el) => {
924
+ el.classList.remove('touch-hold-active', 'touch-hold-pending');
925
+ });
926
+ }
927
+ triggerHapticFeedback() {
928
+ // Use Vibration API if available
929
+ if ('vibrate' in navigator) {
930
+ navigator.vibrate(50); // Short vibration (50ms)
931
+ }
932
+ }
933
+ startDragFromTouch(type, event, clientX, clientY, slotEl) {
934
+ const slot = slotEl
935
+ ? this.getSlotFromElement(slotEl)
936
+ : this.getSlotAtPosition(clientX, clientY);
937
+ if (!slot)
938
+ return;
939
+ let preview;
940
+ if (type === 'create') {
941
+ preview = {
942
+ start: slot.start,
943
+ end: slot.end,
944
+ };
945
+ }
946
+ else if (event) {
947
+ preview = {
948
+ start: event.start,
949
+ end: event.end,
950
+ };
951
+ }
952
+ else {
953
+ return;
954
+ }
955
+ this.stateManager.startDrag({
956
+ type,
957
+ event,
958
+ startSlot: slot,
959
+ currentSlot: slot,
960
+ preview,
961
+ originalEvent: event ? Object.assign({}, event) : undefined,
962
+ meta: type.startsWith('resize-')
963
+ ? { resizeHandle: type.replace('resize-', '') }
964
+ : undefined,
965
+ });
966
+ }
967
+ }
968
+ MpScheduler.observedAttributes = [
969
+ 'view',
970
+ 'date',
971
+ 'locale',
972
+ 'first-day-of-week',
973
+ 'slot-duration',
974
+ 'time-format',
975
+ 'editable',
976
+ 'selectable',
977
+ ];
978
+ // Register the custom element
979
+ if (typeof customElements !== 'undefined' && !customElements.get('mp-scheduler')) {
980
+ customElements.define('mp-scheduler', MpScheduler);
981
+ }
982
+ //# sourceMappingURL=mp-scheduler.js.map