@mintplayer/scheduler-wc 1.0.1 → 1.1.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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/components/mp-scheduler.d.ts +16 -41
  3. package/src/components/mp-scheduler.js +126 -575
  4. package/src/components/mp-scheduler.js.map +1 -1
  5. package/src/drag/drag-manager.d.ts +90 -0
  6. package/src/drag/drag-manager.js +201 -0
  7. package/src/drag/drag-manager.js.map +1 -0
  8. package/src/drag/drag-preview.d.ts +42 -0
  9. package/src/drag/drag-preview.js +87 -0
  10. package/src/drag/drag-preview.js.map +1 -0
  11. package/src/drag/drag-state-machine.d.ts +102 -0
  12. package/src/drag/drag-state-machine.js +320 -0
  13. package/src/drag/drag-state-machine.js.map +1 -0
  14. package/src/drag/drag-types.d.ts +104 -0
  15. package/src/drag/drag-types.js +8 -0
  16. package/src/drag/drag-types.js.map +1 -0
  17. package/src/drag/index.d.ts +4 -0
  18. package/src/drag/index.js +5 -0
  19. package/src/drag/index.js.map +1 -0
  20. package/src/events/event-types.d.ts +43 -0
  21. package/src/events/event-types.js +2 -0
  22. package/src/events/event-types.js.map +1 -0
  23. package/src/events/index.d.ts +2 -0
  24. package/src/events/index.js +3 -0
  25. package/src/events/index.js.map +1 -0
  26. package/src/events/scheduler-event-emitter.d.ts +46 -0
  27. package/src/events/scheduler-event-emitter.js +70 -0
  28. package/src/events/scheduler-event-emitter.js.map +1 -0
  29. package/src/input/index.d.ts +2 -0
  30. package/src/input/index.js +3 -0
  31. package/src/input/index.js.map +1 -0
  32. package/src/input/input-handler.d.ts +93 -0
  33. package/src/input/input-handler.js +340 -0
  34. package/src/input/input-handler.js.map +1 -0
  35. package/src/input/pointer-event.d.ts +39 -0
  36. package/src/input/pointer-event.js +41 -0
  37. package/src/input/pointer-event.js.map +1 -0
  38. package/src/styles/scheduler.styles.d.ts +1 -1
  39. package/src/styles/scheduler.styles.js +14 -0
  40. package/src/styles/scheduler.styles.js.map +1 -1
@@ -6,58 +6,67 @@ import { WeekView } from '../views/week-view';
6
6
  import { DayView } from '../views/day-view';
7
7
  import { TimelineView } from '../views/timeline-view';
8
8
  import { schedulerStyles } from '../styles/scheduler.styles';
9
+ import { DragManager } from '../drag';
10
+ import { InputHandler } from '../input';
11
+ import { SchedulerEventEmitter } from '../events';
9
12
  /**
10
13
  * MpScheduler Web Component
11
14
  *
12
- * A fully-featured scheduler/calendar component
15
+ * A fully-featured scheduler/calendar component.
16
+ * Refactored for clarity with separated concerns:
17
+ * - DragManager: Handles all drag operations
18
+ * - InputHandler: Normalizes mouse/touch input
19
+ * - SchedulerEventEmitter: Dispatches custom events
13
20
  */
