@forcecalendar/interface 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/force-calendar-interface.esm.js +257 -16
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js +40 -7
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ForceCalendar.js +41 -0
- package/src/core/DragController.js +362 -0
- package/src/renderers/DayViewRenderer.js +4 -0
- package/src/renderers/MonthViewRenderer.js +4 -0
- package/src/renderers/WeekViewRenderer.js +4 -0
- package/types/core/DragController.d.ts +56 -0
package/package.json
CHANGED
|
@@ -403,6 +403,39 @@ export class ForceCalendar extends BaseComponent {
|
|
|
403
403
|
outline-offset: -2px;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
.fc-event { touch-action: none; }
|
|
407
|
+
|
|
408
|
+
.fc-dragging {
|
|
409
|
+
opacity: 0.6;
|
|
410
|
+
z-index: 30;
|
|
411
|
+
pointer-events: none;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.fc-drop-target {
|
|
415
|
+
outline: 2px dashed var(--fc-primary-color);
|
|
416
|
+
outline-offset: -2px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.fc-resize-handle {
|
|
420
|
+
position: absolute;
|
|
421
|
+
left: 0;
|
|
422
|
+
right: 0;
|
|
423
|
+
bottom: 0;
|
|
424
|
+
height: 6px;
|
|
425
|
+
cursor: ns-resize;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.fc-drag-selection {
|
|
429
|
+
position: absolute;
|
|
430
|
+
left: 2px;
|
|
431
|
+
right: 2px;
|
|
432
|
+
background: color-mix(in srgb, var(--fc-primary-color) 18%, transparent);
|
|
433
|
+
border: 1px solid var(--fc-primary-color);
|
|
434
|
+
border-radius: 4px;
|
|
435
|
+
pointer-events: none;
|
|
436
|
+
z-index: 20;
|
|
437
|
+
}
|
|
438
|
+
|
|
406
439
|
.fc-nav-arrow:focus-visible,
|
|
407
440
|
.fc-btn:focus-visible,
|
|
408
441
|
.fc-btn-today:focus-visible,
|
|
@@ -834,6 +867,14 @@ export class ForceCalendar extends BaseComponent {
|
|
|
834
867
|
}
|
|
835
868
|
});
|
|
836
869
|
|
|
870
|
+
// Drag-to-create emits a range: surface it and open the form prefilled
|
|
871
|
+
this.addListener(this.shadowRoot, 'range-select', e => {
|
|
872
|
+
this.emit('calendar-range-select', e.detail);
|
|
873
|
+
if (modal) {
|
|
874
|
+
modal.open(e.detail.start);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
837
878
|
// Handle event saving
|
|
838
879
|
if (modal) {
|
|
839
880
|
this.addListener(modal, 'save', e => {
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DragController - pointer-driven interactions for calendar views.
|
|
3
|
+
*
|
|
4
|
+
* Implements drag-to-move (month and time grids), drag-to-resize, and
|
|
5
|
+
* drag-to-create using Pointer Events only: no HTML5 drag-and-drop (which
|
|
6
|
+
* misbehaves in shadow roots), no dependencies, Locker Service safe.
|
|
7
|
+
*
|
|
8
|
+
* Keyboard users have equivalent paths via the WAI-ARIA grid navigation
|
|
9
|
+
* (arrow keys + Enter) that every view already implements.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
13
|
+
const SNAP_MINUTES = 15;
|
|
14
|
+
const PX_PER_MINUTE = 1; // time grids render 60px per hour
|
|
15
|
+
|
|
16
|
+
/** Snap a minute offset to the grid. Exported for tests. */
|
|
17
|
+
export function snapMinutes(minutes, snap = SNAP_MINUTES) {
|
|
18
|
+
return Math.round(minutes / snap) * snap;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Clamp a start minute so [start, start+duration] stays inside a day. */
|
|
22
|
+
export function clampStartMinutes(startMinutes, durationMinutes) {
|
|
23
|
+
return Math.max(0, Math.min(startMinutes, 24 * 60 - durationMinutes));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Combine a target day with an original start's time-of-day.
|
|
28
|
+
* Exported for tests.
|
|
29
|
+
*/
|
|
30
|
+
export function moveDatePreservingTime(targetDay, originalStart) {
|
|
31
|
+
const next = new Date(targetDay);
|
|
32
|
+
next.setHours(
|
|
33
|
+
originalStart.getHours(),
|
|
34
|
+
originalStart.getMinutes(),
|
|
35
|
+
originalStart.getSeconds(),
|
|
36
|
+
originalStart.getMilliseconds()
|
|
37
|
+
);
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class DragController {
|
|
42
|
+
constructor(renderer) {
|
|
43
|
+
this.renderer = renderer;
|
|
44
|
+
this.container = renderer.container;
|
|
45
|
+
this.stateManager = renderer.stateManager;
|
|
46
|
+
this._active = null;
|
|
47
|
+
this._docListeners = [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Month view: drag an event chip onto another day cell. */
|
|
51
|
+
enableMonthMove() {
|
|
52
|
+
this.renderer.addListener(this.container, 'pointerdown', e => {
|
|
53
|
+
if (e.button !== 0) return;
|
|
54
|
+
const eventEl = e.target.closest('.fc-event');
|
|
55
|
+
if (!eventEl || !this.container.contains(eventEl)) return;
|
|
56
|
+
const originCell = eventEl.closest('.fc-month-day');
|
|
57
|
+
if (!originCell) return;
|
|
58
|
+
|
|
59
|
+
this._arm(e, {
|
|
60
|
+
mode: 'month-move',
|
|
61
|
+
eventEl,
|
|
62
|
+
eventId: eventEl.dataset.eventId,
|
|
63
|
+
onDragMove: ev => this._monthDragMove(ev),
|
|
64
|
+
onDrop: ev => this._monthDrop(ev)
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Time grids (week/day): drag events vertically/across columns to move,
|
|
71
|
+
* drag the bottom handle to resize, drag empty grid to create.
|
|
72
|
+
*/
|
|
73
|
+
enableTimeGrid(columnSelector) {
|
|
74
|
+
this._columnSelector = columnSelector;
|
|
75
|
+
this._injectResizeHandles();
|
|
76
|
+
|
|
77
|
+
this.renderer.addListener(this.container, 'pointerdown', e => {
|
|
78
|
+
if (e.button !== 0) return;
|
|
79
|
+
|
|
80
|
+
const handle = e.target.closest('.fc-resize-handle');
|
|
81
|
+
if (handle) {
|
|
82
|
+
const eventEl = handle.closest('.fc-timed-event');
|
|
83
|
+
if (!eventEl) return;
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
this._arm(e, {
|
|
86
|
+
mode: 'resize',
|
|
87
|
+
eventEl,
|
|
88
|
+
eventId: eventEl.dataset.eventId,
|
|
89
|
+
originTop: parseFloat(eventEl.style.top) || 0,
|
|
90
|
+
originHeight: parseFloat(eventEl.style.height) || 30,
|
|
91
|
+
onDragMove: ev => this._resizeDragMove(ev),
|
|
92
|
+
onDrop: () => this._resizeDrop()
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const eventEl = e.target.closest('.fc-timed-event');
|
|
98
|
+
if (eventEl && this.container.contains(eventEl)) {
|
|
99
|
+
this._arm(e, {
|
|
100
|
+
mode: 'time-move',
|
|
101
|
+
eventEl,
|
|
102
|
+
eventId: eventEl.dataset.eventId,
|
|
103
|
+
originTop: parseFloat(eventEl.style.top) || 0,
|
|
104
|
+
originColumn: eventEl.closest(columnSelector),
|
|
105
|
+
onDragMove: ev => this._timeMoveDragMove(ev),
|
|
106
|
+
onDrop: () => this._timeMoveDrop()
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const column = e.target.closest(columnSelector);
|
|
112
|
+
if (column && !e.target.closest('.fc-event')) {
|
|
113
|
+
this._arm(e, {
|
|
114
|
+
mode: 'create',
|
|
115
|
+
column,
|
|
116
|
+
onDragMove: ev => this._createDragMove(ev),
|
|
117
|
+
onDrop: () => this._createDrop()
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Track from pointerdown; promote to a drag past the threshold. */
|
|
124
|
+
_arm(e, spec) {
|
|
125
|
+
this._active = {
|
|
126
|
+
...spec,
|
|
127
|
+
startX: e.clientX,
|
|
128
|
+
startY: e.clientY,
|
|
129
|
+
dragging: false
|
|
130
|
+
};
|
|
131
|
+
const move = ev => this._onPointerMove(ev);
|
|
132
|
+
const up = ev => this._onPointerUp(ev);
|
|
133
|
+
const cancel = () => this._cancel();
|
|
134
|
+
const key = ev => {
|
|
135
|
+
if (ev.key === 'Escape') this._cancel();
|
|
136
|
+
};
|
|
137
|
+
const doc = this.container.ownerDocument;
|
|
138
|
+
doc.addEventListener('pointermove', move);
|
|
139
|
+
doc.addEventListener('pointerup', up);
|
|
140
|
+
doc.addEventListener('pointercancel', cancel);
|
|
141
|
+
doc.addEventListener('keydown', key);
|
|
142
|
+
this._docListeners = [
|
|
143
|
+
['pointermove', move],
|
|
144
|
+
['pointerup', up],
|
|
145
|
+
['pointercancel', cancel],
|
|
146
|
+
['keydown', key]
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_onPointerMove(e) {
|
|
151
|
+
const a = this._active;
|
|
152
|
+
if (!a) return;
|
|
153
|
+
if (!a.dragging) {
|
|
154
|
+
if (
|
|
155
|
+
Math.abs(e.clientX - a.startX) < DRAG_THRESHOLD_PX &&
|
|
156
|
+
Math.abs(e.clientY - a.startY) < DRAG_THRESHOLD_PX
|
|
157
|
+
) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
a.dragging = true;
|
|
161
|
+
if (a.eventEl) {
|
|
162
|
+
a.eventEl.classList.add('fc-dragging');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
a.onDragMove(e);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_onPointerUp(e) {
|
|
170
|
+
const a = this._active;
|
|
171
|
+
this._teardownDocListeners();
|
|
172
|
+
if (!a) return;
|
|
173
|
+
if (a.dragging) {
|
|
174
|
+
// Swallow the click that follows a drag so it doesn't select/open
|
|
175
|
+
const doc = this.container.ownerDocument;
|
|
176
|
+
const swallow = ev => {
|
|
177
|
+
ev.stopPropagation();
|
|
178
|
+
ev.preventDefault();
|
|
179
|
+
};
|
|
180
|
+
doc.addEventListener('click', swallow, { capture: true, once: true });
|
|
181
|
+
setTimeout(() => doc.removeEventListener('click', swallow, { capture: true }), 0);
|
|
182
|
+
a.onDrop(e);
|
|
183
|
+
}
|
|
184
|
+
this._cleanupVisuals(a);
|
|
185
|
+
this._active = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_cancel() {
|
|
189
|
+
const a = this._active;
|
|
190
|
+
this._teardownDocListeners();
|
|
191
|
+
this._active = null;
|
|
192
|
+
if (a) this._cleanupVisuals(a);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_teardownDocListeners() {
|
|
196
|
+
const doc = this.container.ownerDocument;
|
|
197
|
+
for (const [name, fn] of this._docListeners) {
|
|
198
|
+
doc.removeEventListener(name, fn);
|
|
199
|
+
}
|
|
200
|
+
this._docListeners = [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_cleanupVisuals(a) {
|
|
204
|
+
if (a.eventEl) {
|
|
205
|
+
a.eventEl.classList.remove('fc-dragging');
|
|
206
|
+
a.eventEl.style.transform = '';
|
|
207
|
+
}
|
|
208
|
+
this.container
|
|
209
|
+
.querySelectorAll('.fc-drop-target')
|
|
210
|
+
.forEach(el => el.classList.remove('fc-drop-target'));
|
|
211
|
+
this._selectionEl?.remove();
|
|
212
|
+
this._selectionEl = null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ----- month move -----
|
|
216
|
+
|
|
217
|
+
_cellAtPoint(x, y) {
|
|
218
|
+
for (const cell of this.container.querySelectorAll('.fc-month-day')) {
|
|
219
|
+
const r = cell.getBoundingClientRect();
|
|
220
|
+
if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) return cell;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_monthDragMove(e) {
|
|
226
|
+
const a = this._active;
|
|
227
|
+
a.eventEl.style.transform = `translate(${e.clientX - a.startX}px, ${e.clientY - a.startY}px)`;
|
|
228
|
+
const cell = this._cellAtPoint(e.clientX, e.clientY);
|
|
229
|
+
if (cell !== a.dropCell) {
|
|
230
|
+
a.dropCell?.classList.remove('fc-drop-target');
|
|
231
|
+
cell?.classList.add('fc-drop-target');
|
|
232
|
+
a.dropCell = cell;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_monthDrop() {
|
|
237
|
+
const a = this._active ?? {};
|
|
238
|
+
const cell = a.dropCell;
|
|
239
|
+
if (!cell) return;
|
|
240
|
+
const event = this.stateManager.getEvents().find(ev => ev.id === a.eventId);
|
|
241
|
+
if (!event) return;
|
|
242
|
+
const oldStart = new Date(event.start);
|
|
243
|
+
const newStart = moveDatePreservingTime(new Date(cell.dataset.date), oldStart);
|
|
244
|
+
const delta = newStart.getTime() - oldStart.getTime();
|
|
245
|
+
if (delta === 0) return;
|
|
246
|
+
this.stateManager.updateEvent(a.eventId, {
|
|
247
|
+
start: newStart,
|
|
248
|
+
end: new Date(new Date(event.end).getTime() + delta)
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ----- time-grid move -----
|
|
253
|
+
|
|
254
|
+
_columnAtPoint(x) {
|
|
255
|
+
for (const col of this.container.querySelectorAll(this._columnSelector)) {
|
|
256
|
+
const r = col.getBoundingClientRect();
|
|
257
|
+
if (x >= r.left && x < r.right) return col;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_timeMoveDragMove(e) {
|
|
263
|
+
const a = this._active;
|
|
264
|
+
const col = this._columnAtPoint(e.clientX) || a.originColumn;
|
|
265
|
+
a.dropColumn = col;
|
|
266
|
+
a.deltaMinutes = snapMinutes((e.clientY - a.startY) / PX_PER_MINUTE);
|
|
267
|
+
const originRect = a.originColumn.getBoundingClientRect();
|
|
268
|
+
const colRect = col.getBoundingClientRect();
|
|
269
|
+
a.eventEl.style.transform = `translate(${colRect.left - originRect.left}px, ${a.deltaMinutes * PX_PER_MINUTE}px)`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_timeMoveDrop() {
|
|
273
|
+
const a = this._active ?? {};
|
|
274
|
+
const event = this.stateManager.getEvents().find(ev => ev.id === a.eventId);
|
|
275
|
+
if (!event || (!a.deltaMinutes && a.dropColumn === a.originColumn)) return;
|
|
276
|
+
|
|
277
|
+
const oldStart = new Date(event.start);
|
|
278
|
+
const duration = new Date(event.end).getTime() - oldStart.getTime();
|
|
279
|
+
const targetDay = a.dropColumn ? new Date(a.dropColumn.dataset.date) : oldStart;
|
|
280
|
+
const dayAligned = moveDatePreservingTime(targetDay, oldStart);
|
|
281
|
+
const startMinutes = clampStartMinutes(
|
|
282
|
+
dayAligned.getHours() * 60 + dayAligned.getMinutes() + (a.deltaMinutes || 0),
|
|
283
|
+
Math.round(duration / 60000)
|
|
284
|
+
);
|
|
285
|
+
const newStart = new Date(dayAligned);
|
|
286
|
+
newStart.setHours(Math.floor(startMinutes / 60), startMinutes % 60, 0, 0);
|
|
287
|
+
if (newStart.getTime() === oldStart.getTime()) return;
|
|
288
|
+
this.stateManager.updateEvent(a.eventId, {
|
|
289
|
+
start: newStart,
|
|
290
|
+
end: new Date(newStart.getTime() + duration)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ----- resize -----
|
|
295
|
+
|
|
296
|
+
_resizeDragMove(e) {
|
|
297
|
+
const a = this._active;
|
|
298
|
+
const deltaMinutes = snapMinutes((e.clientY - a.startY) / PX_PER_MINUTE);
|
|
299
|
+
a.newHeight = Math.max(SNAP_MINUTES, a.originHeight + deltaMinutes);
|
|
300
|
+
a.newHeight = Math.min(a.newHeight, 24 * 60 - a.originTop);
|
|
301
|
+
a.eventEl.style.height = `${a.newHeight}px`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
_resizeDrop() {
|
|
305
|
+
const a = this._active ?? {};
|
|
306
|
+
if (!a.newHeight || a.newHeight === a.originHeight) return;
|
|
307
|
+
const event = this.stateManager.getEvents().find(ev => ev.id === a.eventId);
|
|
308
|
+
if (!event) return;
|
|
309
|
+
const newEnd = new Date(new Date(event.start).getTime() + a.newHeight * 60000);
|
|
310
|
+
this.stateManager.updateEvent(a.eventId, { end: newEnd });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ----- drag-to-create -----
|
|
314
|
+
|
|
315
|
+
_createDragMove(e) {
|
|
316
|
+
const a = this._active;
|
|
317
|
+
const colRect = a.column.getBoundingClientRect();
|
|
318
|
+
const fromY = a.startY - colRect.top;
|
|
319
|
+
const toY = e.clientY - colRect.top;
|
|
320
|
+
a.fromMinutes = snapMinutes(Math.min(fromY, toY) / PX_PER_MINUTE);
|
|
321
|
+
a.toMinutes = snapMinutes(Math.max(fromY, toY) / PX_PER_MINUTE);
|
|
322
|
+
a.fromMinutes = Math.max(0, a.fromMinutes);
|
|
323
|
+
a.toMinutes = Math.min(24 * 60, Math.max(a.toMinutes, a.fromMinutes + SNAP_MINUTES));
|
|
324
|
+
|
|
325
|
+
if (!this._selectionEl) {
|
|
326
|
+
this._selectionEl = this.container.ownerDocument.createElement('div');
|
|
327
|
+
this._selectionEl.className = 'fc-drag-selection';
|
|
328
|
+
a.column.appendChild(this._selectionEl);
|
|
329
|
+
}
|
|
330
|
+
this._selectionEl.style.top = `${a.fromMinutes * PX_PER_MINUTE}px`;
|
|
331
|
+
this._selectionEl.style.height = `${(a.toMinutes - a.fromMinutes) * PX_PER_MINUTE}px`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_createDrop() {
|
|
335
|
+
const a = this._active ?? {};
|
|
336
|
+
if (a.fromMinutes == null) return;
|
|
337
|
+
const start = new Date(a.column.dataset.date);
|
|
338
|
+
start.setHours(Math.floor(a.fromMinutes / 60), a.fromMinutes % 60, 0, 0);
|
|
339
|
+
const end = new Date(a.column.dataset.date);
|
|
340
|
+
end.setHours(Math.floor(a.toMinutes / 60), a.toMinutes % 60, 0, 0);
|
|
341
|
+
this.container.dispatchEvent(
|
|
342
|
+
new CustomEvent('range-select', {
|
|
343
|
+
detail: { start, end },
|
|
344
|
+
bubbles: true,
|
|
345
|
+
composed: true
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Append a resize handle to every timed event (idempotent per render). */
|
|
351
|
+
_injectResizeHandles() {
|
|
352
|
+
for (const el of this.container.querySelectorAll('.fc-timed-event')) {
|
|
353
|
+
if (el.querySelector('.fc-resize-handle')) continue;
|
|
354
|
+
const handle = this.container.ownerDocument.createElement('div');
|
|
355
|
+
handle.className = 'fc-resize-handle';
|
|
356
|
+
handle.setAttribute('aria-hidden', 'true');
|
|
357
|
+
el.appendChild(handle);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export default DragController;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { BaseViewRenderer } from './BaseViewRenderer.js';
|
|
8
|
+
import { DragController } from '../core/DragController.js';
|
|
8
9
|
import { DateUtils } from '../utils/DateUtils.js';
|
|
9
10
|
|
|
10
11
|
export class DayViewRenderer extends BaseViewRenderer {
|
|
@@ -216,6 +217,9 @@ export class DayViewRenderer extends BaseViewRenderer {
|
|
|
216
217
|
const current = this.stateManager.getState().currentDate || new Date();
|
|
217
218
|
const label = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }).format(current);
|
|
218
219
|
this._enhanceTimeGridAccessibility('.fc-day-column', label);
|
|
220
|
+
|
|
221
|
+
// Drag to move/resize events and drag empty grid to create
|
|
222
|
+
new DragController(this).enableTimeGrid('.fc-day-column');
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
_scrollToCurrentTime() {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { BaseViewRenderer } from './BaseViewRenderer.js';
|
|
8
8
|
import { DateUtils } from '../utils/DateUtils.js';
|
|
9
|
+
import { DragController } from '../core/DragController.js';
|
|
9
10
|
|
|
10
11
|
export class MonthViewRenderer extends BaseViewRenderer {
|
|
11
12
|
constructor(container, stateManager) {
|
|
@@ -228,6 +229,9 @@ export class MonthViewRenderer extends BaseViewRenderer {
|
|
|
228
229
|
|
|
229
230
|
// Common event handlers (event clicks)
|
|
230
231
|
this.attachCommonEventHandlers();
|
|
232
|
+
|
|
233
|
+
// Drag an event chip onto another day to move it
|
|
234
|
+
new DragController(this).enableMonthMove();
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { BaseViewRenderer } from './BaseViewRenderer.js';
|
|
8
8
|
import { DateUtils } from '../utils/DateUtils.js';
|
|
9
|
+
import { DragController } from '../core/DragController.js';
|
|
9
10
|
|
|
10
11
|
export class WeekViewRenderer extends BaseViewRenderer {
|
|
11
12
|
constructor(container, stateManager) {
|
|
@@ -196,6 +197,9 @@ export class WeekViewRenderer extends BaseViewRenderer {
|
|
|
196
197
|
const first = days[0] ? new Date(days[0].date) : new Date();
|
|
197
198
|
const label = `Week of ${new Intl.DateTimeFormat(locale, { month: 'long', day: 'numeric', year: 'numeric' }).format(first)}`;
|
|
198
199
|
this._enhanceTimeGridAccessibility('.fc-week-day-column', label);
|
|
200
|
+
|
|
201
|
+
// Drag to move/resize events and drag empty grid to create
|
|
202
|
+
new DragController(this).enableTimeGrid('.fc-week-day-column');
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
_scrollToCurrentTime() {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DragController - pointer-driven interactions for calendar views.
|
|
3
|
+
*
|
|
4
|
+
* Implements drag-to-move (month and time grids), drag-to-resize, and
|
|
5
|
+
* drag-to-create using Pointer Events only: no HTML5 drag-and-drop (which
|
|
6
|
+
* misbehaves in shadow roots), no dependencies, Locker Service safe.
|
|
7
|
+
*
|
|
8
|
+
* Keyboard users have equivalent paths via the WAI-ARIA grid navigation
|
|
9
|
+
* (arrow keys + Enter) that every view already implements.
|
|
10
|
+
*/
|
|
11
|
+
/** Snap a minute offset to the grid. Exported for tests. */
|
|
12
|
+
export declare function snapMinutes(minutes: any, snap?: number): number;
|
|
13
|
+
/** Clamp a start minute so [start, start+duration] stays inside a day. */
|
|
14
|
+
export declare function clampStartMinutes(startMinutes: any, durationMinutes: any): number;
|
|
15
|
+
/**
|
|
16
|
+
* Combine a target day with an original start's time-of-day.
|
|
17
|
+
* Exported for tests.
|
|
18
|
+
*/
|
|
19
|
+
export declare function moveDatePreservingTime(targetDay: any, originalStart: any): Date;
|
|
20
|
+
export declare class DragController {
|
|
21
|
+
renderer: any;
|
|
22
|
+
container: any;
|
|
23
|
+
stateManager: any;
|
|
24
|
+
_active: any;
|
|
25
|
+
_docListeners: any[];
|
|
26
|
+
_columnSelector: any;
|
|
27
|
+
_selectionEl: any;
|
|
28
|
+
constructor(renderer: any);
|
|
29
|
+
/** Month view: drag an event chip onto another day cell. */
|
|
30
|
+
enableMonthMove(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Time grids (week/day): drag events vertically/across columns to move,
|
|
33
|
+
* drag the bottom handle to resize, drag empty grid to create.
|
|
34
|
+
*/
|
|
35
|
+
enableTimeGrid(columnSelector: any): void;
|
|
36
|
+
/** Track from pointerdown; promote to a drag past the threshold. */
|
|
37
|
+
_arm(e: any, spec: any): void;
|
|
38
|
+
_onPointerMove(e: any): void;
|
|
39
|
+
_onPointerUp(e: any): void;
|
|
40
|
+
_cancel(): void;
|
|
41
|
+
_teardownDocListeners(): void;
|
|
42
|
+
_cleanupVisuals(a: any): void;
|
|
43
|
+
_cellAtPoint(x: any, y: any): any;
|
|
44
|
+
_monthDragMove(e: any): void;
|
|
45
|
+
_monthDrop(): void;
|
|
46
|
+
_columnAtPoint(x: any): any;
|
|
47
|
+
_timeMoveDragMove(e: any): void;
|
|
48
|
+
_timeMoveDrop(): void;
|
|
49
|
+
_resizeDragMove(e: any): void;
|
|
50
|
+
_resizeDrop(): void;
|
|
51
|
+
_createDragMove(e: any): void;
|
|
52
|
+
_createDrop(): void;
|
|
53
|
+
/** Append a resize handle to every timed event (idempotent per render). */
|
|
54
|
+
_injectResizeHandles(): void;
|
|
55
|
+
}
|
|
56
|
+
export default DragController;
|