summaryrefslogtreecommitdiff
path: root/public/js/fullcalendar/packages/interaction/index.global.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/js/fullcalendar/packages/interaction/index.global.js')
-rw-r--r--public/js/fullcalendar/packages/interaction/index.global.js2141
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/*!
2FullCalendar Interaction Plugin v6.1.17
3Docs & License: https://fullcalendar.io/docs/editable
4(c) 2024 Adam Shaw
5*/
6FullCalendar.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);