14
21
  export class MpScheduler extends HTMLElement {
15
22
  constructor() {
16
23
  super();
17
24
  this.currentView = null;
25
+ this.currentViewType = null;
18
26
  this.contentContainer = null;
19
27
  // Track previous state for change detection
20
28
  this.previousView = null;
21
29
  this.previousDate = null;
22
30
  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
- // RAF scheduling for drag updates (ensures browser can repaint during drag)
31
+ // RAF scheduling for drag updates
34
32
  this.pendingDragUpdate = null;
35
33
  this.latestDragState = null;
36
34
  this.shadow = this.attachShadow({ mode: 'open' });
37
35
  this.stateManager = new SchedulerStateManager();
38
- // Bind event handlers
39
- this.boundHandleMouseDown = this.handleMouseDown.bind(this);
40
- this.boundHandleMouseMove = this.handleMouseMove.bind(this);
41
- this.boundHandleMouseUp = this.handleMouseUp.bind(this);
42
- this.boundHandleClick = this.handleClick.bind(this);
43
- this.boundHandleDblClick = this.handleDblClick.bind(this);
36
+ this.eventEmitter = new SchedulerEventEmitter(this);
37
+ // Initialize drag manager
38
+ this.dragManager = new DragManager(this.stateManager);
39
+ this.dragManager.setSlotResolver((x, y) => this.getSlotAtPosition(x, y));
40
+ // Initialize input handler
41
+ this.inputHandler = new InputHandler({
42
+ shadowRoot: this.shadow,
43
+ getEventById: (id) => this.getEventById(id),
44
+ isEditable: () => { var _a; return (_a = this.stateManager.getState().options.editable) !== null && _a !== void 0 ? _a : true; },
45
+ isSelectable: () => { var _a; return (_a = this.stateManager.getState().options.selectable) !== null && _a !== void 0 ? _a : true; },
46
+ }, {
47
+ onPointerDown: (pointer, target, immediate) => this.handlePointerDown(pointer, target, immediate),
48
+ onPointerMove: (pointer) => this.handlePointerMove(pointer),
49
+ onPointerUp: (pointer) => this.handlePointerUp(pointer),
50
+ onClick: (pointer, target) => this.handleClick(pointer, target),
51
+ onDoubleClick: (pointer, target) => this.handleDoubleClick(pointer, target),
52
+ });
53
+ // Bind keyboard handler
44
54
  this.boundHandleKeyDown = this.handleKeyDown.bind(this);
45
- this.boundHandleTouchStart = this.handleTouchStart.bind(this);
46
- this.boundHandleTouchMove = this.handleTouchMove.bind(this);
47
- this.boundHandleTouchEnd = this.handleTouchEnd.bind(this);
48
- this.boundHandleTouchCancel = this.handleTouchCancel.bind(this);
49
55
  // Subscribe to state changes
50
56
  this.stateManager.subscribe((state) => this.onStateChange(state));
51
57
  }
52
58
  connectedCallback() {
53
59
  this.render();
54
- this.attachEventListeners();
60
+ this.inputHandler.attach();
61
+ this.addEventListener('keydown', this.boundHandleKeyDown);
55
62
  }
56
63
  disconnectedCallback() {
57
64
  var _a;
58
- this.detachEventListeners();
65
+ this.inputHandler.detach();
66
+ this.removeEventListener('keydown', this.boundHandleKeyDown);
59
67
  (_a = this.currentView) === null || _a === void 0 ? void 0 : _a.destroy();
60
- // Cancel any pending RAF to prevent memory leaks
68
+ this.dragManager.destroy();
69
+ // Cancel any pending RAF
61
70
  if (this.pendingDragUpdate !== null) {
62
71
  cancelAnimationFrame(this.pendingDragUpdate);
63
72
  this.pendingDragUpdate = null;
@@ -87,7 +96,9 @@ export class MpScheduler extends HTMLElement {
87
96
  if (newValue) {
88
97
  const day = parseInt(newValue, 10);
89
98
  if (day >= 0 && day <= 6) {
90
- this.stateManager.setOptions({ firstDayOfWeek: day });
99
+ this.stateManager.setOptions({
100
+ firstDayOfWeek: day,
101
+ });
91
102
  }
92
103
  }
93
104
  break;
@@ -109,7 +120,9 @@ export class MpScheduler extends HTMLElement {
109
120
  break;
110
121
  }
111
122
  }
123
+ // ============================================
112
124
  // Public API
125
+ // ============================================
113
126
  get view() {
114
127
  return this.stateManager.getState().view;
115
128
  }
@@ -183,27 +196,23 @@ export class MpScheduler extends HTMLElement {
183
196
  }
184
197
  refetchEvents() {
185
198
  var _a;
186
- // Trigger re-render
187
199
  (_a = this.currentView) === null || _a === void 0 ? void 0 : _a.update(this.stateManager.getState());
188
200
  }
189
- // Private methods
201
+ // ============================================
202
+ // Rendering
203
+ // ============================================
190
204
  render() {
191
- // Add styles
192
205
  const style = document.createElement('style');
193
206
  style.textContent = schedulerStyles;
194
207
  this.shadow.appendChild(style);
195
- // Create container
196
208
  const container = document.createElement('div');
197
209
  container.className = 'scheduler-container';
198
- // Header
199
210
  const header = this.createHeader();
200
211
  container.appendChild(header);
201
- // Content
202
212
  this.contentContainer = document.createElement('div');
203
213
  this.contentContainer.className = 'scheduler-content';
204
214
  container.appendChild(this.contentContainer);
205
215
  this.shadow.appendChild(container);
206
- // Initial view render
207
216
  this.renderView();
208
217
  }
209
218
  createHeader() {
@@ -276,7 +285,14 @@ export class MpScheduler extends HTMLElement {
276
285
  case 'timeline': {
277
286
  const weekStart = dateService.getWeekStart(date, options.firstDayOfWeek);
278
287
  const weekEnd = dateService.addDays(weekStart, 6);
279
- titleText = `${dateService.formatDate(weekStart, options.locale, { month: 'short', day: 'numeric' })} - ${dateService.formatDate(weekEnd, options.locale, { month: 'short', day: 'numeric', year: 'numeric' })}`;
288
+ titleText = `${dateService.formatDate(weekStart, options.locale, {
289
+ month: 'short',
290
+ day: 'numeric',
291
+ })} - ${dateService.formatDate(weekEnd, options.locale, {
292
+ month: 'short',
293
+ day: 'numeric',
294
+ year: 'numeric',
295
+ })}`;
280
296
  break;
281
297
  }
282
298
  case 'day':
@@ -294,55 +310,58 @@ export class MpScheduler extends HTMLElement {
294
310
  var _a, _b;
295
311
  if (!this.contentContainer)
296
312
  return;
297
- // Destroy previous view
298
313
  (_a = this.currentView) === null || _a === void 0 ? void 0 : _a.destroy();
299
314
  const state = this.stateManager.getState();
300
- // Create new view
301
315
  switch (state.view) {
302
316
  case 'year':
303
317
  this.currentView = new YearView(this.contentContainer, state);
318
+ this.currentViewType = 'year';
304
319
  break;
305
320
  case 'month':
306
321
  this.currentView = new MonthView(this.contentContainer, state);
322
+ this.currentViewType = 'month';
307
323
  break;
308
324
  case 'week':
309
325
  this.currentView = new WeekView(this.contentContainer, state);
326
+ this.currentViewType = 'week';
310
327
  break;
311
328
  case 'day':
312
329
  this.currentView = new DayView(this.contentContainer, state);
330
+ this.currentViewType = 'day';
313
331
  break;
314
332
  case 'timeline':
315
333
  this.currentView = new TimelineView(this.contentContainer, state);
334
+ this.currentViewType = 'timeline';
316
335
  break;
317
336
  }
318
337
  (_b = this.currentView) === null || _b === void 0 ? void 0 : _b.render();
319
338
  }
339
+ // ============================================
340
+ // State Change Handling
341
+ // ============================================
320
342
  onStateChange(state) {
343
+ this.detectAndEmitChanges(state);
344
+ this.updateUI(state);
345
+ }
346
+ detectAndEmitChanges(state) {
321
347
  var _a, _b;
322
- // Detect view/date changes and dispatch events
323
348
  const viewChanged = this.previousView !== null && this.previousView !== state.view;
324
- const dateChanged = this.previousDate !== null && this.previousDate.getTime() !== state.date.getTime();
349
+ const dateChanged = this.previousDate !== null &&
350
+ this.previousDate.getTime() !== state.date.getTime();
325
351
  const selectedEventId = (_b = (_a = state.selectedEvent) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
326
- const selectionChanged = this.previousSelectedEventId !== null && this.previousSelectedEventId !== selectedEventId;
327
- // Dispatch view-change event if view or date changed (but not on initial render)
352
+ const selectionChanged = this.previousSelectedEventId !== null &&
353
+ this.previousSelectedEventId !== selectedEventId;
328
354
  if (viewChanged || dateChanged) {
329
- this.dispatchEvent(new CustomEvent('view-change', {
330
- detail: { view: state.view, date: state.date },
331
- bubbles: true,
332
- }));
355
+ this.eventEmitter.emitViewChange(state.view, state.date);
333
356
  }
334
- // Dispatch selection-change event if selected event changed (but not on initial render)
335
357
  if (selectionChanged) {
336
- this.dispatchEvent(new CustomEvent('selection-change', {
337
- detail: { selectedEvent: state.selectedEvent },
338
- bubbles: true,
339
- }));
358
+ this.eventEmitter.emitSelectionChange(state.selectedEvent);
340
359
  }
341
- // Update previous state tracking
342
360
  this.previousView = state.view;
343
361
  this.previousDate = new Date(state.date);
344
362
  this.previousSelectedEventId = selectedEventId;
345
- // Update title
363
+ }
364
+ updateUI(state) {
346
365
  this.updateTitle();
347
366
  // Update view switcher active state
348
367
  const buttons = this.shadow.querySelectorAll('.scheduler-view-switcher button');
@@ -352,42 +371,24 @@ export class MpScheduler extends HTMLElement {
352
371
  });
353
372
  // Update or re-render view
354
373
  if (this.currentView) {
355
- // Check if view type changed
356
- const viewTypeChanged = this.viewTypeChanged(state.view);
357
- if (viewTypeChanged) {
374
+ if (this.currentViewType !== state.view) {
358
375
  this.renderView();
359
376
  }
377
+ else if (state.dragState || state.previewEvent) {
378
+ this.scheduleDragUpdate(state);
379
+ }
360
380
  else {
361
- // During drag operations, use requestAnimationFrame to ensure the browser
362
- // can repaint between updates. This fixes the issue where drag previews
363
- // don't appear in production builds with zoneless Angular.
364
- if (state.dragState || state.previewEvent) {
365
- this.scheduleDragUpdate(state);
366
- }
367
- else {
368
- // For non-drag updates, process synchronously for responsiveness
369
- this.currentView.update(state);
370
- }
381
+ this.currentView.update(state);
371
382
  }
372
383
  }
373
384
  }
374
- /**
375
- * Schedule a drag-related view update using requestAnimationFrame.
376
- * This ensures the browser has time to repaint between drag updates,
377
- * which is necessary for drag previews to appear in production builds.
378
- */
379
385
  scheduleDragUpdate(state) {
380
- // Store the latest state
381
386
  this.latestDragState = state;
382
- // If there's already a pending update, don't schedule another one
383
- // The pending update will use the latest state when it executes
384
387
  if (this.pendingDragUpdate !== null) {
385
388
  return;
386
389
  }
387
- // Schedule the update for the next animation frame
388
390
  this.pendingDragUpdate = requestAnimationFrame(() => {
389
391
  this.pendingDragUpdate = null;
390
- // Use the latest state (in case multiple updates were batched)
391
392
  const stateToApply = this.latestDragState;
392
393
  this.latestDragState = null;
393
394
  if (stateToApply && this.currentView) {
@@ -395,180 +396,60 @@ export class MpScheduler extends HTMLElement {
395
396
  }
396
397
  });
397
398
  }
398
- viewTypeChanged(newView) {
399
- if (!this.currentView)
400
- return true;
401
- const viewClass = this.currentView.constructor.name.toLowerCase();
402
- return !viewClass.includes(newView);
399
+ // ============================================
400
+ // Input Handling (Callbacks from InputHandler)
401
+ // ============================================
402
+ handlePointerDown(pointer, target, immediate) {
403
+ this.dragManager.handlePointerDown(pointer, target, immediate);
403
404
  }
404
- attachEventListeners() {
405
- const root = this.shadow;
406
- root.addEventListener('mousedown', this.boundHandleMouseDown);
407
- document.addEventListener('mousemove', this.boundHandleMouseMove);
408
- document.addEventListener('mouseup', this.boundHandleMouseUp);
409
- root.addEventListener('click', this.boundHandleClick);
410
- root.addEventListener('dblclick', this.boundHandleDblClick);
411
- this.addEventListener('keydown', this.boundHandleKeyDown);
412
- // Touch events
413
- root.addEventListener('touchstart', this.boundHandleTouchStart, { passive: false });
414
- root.addEventListener('touchmove', this.boundHandleTouchMove, { passive: false });
415
- root.addEventListener('touchend', this.boundHandleTouchEnd);
416
- root.addEventListener('touchcancel', this.boundHandleTouchCancel);
405
+ handlePointerMove(pointer) {
406
+ this.dragManager.handlePointerMove(pointer);
417
407
  }
418
- detachEventListeners() {
419
- const root = this.shadow;
420
- root.removeEventListener('mousedown', this.boundHandleMouseDown);
421
- document.removeEventListener('mousemove', this.boundHandleMouseMove);
422
- document.removeEventListener('mouseup', this.boundHandleMouseUp);
423
- root.removeEventListener('click', this.boundHandleClick);
424
- root.removeEventListener('dblclick', this.boundHandleDblClick);
425
- this.removeEventListener('keydown', this.boundHandleKeyDown);
426
- // Touch events
427
- root.removeEventListener('touchstart', this.boundHandleTouchStart);
428
- root.removeEventListener('touchmove', this.boundHandleTouchMove);
429
- root.removeEventListener('touchend', this.boundHandleTouchEnd);
430
- root.removeEventListener('touchcancel', this.boundHandleTouchCancel);
431
- this.cancelTouchHold();
432
- }
433
- handleMouseDown(e) {
434
- const state = this.stateManager.getState();
435
- if (!state.options.editable)
436
- return;
437
- const target = e.target;
438
- // Check for resize handle - start drag immediately (no click behavior)
439
- const resizeHandle = target.closest('.resize-handle');
440
- if (resizeHandle) {
441
- const eventEl = resizeHandle.closest('.scheduler-event');
442
- const eventId = eventEl === null || eventEl === void 0 ? void 0 : eventEl.dataset['eventId'];
443
- const event = eventId ? this.getEventById(eventId) : null;
444
- if (event) {
445
- const handleType = resizeHandle.dataset['handle'];
446
- this.pendingDrag = {
447
- type: ('resize-' + handleType),
448
- event,
449
- startX: e.clientX,
450
- startY: e.clientY,
451
- };
452
- e.preventDefault();
453
- return;
454
- }
455
- }
456
- // Check for event - set up pending drag (actual drag starts on mouse move)
457
- const eventEl = target.closest('.scheduler-event:not(.preview)');
458
- if (eventEl && !eventEl.classList.contains('preview')) {
459
- const eventId = eventEl.dataset['eventId'];
460
- const event = eventId ? this.getEventById(eventId) : null;
461
- if (event && event.draggable !== false) {
462
- this.pendingDrag = {
463
- type: 'move',
464
- event,
465
- startX: e.clientX,
466
- startY: e.clientY,
467
- };
468
- e.preventDefault();
469
- return;
470
- }
471
- }
472
- // Check for slot - set up pending drag for create
473
- const slotEl = target.closest('.scheduler-time-slot, .scheduler-timeline-slot');
474
- if (slotEl && state.options.selectable) {
475
- this.pendingDrag = {
476
- type: 'create',
477
- event: null,
478
- startX: e.clientX,
479
- startY: e.clientY,
480
- slotEl,
481
- };
482
- e.preventDefault();
408
+ handlePointerUp(pointer) {
409
+ const result = this.dragManager.handlePointerUp(pointer);
410
+ if (result) {
411
+ this.handleDragComplete(result, pointer.originalEvent);
483
412
  }
484
413
  }
485
- handleMouseMove(e) {
486
- // Check if we have a pending drag that should start
487
- if (this.pendingDrag) {
488
- const dx = e.clientX - this.pendingDrag.startX;
489
- const dy = e.clientY - this.pendingDrag.startY;
490
- const distance = Math.sqrt(dx * dx + dy * dy);
491
- if (distance >= this.DRAG_THRESHOLD) {
492
- // Start the actual drag
493
- this.startDrag(this.pendingDrag.type, this.pendingDrag.event, e, this.pendingDrag.slotEl);
494
- this.pendingDrag = null;
414
+ handleDragComplete(result, originalEvent) {
415
+ if (result.wasClick) {
416
+ // It was a click, not a drag
417
+ if (result.event) {
418
+ this.stateManager.setSelectedEvent(result.event);
419
+ this.eventEmitter.emitEventClick(result.event, originalEvent);
495
420
  }
496
421
  return;
497
422
  }
498
- const state = this.stateManager.getState();
499
- if (!state.dragState)
500
- return;
501
- const slot = this.getSlotAtPosition(e.clientX, e.clientY);
502
- if (!slot)
503
- return;
504
- const preview = this.calculatePreview(state.dragState.type, state.dragState, slot);
505
- if (preview) {
506
- this.stateManager.updateDrag(slot, preview);
507
- }
508
- }
509
- handleMouseUp(e) {
510
- // If we had a pending drag that never started, it's a click
511
- if (this.pendingDrag) {
512
- const { type, event } = this.pendingDrag;
513
- this.pendingDrag = null;
514
- // If it was on an event, select it
515
- if (type === 'move' && event) {
516
- this.stateManager.setSelectedEvent(event);
517
- this.dispatchEvent(new CustomEvent('event-click', {
518
- detail: { event, originalEvent: e },
519
- bubbles: true,
520
- }));
521
- }
522
- return;
523
- }
524
- const state = this.stateManager.getState();
525
- if (!state.dragState)
526
- return;
527
- const { type, event, preview } = state.dragState;
528
- // Finalize the drag operation
529
- if (preview) {
530
- if (type === 'create') {
531
- // Create new event
423
+ // Handle actual drag completion
424
+ switch (result.type) {
425
+ case 'create': {
532
426
  const newEvent = {
533
427
  id: generateEventId(),
534
428
  title: 'New Event',
535
- start: preview.start,
536
- end: preview.end,
429
+ start: result.preview.start,
430
+ end: result.preview.end,
537
431
  color: '#3788d8',
538
432
  };
539
433
  this.stateManager.addEvent(newEvent);
540
- this.dispatchEvent(new CustomEvent('event-create', {
541
- detail: { event: newEvent, originalEvent: e },
542
- bubbles: true,
543
- }));
544
- }
545
- else if (type === 'move' && event) {
546
- // Move event
547
- const oldEvent = Object.assign({}, event);
548
- const updatedEvent = Object.assign(Object.assign({}, event), { start: preview.start, end: preview.end });
549
- this.stateManager.updateEvent(updatedEvent);
550
- this.dispatchEvent(new CustomEvent('event-update', {
551
- detail: { event: updatedEvent, oldEvent, originalEvent: e },
552
- bubbles: true,
553
- }));
434
+ this.eventEmitter.emitEventCreate(newEvent, originalEvent);
435
+ break;
554
436
  }
555
- else if ((type === 'resize-start' || type === 'resize-end') && event) {
556
- // Resize event
557
- const oldEvent = Object.assign({}, event);
558
- const updatedEvent = Object.assign(Object.assign({}, event), { start: preview.start, end: preview.end });
559
- this.stateManager.updateEvent(updatedEvent);
560
- this.dispatchEvent(new CustomEvent('event-update', {
561
- detail: { event: updatedEvent, oldEvent, originalEvent: e },
562
- bubbles: true,
563
- }));
437
+ case 'move':
438
+ case 'resize-start':
439
+ case 'resize-end': {
440
+ if (result.event && result.originalEvent) {
441
+ const updatedEvent = Object.assign(Object.assign({}, result.event), { start: result.preview.start, end: result.preview.end });
442
+ this.stateManager.updateEvent(updatedEvent);
443
+ this.eventEmitter.emitEventUpdate(updatedEvent, result.originalEvent, originalEvent);
444
+ }
445
+ break;
564
446
  }
565
447
  }
566
- this.stateManager.endDrag();
567
448
  }
568
- handleClick(e) {
569
- const target = e.target;
449
+ handleClick(pointer, target) {
450
+ const targetEl = pointer.target;
570
451
  // Group toggle
571
- const toggle = target.closest('.expand-toggle');
452
+ const toggle = targetEl.closest('.expand-toggle');
572
453
  if (toggle) {
573
454
  const groupId = toggle.dataset['groupId'];
574
455
  if (groupId) {
@@ -577,25 +458,16 @@ export class MpScheduler extends HTMLElement {
577
458
  return;
578
459
  }
579
460
  }
580
- // Event clicks are handled in handleMouseUp (via pendingDrag)
581
- // Skip event click handling here to avoid duplicates
582
- const eventEl = target.closest('.scheduler-event');
583
- if (eventEl) {
584
- return;
585
- }
586
461
  // Date click
587
- const dayEl = target.closest('[data-date]');
462
+ const dayEl = targetEl.closest('[data-date]');
588
463
  if (dayEl) {
589
464
  const dateStr = dayEl.dataset['date'];
590
465
  if (dateStr) {
591
- this.dispatchEvent(new CustomEvent('date-click', {
592
- detail: { date: new Date(dateStr), originalEvent: e },
593
- bubbles: true,
594
- }));
466
+ this.eventEmitter.emitDateClick(new Date(dateStr), pointer.originalEvent);
595
467
  }
596
468
  }
597
469
  // Month click in year view
598
- const monthHeader = target.closest('.scheduler-year-month-header');
470
+ const monthHeader = targetEl.closest('.scheduler-year-month-header');
599
471
  if (monthHeader) {
600
472
  const monthStr = monthHeader.dataset['month'];
601
473
  if (monthStr) {
@@ -604,7 +476,7 @@ export class MpScheduler extends HTMLElement {
604
476
  }
605
477
  }
606
478
  // More link click
607
- const moreLink = target.closest('.scheduler-more-link');
479
+ const moreLink = targetEl.closest('.scheduler-more-link');
608
480
  if (moreLink) {
609
481
  const dateStr = moreLink.dataset['date'];
610
482
  if (dateStr) {
@@ -613,18 +485,9 @@ export class MpScheduler extends HTMLElement {
613
485
  }
614
486
  }
615
487
  }
616
- handleDblClick(e) {
617
- const target = e.target;
618
- const eventEl = target.closest('.scheduler-event');
619
- if (eventEl) {
620
- const eventId = eventEl.dataset['eventId'];
621
- const event = eventId ? this.getEventById(eventId) : null;
622
- if (event) {
623
- this.dispatchEvent(new CustomEvent('event-dblclick', {
624
- detail: { event, originalEvent: e },
625
- bubbles: true,
626
- }));
627
- }
488
+ handleDoubleClick(pointer, target) {
489
+ if (target.type === 'event' && target.event) {
490
+ this.eventEmitter.emitEventDblClick(target.event, pointer.originalEvent);
628
491
  }
629
492
  }
630
493
  handleKeyDown(e) {
@@ -666,84 +529,22 @@ export class MpScheduler extends HTMLElement {
666
529
  case 'Delete':
667
530
  case 'Backspace':
668
531
  if (state.selectedEvent) {
669
- this.dispatchEvent(new CustomEvent('event-delete', {
670
- detail: { event: state.selectedEvent },
671
- bubbles: true,
672
- }));
532
+ this.eventEmitter.emitEventDelete(state.selectedEvent);
673
533
  }
674
534
  break;
675
535
  case 'Escape':
676
- if (state.dragState) {
677
- this.stateManager.endDrag();
536
+ if (this.dragManager.isDragging()) {
537
+ this.dragManager.cancel();
678
538
  }
679
539
  break;
680
540
  }
681
541
  }
682
- startDrag(type, event, mouseEvent, slotEl) {
683
- const slot = slotEl
684
- ? this.getSlotFromElement(slotEl)
685
- : this.getSlotAtPosition(mouseEvent.clientX, mouseEvent.clientY);
686
- if (!slot)
687
- return;
688
- let preview;
689
- if (type === 'create') {
690
- preview = {
691
- start: slot.start,
692
- end: slot.end,
693
- };
694
- }
695
- else if (event) {
696
- preview = {
697
- start: event.start,
698
- end: event.end,
699
- };
700
- }
701
- else {
702
- return;
703
- }
704
- this.stateManager.startDrag({
705
- type,
706
- event,
707
- startSlot: slot,
708
- currentSlot: slot,
709
- preview,
710
- originalEvent: event ? Object.assign({}, event) : undefined,
711
- meta: type.startsWith('resize-')
712
- ? { resizeHandle: type.replace('resize-', '') }
713
- : undefined,
714
- });
715
- }
716
- calculatePreview(type, dragState, currentSlot) {
717
- const { startSlot, event, originalEvent } = dragState;
718
- if (type === 'create') {
719
- // Extend selection from start slot to current slot
720
- const start = new Date(Math.min(startSlot.start.getTime(), currentSlot.start.getTime()));
721
- const end = new Date(Math.max(startSlot.end.getTime(), currentSlot.end.getTime()));
722
- return { start, end };
723
- }
724
- if (type === 'move' && originalEvent) {
725
- // Calculate offset and apply to event
726
- const offsetMs = currentSlot.start.getTime() - startSlot.start.getTime();
727
- const duration = originalEvent.end.getTime() - originalEvent.start.getTime();
728
- const newStart = new Date(originalEvent.start.getTime() + offsetMs);
729
- const newEnd = new Date(newStart.getTime() + duration);
730
- return { start: newStart, end: newEnd };
731
- }
732
- if (type === 'resize-start' && originalEvent) {
733
- // Move start, keep end fixed
734
- const newStart = new Date(Math.min(currentSlot.start.getTime(), originalEvent.end.getTime() - 1800000));
735
- return { start: newStart, end: originalEvent.end };
736
- }
737
- if (type === 'resize-end' && originalEvent) {
738
- // Move end, keep start fixed
739
- const newEnd = new Date(Math.max(currentSlot.end.getTime(), originalEvent.start.getTime() + 1800000));
740
- return { start: originalEvent.start, end: newEnd };
741
- }
742
- return null;
743
- }
542
+ // ============================================
543
+ // Slot Resolution
544
+ // ============================================
744
545
  getSlotAtPosition(clientX, clientY) {
745
- const slotEl = this.shadow.elementsFromPoint(clientX, clientY)
746
- .find((el) => el.matches('.scheduler-time-slot, .scheduler-timeline-slot'));
546
+ const elements = this.shadow.elementsFromPoint(clientX, clientY);
547
+ const slotEl = elements.find((el) => el.matches('.scheduler-time-slot, .scheduler-timeline-slot'));
747
548
  return slotEl ? this.getSlotFromElement(slotEl) : null;
748
549
  }
749
550
  getSlotFromElement(el) {
@@ -756,256 +557,6 @@ export class MpScheduler extends HTMLElement {
756
557
  end: new Date(endStr),
757
558
  };
758
559
  }
759
- // Touch event handlers
760
- handleTouchStart(e) {
761
- const state = this.stateManager.getState();
762
- if (!state.options.editable)
763
- return;
764
- // Only handle single touch
765
- if (e.touches.length !== 1) {
766
- this.cancelTouchHold();
767
- return;
768
- }
769
- const touch = e.touches[0];
770
- const target = touch.target;
771
- this.touchStartPosition = { x: touch.clientX, y: touch.clientY };
772
- // Check if touching an event or slot that could be dragged
773
- const eventEl = target.closest('.scheduler-event:not(.preview)');
774
- const resizeHandle = target.closest('.resize-handle');
775
- const slotEl = target.closest('.scheduler-time-slot, .scheduler-timeline-slot');
776
- if (!eventEl && !slotEl) {
777
- // Not touching a draggable element
778
- return;
779
- }
780
- // Store the target for the hold callback
781
- this.touchHoldTarget = eventEl || slotEl;
782
- // Add visual feedback class
783
- if (eventEl) {
784
- eventEl.classList.add('touch-hold-pending');
785
- }
786
- else if (slotEl) {
787
- slotEl.classList.add('touch-hold-pending');
788
- }
789
- // Start the hold timer
790
- this.touchHoldTimer = setTimeout(() => {
791
- this.activateTouchDragMode(touch, resizeHandle);
792
- }, this.TOUCH_HOLD_DURATION);
793
- }
794
- activateTouchDragMode(touch, resizeHandle) {
795
- const state = this.stateManager.getState();
796
- const target = this.touchHoldTarget;
797
- if (!target)
798
- return;
799
- // Trigger haptic feedback if available
800
- this.triggerHapticFeedback();
801
- // Enter touch drag mode
802
- this.isTouchDragMode = true;
803
- // Add visual feedback
804
- const container = this.shadow.querySelector('.scheduler-container');
805
- container === null || container === void 0 ? void 0 : container.classList.add('touch-drag-mode');
806
- // Remove pending class, add active class
807
- target.classList.remove('touch-hold-pending');
808
- target.classList.add('touch-hold-active');
809
- // Determine the drag type and start the drag
810
- const eventEl = target.closest('.scheduler-event:not(.preview)');
811
- if (resizeHandle) {
812
- // Resize operation
813
- const parentEventEl = resizeHandle.closest('.scheduler-event');
814
- const eventId = parentEventEl === null || parentEventEl === void 0 ? void 0 : parentEventEl.dataset['eventId'];
815
- const event = eventId ? this.getEventById(eventId) : null;
816
- if (event) {
817
- const handleType = resizeHandle.dataset['handle'];
818
- this.startDragFromTouch(('resize-' + handleType), event, touch.clientX, touch.clientY);
819
- }
820
- }
821
- else if (eventEl) {
822
- // Move operation
823
- const eventId = eventEl.dataset['eventId'];
824
- const event = eventId ? this.getEventById(eventId) : null;
825
- if (event && event.draggable !== false) {
826
- this.startDragFromTouch('move', event, touch.clientX, touch.clientY);
827
- }
828
- }
829
- else if (target.matches('.scheduler-time-slot, .scheduler-timeline-slot') && state.options.selectable) {
830
- // Create operation
831
- this.startDragFromTouch('create', null, touch.clientX, touch.clientY, target);
832
- }
833
- }
834
- handleTouchMove(e) {
835
- if (e.touches.length !== 1) {
836
- this.cancelTouchHold();
837
- return;
838
- }
839
- const touch = e.touches[0];
840
- // If we have a pending touch hold, check if user moved too much
841
- if (this.touchHoldTimer && this.touchStartPosition) {
842
- const dx = touch.clientX - this.touchStartPosition.x;
843
- const dy = touch.clientY - this.touchStartPosition.y;
844
- const distance = Math.sqrt(dx * dx + dy * dy);
845
- if (distance > this.TOUCH_MOVE_THRESHOLD) {
846
- // User moved too much, cancel the hold and allow normal scrolling
847
- this.cancelTouchHold();
848
- return;
849
- }
850
- // Still waiting for hold, prevent default to avoid scroll during hold detection
851
- // But only if we're close to the threshold
852
- if (distance < this.TOUCH_MOVE_THRESHOLD / 2) {
853
- // Don't prevent default yet to allow small movements
854
- }
855
- return;
856
- }
857
- // If in touch drag mode, handle the drag
858
- if (this.isTouchDragMode) {
859
- e.preventDefault(); // Prevent scrolling while dragging
860
- const state = this.stateManager.getState();
861
- if (!state.dragState)
862
- return;
863
- const slot = this.getSlotAtPosition(touch.clientX, touch.clientY);
864
- if (!slot)
865
- return;
866
- const preview = this.calculatePreview(state.dragState.type, state.dragState, slot);
867
- if (preview) {
868
- this.stateManager.updateDrag(slot, preview);
869
- }
870
- }
871
- }
872
- handleTouchEnd(e) {
873
- // If we had a pending hold that never activated, treat as a tap
874
- if (this.touchHoldTimer) {
875
- this.cancelTouchHold();
876
- // Handle as a tap/click on the target
877
- if (this.touchHoldTarget) {
878
- const eventEl = this.touchHoldTarget.closest('.scheduler-event:not(.preview)');
879
- if (eventEl) {
880
- const eventId = eventEl.dataset['eventId'];
881
- const event = eventId ? this.getEventById(eventId) : null;
882
- if (event) {
883
- this.stateManager.setSelectedEvent(event);
884
- this.dispatchEvent(new CustomEvent('event-click', {
885
- detail: { event, originalEvent: e },
886
- bubbles: true,
887
- }));
888
- }
889
- }
890
- }
891
- this.touchHoldTarget = null;
892
- return;
893
- }
894
- // If in touch drag mode, finalize the drag
895
- if (this.isTouchDragMode) {
896
- const state = this.stateManager.getState();
897
- if (state.dragState) {
898
- const { type, event, preview } = state.dragState;
899
- if (preview) {
900
- if (type === 'create') {
901
- const newEvent = {
902
- id: generateEventId(),
903
- title: 'New Event',
904
- start: preview.start,
905
- end: preview.end,
906
- color: '#3788d8',
907
- };
908
- this.stateManager.addEvent(newEvent);
909
- this.dispatchEvent(new CustomEvent('event-create', {
910
- detail: { event: newEvent, originalEvent: e },
911
- bubbles: true,
912
- }));
913
- }
914
- else if (type === 'move' && event) {
915
- const oldEvent = Object.assign({}, event);
916
- const updatedEvent = Object.assign(Object.assign({}, event), { start: preview.start, end: preview.end });
917
- this.stateManager.updateEvent(updatedEvent);
918
- this.dispatchEvent(new CustomEvent('event-update', {
919
- detail: { event: updatedEvent, oldEvent, originalEvent: e },
920
- bubbles: true,
921
- }));
922
- }
923
- else if ((type === 'resize-start' || type === 'resize-end') && event) {
924
- const oldEvent = Object.assign({}, event);
925
- const updatedEvent = Object.assign(Object.assign({}, event), { start: preview.start, end: preview.end });
926
- this.stateManager.updateEvent(updatedEvent);
927
- this.dispatchEvent(new CustomEvent('event-update', {
928
- detail: { event: updatedEvent, oldEvent, originalEvent: e },
929
- bubbles: true,
930
- }));
931
- }
932
- }
933
- this.stateManager.endDrag();
934
- }
935
- this.exitTouchDragMode();
936
- }
937
- }
938
- handleTouchCancel(_e) {
939
- this.cancelTouchHold();
940
- if (this.isTouchDragMode) {
941
- this.stateManager.endDrag();
942
- this.exitTouchDragMode();
943
- }
944
- }
945
- cancelTouchHold() {
946
- if (this.touchHoldTimer) {
947
- clearTimeout(this.touchHoldTimer);
948
- this.touchHoldTimer = null;
949
- }
950
- // Remove pending visual feedback
951
- if (this.touchHoldTarget) {
952
- this.touchHoldTarget.classList.remove('touch-hold-pending');
953
- this.touchHoldTarget.classList.remove('touch-hold-active');
954
- }
955
- this.touchStartPosition = null;
956
- }
957
- exitTouchDragMode() {
958
- this.isTouchDragMode = false;
959
- this.touchStartPosition = null;
960
- this.touchHoldTarget = null;
961
- // Remove visual feedback
962
- const container = this.shadow.querySelector('.scheduler-container');
963
- container === null || container === void 0 ? void 0 : container.classList.remove('touch-drag-mode');
964
- // Remove active classes from all elements
965
- this.shadow.querySelectorAll('.touch-hold-active, .touch-hold-pending').forEach((el) => {
966
- el.classList.remove('touch-hold-active', 'touch-hold-pending');
967
- });
968
- }
969
- triggerHapticFeedback() {
970
- // Use Vibration API if available
971
- if ('vibrate' in navigator) {
972
- navigator.vibrate(50); // Short vibration (50ms)
973
- }
974
- }
975
- startDragFromTouch(type, event, clientX, clientY, slotEl) {
976
- const slot = slotEl
977
- ? this.getSlotFromElement(slotEl)
978
- : this.getSlotAtPosition(clientX, clientY);
979
- if (!slot)
980
- return;
981
- let preview;
982
- if (type === 'create') {
983
- preview = {
984
- start: slot.start,
985
- end: slot.end,
986
- };
987
- }
988
- else if (event) {
989
- preview = {
990
- start: event.start,
991
- end: event.end,
992
- };
993
- }
994
- else {
995
- return;
996
- }
997
- this.stateManager.startDrag({
998
- type,
999
- event,
1000
- startSlot: slot,
1001
- currentSlot: slot,
1002
- preview,
1003
- originalEvent: event ? Object.assign({}, event) : undefined,
1004
- meta: type.startsWith('resize-')
1005
- ? { resizeHandle: type.replace('resize-', '') }
1006
- : undefined,
1007
- });
1008
- }
1009
560
  }
1010
561
  MpScheduler.observedAttributes = [
1011
562
  'view',