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, 0 insertions, 2141 deletions
diff --git a/public/js/fullcalendar/packages/interaction/index.global.js b/public/js/fullcalendar/packages/interaction/index.global.js deleted file mode 100644 index d05a22e..0000000 --- a/public/js/fullcalendar/packages/interaction/index.global.js +++ /dev/null | |||
| @@ -1,2141 +0,0 @@ | |||
| 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); | ||
