diff options
Diffstat (limited to 'public/js/fullcalendar/packages/interaction/index.global.js')
-rw-r--r-- | public/js/fullcalendar/packages/interaction/index.global.js | 2141 |
1 files changed, 2141 insertions, 0 deletions
diff --git a/public/js/fullcalendar/packages/interaction/index.global.js b/public/js/fullcalendar/packages/interaction/index.global.js new file mode 100644 index 0000000..d05a22e --- /dev/null +++ b/public/js/fullcalendar/packages/interaction/index.global.js | |||
@@ -0,0 +1,2141 @@ | |||
1 | /*! | ||
2 | FullCalendar Interaction Plugin v6.1.17 | ||
3 | Docs & License: https://fullcalendar.io/docs/editable | ||
4 | (c) 2024 Adam Shaw | ||
5 | */ | ||
6 | FullCalendar.Interaction = (function (exports, core, internal) { | ||
7 | 'use strict'; | ||
8 | |||
9 | internal.config.touchMouseIgnoreWait = 500; | ||
10 | let ignoreMouseDepth = 0; | ||
11 | let listenerCnt = 0; | ||
12 | let isWindowTouchMoveCancelled = false; | ||
13 | /* | ||
14 | Uses a "pointer" abstraction, which monitors UI events for both mouse and touch. | ||
15 | Tracks when the pointer "drags" on a certain element, meaning down+move+up. | ||
16 | |||
17 | Also, tracks if there was touch-scrolling. | ||
18 | Also, can prevent touch-scrolling from happening. | ||
19 | Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement. | ||
20 | |||
21 | emits: | ||
22 | - pointerdown | ||
23 | - pointermove | ||
24 | - pointerup | ||
25 | */ | ||
26 | class PointerDragging { | ||
27 | constructor(containerEl) { | ||
28 | this.subjectEl = null; | ||
29 | // options that can be directly assigned by caller | ||
30 | this.selector = ''; // will cause subjectEl in all emitted events to be this element | ||
31 | this.handleSelector = ''; | ||
32 | this.shouldIgnoreMove = false; | ||
33 | this.shouldWatchScroll = true; // for simulating pointermove on scroll | ||
34 | // internal states | ||
35 | this.isDragging = false; | ||
36 | this.isTouchDragging = false; | ||
37 | this.wasTouchScroll = false; | ||
38 | // Mouse | ||
39 | // ---------------------------------------------------------------------------------------------------- | ||
40 | this.handleMouseDown = (ev) => { | ||
41 | if (!this.shouldIgnoreMouse() && | ||
42 | isPrimaryMouseButton(ev) && | ||
43 | this.tryStart(ev)) { | ||
44 | let pev = this.createEventFromMouse(ev, true); | ||
45 | this.emitter.trigger('pointerdown', pev); | ||
46 | this.initScrollWatch(pev); | ||
47 | if (!this.shouldIgnoreMove) { | ||
48 | document.addEventListener('mousemove', this.handleMouseMove); | ||
49 | } | ||
50 | document.addEventListener('mouseup', this.handleMouseUp); | ||
51 | } | ||
52 | }; | ||
53 | this.handleMouseMove = (ev) => { | ||
54 | let pev = this.createEventFromMouse(ev); | ||
55 | this.recordCoords(pev); | ||
56 | this.emitter.trigger('pointermove', pev); | ||
57 | }; | ||
58 | this.handleMouseUp = (ev) => { | ||
59 | document.removeEventListener('mousemove', this.handleMouseMove); | ||
60 | document.removeEventListener('mouseup', this.handleMouseUp); | ||
61 | this.emitter.trigger('pointerup', this.createEventFromMouse(ev)); | ||
62 | this.cleanup(); // call last so that pointerup has access to props | ||
63 | }; | ||
64 | // Touch | ||
65 | // ---------------------------------------------------------------------------------------------------- | ||
66 | this.handleTouchStart = (ev) => { | ||
67 | if (this.tryStart(ev)) { | ||
68 | this.isTouchDragging = true; | ||
69 | let pev = this.createEventFromTouch(ev, true); | ||
70 | this.emitter.trigger('pointerdown', pev); | ||
71 | this.initScrollWatch(pev); | ||
72 | // unlike mouse, need to attach to target, not document | ||
73 | // https://stackoverflow.com/a/45760014 | ||
74 | let targetEl = ev.target; | ||
75 | if (!this.shouldIgnoreMove) { | ||
76 | targetEl.addEventListener('touchmove', this.handleTouchMove); | ||
77 | } | ||
78 | targetEl.addEventListener('touchend', this.handleTouchEnd); | ||
79 | targetEl.addEventListener('touchcancel', this.handleTouchEnd); // treat it as a touch end | ||
80 | // attach a handler to get called when ANY scroll action happens on the page. | ||
81 | // this was impossible to do with normal on/off because 'scroll' doesn't bubble. | ||
82 | // http://stackoverflow.com/a/32954565/96342 | ||
83 | window.addEventListener('scroll', this.handleTouchScroll, true); | ||
84 | } | ||
85 | }; | ||
86 | this.handleTouchMove = (ev) => { | ||
87 | let pev = this.createEventFromTouch(ev); | ||
88 | this.recordCoords(pev); | ||
89 | this.emitter.trigger('pointermove', pev); | ||
90 | }; | ||
91 | this.handleTouchEnd = (ev) => { | ||
92 | if (this.isDragging) { // done to guard against touchend followed by touchcancel | ||
93 | let targetEl = ev.target; | ||
94 | targetEl.removeEventListener('touchmove', this.handleTouchMove); | ||
95 | targetEl.removeEventListener('touchend', this.handleTouchEnd); | ||
96 | targetEl.removeEventListener('touchcancel', this.handleTouchEnd); | ||
97 | window.removeEventListener('scroll', this.handleTouchScroll, true); // useCaptured=true | ||
98 | this.emitter.trigger('pointerup', this.createEventFromTouch(ev)); | ||
99 | this.cleanup(); // call last so that pointerup has access to props | ||
100 | this.isTouchDragging = false; | ||
101 | startIgnoringMouse(); | ||
102 | } | ||
103 | }; | ||
104 | this.handleTouchScroll = () => { | ||
105 | this.wasTouchScroll = true; | ||
106 | }; | ||
107 | this.handleScroll = (ev) => { | ||
108 | if (!this.shouldIgnoreMove) { | ||
109 | let pageX = (window.scrollX - this.prevScrollX) + this.prevPageX; | ||
110 | let pageY = (window.scrollY - this.prevScrollY) + this.prevPageY; | ||
111 | this.emitter.trigger('pointermove', { | ||
112 | origEvent: ev, | ||
113 | isTouch: this.isTouchDragging, | ||
114 | subjectEl: this.subjectEl, | ||
115 | pageX, | ||
116 | pageY, | ||
117 | deltaX: pageX - this.origPageX, | ||
118 | deltaY: pageY - this.origPageY, | ||
119 | }); | ||
120 | } | ||
121 | }; | ||
122 | this.containerEl = containerEl; | ||
123 | this.emitter = new internal.Emitter(); | ||
124 | containerEl.addEventListener('mousedown', this.handleMouseDown); | ||
125 | containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true }); | ||
126 | listenerCreated(); | ||
127 | } | ||
128 | destroy() { | ||
129 | this.containerEl.removeEventListener('mousedown', this.handleMouseDown); | ||
130 | this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true }); | ||
131 | listenerDestroyed(); | ||
132 | } | ||
133 | tryStart(ev) { | ||
134 | let subjectEl = this.querySubjectEl(ev); | ||
135 | let downEl = ev.target; | ||
136 | if (subjectEl && | ||
137 | (!this.handleSelector || internal.elementClosest(downEl, this.handleSelector))) { | ||
138 | this.subjectEl = subjectEl; | ||
139 | this.isDragging = true; // do this first so cancelTouchScroll will work | ||
140 | this.wasTouchScroll = false; | ||
141 | return true; | ||
142 | } | ||
143 | return false; | ||
144 | } | ||
145 | cleanup() { | ||
146 | isWindowTouchMoveCancelled = false; | ||
147 | this.isDragging = false; | ||
148 | this.subjectEl = null; | ||
149 | // keep wasTouchScroll around for later access | ||
150 | this.destroyScrollWatch(); | ||
151 | } | ||
152 | querySubjectEl(ev) { | ||
153 | if (this.selector) { | ||
154 | return internal.elementClosest(ev.target, this.selector); | ||
155 | } | ||
156 | return this.containerEl; | ||
157 | } | ||
158 | shouldIgnoreMouse() { | ||
159 | return ignoreMouseDepth || this.isTouchDragging; | ||
160 | } | ||
161 | // can be called by user of this class, to cancel touch-based scrolling for the current drag | ||
162 | cancelTouchScroll() { | ||
163 | if (this.isDragging) { | ||
164 | isWindowTouchMoveCancelled = true; | ||
165 | } | ||
166 | } | ||
167 | // Scrolling that simulates pointermoves | ||
168 | // ---------------------------------------------------------------------------------------------------- | ||
169 | initScrollWatch(ev) { | ||
170 | if (this.shouldWatchScroll) { | ||
171 | this.recordCoords(ev); | ||
172 | window.addEventListener('scroll', this.handleScroll, true); // useCapture=true | ||
173 | } | ||
174 | } | ||
175 | recordCoords(ev) { | ||
176 | if (this.shouldWatchScroll) { | ||
177 | this.prevPageX = ev.pageX; | ||
178 | this.prevPageY = ev.pageY; | ||
179 | this.prevScrollX = window.scrollX; | ||
180 | this.prevScrollY = window.scrollY; | ||
181 | } | ||
182 | } | ||
183 | destroyScrollWatch() { | ||
184 | if (this.shouldWatchScroll) { | ||
185 | window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true | ||
186 | } | ||
187 | } | ||
188 | // Event Normalization | ||
189 | // ---------------------------------------------------------------------------------------------------- | ||
190 | createEventFromMouse(ev, isFirst) { | ||
191 | let deltaX = 0; | ||
192 | let deltaY = 0; | ||
193 | // TODO: repeat code | ||
194 | if (isFirst) { | ||
195 | this.origPageX = ev.pageX; | ||
196 | this.origPageY = ev.pageY; | ||
197 | } | ||
198 | else { | ||
199 | deltaX = ev.pageX - this.origPageX; | ||
200 | deltaY = ev.pageY - this.origPageY; | ||
201 | } | ||
202 | return { | ||
203 | origEvent: ev, | ||
204 | isTouch: false, | ||
205 | subjectEl: this.subjectEl, | ||
206 | pageX: ev.pageX, | ||
207 | pageY: ev.pageY, | ||
208 | deltaX, | ||
209 | deltaY, | ||
210 | }; | ||
211 | } | ||
212 | createEventFromTouch(ev, isFirst) { | ||
213 | let touches = ev.touches; | ||
214 | let pageX; | ||
215 | let pageY; | ||
216 | let deltaX = 0; | ||
217 | let deltaY = 0; | ||
218 | // if touch coords available, prefer, | ||
219 | // because FF would give bad ev.pageX ev.pageY | ||
220 | if (touches && touches.length) { | ||
221 | pageX = touches[0].pageX; | ||
222 | pageY = touches[0].pageY; | ||
223 | } | ||
224 | else { | ||
225 | pageX = ev.pageX; | ||
226 | pageY = ev.pageY; | ||
227 | } | ||
228 | // TODO: repeat code | ||
229 | if (isFirst) { | ||
230 | this.origPageX = pageX; | ||
231 | this.origPageY = pageY; | ||
232 | } | ||
233 | else { | ||
234 | deltaX = pageX - this.origPageX; | ||
235 | deltaY = pageY - this.origPageY; | ||
236 | } | ||
237 | return { | ||
238 | origEvent: ev, | ||
239 | isTouch: true, | ||
240 | subjectEl: this.subjectEl, | ||
241 | pageX, | ||
242 | pageY, | ||
243 | deltaX, | ||
244 | deltaY, | ||
245 | }; | ||
246 | } | ||
247 | } | ||
248 | // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) | ||
249 | function isPrimaryMouseButton(ev) { | ||
250 | return ev.button === 0 && !ev.ctrlKey; | ||
251 | } | ||
252 | // Ignoring fake mouse events generated by touch | ||
253 | // ---------------------------------------------------------------------------------------------------- | ||
254 | function startIgnoringMouse() { | ||
255 | ignoreMouseDepth += 1; | ||
256 | setTimeout(() => { | ||
257 | ignoreMouseDepth -= 1; | ||
258 | }, internal.config.touchMouseIgnoreWait); | ||
259 | } | ||
260 | // We want to attach touchmove as early as possible for Safari | ||
261 | // ---------------------------------------------------------------------------------------------------- | ||
262 | function listenerCreated() { | ||
263 | listenerCnt += 1; | ||
264 | if (listenerCnt === 1) { | ||
265 | window.addEventListener('touchmove', onWindowTouchMove, { passive: false }); | ||
266 | } | ||
267 | } | ||
268 | function listenerDestroyed() { | ||
269 | listenerCnt -= 1; | ||
270 | if (!listenerCnt) { | ||
271 | window.removeEventListener('touchmove', onWindowTouchMove, { passive: false }); | ||
272 | } | ||
273 | } | ||
274 | function onWindowTouchMove(ev) { | ||
275 | if (isWindowTouchMoveCancelled) { | ||
276 | ev.preventDefault(); | ||
277 | } | ||
278 | } | ||
279 | |||
280 | /* | ||
281 | An effect in which an element follows the movement of a pointer across the screen. | ||
282 | The moving element is a clone of some other element. | ||
283 | Must call start + handleMove + stop. | ||
284 | */ | ||
285 | class ElementMirror { | ||
286 | constructor() { | ||
287 | this.isVisible = false; // must be explicitly enabled | ||
288 | this.sourceEl = null; | ||
289 | this.mirrorEl = null; | ||
290 | this.sourceElRect = null; // screen coords relative to viewport | ||
291 | // options that can be set directly by caller | ||
292 | this.parentNode = document.body; // HIGHLY SUGGESTED to set this to sidestep ShadowDOM issues | ||
293 | this.zIndex = 9999; | ||
294 | this.revertDuration = 0; | ||
295 | } | ||
296 | start(sourceEl, pageX, pageY) { | ||
297 | this.sourceEl = sourceEl; | ||
298 | this.sourceElRect = this.sourceEl.getBoundingClientRect(); | ||
299 | this.origScreenX = pageX - window.scrollX; | ||
300 | this.origScreenY = pageY - window.scrollY; | ||
301 | this.deltaX = 0; | ||
302 | this.deltaY = 0; | ||
303 | this.updateElPosition(); | ||
304 | } | ||
305 | handleMove(pageX, pageY) { | ||
306 | this.deltaX = (pageX - window.scrollX) - this.origScreenX; | ||
307 | this.deltaY = (pageY - window.scrollY) - this.origScreenY; | ||
308 | this.updateElPosition(); | ||
309 | } | ||
310 | // can be called before start | ||
311 | setIsVisible(bool) { | ||
312 | if (bool) { | ||
313 | if (!this.isVisible) { | ||
314 | if (this.mirrorEl) { | ||
315 | this.mirrorEl.style.display = ''; | ||
316 | } | ||
317 | this.isVisible = bool; // needs to happen before updateElPosition | ||
318 | this.updateElPosition(); // because was not updating the position while invisible | ||
319 | } | ||
320 | } | ||
321 | else if (this.isVisible) { | ||
322 | if (this.mirrorEl) { | ||
323 | this.mirrorEl.style.display = 'none'; | ||
324 | } | ||
325 | this.isVisible = bool; | ||
326 | } | ||
327 | } | ||
328 | // always async | ||
329 | stop(needsRevertAnimation, callback) { | ||
330 | let done = () => { | ||
331 | this.cleanup(); | ||
332 | callback(); | ||
333 | }; | ||
334 | if (needsRevertAnimation && | ||
335 | this.mirrorEl && | ||
336 | this.isVisible && | ||
337 | this.revertDuration && // if 0, transition won't work | ||
338 | (this.deltaX || this.deltaY) // if same coords, transition won't work | ||
339 | ) { | ||
340 | this.doRevertAnimation(done, this.revertDuration); | ||
341 | } | ||
342 | else { | ||
343 | setTimeout(done, 0); | ||
344 | } | ||
345 | } | ||
346 | doRevertAnimation(callback, revertDuration) { | ||
347 | let mirrorEl = this.mirrorEl; | ||
348 | let finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened | ||
349 | mirrorEl.style.transition = | ||
350 | 'top ' + revertDuration + 'ms,' + | ||
351 | 'left ' + revertDuration + 'ms'; | ||
352 | internal.applyStyle(mirrorEl, { | ||
353 | left: finalSourceElRect.left, | ||
354 | top: finalSourceElRect.top, | ||
355 | }); | ||
356 | internal.whenTransitionDone(mirrorEl, () => { | ||
357 | mirrorEl.style.transition = ''; | ||
358 | callback(); | ||
359 | }); | ||
360 | } | ||
361 | cleanup() { | ||
362 | if (this.mirrorEl) { | ||
363 | internal.removeElement(this.mirrorEl); | ||
364 | this.mirrorEl = null; | ||
365 | } | ||
366 | this.sourceEl = null; | ||
367 | } | ||
368 | updateElPosition() { | ||
369 | if (this.sourceEl && this.isVisible) { | ||
370 | internal.applyStyle(this.getMirrorEl(), { | ||
371 | left: this.sourceElRect.left + this.deltaX, | ||
372 | top: this.sourceElRect.top + this.deltaY, | ||
373 | }); | ||
374 | } | ||
375 | } | ||
376 | getMirrorEl() { | ||
377 | let sourceElRect = this.sourceElRect; | ||
378 | let mirrorEl = this.mirrorEl; | ||
379 | if (!mirrorEl) { | ||
380 | mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true | ||
381 | // we don't want long taps or any mouse interaction causing selection/menus. | ||
382 | // would use preventSelection(), but that prevents selectstart, causing problems. | ||
383 | mirrorEl.style.userSelect = 'none'; | ||
384 | mirrorEl.style.webkitUserSelect = 'none'; | ||
385 | mirrorEl.style.pointerEvents = 'none'; | ||
386 | mirrorEl.classList.add('fc-event-dragging'); | ||
387 | internal.applyStyle(mirrorEl, { | ||
388 | position: 'fixed', | ||
389 | zIndex: this.zIndex, | ||
390 | visibility: '', | ||
391 | boxSizing: 'border-box', | ||
392 | width: sourceElRect.right - sourceElRect.left, | ||
393 | height: sourceElRect.bottom - sourceElRect.top, | ||
394 | right: 'auto', | ||
395 | bottom: 'auto', | ||
396 | margin: 0, | ||
397 | }); | ||
398 | this.parentNode.appendChild(mirrorEl); | ||
399 | } | ||
400 | return mirrorEl; | ||
401 | } | ||
402 | } | ||
403 | |||
404 | /* | ||
405 | Is a cache for a given element's scroll information (all the info that ScrollController stores) | ||
406 | in addition the "client rectangle" of the element.. the area within the scrollbars. | ||
407 | |||
408 | The cache can be in one of two modes: | ||
409 | - doesListening:false - ignores when the container is scrolled by someone else | ||
410 | - doesListening:true - watch for scrolling and update the cache | ||
411 | */ | ||
412 | class ScrollGeomCache extends internal.ScrollController { | ||
413 | constructor(scrollController, doesListening) { | ||
414 | super(); | ||
415 | this.handleScroll = () => { | ||
416 | this.scrollTop = this.scrollController.getScrollTop(); | ||
417 | this.scrollLeft = this.scrollController.getScrollLeft(); | ||
418 | this.handleScrollChange(); | ||
419 | }; | ||
420 | this.scrollController = scrollController; | ||
421 | this.doesListening = doesListening; | ||
422 | this.scrollTop = this.origScrollTop = scrollController.getScrollTop(); | ||
423 | this.scrollLeft = this.origScrollLeft = scrollController.getScrollLeft(); | ||
424 | this.scrollWidth = scrollController.getScrollWidth(); | ||
425 | this.scrollHeight = scrollController.getScrollHeight(); | ||
426 | this.clientWidth = scrollController.getClientWidth(); | ||
427 | this.clientHeight = scrollController.getClientHeight(); | ||
428 | this.clientRect = this.computeClientRect(); // do last in case it needs cached values | ||
429 | if (this.doesListening) { | ||
430 | this.getEventTarget().addEventListener('scroll', this.handleScroll); | ||
431 | } | ||
432 | } | ||
433 | destroy() { | ||
434 | if (this.doesListening) { | ||
435 | this.getEventTarget().removeEventListener('scroll', this.handleScroll); | ||
436 | } | ||
437 | } | ||
438 | getScrollTop() { | ||
439 | return this.scrollTop; | ||
440 | } | ||
441 | getScrollLeft() { | ||
442 | return this.scrollLeft; | ||
443 | } | ||
444 | setScrollTop(top) { | ||
445 | this.scrollController.setScrollTop(top); | ||
446 | if (!this.doesListening) { | ||
447 | // we are not relying on the element to normalize out-of-bounds scroll values | ||
448 | // so we need to sanitize ourselves | ||
449 | this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0); | ||
450 | this.handleScrollChange(); | ||
451 | } | ||
452 | } | ||
453 | setScrollLeft(top) { | ||
454 | this.scrollController.setScrollLeft(top); | ||
455 | if (!this.doesListening) { | ||
456 | // we are not relying on the element to normalize out-of-bounds scroll values | ||
457 | // so we need to sanitize ourselves | ||
458 | this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0); | ||
459 | this.handleScrollChange(); | ||
460 | } | ||
461 | } | ||
462 | getClientWidth() { | ||
463 | return this.clientWidth; | ||
464 | } | ||
465 | getClientHeight() { | ||
466 | return this.clientHeight; | ||
467 | } | ||
468 | getScrollWidth() { | ||
469 | return this.scrollWidth; | ||
470 | } | ||
471 | getScrollHeight() { | ||
472 | return this.scrollHeight; | ||
473 | } | ||
474 | handleScrollChange() { | ||
475 | } | ||
476 | } | ||
477 | |||
478 | class ElementScrollGeomCache extends ScrollGeomCache { | ||
479 | constructor(el, doesListening) { | ||
480 | super(new internal.ElementScrollController(el), doesListening); | ||
481 | } | ||
482 | getEventTarget() { | ||
483 | return this.scrollController.el; | ||
484 | } | ||
485 | computeClientRect() { | ||
486 | return internal.computeInnerRect(this.scrollController.el); | ||
487 | } | ||
488 | } | ||
489 | |||
490 | class WindowScrollGeomCache extends ScrollGeomCache { | ||
491 | constructor(doesListening) { | ||
492 | super(new internal.WindowScrollController(), doesListening); | ||
493 | } | ||
494 | getEventTarget() { | ||
495 | return window; | ||
496 | } | ||
497 | computeClientRect() { | ||
498 | return { | ||
499 | left: this.scrollLeft, | ||
500 | right: this.scrollLeft + this.clientWidth, | ||
501 | top: this.scrollTop, | ||
502 | bottom: this.scrollTop + this.clientHeight, | ||
503 | }; | ||
504 | } | ||
505 | // the window is the only scroll object that changes it's rectangle relative | ||
506 | // to the document's topleft as it scrolls | ||
507 | handleScrollChange() { | ||
508 | this.clientRect = this.computeClientRect(); | ||
509 | } | ||
510 | } | ||
511 | |||
512 | // If available we are using native "performance" API instead of "Date" | ||
513 | // Read more about it on MDN: | ||
514 | // https://developer.mozilla.org/en-US/docs/Web/API/Performance | ||
515 | const getTime = typeof performance === 'function' ? performance.now : Date.now; | ||
516 | /* | ||
517 | For a pointer interaction, automatically scrolls certain scroll containers when the pointer | ||
518 | approaches the edge. | ||
519 | |||
520 | The caller must call start + handleMove + stop. | ||
521 | */ | ||
522 | class AutoScroller { | ||
523 | constructor() { | ||
524 | // options that can be set by caller | ||
525 | this.isEnabled = true; | ||
526 | this.scrollQuery = [window, '.fc-scroller']; | ||
527 | this.edgeThreshold = 50; // pixels | ||
528 | this.maxVelocity = 300; // pixels per second | ||
529 | // internal state | ||
530 | this.pointerScreenX = null; | ||
531 | this.pointerScreenY = null; | ||
532 | this.isAnimating = false; | ||
533 | this.scrollCaches = null; | ||
534 | // protect against the initial pointerdown being too close to an edge and starting the scroll | ||
535 | this.everMovedUp = false; | ||
536 | this.everMovedDown = false; | ||
537 | this.everMovedLeft = false; | ||
538 | this.everMovedRight = false; | ||
539 | this.animate = () => { | ||
540 | if (this.isAnimating) { // wasn't cancelled between animation calls | ||
541 | let edge = this.computeBestEdge(this.pointerScreenX + window.scrollX, this.pointerScreenY + window.scrollY); | ||
542 | if (edge) { | ||
543 | let now = getTime(); | ||
544 | this.handleSide(edge, (now - this.msSinceRequest) / 1000); | ||
545 | this.requestAnimation(now); | ||
546 | } | ||
547 | else { | ||
548 | this.isAnimating = false; // will stop animation | ||
549 | } | ||
550 | } | ||
551 | }; | ||
552 | } | ||
553 | start(pageX, pageY, scrollStartEl) { | ||
554 | if (this.isEnabled) { | ||
555 | this.scrollCaches = this.buildCaches(scrollStartEl); | ||
556 | this.pointerScreenX = null; | ||
557 | this.pointerScreenY = null; | ||
558 | this.everMovedUp = false; | ||
559 | this.everMovedDown = false; | ||
560 | this.everMovedLeft = false; | ||
561 | this.everMovedRight = false; | ||
562 | this.handleMove(pageX, pageY); | ||
563 | } | ||
564 | } | ||
565 | handleMove(pageX, pageY) { | ||
566 | if (this.isEnabled) { | ||
567 | let pointerScreenX = pageX - window.scrollX; | ||
568 | let pointerScreenY = pageY - window.scrollY; | ||
569 | let yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY; | ||
570 | let xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX; | ||
571 | if (yDelta < 0) { | ||
572 | this.everMovedUp = true; | ||
573 | } | ||
574 | else if (yDelta > 0) { | ||
575 | this.everMovedDown = true; | ||
576 | } | ||
577 | if (xDelta < 0) { | ||
578 | this.everMovedLeft = true; | ||
579 | } | ||
580 | else if (xDelta > 0) { | ||
581 | this.everMovedRight = true; | ||
582 | } | ||
583 | this.pointerScreenX = pointerScreenX; | ||
584 | this.pointerScreenY = pointerScreenY; | ||
585 | if (!this.isAnimating) { | ||
586 | this.isAnimating = true; | ||
587 | this.requestAnimation(getTime()); | ||
588 | } | ||
589 | } | ||
590 | } | ||
591 | stop() { | ||
592 | if (this.isEnabled) { | ||
593 | this.isAnimating = false; // will stop animation | ||
594 | for (let scrollCache of this.scrollCaches) { | ||
595 | scrollCache.destroy(); | ||
596 | } | ||
597 | this.scrollCaches = null; | ||
598 | } | ||
599 | } | ||
600 | requestAnimation(now) { | ||
601 | this.msSinceRequest = now; | ||
602 | requestAnimationFrame(this.animate); | ||
603 | } | ||
604 | handleSide(edge, seconds) { | ||
605 | let { scrollCache } = edge; | ||
606 | let { edgeThreshold } = this; | ||
607 | let invDistance = edgeThreshold - edge.distance; | ||
608 | let velocity = // the closer to the edge, the faster we scroll | ||
609 | ((invDistance * invDistance) / (edgeThreshold * edgeThreshold)) * // quadratic | ||
610 | this.maxVelocity * seconds; | ||
611 | let sign = 1; | ||
612 | switch (edge.name) { | ||
613 | case 'left': | ||
614 | sign = -1; | ||
615 | // falls through | ||
616 | case 'right': | ||
617 | scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign); | ||
618 | break; | ||
619 | case 'top': | ||
620 | sign = -1; | ||
621 | // falls through | ||
622 | case 'bottom': | ||
623 | scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign); | ||
624 | break; | ||
625 | } | ||
626 | } | ||
627 | // left/top are relative to document topleft | ||
628 | computeBestEdge(left, top) { | ||
629 | let { edgeThreshold } = this; | ||
630 | let bestSide = null; | ||
631 | let scrollCaches = this.scrollCaches || []; | ||
632 | for (let scrollCache of scrollCaches) { | ||
633 | let rect = scrollCache.clientRect; | ||
634 | let leftDist = left - rect.left; | ||
635 | let rightDist = rect.right - left; | ||
636 | let topDist = top - rect.top; | ||
637 | let bottomDist = rect.bottom - top; | ||
638 | // completely within the rect? | ||
639 | if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) { | ||
640 | if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() && | ||
641 | (!bestSide || bestSide.distance > topDist)) { | ||
642 | bestSide = { scrollCache, name: 'top', distance: topDist }; | ||
643 | } | ||
644 | if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() && | ||
645 | (!bestSide || bestSide.distance > bottomDist)) { | ||
646 | bestSide = { scrollCache, name: 'bottom', distance: bottomDist }; | ||
647 | } | ||
648 | /* | ||
649 | TODO: fix broken RTL scrolling. canScrollLeft always returning false | ||
650 | https://github.com/fullcalendar/fullcalendar/issues/4837 | ||
651 | */ | ||
652 | if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() && | ||
653 | (!bestSide || bestSide.distance > leftDist)) { | ||
654 | bestSide = { scrollCache, name: 'left', distance: leftDist }; | ||
655 | } | ||
656 | if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() && | ||
657 | (!bestSide || bestSide.distance > rightDist)) { | ||
658 | bestSide = { scrollCache, name: 'right', distance: rightDist }; | ||
659 | } | ||
660 | } | ||
661 | } | ||
662 | return bestSide; | ||
663 | } | ||
664 | buildCaches(scrollStartEl) { | ||
665 | return this.queryScrollEls(scrollStartEl).map((el) => { | ||
666 | if (el === window) { | ||
667 | return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls | ||
668 | } | ||
669 | return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls | ||
670 | }); | ||
671 | } | ||
672 | queryScrollEls(scrollStartEl) { | ||
673 | let els = []; | ||
674 | for (let query of this.scrollQuery) { | ||
675 | if (typeof query === 'object') { | ||
676 | els.push(query); | ||
677 | } | ||
678 | else { | ||
679 | /* | ||
680 | TODO: in the future, always have auto-scroll happen on element where current Hit came from | ||
681 | Ticket: https://github.com/fullcalendar/fullcalendar/issues/4593 | ||
682 | */ | ||
683 | els.push(...Array.prototype.slice.call(scrollStartEl.getRootNode().querySelectorAll(query))); | ||
684 | } | ||
685 | } | ||
686 | return els; | ||
687 | } | ||
688 | } | ||
689 | |||
690 | /* | ||
691 | Monitors dragging on an element. Has a number of high-level features: | ||
692 | - minimum distance required before dragging | ||
693 | - minimum wait time ("delay") before dragging | ||
694 | - a mirror element that follows the pointer | ||
695 | */ | ||
696 | class FeaturefulElementDragging extends internal.ElementDragging { | ||
697 | constructor(containerEl, selector) { | ||
698 | super(containerEl); | ||
699 | this.containerEl = containerEl; | ||
700 | // options that can be directly set by caller | ||
701 | // the caller can also set the PointerDragging's options as well | ||
702 | this.delay = null; | ||
703 | this.minDistance = 0; | ||
704 | this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag | ||
705 | this.mirrorNeedsRevert = false; | ||
706 | this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup | ||
707 | this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation | ||
708 | this.isDelayEnded = false; | ||
709 | this.isDistanceSurpassed = false; | ||
710 | this.delayTimeoutId = null; | ||
711 | this.onPointerDown = (ev) => { | ||
712 | if (!this.isDragging) { // so new drag doesn't happen while revert animation is going | ||
713 | this.isInteracting = true; | ||
714 | this.isDelayEnded = false; | ||
715 | this.isDistanceSurpassed = false; | ||
716 | internal.preventSelection(document.body); | ||
717 | internal.preventContextMenu(document.body); | ||
718 | // prevent links from being visited if there's an eventual drag. | ||
719 | // also prevents selection in older browsers (maybe?). | ||
720 | // not necessary for touch, besides, browser would complain about passiveness. | ||
721 | if (!ev.isTouch) { | ||
722 | ev.origEvent.preventDefault(); | ||
723 | } | ||
724 | this.emitter.trigger('pointerdown', ev); | ||
725 | if (this.isInteracting && // not destroyed via pointerdown handler | ||
726 | !this.pointer.shouldIgnoreMove) { | ||
727 | // actions related to initiating dragstart+dragmove+dragend... | ||
728 | this.mirror.setIsVisible(false); // reset. caller must set-visible | ||
729 | this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down | ||
730 | this.startDelay(ev); | ||
731 | if (!this.minDistance) { | ||
732 | this.handleDistanceSurpassed(ev); | ||
733 | } | ||
734 | } | ||
735 | } | ||
736 | }; | ||
737 | this.onPointerMove = (ev) => { | ||
738 | if (this.isInteracting) { | ||
739 | this.emitter.trigger('pointermove', ev); | ||
740 | if (!this.isDistanceSurpassed) { | ||
741 | let minDistance = this.minDistance; | ||
742 | let distanceSq; // current distance from the origin, squared | ||
743 | let { deltaX, deltaY } = ev; | ||
744 | distanceSq = deltaX * deltaX + deltaY * deltaY; | ||
745 | if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem | ||
746 | this.handleDistanceSurpassed(ev); | ||
747 | } | ||
748 | } | ||
749 | if (this.isDragging) { | ||
750 | // a real pointer move? (not one simulated by scrolling) | ||
751 | if (ev.origEvent.type !== 'scroll') { | ||
752 | this.mirror.handleMove(ev.pageX, ev.pageY); | ||
753 | this.autoScroller.handleMove(ev.pageX, ev.pageY); | ||
754 | } | ||
755 | this.emitter.trigger('dragmove', ev); | ||
756 | } | ||
757 | } | ||
758 | }; | ||
759 | this.onPointerUp = (ev) => { | ||
760 | if (this.isInteracting) { | ||
761 | this.isInteracting = false; | ||
762 | internal.allowSelection(document.body); | ||
763 | internal.allowContextMenu(document.body); | ||
764 | this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert | ||
765 | if (this.isDragging) { | ||
766 | this.autoScroller.stop(); | ||
767 | this.tryStopDrag(ev); // which will stop the mirror | ||
768 | } | ||
769 | if (this.delayTimeoutId) { | ||
770 | clearTimeout(this.delayTimeoutId); | ||
771 | this.delayTimeoutId = null; | ||
772 | } | ||
773 | } | ||
774 | }; | ||
775 | let pointer = this.pointer = new PointerDragging(containerEl); | ||
776 | pointer.emitter.on('pointerdown', this.onPointerDown); | ||
777 | pointer.emitter.on('pointermove', this.onPointerMove); | ||
778 | pointer.emitter.on('pointerup', this.onPointerUp); | ||
779 | if (selector) { | ||
780 | pointer.selector = selector; | ||
781 | } | ||
782 | this.mirror = new ElementMirror(); | ||
783 | this.autoScroller = new AutoScroller(); | ||
784 | } | ||
785 | destroy() { | ||
786 | this.pointer.destroy(); | ||
787 | // HACK: simulate a pointer-up to end the current drag | ||
788 | // TODO: fire 'dragend' directly and stop interaction. discourage use of pointerup event (b/c might not fire) | ||
789 | this.onPointerUp({}); | ||
790 | } | ||
791 | startDelay(ev) { | ||
792 | if (typeof this.delay === 'number') { | ||
793 | this.delayTimeoutId = setTimeout(() => { | ||
794 | this.delayTimeoutId = null; | ||
795 | this.handleDelayEnd(ev); | ||
796 | }, this.delay); // not assignable to number! | ||
797 | } | ||
798 | else { | ||
799 | this.handleDelayEnd(ev); | ||
800 | } | ||
801 | } | ||
802 | handleDelayEnd(ev) { | ||
803 | this.isDelayEnded = true; | ||
804 | this.tryStartDrag(ev); | ||
805 | } | ||
806 | handleDistanceSurpassed(ev) { | ||
807 | this.isDistanceSurpassed = true; | ||
808 | this.tryStartDrag(ev); | ||
809 | } | ||
810 | tryStartDrag(ev) { | ||
811 | if (this.isDelayEnded && this.isDistanceSurpassed) { | ||
812 | if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) { | ||
813 | this.isDragging = true; | ||
814 | this.mirrorNeedsRevert = false; | ||
815 | this.autoScroller.start(ev.pageX, ev.pageY, this.containerEl); | ||
816 | this.emitter.trigger('dragstart', ev); | ||
817 | if (this.touchScrollAllowed === false) { | ||
818 | this.pointer.cancelTouchScroll(); | ||
819 | } | ||
820 | } | ||
821 | } | ||
822 | } | ||
823 | tryStopDrag(ev) { | ||
824 | // .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events | ||
825 | // that come from the document to fire beforehand. much more convenient this way. | ||
826 | this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev)); | ||
827 | } | ||
828 | stopDrag(ev) { | ||
829 | this.isDragging = false; | ||
830 | this.emitter.trigger('dragend', ev); | ||
831 | } | ||
832 | // fill in the implementations... | ||
833 | setIgnoreMove(bool) { | ||
834 | this.pointer.shouldIgnoreMove = bool; | ||
835 | } | ||
836 | setMirrorIsVisible(bool) { | ||
837 | this.mirror.setIsVisible(bool); | ||
838 | } | ||
839 | setMirrorNeedsRevert(bool) { | ||
840 | this.mirrorNeedsRevert = bool; | ||
841 | } | ||
842 | setAutoScrollEnabled(bool) { | ||
843 | this.autoScroller.isEnabled = bool; | ||
844 | } | ||
845 | } | ||
846 | |||
847 | /* | ||
848 | When this class is instantiated, it records the offset of an element (relative to the document topleft), | ||
849 | and continues to monitor scrolling, updating the cached coordinates if it needs to. | ||
850 | Does not access the DOM after instantiation, so highly performant. | ||
851 | |||
852 | Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element | ||
853 | and an determine if a given point is inside the combined clipping rectangle. | ||
854 | */ | ||
855 | class OffsetTracker { | ||
856 | constructor(el) { | ||
857 | this.el = el; | ||
858 | this.origRect = internal.computeRect(el); | ||
859 | // will work fine for divs that have overflow:hidden | ||
860 | this.scrollCaches = internal.getClippingParents(el).map((scrollEl) => new ElementScrollGeomCache(scrollEl, true)); | ||
861 | } | ||
862 | destroy() { | ||
863 | for (let scrollCache of this.scrollCaches) { | ||
864 | scrollCache.destroy(); | ||
865 | } | ||
866 | } | ||
867 | computeLeft() { | ||
868 | let left = this.origRect.left; | ||
869 | for (let scrollCache of this.scrollCaches) { | ||
870 | left += scrollCache.origScrollLeft - scrollCache.getScrollLeft(); | ||
871 | } | ||
872 | return left; | ||
873 | } | ||
874 | computeTop() { | ||
875 | let top = this.origRect.top; | ||
876 | for (let scrollCache of this.scrollCaches) { | ||
877 | top += scrollCache.origScrollTop - scrollCache.getScrollTop(); | ||
878 | } | ||
879 | return top; | ||
880 | } | ||
881 | isWithinClipping(pageX, pageY) { | ||
882 | let point = { left: pageX, top: pageY }; | ||
883 | for (let scrollCache of this.scrollCaches) { | ||
884 | if (!isIgnoredClipping(scrollCache.getEventTarget()) && | ||
885 | !internal.pointInsideRect(point, scrollCache.clientRect)) { | ||
886 | return false; | ||
887 | } | ||
888 | } | ||
889 | return true; | ||
890 | } | ||
891 | } | ||
892 | // certain clipping containers should never constrain interactions, like <html> and <body> | ||
893 | // https://github.com/fullcalendar/fullcalendar/issues/3615 | ||
894 | function isIgnoredClipping(node) { | ||
895 | let tagName = node.tagName; | ||
896 | return tagName === 'HTML' || tagName === 'BODY'; | ||
897 | } | ||
898 | |||
899 | /* | ||
900 | Tracks movement over multiple droppable areas (aka "hits") | ||
901 | that exist in one or more DateComponents. | ||
902 | Relies on an existing draggable. | ||
903 | |||
904 | emits: | ||
905 | - pointerdown | ||
906 | - dragstart | ||
907 | - hitchange - fires initially, even if not over a hit | ||
908 | - pointerup | ||
909 | - (hitchange - again, to null, if ended over a hit) | ||
910 | - dragend | ||
911 | */ | ||
912 | class HitDragging { | ||
913 | constructor(dragging, droppableStore) { | ||
914 | // options that can be set by caller | ||
915 | this.useSubjectCenter = false; | ||
916 | this.requireInitial = true; // if doesn't start out on a hit, won't emit any events | ||
917 | this.disablePointCheck = false; | ||
918 | this.initialHit = null; | ||
919 | this.movingHit = null; | ||
920 | this.finalHit = null; // won't ever be populated if shouldIgnoreMove | ||
921 | this.handlePointerDown = (ev) => { | ||
922 | let { dragging } = this; | ||
923 | this.initialHit = null; | ||
924 | this.movingHit = null; | ||
925 | this.finalHit = null; | ||
926 | this.prepareHits(); | ||
927 | this.processFirstCoord(ev); | ||
928 | if (this.initialHit || !this.requireInitial) { | ||
929 | dragging.setIgnoreMove(false); | ||
930 | // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :( | ||
931 | this.emitter.trigger('pointerdown', ev); | ||
932 | } | ||
933 | else { | ||
934 | dragging.setIgnoreMove(true); | ||
935 | } | ||
936 | }; | ||
937 | this.handleDragStart = (ev) => { | ||
938 | this.emitter.trigger('dragstart', ev); | ||
939 | this.handleMove(ev, true); // force = fire even if initially null | ||
940 | }; | ||
941 | this.handleDragMove = (ev) => { | ||
942 | this.emitter.trigger('dragmove', ev); | ||
943 | this.handleMove(ev); | ||
944 | }; | ||
945 | this.handlePointerUp = (ev) => { | ||
946 | this.releaseHits(); | ||
947 | this.emitter.trigger('pointerup', ev); | ||
948 | }; | ||
949 | this.handleDragEnd = (ev) => { | ||
950 | if (this.movingHit) { | ||
951 | this.emitter.trigger('hitupdate', null, true, ev); | ||
952 | } | ||
953 | this.finalHit = this.movingHit; | ||
954 | this.movingHit = null; | ||
955 | this.emitter.trigger('dragend', ev); | ||
956 | }; | ||
957 | this.droppableStore = droppableStore; | ||
958 | dragging.emitter.on('pointerdown', this.handlePointerDown); | ||
959 | dragging.emitter.on('dragstart', this.handleDragStart); | ||
960 | dragging.emitter.on('dragmove', this.handleDragMove); | ||
961 | dragging.emitter.on('pointerup', this.handlePointerUp); | ||
962 | dragging.emitter.on('dragend', this.handleDragEnd); | ||
963 | this.dragging = dragging; | ||
964 | this.emitter = new internal.Emitter(); | ||
965 | } | ||
966 | // sets initialHit | ||
967 | // sets coordAdjust | ||
968 | processFirstCoord(ev) { | ||
969 | let origPoint = { left: ev.pageX, top: ev.pageY }; | ||
970 | let adjustedPoint = origPoint; | ||
971 | let subjectEl = ev.subjectEl; | ||
972 | let subjectRect; | ||
973 | if (subjectEl instanceof HTMLElement) { // i.e. not a Document/ShadowRoot | ||
974 | subjectRect = internal.computeRect(subjectEl); | ||
975 | adjustedPoint = internal.constrainPoint(adjustedPoint, subjectRect); | ||
976 | } | ||
977 | let initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top); | ||
978 | if (initialHit) { | ||
979 | if (this.useSubjectCenter && subjectRect) { | ||
980 | let slicedSubjectRect = internal.intersectRects(subjectRect, initialHit.rect); | ||
981 | if (slicedSubjectRect) { | ||
982 | adjustedPoint = internal.getRectCenter(slicedSubjectRect); | ||
983 | } | ||
984 | } | ||
985 | this.coordAdjust = internal.diffPoints(adjustedPoint, origPoint); | ||
986 | } | ||
987 | else { | ||
988 | this.coordAdjust = { left: 0, top: 0 }; | ||
989 | } | ||
990 | } | ||
991 | handleMove(ev, forceHandle) { | ||
992 | let hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top); | ||
993 | if (forceHandle || !isHitsEqual(this.movingHit, hit)) { | ||
994 | this.movingHit = hit; | ||
995 | this.emitter.trigger('hitupdate', hit, false, ev); | ||
996 | } | ||
997 | } | ||
998 | prepareHits() { | ||
999 | this.offsetTrackers = internal.mapHash(this.droppableStore, (interactionSettings) => { | ||
1000 | interactionSettings.component.prepareHits(); | ||
1001 | return new OffsetTracker(interactionSettings.el); | ||
1002 | }); | ||
1003 | } | ||
1004 | releaseHits() { | ||
1005 | let { offsetTrackers } = this; | ||
1006 | for (let id in offsetTrackers) { | ||
1007 | offsetTrackers[id].destroy(); | ||
1008 | } | ||
1009 | this.offsetTrackers = {}; | ||
1010 | } | ||
1011 | queryHitForOffset(offsetLeft, offsetTop) { | ||
1012 | let { droppableStore, offsetTrackers } = this; | ||
1013 | let bestHit = null; | ||
1014 | for (let id in droppableStore) { | ||
1015 | let component = droppableStore[id].component; | ||
1016 | let offsetTracker = offsetTrackers[id]; | ||
1017 | if (offsetTracker && // wasn't destroyed mid-drag | ||
1018 | offsetTracker.isWithinClipping(offsetLeft, offsetTop)) { | ||
1019 | let originLeft = offsetTracker.computeLeft(); | ||
1020 | let originTop = offsetTracker.computeTop(); | ||
1021 | let positionLeft = offsetLeft - originLeft; | ||
1022 | let positionTop = offsetTop - originTop; | ||
1023 | let { origRect } = offsetTracker; | ||
1024 | let width = origRect.right - origRect.left; | ||
1025 | let height = origRect.bottom - origRect.top; | ||
1026 | if ( | ||
1027 | // must be within the element's bounds | ||
1028 | positionLeft >= 0 && positionLeft < width && | ||
1029 | positionTop >= 0 && positionTop < height) { | ||
1030 | let hit = component.queryHit(positionLeft, positionTop, width, height); | ||
1031 | if (hit && ( | ||
1032 | // make sure the hit is within activeRange, meaning it's not a dead cell | ||
1033 | internal.rangeContainsRange(hit.dateProfile.activeRange, hit.dateSpan.range)) && | ||
1034 | // Ensure the component we are querying for the hit is accessibly my the pointer | ||
1035 | // Prevents obscured calendars (ex: under a modal dialog) from accepting hit | ||
1036 | // https://github.com/fullcalendar/fullcalendar/issues/5026 | ||
1037 | (this.disablePointCheck || | ||
1038 | offsetTracker.el.contains(offsetTracker.el.getRootNode().elementFromPoint( | ||
1039 | // add-back origins to get coordinate relative to top-left of window viewport | ||
1040 | positionLeft + originLeft - window.scrollX, positionTop + originTop - window.scrollY))) && | ||
1041 | (!bestHit || hit.layer > bestHit.layer)) { | ||
1042 | hit.componentId = id; | ||
1043 | hit.context = component.context; | ||
1044 | // TODO: better way to re-orient rectangle | ||
1045 | hit.rect.left += originLeft; | ||
1046 | hit.rect.right += originLeft; | ||
1047 | hit.rect.top += originTop; | ||
1048 | hit.rect.bottom += originTop; | ||
1049 | bestHit = hit; | ||
1050 | } | ||
1051 | } | ||
1052 | } | ||
1053 | } | ||
1054 | return bestHit; | ||
1055 | } | ||
1056 | } | ||
1057 | function isHitsEqual(hit0, hit1) { | ||
1058 | if (!hit0 && !hit1) { | ||
1059 | return true; | ||
1060 | } | ||
1061 | if (Boolean(hit0) !== Boolean(hit1)) { | ||
1062 | return false; | ||
1063 | } | ||
1064 | return internal.isDateSpansEqual(hit0.dateSpan, hit1.dateSpan); | ||
1065 | } | ||
1066 | |||
1067 | function buildDatePointApiWithContext(dateSpan, context) { | ||
1068 | let props = {}; | ||
1069 | for (let transform of context.pluginHooks.datePointTransforms) { | ||
1070 | Object.assign(props, transform(dateSpan, context)); | ||
1071 | } | ||
1072 | Object.assign(props, buildDatePointApi(dateSpan, context.dateEnv)); | ||
1073 | return props; | ||
1074 | } | ||
1075 | function buildDatePointApi(span, dateEnv) { | ||
1076 | return { | ||
1077 | date: dateEnv.toDate(span.range.start), | ||
1078 | dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }), | ||
1079 | allDay: span.allDay, | ||
1080 | }; | ||
1081 | } | ||
1082 | |||
1083 | /* | ||
1084 | Monitors when the user clicks on a specific date/time of a component. | ||
1085 | A pointerdown+pointerup on the same "hit" constitutes a click. | ||
1086 | */ | ||
1087 | class DateClicking extends internal.Interaction { | ||
1088 | constructor(settings) { | ||
1089 | super(settings); | ||
1090 | this.handlePointerDown = (pev) => { | ||
1091 | let { dragging } = this; | ||
1092 | let downEl = pev.origEvent.target; | ||
1093 | // do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired | ||
1094 | dragging.setIgnoreMove(!this.component.isValidDateDownEl(downEl)); | ||
1095 | }; | ||
1096 | // won't even fire if moving was ignored | ||
1097 | this.handleDragEnd = (ev) => { | ||
1098 | let { component } = this; | ||
1099 | let { pointer } = this.dragging; | ||
1100 | if (!pointer.wasTouchScroll) { | ||
1101 | let { initialHit, finalHit } = this.hitDragging; | ||
1102 | if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) { | ||
1103 | let { context } = component; | ||
1104 | let arg = Object.assign(Object.assign({}, buildDatePointApiWithContext(initialHit.dateSpan, context)), { dayEl: initialHit.dayEl, jsEvent: ev.origEvent, view: context.viewApi || context.calendarApi.view }); | ||
1105 | context.emitter.trigger('dateClick', arg); | ||
1106 | } | ||
1107 | } | ||
1108 | }; | ||
1109 | // we DO want to watch pointer moves because otherwise finalHit won't get populated | ||
1110 | this.dragging = new FeaturefulElementDragging(settings.el); | ||
1111 | this.dragging.autoScroller.isEnabled = false; | ||
1112 | let hitDragging = this.hitDragging = new HitDragging(this.dragging, internal.interactionSettingsToStore(settings)); | ||
1113 | hitDragging.emitter.on('pointerdown', this.handlePointerDown); | ||
1114 | hitDragging.emitter.on('dragend', this.handleDragEnd); | ||
1115 | } | ||
1116 | destroy() { | ||
1117 | this.dragging.destroy(); | ||
1118 | } | ||
1119 | } | ||
1120 | |||
1121 | /* | ||
1122 | Tracks when the user selects a portion of time of a component, | ||
1123 | constituted by a drag over date cells, with a possible delay at the beginning of the drag. | ||
1124 | */ | ||
1125 | class DateSelecting extends internal.Interaction { | ||
1126 | constructor(settings) { | ||
1127 | super(settings); | ||
1128 | this.dragSelection = null; | ||
1129 | this.handlePointerDown = (ev) => { | ||
1130 | let { component, dragging } = this; | ||
1131 | let { options } = component.context; | ||
1132 | let canSelect = options.selectable && | ||
1133 | component.isValidDateDownEl(ev.origEvent.target); | ||
1134 | // don't bother to watch expensive moves if component won't do selection | ||
1135 | dragging.setIgnoreMove(!canSelect); | ||
1136 | // if touch, require user to hold down | ||
1137 | dragging.delay = ev.isTouch ? getComponentTouchDelay$1(component) : null; | ||
1138 | }; | ||
1139 | this.handleDragStart = (ev) => { | ||
1140 | this.component.context.calendarApi.unselect(ev); // unselect previous selections | ||
1141 | }; | ||
1142 | this.handleHitUpdate = (hit, isFinal) => { | ||
1143 | let { context } = this.component; | ||
1144 | let dragSelection = null; | ||
1145 | let isInvalid = false; | ||
1146 | if (hit) { | ||
1147 | let initialHit = this.hitDragging.initialHit; | ||
1148 | let disallowed = hit.componentId === initialHit.componentId | ||
1149 | && this.isHitComboAllowed | ||
1150 | && !this.isHitComboAllowed(initialHit, hit); | ||
1151 | if (!disallowed) { | ||
1152 | dragSelection = joinHitsIntoSelection(initialHit, hit, context.pluginHooks.dateSelectionTransformers); | ||
1153 | } | ||
1154 | if (!dragSelection || !internal.isDateSelectionValid(dragSelection, hit.dateProfile, context)) { | ||
1155 | isInvalid = true; | ||
1156 | dragSelection = null; | ||
1157 | } | ||
1158 | } | ||
1159 | if (dragSelection) { | ||
1160 | context.dispatch({ type: 'SELECT_DATES', selection: dragSelection }); | ||
1161 | } | ||
1162 | else if (!isFinal) { // only unselect if moved away while dragging | ||
1163 | context.dispatch({ type: 'UNSELECT_DATES' }); | ||
1164 | } | ||
1165 | if (!isInvalid) { | ||
1166 | internal.enableCursor(); | ||
1167 | } | ||
1168 | else { | ||
1169 | internal.disableCursor(); | ||
1170 | } | ||
1171 | if (!isFinal) { | ||
1172 | this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging | ||
1173 | } | ||
1174 | }; | ||
1175 | this.handlePointerUp = (pev) => { | ||
1176 | if (this.dragSelection) { | ||
1177 | // selection is already rendered, so just need to report selection | ||
1178 | internal.triggerDateSelect(this.dragSelection, pev, this.component.context); | ||
1179 | this.dragSelection = null; | ||
1180 | } | ||
1181 | }; | ||
1182 | let { component } = settings; | ||
1183 | let { options } = component.context; | ||
1184 | let dragging = this.dragging = new FeaturefulElementDragging(settings.el); | ||
1185 | dragging.touchScrollAllowed = false; | ||
1186 | dragging.minDistance = options.selectMinDistance || 0; | ||
1187 | dragging.autoScroller.isEnabled = options.dragScroll; | ||
1188 | let hitDragging = this.hitDragging = new HitDragging(this.dragging, internal.interactionSettingsToStore(settings)); | ||
1189 | hitDragging.emitter.on('pointerdown', this.handlePointerDown); | ||
1190 | hitDragging.emitter.on('dragstart', this.handleDragStart); | ||
1191 | hitDragging.emitter.on('hitupdate', this.handleHitUpdate); | ||
1192 | hitDragging.emitter.on('pointerup', this.handlePointerUp); | ||
1193 | } | ||
1194 | destroy() { | ||
1195 | this.dragging.destroy(); | ||
1196 | } | ||
1197 | } | ||
1198 | function getComponentTouchDelay$1(component) { | ||
1199 | let { options } = component.context; | ||
1200 | let delay = options.selectLongPressDelay; | ||
1201 | if (delay == null) { | ||
1202 | delay = options.longPressDelay; | ||
1203 | } | ||
1204 | return delay; | ||
1205 | } | ||
1206 | function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) { | ||
1207 | let dateSpan0 = hit0.dateSpan; | ||
1208 | let dateSpan1 = hit1.dateSpan; | ||
1209 | let ms = [ | ||
1210 | dateSpan0.range.start, | ||
1211 | dateSpan0.range.end, | ||
1212 | dateSpan1.range.start, | ||
1213 | dateSpan1.range.end, | ||
1214 | ]; | ||
1215 | ms.sort(internal.compareNumbers); | ||
1216 | let props = {}; | ||
1217 | for (let transformer of dateSelectionTransformers) { | ||
1218 | let res = transformer(hit0, hit1); | ||
1219 | if (res === false) { | ||
1220 | return null; | ||
1221 | } | ||
1222 | if (res) { | ||
1223 | Object.assign(props, res); | ||
1224 | } | ||
1225 | } | ||
1226 | props.range = { start: ms[0], end: ms[3] }; | ||
1227 | props.allDay = dateSpan0.allDay; | ||
1228 | return props; | ||
1229 | } | ||
1230 | |||
1231 | class EventDragging extends internal.Interaction { | ||
1232 | constructor(settings) { | ||
1233 | super(settings); | ||
1234 | // internal state | ||
1235 | this.subjectEl = null; | ||
1236 | this.subjectSeg = null; // the seg being selected/dragged | ||
1237 | this.isDragging = false; | ||
1238 | this.eventRange = null; | ||
1239 | this.relevantEvents = null; // the events being dragged | ||
1240 | this.receivingContext = null; | ||
1241 | this.validMutation = null; | ||
1242 | this.mutatedRelevantEvents = null; | ||
1243 | this.handlePointerDown = (ev) => { | ||
1244 | let origTarget = ev.origEvent.target; | ||
1245 | let { component, dragging } = this; | ||
1246 | let { mirror } = dragging; | ||
1247 | let { options } = component.context; | ||
1248 | let initialContext = component.context; | ||
1249 | this.subjectEl = ev.subjectEl; | ||
1250 | let subjectSeg = this.subjectSeg = internal.getElSeg(ev.subjectEl); | ||
1251 | let eventRange = this.eventRange = subjectSeg.eventRange; | ||
1252 | let eventInstanceId = eventRange.instance.instanceId; | ||
1253 | this.relevantEvents = internal.getRelevantEvents(initialContext.getCurrentData().eventStore, eventInstanceId); | ||
1254 | dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance; | ||
1255 | dragging.delay = | ||
1256 | // only do a touch delay if touch and this event hasn't been selected yet | ||
1257 | (ev.isTouch && eventInstanceId !== component.props.eventSelection) ? | ||
1258 | getComponentTouchDelay(component) : | ||
1259 | null; | ||
1260 | if (options.fixedMirrorParent) { | ||
1261 | mirror.parentNode = options.fixedMirrorParent; | ||
1262 | } | ||
1263 | else { | ||
1264 | mirror.parentNode = internal.elementClosest(origTarget, '.fc'); | ||
1265 | } | ||
1266 | mirror.revertDuration = options.dragRevertDuration; | ||
1267 | let isValid = component.isValidSegDownEl(origTarget) && | ||
1268 | !internal.elementClosest(origTarget, '.fc-event-resizer'); // NOT on a resizer | ||
1269 | dragging.setIgnoreMove(!isValid); | ||
1270 | // disable dragging for elements that are resizable (ie, selectable) | ||
1271 | // but are not draggable | ||
1272 | this.isDragging = isValid && | ||
1273 | ev.subjectEl.classList.contains('fc-event-draggable'); | ||
1274 | }; | ||
1275 | this.handleDragStart = (ev) => { | ||
1276 | let initialContext = this.component.context; | ||
1277 | let eventRange = this.eventRange; | ||
1278 | let eventInstanceId = eventRange.instance.instanceId; | ||
1279 | if (ev.isTouch) { | ||
1280 | // need to select a different event? | ||
1281 | if (eventInstanceId !== this.component.props.eventSelection) { | ||
1282 | initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId }); | ||
1283 | } | ||
1284 | } | ||
1285 | else { | ||
1286 | // if now using mouse, but was previous touch interaction, clear selected event | ||
1287 | initialContext.dispatch({ type: 'UNSELECT_EVENT' }); | ||
1288 | } | ||
1289 | if (this.isDragging) { | ||
1290 | initialContext.calendarApi.unselect(ev); // unselect *date* selection | ||
1291 | initialContext.emitter.trigger('eventDragStart', { | ||
1292 | el: this.subjectEl, | ||
1293 | event: new internal.EventImpl(initialContext, eventRange.def, eventRange.instance), | ||
1294 | jsEvent: ev.origEvent, | ||
1295 | view: initialContext.viewApi, | ||
1296 | }); | ||
1297 | } | ||
1298 | }; | ||
1299 | this.handleHitUpdate = (hit, isFinal) => { | ||
1300 | if (!this.isDragging) { | ||
1301 | return; | ||
1302 | } | ||
1303 | let relevantEvents = this.relevantEvents; | ||
1304 | let initialHit = this.hitDragging.initialHit; | ||
1305 | let initialContext = this.component.context; | ||
1306 | // states based on new hit | ||
1307 | let receivingContext = null; | ||
1308 | let mutation = null; | ||
1309 | let mutatedRelevantEvents = null; | ||
1310 | let isInvalid = false; | ||
1311 | let interaction = { | ||
1312 | affectedEvents: relevantEvents, | ||
1313 | mutatedEvents: internal.createEmptyEventStore(), | ||
1314 | isEvent: true, | ||
1315 | }; | ||
1316 | if (hit) { | ||
1317 | receivingContext = hit.context; | ||
1318 | let receivingOptions = receivingContext.options; | ||
1319 | if (initialContext === receivingContext || | ||
1320 | (receivingOptions.editable && receivingOptions.droppable)) { | ||
1321 | mutation = computeEventMutation(initialHit, hit, this.eventRange.instance.range.start, receivingContext.getCurrentData().pluginHooks.eventDragMutationMassagers); | ||
1322 | if (mutation) { | ||
1323 | mutatedRelevantEvents = internal.applyMutationToEventStore(relevantEvents, receivingContext.getCurrentData().eventUiBases, mutation, receivingContext); | ||
1324 | interaction.mutatedEvents = mutatedRelevantEvents; | ||
1325 | if (!internal.isInteractionValid(interaction, hit.dateProfile, receivingContext)) { | ||
1326 | isInvalid = true; | ||
1327 | mutation = null; | ||
1328 | mutatedRelevantEvents = null; | ||
1329 | interaction.mutatedEvents = internal.createEmptyEventStore(); | ||
1330 | } | ||
1331 | } | ||
1332 | } | ||
1333 | else { | ||
1334 | receivingContext = null; | ||
1335 | } | ||
1336 | } | ||
1337 | this.displayDrag(receivingContext, interaction); | ||
1338 | if (!isInvalid) { | ||
1339 | internal.enableCursor(); | ||
1340 | } | ||
1341 | else { | ||
1342 | internal.disableCursor(); | ||
1343 | } | ||
1344 | if (!isFinal) { | ||
1345 | if (initialContext === receivingContext && // TODO: write test for this | ||
1346 | isHitsEqual(initialHit, hit)) { | ||
1347 | mutation = null; | ||
1348 | } | ||
1349 | this.dragging.setMirrorNeedsRevert(!mutation); | ||
1350 | // render the mirror if no already-rendered mirror | ||
1351 | // TODO: wish we could somehow wait for dispatch to guarantee render | ||
1352 | this.dragging.setMirrorIsVisible(!hit || !this.subjectEl.getRootNode().querySelector('.fc-event-mirror')); | ||
1353 | // assign states based on new hit | ||
1354 | this.receivingContext = receivingContext; | ||
1355 | this.validMutation = mutation; | ||
1356 | this.mutatedRelevantEvents = mutatedRelevantEvents; | ||
1357 | } | ||
1358 | }; | ||
1359 | this.handlePointerUp = () => { | ||
1360 | if (!this.isDragging) { | ||
1361 | this.cleanup(); // because handleDragEnd won't fire | ||
1362 | } | ||
1363 | }; | ||
1364 | this.handleDragEnd = (ev) => { | ||
1365 | if (this.isDragging) { | ||
1366 | let initialContext = this.component.context; | ||
1367 | let initialView = initialContext.viewApi; | ||
1368 | let { receivingContext, validMutation } = this; | ||
1369 | let eventDef = this.eventRange.def; | ||
1370 | let eventInstance = this.eventRange.instance; | ||
1371 | let eventApi = new internal.EventImpl(initialContext, eventDef, eventInstance); | ||
1372 | let relevantEvents = this.relevantEvents; | ||
1373 | let mutatedRelevantEvents = this.mutatedRelevantEvents; | ||
1374 | let { finalHit } = this.hitDragging; | ||
1375 | this.clearDrag(); // must happen after revert animation | ||
1376 | initialContext.emitter.trigger('eventDragStop', { | ||
1377 | el: this.subjectEl, | ||
1378 | event: eventApi, | ||
1379 | jsEvent: ev.origEvent, | ||
1380 | view: initialView, | ||
1381 | }); | ||
1382 | if (validMutation) { | ||
1383 | // dropped within same calendar | ||
1384 | if (receivingContext === initialContext) { | ||
1385 | let updatedEventApi = new internal.EventImpl(initialContext, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null); | ||
1386 | initialContext.dispatch({ | ||
1387 | type: 'MERGE_EVENTS', | ||
1388 | eventStore: mutatedRelevantEvents, | ||
1389 | }); | ||
1390 | let eventChangeArg = { | ||
1391 | oldEvent: eventApi, | ||
1392 | event: updatedEventApi, | ||
1393 | relatedEvents: internal.buildEventApis(mutatedRelevantEvents, initialContext, eventInstance), | ||
1394 | revert() { | ||
1395 | initialContext.dispatch({ | ||
1396 | type: 'MERGE_EVENTS', | ||
1397 | eventStore: relevantEvents, // the pre-change data | ||
1398 | }); | ||
1399 | }, | ||
1400 | }; | ||
1401 | let transformed = {}; | ||
1402 | for (let transformer of initialContext.getCurrentData().pluginHooks.eventDropTransformers) { | ||
1403 | Object.assign(transformed, transformer(validMutation, initialContext)); | ||
1404 | } | ||
1405 | initialContext.emitter.trigger('eventDrop', Object.assign(Object.assign(Object.assign({}, eventChangeArg), transformed), { el: ev.subjectEl, delta: validMutation.datesDelta, jsEvent: ev.origEvent, view: initialView })); | ||
1406 | initialContext.emitter.trigger('eventChange', eventChangeArg); | ||
1407 | // dropped in different calendar | ||
1408 | } | ||
1409 | else if (receivingContext) { | ||
1410 | let eventRemoveArg = { | ||
1411 | event: eventApi, | ||
1412 | relatedEvents: internal.buildEventApis(relevantEvents, initialContext, eventInstance), | ||
1413 | revert() { | ||
1414 | initialContext.dispatch({ | ||
1415 | type: 'MERGE_EVENTS', | ||
1416 | eventStore: relevantEvents, | ||
1417 | }); | ||
1418 | }, | ||
1419 | }; | ||
1420 | initialContext.emitter.trigger('eventLeave', Object.assign(Object.assign({}, eventRemoveArg), { draggedEl: ev.subjectEl, view: initialView })); | ||
1421 | initialContext.dispatch({ | ||
1422 | type: 'REMOVE_EVENTS', | ||
1423 | eventStore: relevantEvents, | ||
1424 | }); | ||
1425 | initialContext.emitter.trigger('eventRemove', eventRemoveArg); | ||
1426 | let addedEventDef = mutatedRelevantEvents.defs[eventDef.defId]; | ||
1427 | let addedEventInstance = mutatedRelevantEvents.instances[eventInstance.instanceId]; | ||
1428 | let addedEventApi = new internal.EventImpl(receivingContext, addedEventDef, addedEventInstance); | ||
1429 | receivingContext.dispatch({ | ||
1430 | type: 'MERGE_EVENTS', | ||
1431 | eventStore: mutatedRelevantEvents, | ||
1432 | }); | ||
1433 | let eventAddArg = { | ||
1434 | event: addedEventApi, | ||
1435 | relatedEvents: internal.buildEventApis(mutatedRelevantEvents, receivingContext, addedEventInstance), | ||
1436 | revert() { | ||
1437 | receivingContext.dispatch({ | ||
1438 | type: 'REMOVE_EVENTS', | ||
1439 | eventStore: mutatedRelevantEvents, | ||
1440 | }); | ||
1441 | }, | ||
1442 | }; | ||
1443 | receivingContext.emitter.trigger('eventAdd', eventAddArg); | ||
1444 | if (ev.isTouch) { | ||
1445 | receivingContext.dispatch({ | ||
1446 | type: 'SELECT_EVENT', | ||
1447 | eventInstanceId: eventInstance.instanceId, | ||
1448 | }); | ||
1449 | } | ||
1450 | receivingContext.emitter.trigger('drop', Object.assign(Object.assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.context.viewApi })); | ||
1451 | receivingContext.emitter.trigger('eventReceive', Object.assign(Object.assign({}, eventAddArg), { draggedEl: ev.subjectEl, view: finalHit.context.viewApi })); | ||
1452 | } | ||
1453 | } | ||
1454 | else { | ||
1455 | initialContext.emitter.trigger('_noEventDrop'); | ||
1456 | } | ||
1457 | } | ||
1458 | this.cleanup(); | ||
1459 | }; | ||
1460 | let { component } = this; | ||
1461 | let { options } = component.context; | ||
1462 | let dragging = this.dragging = new FeaturefulElementDragging(settings.el); | ||
1463 | dragging.pointer.selector = EventDragging.SELECTOR; | ||
1464 | dragging.touchScrollAllowed = false; | ||
1465 | dragging.autoScroller.isEnabled = options.dragScroll; | ||
1466 | let hitDragging = this.hitDragging = new HitDragging(this.dragging, internal.interactionSettingsStore); | ||
1467 | hitDragging.useSubjectCenter = settings.useEventCenter; | ||
1468 | hitDragging.emitter.on('pointerdown', this.handlePointerDown); | ||
1469 | hitDragging.emitter.on('dragstart', this.handleDragStart); | ||
1470 | hitDragging.emitter.on('hitupdate', this.handleHitUpdate); | ||
1471 | hitDragging.emitter.on('pointerup', this.handlePointerUp); | ||
1472 | hitDragging.emitter.on('dragend', this.handleDragEnd); | ||
1473 | } | ||
1474 | destroy() { | ||
1475 | this.dragging.destroy(); | ||
1476 | } | ||
1477 | // render a drag state on the next receivingCalendar | ||
1478 | displayDrag(nextContext, state) { | ||
1479 | let initialContext = this.component.context; | ||
1480 | let prevContext = this.receivingContext; | ||
1481 | // does the previous calendar need to be cleared? | ||
1482 | if (prevContext && prevContext !== nextContext) { | ||
1483 | // does the initial calendar need to be cleared? | ||
1484 | // if so, don't clear all the way. we still need to to hide the affectedEvents | ||
1485 | if (prevContext === initialContext) { | ||
1486 | prevContext.dispatch({ | ||
1487 | type: 'SET_EVENT_DRAG', | ||
1488 | state: { | ||
1489 | affectedEvents: state.affectedEvents, | ||
1490 | mutatedEvents: internal.createEmptyEventStore(), | ||
1491 | isEvent: true, | ||
1492 | }, | ||
1493 | }); | ||
1494 | // completely clear the old calendar if it wasn't the initial | ||
1495 | } | ||
1496 | else { | ||
1497 | prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); | ||
1498 | } | ||
1499 | } | ||
1500 | if (nextContext) { | ||
1501 | nextContext.dispatch({ type: 'SET_EVENT_DRAG', state }); | ||
1502 | } | ||
1503 | } | ||
1504 | clearDrag() { | ||
1505 | let initialCalendar = this.component.context; | ||
1506 | let { receivingContext } = this; | ||
1507 | if (receivingContext) { | ||
1508 | receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); | ||
1509 | } | ||
1510 | // the initial calendar might have an dummy drag state from displayDrag | ||
1511 | if (initialCalendar !== receivingContext) { | ||
1512 | initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); | ||
1513 | } | ||
1514 | } | ||
1515 | cleanup() { | ||
1516 | this.subjectSeg = null; | ||
1517 | this.isDragging = false; | ||
1518 | this.eventRange = null; | ||
1519 | this.relevantEvents = null; | ||
1520 | this.receivingContext = null; | ||
1521 | this.validMutation = null; | ||
1522 | this.mutatedRelevantEvents = null; | ||
1523 | } | ||
1524 | } | ||
1525 | // TODO: test this in IE11 | ||
1526 | // QUESTION: why do we need it on the resizable??? | ||
1527 | EventDragging.SELECTOR = '.fc-event-draggable, .fc-event-resizable'; | ||
1528 | function computeEventMutation(hit0, hit1, eventInstanceStart, massagers) { | ||
1529 | let dateSpan0 = hit0.dateSpan; | ||
1530 | let dateSpan1 = hit1.dateSpan; | ||
1531 | let date0 = dateSpan0.range.start; | ||
1532 | let date1 = dateSpan1.range.start; | ||
1533 | let standardProps = {}; | ||
1534 | if (dateSpan0.allDay !== dateSpan1.allDay) { | ||
1535 | standardProps.allDay = dateSpan1.allDay; | ||
1536 | standardProps.hasEnd = hit1.context.options.allDayMaintainDuration; | ||
1537 | if (dateSpan1.allDay) { | ||
1538 | // means date1 is already start-of-day, | ||
1539 | // but date0 needs to be converted | ||
1540 | date0 = internal.startOfDay(eventInstanceStart); | ||
1541 | } | ||
1542 | else { | ||
1543 | // Moving from allDate->timed | ||
1544 | // Doesn't matter where on the event the drag began, mutate the event's start-date to date1 | ||
1545 | date0 = eventInstanceStart; | ||
1546 | } | ||
1547 | } | ||
1548 | let delta = internal.diffDates(date0, date1, hit0.context.dateEnv, hit0.componentId === hit1.componentId ? | ||
1549 | hit0.largeUnit : | ||
1550 | null); | ||
1551 | if (delta.milliseconds) { // has hours/minutes/seconds | ||
1552 | standardProps.allDay = false; | ||
1553 | } | ||
1554 | let mutation = { | ||
1555 | datesDelta: delta, | ||
1556 | standardProps, | ||
1557 | }; | ||
1558 | for (let massager of massagers) { | ||
1559 | massager(mutation, hit0, hit1); | ||
1560 | } | ||
1561 | return mutation; | ||
1562 | } | ||
1563 | function getComponentTouchDelay(component) { | ||
1564 | let { options } = component.context; | ||
1565 | let delay = options.eventLongPressDelay; | ||
1566 | if (delay == null) { | ||
1567 | delay = options.longPressDelay; | ||
1568 | } | ||
1569 | return delay; | ||
1570 | } | ||
1571 | |||
1572 | class EventResizing extends internal.Interaction { | ||
1573 | constructor(settings) { | ||
1574 | super(settings); | ||
1575 | // internal state | ||
1576 | this.draggingSegEl = null; | ||
1577 | this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg? | ||
1578 | this.eventRange = null; | ||
1579 | this.relevantEvents = null; | ||
1580 | this.validMutation = null; | ||
1581 | this.mutatedRelevantEvents = null; | ||
1582 | this.handlePointerDown = (ev) => { | ||
1583 | let { component } = this; | ||
1584 | let segEl = this.querySegEl(ev); | ||
1585 | let seg = internal.getElSeg(segEl); | ||
1586 | let eventRange = this.eventRange = seg.eventRange; | ||
1587 | this.dragging.minDistance = component.context.options.eventDragMinDistance; | ||
1588 | // if touch, need to be working with a selected event | ||
1589 | this.dragging.setIgnoreMove(!this.component.isValidSegDownEl(ev.origEvent.target) || | ||
1590 | (ev.isTouch && this.component.props.eventSelection !== eventRange.instance.instanceId)); | ||
1591 | }; | ||
1592 | this.handleDragStart = (ev) => { | ||
1593 | let { context } = this.component; | ||
1594 | let eventRange = this.eventRange; | ||
1595 | this.relevantEvents = internal.getRelevantEvents(context.getCurrentData().eventStore, this.eventRange.instance.instanceId); | ||
1596 | let segEl = this.querySegEl(ev); | ||
1597 | this.draggingSegEl = segEl; | ||
1598 | this.draggingSeg = internal.getElSeg(segEl); | ||
1599 | context.calendarApi.unselect(); | ||
1600 | context.emitter.trigger('eventResizeStart', { | ||
1601 | el: segEl, | ||
1602 | event: new internal.EventImpl(context, eventRange.def, eventRange.instance), | ||
1603 | jsEvent: ev.origEvent, | ||
1604 | view: context.viewApi, | ||
1605 | }); | ||
1606 | }; | ||
1607 | this.handleHitUpdate = (hit, isFinal, ev) => { | ||
1608 | let { context } = this.component; | ||
1609 | let relevantEvents = this.relevantEvents; | ||
1610 | let initialHit = this.hitDragging.initialHit; | ||
1611 | let eventInstance = this.eventRange.instance; | ||
1612 | let mutation = null; | ||
1613 | let mutatedRelevantEvents = null; | ||
1614 | let isInvalid = false; | ||
1615 | let interaction = { | ||
1616 | affectedEvents: relevantEvents, | ||
1617 | mutatedEvents: internal.createEmptyEventStore(), | ||
1618 | isEvent: true, | ||
1619 | }; | ||
1620 | if (hit) { | ||
1621 | let disallowed = hit.componentId === initialHit.componentId | ||
1622 | && this.isHitComboAllowed | ||
1623 | && !this.isHitComboAllowed(initialHit, hit); | ||
1624 | if (!disallowed) { | ||
1625 | mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-event-resizer-start'), eventInstance.range); | ||
1626 | } | ||
1627 | } | ||
1628 | if (mutation) { | ||
1629 | mutatedRelevantEvents = internal.applyMutationToEventStore(relevantEvents, context.getCurrentData().eventUiBases, mutation, context); | ||
1630 | interaction.mutatedEvents = mutatedRelevantEvents; | ||
1631 | if (!internal.isInteractionValid(interaction, hit.dateProfile, context)) { | ||
1632 | isInvalid = true; | ||
1633 | mutation = null; | ||
1634 | mutatedRelevantEvents = null; | ||
1635 | interaction.mutatedEvents = null; | ||
1636 | } | ||
1637 | } | ||
1638 | if (mutatedRelevantEvents) { | ||
1639 | context.dispatch({ | ||
1640 | type: 'SET_EVENT_RESIZE', | ||
1641 | state: interaction, | ||
1642 | }); | ||
1643 | } | ||
1644 | else { | ||
1645 | context.dispatch({ type: 'UNSET_EVENT_RESIZE' }); | ||
1646 | } | ||
1647 | if (!isInvalid) { | ||
1648 | internal.enableCursor(); | ||
1649 | } | ||
1650 | else { | ||
1651 | internal.disableCursor(); | ||
1652 | } | ||
1653 | if (!isFinal) { | ||
1654 | if (mutation && isHitsEqual(initialHit, hit)) { | ||
1655 | mutation = null; | ||
1656 | } | ||
1657 | this.validMutation = mutation; | ||
1658 | this.mutatedRelevantEvents = mutatedRelevantEvents; | ||
1659 | } | ||
1660 | }; | ||
1661 | this.handleDragEnd = (ev) => { | ||
1662 | let { context } = this.component; | ||
1663 | let eventDef = this.eventRange.def; | ||
1664 | let eventInstance = this.eventRange.instance; | ||
1665 | let eventApi = new internal.EventImpl(context, eventDef, eventInstance); | ||
1666 | let relevantEvents = this.relevantEvents; | ||
1667 | let mutatedRelevantEvents = this.mutatedRelevantEvents; | ||
1668 | context.emitter.trigger('eventResizeStop', { | ||
1669 | el: this.draggingSegEl, | ||
1670 | event: eventApi, | ||
1671 | jsEvent: ev.origEvent, | ||
1672 | view: context.viewApi, | ||
1673 | }); | ||
1674 | if (this.validMutation) { | ||
1675 | let updatedEventApi = new internal.EventImpl(context, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null); | ||
1676 | context.dispatch({ | ||
1677 | type: 'MERGE_EVENTS', | ||
1678 | eventStore: mutatedRelevantEvents, | ||
1679 | }); | ||
1680 | let eventChangeArg = { | ||
1681 | oldEvent: eventApi, | ||
1682 | event: updatedEventApi, | ||
1683 | relatedEvents: internal.buildEventApis(mutatedRelevantEvents, context, eventInstance), | ||
1684 | revert() { | ||
1685 | context.dispatch({ | ||
1686 | type: 'MERGE_EVENTS', | ||
1687 | eventStore: relevantEvents, // the pre-change events | ||
1688 | }); | ||
1689 | }, | ||
1690 | }; | ||
1691 | context.emitter.trigger('eventResize', Object.assign(Object.assign({}, eventChangeArg), { el: this.draggingSegEl, startDelta: this.validMutation.startDelta || internal.createDuration(0), endDelta: this.validMutation.endDelta || internal.createDuration(0), jsEvent: ev.origEvent, view: context.viewApi })); | ||
1692 | context.emitter.trigger('eventChange', eventChangeArg); | ||
1693 | } | ||
1694 | else { | ||
1695 | context.emitter.trigger('_noEventResize'); | ||
1696 | } | ||
1697 | // reset all internal state | ||
1698 | this.draggingSeg = null; | ||
1699 | this.relevantEvents = null; | ||
1700 | this.validMutation = null; | ||
1701 | // okay to keep eventInstance around. useful to set it in handlePointerDown | ||
1702 | }; | ||
1703 | let { component } = settings; | ||
1704 | let dragging = this.dragging = new FeaturefulElementDragging(settings.el); | ||
1705 | dragging.pointer.selector = '.fc-event-resizer'; | ||
1706 | dragging.touchScrollAllowed = false; | ||
1707 | dragging.autoScroller.isEnabled = component.context.options.dragScroll; | ||
1708 | let hitDragging = this.hitDragging = new HitDragging(this.dragging, internal.interactionSettingsToStore(settings)); | ||
1709 | hitDragging.emitter.on('pointerdown', this.handlePointerDown); | ||
1710 | hitDragging.emitter.on('dragstart', this.handleDragStart); | ||
1711 | hitDragging.emitter.on('hitupdate', this.handleHitUpdate); | ||
1712 | hitDragging.emitter.on('dragend', this.handleDragEnd); | ||
1713 | } | ||
1714 | destroy() { | ||
1715 | this.dragging.destroy(); | ||
1716 | } | ||
1717 | querySegEl(ev) { | ||
1718 | return internal.elementClosest(ev.subjectEl, '.fc-event'); | ||
1719 | } | ||
1720 | } | ||
1721 | function computeMutation(hit0, hit1, isFromStart, instanceRange) { | ||
1722 | let dateEnv = hit0.context.dateEnv; | ||
1723 | let date0 = hit0.dateSpan.range.start; | ||
1724 | let date1 = hit1.dateSpan.range.start; | ||
1725 | let delta = internal.diffDates(date0, date1, dateEnv, hit0.largeUnit); | ||
1726 | if (isFromStart) { | ||
1727 | if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) { | ||
1728 | return { startDelta: delta }; | ||
1729 | } | ||
1730 | } | ||
1731 | else if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) { | ||
1732 | return { endDelta: delta }; | ||
1733 | } | ||
1734 | return null; | ||
1735 | } | ||
1736 | |||
1737 | class UnselectAuto { | ||
1738 | constructor(context) { | ||
1739 | this.context = context; | ||
1740 | this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system | ||
1741 | this.matchesCancel = false; | ||
1742 | this.matchesEvent = false; | ||
1743 | this.onSelect = (selectInfo) => { | ||
1744 | if (selectInfo.jsEvent) { | ||
1745 | this.isRecentPointerDateSelect = true; | ||
1746 | } | ||
1747 | }; | ||
1748 | this.onDocumentPointerDown = (pev) => { | ||
1749 | let unselectCancel = this.context.options.unselectCancel; | ||
1750 | let downEl = internal.getEventTargetViaRoot(pev.origEvent); | ||
1751 | this.matchesCancel = !!internal.elementClosest(downEl, unselectCancel); | ||
1752 | this.matchesEvent = !!internal.elementClosest(downEl, EventDragging.SELECTOR); // interaction started on an event? | ||
1753 | }; | ||
1754 | this.onDocumentPointerUp = (pev) => { | ||
1755 | let { context } = this; | ||
1756 | let { documentPointer } = this; | ||
1757 | let calendarState = context.getCurrentData(); | ||
1758 | // touch-scrolling should never unfocus any type of selection | ||
1759 | if (!documentPointer.wasTouchScroll) { | ||
1760 | if (calendarState.dateSelection && // an existing date selection? | ||
1761 | !this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp? | ||
1762 | ) { | ||
1763 | let unselectAuto = context.options.unselectAuto; | ||
1764 | if (unselectAuto && (!unselectAuto || !this.matchesCancel)) { | ||
1765 | context.calendarApi.unselect(pev); | ||
1766 | } | ||
1767 | } | ||
1768 | if (calendarState.eventSelection && // an existing event selected? | ||
1769 | !this.matchesEvent // interaction DIDN'T start on an event | ||
1770 | ) { | ||
1771 | context.dispatch({ type: 'UNSELECT_EVENT' }); | ||
1772 | } | ||
1773 | } | ||
1774 | this.isRecentPointerDateSelect = false; | ||
1775 | }; | ||
1776 | let documentPointer = this.documentPointer = new PointerDragging(document); | ||
1777 | documentPointer.shouldIgnoreMove = true; | ||
1778 | documentPointer.shouldWatchScroll = false; | ||
1779 | documentPointer.emitter.on('pointerdown', this.onDocumentPointerDown); | ||
1780 | documentPointer.emitter.on('pointerup', this.onDocumentPointerUp); | ||
1781 | /* | ||
1782 | TODO: better way to know about whether there was a selection with the pointer | ||
1783 | */ | ||
1784 | context.emitter.on('select', this.onSelect); | ||
1785 | } | ||
1786 | destroy() { | ||
1787 | this.context.emitter.off('select', this.onSelect); | ||
1788 | this.documentPointer.destroy(); | ||
1789 | } | ||
1790 | } | ||
1791 | |||
1792 | const OPTION_REFINERS = { | ||
1793 | fixedMirrorParent: internal.identity, | ||
1794 | }; | ||
1795 | const LISTENER_REFINERS = { | ||
1796 | dateClick: internal.identity, | ||
1797 | eventDragStart: internal.identity, | ||
1798 | eventDragStop: internal.identity, | ||
1799 | eventDrop: internal.identity, | ||
1800 | eventResizeStart: internal.identity, | ||
1801 | eventResizeStop: internal.identity, | ||
1802 | eventResize: internal.identity, | ||
1803 | drop: internal.identity, | ||
1804 | eventReceive: internal.identity, | ||
1805 | eventLeave: internal.identity, | ||
1806 | }; | ||
1807 | |||
1808 | /* | ||
1809 | Given an already instantiated draggable object for one-or-more elements, | ||
1810 | Interprets any dragging as an attempt to drag an events that lives outside | ||
1811 | of a calendar onto a calendar. | ||
1812 | */ | ||
1813 | class ExternalElementDragging { | ||
1814 | constructor(dragging, suppliedDragMeta) { | ||
1815 | this.receivingContext = null; | ||
1816 | this.droppableEvent = null; // will exist for all drags, even if create:false | ||
1817 | this.suppliedDragMeta = null; | ||
1818 | this.dragMeta = null; | ||
1819 | this.handleDragStart = (ev) => { | ||
1820 | this.dragMeta = this.buildDragMeta(ev.subjectEl); | ||
1821 | }; | ||
1822 | this.handleHitUpdate = (hit, isFinal, ev) => { | ||
1823 | let { dragging } = this.hitDragging; | ||
1824 | let receivingContext = null; | ||
1825 | let droppableEvent = null; | ||
1826 | let isInvalid = false; | ||
1827 | let interaction = { | ||
1828 | affectedEvents: internal.createEmptyEventStore(), | ||
1829 | mutatedEvents: internal.createEmptyEventStore(), | ||
1830 | isEvent: this.dragMeta.create, | ||
1831 | }; | ||
1832 | if (hit) { | ||
1833 | receivingContext = hit.context; | ||
1834 | if (this.canDropElOnCalendar(ev.subjectEl, receivingContext)) { | ||
1835 | droppableEvent = computeEventForDateSpan(hit.dateSpan, this.dragMeta, receivingContext); | ||
1836 | interaction.mutatedEvents = internal.eventTupleToStore(droppableEvent); | ||
1837 | isInvalid = !internal.isInteractionValid(interaction, hit.dateProfile, receivingContext); | ||
1838 | if (isInvalid) { | ||
1839 | interaction.mutatedEvents = internal.createEmptyEventStore(); | ||
1840 | droppableEvent = null; | ||
1841 | } | ||
1842 | } | ||
1843 | } | ||
1844 | this.displayDrag(receivingContext, interaction); | ||
1845 | // show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?) | ||
1846 | // TODO: wish we could somehow wait for dispatch to guarantee render | ||
1847 | dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-event-mirror')); | ||
1848 | if (!isInvalid) { | ||
1849 | internal.enableCursor(); | ||
1850 | } | ||
1851 | else { | ||
1852 | internal.disableCursor(); | ||
1853 | } | ||
1854 | if (!isFinal) { | ||
1855 | dragging.setMirrorNeedsRevert(!droppableEvent); | ||
1856 | this.receivingContext = receivingContext; | ||
1857 | this.droppableEvent = droppableEvent; | ||
1858 | } | ||
1859 | }; | ||
1860 | this.handleDragEnd = (pev) => { | ||
1861 | let { receivingContext, droppableEvent } = this; | ||
1862 | this.clearDrag(); | ||
1863 | if (receivingContext && droppableEvent) { | ||
1864 | let finalHit = this.hitDragging.finalHit; | ||
1865 | let finalView = finalHit.context.viewApi; | ||
1866 | let dragMeta = this.dragMeta; | ||
1867 | receivingContext.emitter.trigger('drop', Object.assign(Object.assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, view: finalView })); | ||
1868 | if (dragMeta.create) { | ||
1869 | let addingEvents = internal.eventTupleToStore(droppableEvent); | ||
1870 | receivingContext.dispatch({ | ||
1871 | type: 'MERGE_EVENTS', | ||
1872 | eventStore: addingEvents, | ||
1873 | }); | ||
1874 | if (pev.isTouch) { | ||
1875 | receivingContext.dispatch({ | ||
1876 | type: 'SELECT_EVENT', | ||
1877 | eventInstanceId: droppableEvent.instance.instanceId, | ||
1878 | }); | ||
1879 | } | ||
1880 | // signal that an external event landed | ||
1881 | receivingContext.emitter.trigger('eventReceive', { | ||
1882 | event: new internal.EventImpl(receivingContext, droppableEvent.def, droppableEvent.instance), | ||
1883 | relatedEvents: [], | ||
1884 | revert() { | ||
1885 | receivingContext.dispatch({ | ||
1886 | type: 'REMOVE_EVENTS', | ||
1887 | eventStore: addingEvents, | ||
1888 | }); | ||
1889 | }, | ||
1890 | draggedEl: pev.subjectEl, | ||
1891 | view: finalView, | ||
1892 | }); | ||
1893 | } | ||
1894 | } | ||
1895 | this.receivingContext = null; | ||
1896 | this.droppableEvent = null; | ||
1897 | }; | ||
1898 | let hitDragging = this.hitDragging = new HitDragging(dragging, internal.interactionSettingsStore); | ||
1899 | hitDragging.requireInitial = false; // will start outside of a component | ||
1900 | hitDragging.emitter.on('dragstart', this.handleDragStart); | ||
1901 | hitDragging.emitter.on('hitupdate', this.handleHitUpdate); | ||
1902 | hitDragging.emitter.on('dragend', this.handleDragEnd); | ||
1903 | this.suppliedDragMeta = suppliedDragMeta; | ||
1904 | } | ||
1905 | buildDragMeta(subjectEl) { | ||
1906 | if (typeof this.suppliedDragMeta === 'object') { | ||
1907 | return internal.parseDragMeta(this.suppliedDragMeta); | ||
1908 | } | ||
1909 | if (typeof this.suppliedDragMeta === 'function') { | ||
1910 | return internal.parseDragMeta(this.suppliedDragMeta(subjectEl)); | ||
1911 | } | ||
1912 | return getDragMetaFromEl(subjectEl); | ||
1913 | } | ||
1914 | displayDrag(nextContext, state) { | ||
1915 | let prevContext = this.receivingContext; | ||
1916 | if (prevContext && prevContext !== nextContext) { | ||
1917 | prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); | ||
1918 | } | ||
1919 | if (nextContext) { | ||
1920 | nextContext.dispatch({ type: 'SET_EVENT_DRAG', state }); | ||
1921 | } | ||
1922 | } | ||
1923 | clearDrag() { | ||
1924 | if (this.receivingContext) { | ||
1925 | this.receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' }); | ||
1926 | } | ||
1927 | } | ||
1928 | canDropElOnCalendar(el, receivingContext) { | ||
1929 | let dropAccept = receivingContext.options.dropAccept; | ||
1930 | if (typeof dropAccept === 'function') { | ||
1931 | return dropAccept.call(receivingContext.calendarApi, el); | ||
1932 | } | ||
1933 | if (typeof dropAccept === 'string' && dropAccept) { | ||
1934 | return Boolean(internal.elementMatches(el, dropAccept)); | ||
1935 | } | ||
1936 | return true; | ||
1937 | } | ||
1938 | } | ||
1939 | // Utils for computing event store from the DragMeta | ||
1940 | // ---------------------------------------------------------------------------------------------------- | ||
1941 | function computeEventForDateSpan(dateSpan, dragMeta, context) { | ||
1942 | let defProps = Object.assign({}, dragMeta.leftoverProps); | ||
1943 | for (let transform of context.pluginHooks.externalDefTransforms) { | ||
1944 | Object.assign(defProps, transform(dateSpan, dragMeta)); | ||
1945 | } | ||
1946 | let { refined, extra } = internal.refineEventDef(defProps, context); | ||
1947 | let def = internal.parseEventDef(refined, extra, dragMeta.sourceId, dateSpan.allDay, context.options.forceEventDuration || Boolean(dragMeta.duration), // hasEnd | ||
1948 | context); | ||
1949 | let start = dateSpan.range.start; | ||
1950 | // only rely on time info if drop zone is all-day, | ||
1951 | // otherwise, we already know the time | ||
1952 | if (dateSpan.allDay && dragMeta.startTime) { | ||
1953 | start = context.dateEnv.add(start, dragMeta.startTime); | ||
1954 | } | ||
1955 | let end = dragMeta.duration ? | ||
1956 | context.dateEnv.add(start, dragMeta.duration) : | ||
1957 | internal.getDefaultEventEnd(dateSpan.allDay, start, context); | ||
1958 | let instance = internal.createEventInstance(def.defId, { start, end }); | ||
1959 | return { def, instance }; | ||
1960 | } | ||
1961 | // Utils for extracting data from element | ||
1962 | // ---------------------------------------------------------------------------------------------------- | ||
1963 | function getDragMetaFromEl(el) { | ||
1964 | let str = getEmbeddedElData(el, 'event'); | ||
1965 | let obj = str ? | ||
1966 | JSON.parse(str) : | ||
1967 | { create: false }; // if no embedded data, assume no event creation | ||
1968 | return internal.parseDragMeta(obj); | ||
1969 | } | ||
1970 | internal.config.dataAttrPrefix = ''; | ||
1971 | function getEmbeddedElData(el, name) { | ||
1972 | let prefix = internal.config.dataAttrPrefix; | ||
1973 | let prefixedName = (prefix ? prefix + '-' : '') + name; | ||
1974 | return el.getAttribute('data-' + prefixedName) || ''; | ||
1975 | } | ||
1976 | |||
1977 | /* | ||
1978 | Makes an element (that is *external* to any calendar) draggable. | ||
1979 | Can pass in data that determines how an event will be created when dropped onto a calendar. | ||
1980 | Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system. | ||
1981 | */ | ||
1982 | class ExternalDraggable { | ||
1983 | constructor(el, settings = {}) { | ||
1984 | this.handlePointerDown = (ev) => { | ||
1985 | let { dragging } = this; | ||
1986 | let { minDistance, longPressDelay } = this.settings; | ||
1987 | dragging.minDistance = | ||
1988 | minDistance != null ? | ||
1989 | minDistance : | ||
1990 | (ev.isTouch ? 0 : internal.BASE_OPTION_DEFAULTS.eventDragMinDistance); | ||
1991 | dragging.delay = | ||
1992 | ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv | ||
1993 | (longPressDelay != null ? longPressDelay : internal.BASE_OPTION_DEFAULTS.longPressDelay) : | ||
1994 | 0; | ||
1995 | }; | ||
1996 | this.handleDragStart = (ev) => { | ||
1997 | if (ev.isTouch && | ||
1998 | this.dragging.delay && | ||
1999 | ev.subjectEl.classList.contains('fc-event')) { | ||
2000 | this.dragging.mirror.getMirrorEl().classList.add('fc-event-selected'); | ||
2001 | } | ||
2002 | }; | ||
2003 | this.settings = settings; | ||
2004 | let dragging = this.dragging = new FeaturefulElementDragging(el); | ||
2005 | dragging.touchScrollAllowed = false; | ||
2006 | if (settings.itemSelector != null) { | ||
2007 | dragging.pointer.selector = settings.itemSelector; | ||
2008 | } | ||
2009 | if (settings.appendTo != null) { | ||
2010 | dragging.mirror.parentNode = settings.appendTo; // TODO: write tests | ||
2011 | } | ||
2012 | dragging.emitter.on('pointerdown', this.handlePointerDown); | ||
2013 | dragging.emitter.on('dragstart', this.handleDragStart); | ||
2014 | new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new | ||
2015 | } | ||
2016 | destroy() { | ||
2017 | this.dragging.destroy(); | ||
2018 | } | ||
2019 | } | ||
2020 | |||
2021 | /* | ||
2022 | Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements. | ||
2023 | The third-party system is responsible for drawing the visuals effects of the drag. | ||
2024 | This class simply monitors for pointer movements and fires events. | ||
2025 | It also has the ability to hide the moving element (the "mirror") during the drag. | ||
2026 | */ | ||
2027 | class InferredElementDragging extends internal.ElementDragging { | ||
2028 | constructor(containerEl) { | ||
2029 | super(containerEl); | ||
2030 | this.shouldIgnoreMove = false; | ||
2031 | this.mirrorSelector = ''; | ||
2032 | this.currentMirrorEl = null; | ||
2033 | this.handlePointerDown = (ev) => { | ||
2034 | this.emitter.trigger('pointerdown', ev); | ||
2035 | if (!this.shouldIgnoreMove) { | ||
2036 | // fire dragstart right away. does not support delay or min-distance | ||
2037 | this.emitter.trigger('dragstart', ev); | ||
2038 | } | ||
2039 | }; | ||
2040 | this.handlePointerMove = (ev) => { | ||
2041 | if (!this.shouldIgnoreMove) { | ||
2042 | this.emitter.trigger('dragmove', ev); | ||
2043 | } | ||
2044 | }; | ||
2045 | this.handlePointerUp = (ev) => { | ||
2046 | this.emitter.trigger('pointerup', ev); | ||
2047 | if (!this.shouldIgnoreMove) { | ||
2048 | // fire dragend right away. does not support a revert animation | ||
2049 | this.emitter.trigger('dragend', ev); | ||
2050 | } | ||
2051 | }; | ||
2052 | let pointer = this.pointer = new PointerDragging(containerEl); | ||
2053 | pointer.emitter.on('pointerdown', this.handlePointerDown); | ||
2054 | pointer.emitter.on('pointermove', this.handlePointerMove); | ||
2055 | pointer.emitter.on('pointerup', this.handlePointerUp); | ||
2056 | } | ||
2057 | destroy() { | ||
2058 | this.pointer.destroy(); | ||
2059 | } | ||
2060 | setIgnoreMove(bool) { | ||
2061 | this.shouldIgnoreMove = bool; | ||
2062 | } | ||
2063 | setMirrorIsVisible(bool) { | ||
2064 | if (bool) { | ||
2065 | // restore a previously hidden element. | ||
2066 | // use the reference in case the selector class has already been removed. | ||
2067 | if (this.currentMirrorEl) { | ||
2068 | this.currentMirrorEl.style.visibility = ''; | ||
2069 | this.currentMirrorEl = null; | ||
2070 | } | ||
2071 | } | ||
2072 | else { | ||
2073 | let mirrorEl = this.mirrorSelector | ||
2074 | // TODO: somehow query FullCalendars WITHIN shadow-roots | ||
2075 | ? document.querySelector(this.mirrorSelector) | ||
2076 | : null; | ||
2077 | if (mirrorEl) { | ||
2078 | this.currentMirrorEl = mirrorEl; | ||
2079 | mirrorEl.style.visibility = 'hidden'; | ||
2080 | } | ||
2081 | } | ||
2082 | } | ||
2083 | } | ||
2084 | |||
2085 | /* | ||
2086 | Bridges third-party drag-n-drop systems with FullCalendar. | ||
2087 | Must be instantiated and destroyed by caller. | ||
2088 | */ | ||
2089 | class ThirdPartyDraggable { | ||
2090 | constructor(containerOrSettings, settings) { | ||
2091 | let containerEl = document; | ||
2092 | if ( | ||
2093 | // wish we could just test instanceof EventTarget, but doesn't work in IE11 | ||
2094 | containerOrSettings === document || | ||
2095 | containerOrSettings instanceof Element) { | ||
2096 | containerEl = containerOrSettings; | ||
2097 | settings = settings || {}; | ||
2098 | } | ||
2099 | else { | ||
2100 | settings = (containerOrSettings || {}); | ||
2101 | } | ||
2102 | let dragging = this.dragging = new InferredElementDragging(containerEl); | ||
2103 | if (typeof settings.itemSelector === 'string') { | ||
2104 | dragging.pointer.selector = settings.itemSelector; | ||
2105 | } | ||
2106 | else if (containerEl === document) { | ||
2107 | dragging.pointer.selector = '[data-event]'; | ||
2108 | } | ||
2109 | if (typeof settings.mirrorSelector === 'string') { | ||
2110 | dragging.mirrorSelector = settings.mirrorSelector; | ||
2111 | } | ||
2112 | let externalDragging = new ExternalElementDragging(dragging, settings.eventData); | ||
2113 | // The hit-detection system requires that the dnd-mirror-element be pointer-events:none, | ||
2114 | // but this can't be guaranteed for third-party draggables, so disable | ||
2115 | externalDragging.hitDragging.disablePointCheck = true; | ||
2116 | } | ||
2117 | destroy() { | ||
2118 | this.dragging.destroy(); | ||
2119 | } | ||
2120 | } | ||
2121 | |||
2122 | var plugin = core.createPlugin({ | ||
2123 | name: '@fullcalendar/interaction', | ||
2124 | componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing], | ||
2125 | calendarInteractions: [UnselectAuto], | ||
2126 | elementDraggingImpl: FeaturefulElementDragging, | ||
2127 | optionRefiners: OPTION_REFINERS, | ||
2128 | listenerRefiners: LISTENER_REFINERS, | ||
2129 | }); | ||
2130 | |||
2131 | core.globalPlugins.push(plugin); | ||
2132 | |||
2133 | exports.Draggable = ExternalDraggable; | ||
2134 | exports.ThirdPartyDraggable = ThirdPartyDraggable; | ||
2135 | exports["default"] = plugin; | ||
2136 | |||
2137 | Object.defineProperty(exports, '__esModule', { value: true }); | ||
2138 | |||
2139 | return exports; | ||
2140 | |||
2141 | })({}, FullCalendar, FullCalendar.Internal); | ||