mirror of https://github.com/espruino/BangleApps
Delete apps/schoolCalendar/fullcalendar/interaction/src directory
parent
1bf5132a5c
commit
3863be1830
|
@ -1,16 +0,0 @@
|
|||
import { computeInnerRect, ElementScrollController } from '@fullcalendar/common'
|
||||
import { ScrollGeomCache } from './ScrollGeomCache'
|
||||
|
||||
export class ElementScrollGeomCache extends ScrollGeomCache {
|
||||
constructor(el: HTMLElement, doesListening: boolean) {
|
||||
super(new ElementScrollController(el), doesListening)
|
||||
}
|
||||
|
||||
getEventTarget(): EventTarget {
|
||||
return (this.scrollController as ElementScrollController).el
|
||||
}
|
||||
|
||||
computeClientRect() {
|
||||
return computeInnerRect((this.scrollController as ElementScrollController).el)
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
import {
|
||||
getClippingParents, computeRect,
|
||||
pointInsideRect, Rect,
|
||||
} from '@fullcalendar/common'
|
||||
import { ElementScrollGeomCache } from './ElementScrollGeomCache'
|
||||
|
||||
/*
|
||||
When this class is instantiated, it records the offset of an element (relative to the document topleft),
|
||||
and continues to monitor scrolling, updating the cached coordinates if it needs to.
|
||||
Does not access the DOM after instantiation, so highly performant.
|
||||
|
||||
Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element
|
||||
and an determine if a given point is inside the combined clipping rectangle.
|
||||
*/
|
||||
export class OffsetTracker { // ElementOffsetTracker
|
||||
scrollCaches: ElementScrollGeomCache[]
|
||||
origRect: Rect
|
||||
|
||||
constructor(el: HTMLElement) {
|
||||
this.origRect = computeRect(el)
|
||||
|
||||
// will work fine for divs that have overflow:hidden
|
||||
this.scrollCaches = getClippingParents(el).map(
|
||||
(scrollEl) => new ElementScrollGeomCache(scrollEl, true), // listen=true
|
||||
)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (let scrollCache of this.scrollCaches) {
|
||||
scrollCache.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
computeLeft() {
|
||||
let left = this.origRect.left
|
||||
|
||||
for (let scrollCache of this.scrollCaches) {
|
||||
left += scrollCache.origScrollLeft - scrollCache.getScrollLeft()
|
||||
}
|
||||
|
||||
return left
|
||||
}
|
||||
|
||||
computeTop() {
|
||||
let top = this.origRect.top
|
||||
|
||||
for (let scrollCache of this.scrollCaches) {
|
||||
top += scrollCache.origScrollTop - scrollCache.getScrollTop()
|
||||
}
|
||||
|
||||
return top
|
||||
}
|
||||
|
||||
isWithinClipping(pageX: number, pageY: number): boolean {
|
||||
let point = { left: pageX, top: pageY }
|
||||
|
||||
for (let scrollCache of this.scrollCaches) {
|
||||
if (
|
||||
!isIgnoredClipping(scrollCache.getEventTarget()) &&
|
||||
!pointInsideRect(point, scrollCache.clientRect)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// certain clipping containers should never constrain interactions, like <html> and <body>
|
||||
// https://github.com/fullcalendar/fullcalendar/issues/3615
|
||||
function isIgnoredClipping(node: EventTarget) {
|
||||
let tagName = (node as HTMLElement).tagName
|
||||
|
||||
return tagName === 'HTML' || tagName === 'BODY'
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
import { Rect, ScrollController } from '@fullcalendar/common'
|
||||
|
||||
/*
|
||||
Is a cache for a given element's scroll information (all the info that ScrollController stores)
|
||||
in addition the "client rectangle" of the element.. the area within the scrollbars.
|
||||
|
||||
The cache can be in one of two modes:
|
||||
- doesListening:false - ignores when the container is scrolled by someone else
|
||||
- doesListening:true - watch for scrolling and update the cache
|
||||
*/
|
||||
export abstract class ScrollGeomCache extends ScrollController {
|
||||
clientRect: Rect
|
||||
origScrollTop: number
|
||||
origScrollLeft: number
|
||||
|
||||
protected scrollController: ScrollController
|
||||
protected doesListening: boolean
|
||||
protected scrollTop: number
|
||||
protected scrollLeft: number
|
||||
protected scrollWidth: number
|
||||
protected scrollHeight: number
|
||||
protected clientWidth: number
|
||||
protected clientHeight: number
|
||||
|
||||
constructor(scrollController: ScrollController, doesListening: boolean) {
|
||||
super()
|
||||
this.scrollController = scrollController
|
||||
this.doesListening = doesListening
|
||||
this.scrollTop = this.origScrollTop = scrollController.getScrollTop()
|
||||
this.scrollLeft = this.origScrollLeft = scrollController.getScrollLeft()
|
||||
this.scrollWidth = scrollController.getScrollWidth()
|
||||
this.scrollHeight = scrollController.getScrollHeight()
|
||||
this.clientWidth = scrollController.getClientWidth()
|
||||
this.clientHeight = scrollController.getClientHeight()
|
||||
this.clientRect = this.computeClientRect() // do last in case it needs cached values
|
||||
|
||||
if (this.doesListening) {
|
||||
this.getEventTarget().addEventListener('scroll', this.handleScroll)
|
||||
}
|
||||
}
|
||||
|
||||
abstract getEventTarget(): EventTarget
|
||||
abstract computeClientRect(): Rect
|
||||
|
||||
destroy() {
|
||||
if (this.doesListening) {
|
||||
this.getEventTarget().removeEventListener('scroll', this.handleScroll)
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll = () => {
|
||||
this.scrollTop = this.scrollController.getScrollTop()
|
||||
this.scrollLeft = this.scrollController.getScrollLeft()
|
||||
this.handleScrollChange()
|
||||
}
|
||||
|
||||
getScrollTop() {
|
||||
return this.scrollTop
|
||||
}
|
||||
|
||||
getScrollLeft() {
|
||||
return this.scrollLeft
|
||||
}
|
||||
|
||||
setScrollTop(top: number) {
|
||||
this.scrollController.setScrollTop(top)
|
||||
|
||||
if (!this.doesListening) {
|
||||
// we are not relying on the element to normalize out-of-bounds scroll values
|
||||
// so we need to sanitize ourselves
|
||||
this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0)
|
||||
|
||||
this.handleScrollChange()
|
||||
}
|
||||
}
|
||||
|
||||
setScrollLeft(top: number) {
|
||||
this.scrollController.setScrollLeft(top)
|
||||
|
||||
if (!this.doesListening) {
|
||||
// we are not relying on the element to normalize out-of-bounds scroll values
|
||||
// so we need to sanitize ourselves
|
||||
this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0)
|
||||
|
||||
this.handleScrollChange()
|
||||
}
|
||||
}
|
||||
|
||||
getClientWidth() {
|
||||
return this.clientWidth
|
||||
}
|
||||
|
||||
getClientHeight() {
|
||||
return this.clientHeight
|
||||
}
|
||||
|
||||
getScrollWidth() {
|
||||
return this.scrollWidth
|
||||
}
|
||||
|
||||
getScrollHeight() {
|
||||
return this.scrollHeight
|
||||
}
|
||||
|
||||
handleScrollChange() {
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import { Rect, WindowScrollController } from '@fullcalendar/common'
|
||||
import { ScrollGeomCache } from './ScrollGeomCache'
|
||||
|
||||
export class WindowScrollGeomCache extends ScrollGeomCache {
|
||||
constructor(doesListening: boolean) {
|
||||
super(new WindowScrollController(), doesListening)
|
||||
}
|
||||
|
||||
getEventTarget(): EventTarget {
|
||||
return window
|
||||
}
|
||||
|
||||
computeClientRect(): Rect {
|
||||
return {
|
||||
left: this.scrollLeft,
|
||||
right: this.scrollLeft + this.clientWidth,
|
||||
top: this.scrollTop,
|
||||
bottom: this.scrollTop + this.clientHeight,
|
||||
}
|
||||
}
|
||||
|
||||
// the window is the only scroll object that changes it's rectangle relative
|
||||
// to the document's topleft as it scrolls
|
||||
handleScrollChange() {
|
||||
this.clientRect = this.computeClientRect()
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
// TODO: rename file to public-types.ts
|
||||
|
||||
export { DateClickArg } from './interactions/DateClicking'
|
||||
export { EventDragStartArg, EventDragStopArg } from './interactions/EventDragging'
|
||||
export { EventResizeStartArg, EventResizeStopArg, EventResizeDoneArg } from './interactions/EventResizing'
|
||||
export { DropArg, EventReceiveArg, EventLeaveArg } from './utils'
|
|
@ -1,217 +0,0 @@
|
|||
import { getElRoot } from '@fullcalendar/common'
|
||||
import { ScrollGeomCache } from '../ScrollGeomCache'
|
||||
import { ElementScrollGeomCache } from '../ElementScrollGeomCache'
|
||||
import { WindowScrollGeomCache } from '../WindowScrollGeomCache'
|
||||
|
||||
interface Edge {
|
||||
scrollCache: ScrollGeomCache
|
||||
name: 'top' | 'left' | 'right' | 'bottom'
|
||||
distance: number // how many pixels the current pointer is from the edge
|
||||
}
|
||||
|
||||
// If available we are using native "performance" API instead of "Date"
|
||||
// Read more about it on MDN:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
||||
const getTime = typeof performance === 'function' ? (performance as any).now : Date.now
|
||||
|
||||
/*
|
||||
For a pointer interaction, automatically scrolls certain scroll containers when the pointer
|
||||
approaches the edge.
|
||||
|
||||
The caller must call start + handleMove + stop.
|
||||
*/
|
||||
export class AutoScroller {
|
||||
// options that can be set by caller
|
||||
isEnabled: boolean = true
|
||||
scrollQuery: (Window | string)[] = [window, '.fc-scroller']
|
||||
edgeThreshold: number = 50 // pixels
|
||||
maxVelocity: number = 300 // pixels per second
|
||||
|
||||
// internal state
|
||||
pointerScreenX: number | null = null
|
||||
pointerScreenY: number | null = null
|
||||
isAnimating: boolean = false
|
||||
scrollCaches: ScrollGeomCache[] | null = null
|
||||
msSinceRequest?: number
|
||||
|
||||
// protect against the initial pointerdown being too close to an edge and starting the scroll
|
||||
everMovedUp: boolean = false
|
||||
everMovedDown: boolean = false
|
||||
everMovedLeft: boolean = false
|
||||
everMovedRight: boolean = false
|
||||
|
||||
start(pageX: number, pageY: number, scrollStartEl: HTMLElement) {
|
||||
if (this.isEnabled) {
|
||||
this.scrollCaches = this.buildCaches(scrollStartEl)
|
||||
this.pointerScreenX = null
|
||||
this.pointerScreenY = null
|
||||
this.everMovedUp = false
|
||||
this.everMovedDown = false
|
||||
this.everMovedLeft = false
|
||||
this.everMovedRight = false
|
||||
this.handleMove(pageX, pageY)
|
||||
}
|
||||
}
|
||||
|
||||
handleMove(pageX: number, pageY: number) {
|
||||
if (this.isEnabled) {
|
||||
let pointerScreenX = pageX - window.pageXOffset
|
||||
let pointerScreenY = pageY - window.pageYOffset
|
||||
|
||||
let yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY
|
||||
let xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX
|
||||
|
||||
if (yDelta < 0) {
|
||||
this.everMovedUp = true
|
||||
} else if (yDelta > 0) {
|
||||
this.everMovedDown = true
|
||||
}
|
||||
|
||||
if (xDelta < 0) {
|
||||
this.everMovedLeft = true
|
||||
} else if (xDelta > 0) {
|
||||
this.everMovedRight = true
|
||||
}
|
||||
|
||||
this.pointerScreenX = pointerScreenX
|
||||
this.pointerScreenY = pointerScreenY
|
||||
|
||||
if (!this.isAnimating) {
|
||||
this.isAnimating = true
|
||||
this.requestAnimation(getTime())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.isEnabled) {
|
||||
this.isAnimating = false // will stop animation
|
||||
|
||||
for (let scrollCache of this.scrollCaches!) {
|
||||
scrollCache.destroy()
|
||||
}
|
||||
|
||||
this.scrollCaches = null
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimation(now: number) {
|
||||
this.msSinceRequest = now
|
||||
requestAnimationFrame(this.animate)
|
||||
}
|
||||
|
||||
private animate = () => {
|
||||
if (this.isAnimating) { // wasn't cancelled between animation calls
|
||||
let edge = this.computeBestEdge(
|
||||
this.pointerScreenX! + window.pageXOffset,
|
||||
this.pointerScreenY! + window.pageYOffset,
|
||||
)
|
||||
|
||||
if (edge) {
|
||||
let now = getTime()
|
||||
this.handleSide(edge, (now - this.msSinceRequest!) / 1000)
|
||||
this.requestAnimation(now)
|
||||
} else {
|
||||
this.isAnimating = false // will stop animation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleSide(edge: Edge, seconds: number) {
|
||||
let { scrollCache } = edge
|
||||
let { edgeThreshold } = this
|
||||
let invDistance = edgeThreshold - edge.distance
|
||||
let velocity = // the closer to the edge, the faster we scroll
|
||||
((invDistance * invDistance) / (edgeThreshold * edgeThreshold)) * // quadratic
|
||||
this.maxVelocity * seconds
|
||||
let sign = 1
|
||||
|
||||
switch (edge.name) {
|
||||
case 'left':
|
||||
sign = -1
|
||||
// falls through
|
||||
case 'right':
|
||||
scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign)
|
||||
break
|
||||
|
||||
case 'top':
|
||||
sign = -1
|
||||
// falls through
|
||||
case 'bottom':
|
||||
scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// left/top are relative to document topleft
|
||||
private computeBestEdge(left: number, top: number): Edge | null {
|
||||
let { edgeThreshold } = this
|
||||
let bestSide: Edge | null = null
|
||||
|
||||
for (let scrollCache of this.scrollCaches!) {
|
||||
let rect = scrollCache.clientRect
|
||||
let leftDist = left - rect.left
|
||||
let rightDist = rect.right - left
|
||||
let topDist = top - rect.top
|
||||
let bottomDist = rect.bottom - top
|
||||
|
||||
// completely within the rect?
|
||||
if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) {
|
||||
if (
|
||||
topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() &&
|
||||
(!bestSide || bestSide.distance > topDist)
|
||||
) {
|
||||
bestSide = { scrollCache, name: 'top', distance: topDist }
|
||||
}
|
||||
|
||||
if (
|
||||
bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() &&
|
||||
(!bestSide || bestSide.distance > bottomDist)
|
||||
) {
|
||||
bestSide = { scrollCache, name: 'bottom', distance: bottomDist }
|
||||
}
|
||||
|
||||
if (
|
||||
leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() &&
|
||||
(!bestSide || bestSide.distance > leftDist)
|
||||
) {
|
||||
bestSide = { scrollCache, name: 'left', distance: leftDist }
|
||||
}
|
||||
|
||||
if (
|
||||
rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() &&
|
||||
(!bestSide || bestSide.distance > rightDist)
|
||||
) {
|
||||
bestSide = { scrollCache, name: 'right', distance: rightDist }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestSide
|
||||
}
|
||||
|
||||
private buildCaches(scrollStartEl: HTMLElement) {
|
||||
return this.queryScrollEls(scrollStartEl).map((el) => {
|
||||
if (el === window) {
|
||||
return new WindowScrollGeomCache(false) // false = don't listen to user-generated scrolls
|
||||
}
|
||||
return new ElementScrollGeomCache(el, false) // false = don't listen to user-generated scrolls
|
||||
})
|
||||
}
|
||||
|
||||
private queryScrollEls(scrollStartEl: HTMLElement) {
|
||||
let els = []
|
||||
|
||||
for (let query of this.scrollQuery) {
|
||||
if (typeof query === 'object') {
|
||||
els.push(query)
|
||||
} else {
|
||||
els.push(...Array.prototype.slice.call(
|
||||
getElRoot(scrollStartEl).querySelectorAll(query),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return els
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
import { removeElement, applyStyle, whenTransitionDone, Rect } from '@fullcalendar/common'
|
||||
|
||||
/*
|
||||
An effect in which an element follows the movement of a pointer across the screen.
|
||||
The moving element is a clone of some other element.
|
||||
Must call start + handleMove + stop.
|
||||
*/
|
||||
export class ElementMirror {
|
||||
isVisible: boolean = false // must be explicitly enabled
|
||||
origScreenX?: number
|
||||
origScreenY?: number
|
||||
deltaX?: number
|
||||
deltaY?: number
|
||||
sourceEl: HTMLElement | null = null
|
||||
mirrorEl: HTMLElement | null = null
|
||||
sourceElRect: Rect | null = null // screen coords relative to viewport
|
||||
|
||||
// options that can be set directly by caller
|
||||
parentNode: HTMLElement = document.body // HIGHLY SUGGESTED to set this to sidestep ShadowDOM issues
|
||||
zIndex: number = 9999
|
||||
revertDuration: number = 0
|
||||
|
||||
start(sourceEl: HTMLElement, pageX: number, pageY: number) {
|
||||
this.sourceEl = sourceEl
|
||||
this.sourceElRect = this.sourceEl.getBoundingClientRect()
|
||||
this.origScreenX = pageX - window.pageXOffset
|
||||
this.origScreenY = pageY - window.pageYOffset
|
||||
this.deltaX = 0
|
||||
this.deltaY = 0
|
||||
this.updateElPosition()
|
||||
}
|
||||
|
||||
handleMove(pageX: number, pageY: number) {
|
||||
this.deltaX = (pageX - window.pageXOffset) - this.origScreenX!
|
||||
this.deltaY = (pageY - window.pageYOffset) - this.origScreenY!
|
||||
this.updateElPosition()
|
||||
}
|
||||
|
||||
// can be called before start
|
||||
setIsVisible(bool: boolean) {
|
||||
if (bool) {
|
||||
if (!this.isVisible) {
|
||||
if (this.mirrorEl) {
|
||||
this.mirrorEl.style.display = ''
|
||||
}
|
||||
|
||||
this.isVisible = bool // needs to happen before updateElPosition
|
||||
this.updateElPosition() // because was not updating the position while invisible
|
||||
}
|
||||
} else if (this.isVisible) {
|
||||
if (this.mirrorEl) {
|
||||
this.mirrorEl.style.display = 'none'
|
||||
}
|
||||
|
||||
this.isVisible = bool
|
||||
}
|
||||
}
|
||||
|
||||
// always async
|
||||
stop(needsRevertAnimation: boolean, callback: () => void) {
|
||||
let done = () => {
|
||||
this.cleanup()
|
||||
callback()
|
||||
}
|
||||
|
||||
if (
|
||||
needsRevertAnimation &&
|
||||
this.mirrorEl &&
|
||||
this.isVisible &&
|
||||
this.revertDuration && // if 0, transition won't work
|
||||
(this.deltaX || this.deltaY) // if same coords, transition won't work
|
||||
) {
|
||||
this.doRevertAnimation(done, this.revertDuration)
|
||||
} else {
|
||||
setTimeout(done, 0)
|
||||
}
|
||||
}
|
||||
|
||||
doRevertAnimation(callback: () => void, revertDuration: number) {
|
||||
let mirrorEl = this.mirrorEl!
|
||||
let finalSourceElRect = this.sourceEl!.getBoundingClientRect() // because autoscrolling might have happened
|
||||
|
||||
mirrorEl.style.transition =
|
||||
'top ' + revertDuration + 'ms,' +
|
||||
'left ' + revertDuration + 'ms'
|
||||
|
||||
applyStyle(mirrorEl, {
|
||||
left: finalSourceElRect.left,
|
||||
top: finalSourceElRect.top,
|
||||
})
|
||||
|
||||
whenTransitionDone(mirrorEl, () => {
|
||||
mirrorEl.style.transition = ''
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.mirrorEl) {
|
||||
removeElement(this.mirrorEl)
|
||||
this.mirrorEl = null
|
||||
}
|
||||
|
||||
this.sourceEl = null
|
||||
}
|
||||
|
||||
updateElPosition() {
|
||||
if (this.sourceEl && this.isVisible) {
|
||||
applyStyle(this.getMirrorEl(), {
|
||||
left: this.sourceElRect!.left + this.deltaX!,
|
||||
top: this.sourceElRect!.top + this.deltaY!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getMirrorEl(): HTMLElement {
|
||||
let sourceElRect = this.sourceElRect!
|
||||
let mirrorEl = this.mirrorEl
|
||||
|
||||
if (!mirrorEl) {
|
||||
mirrorEl = this.mirrorEl = this.sourceEl!.cloneNode(true) as HTMLElement // cloneChildren=true
|
||||
|
||||
// we don't want long taps or any mouse interaction causing selection/menus.
|
||||
// would use preventSelection(), but that prevents selectstart, causing problems.
|
||||
mirrorEl.classList.add('fc-unselectable')
|
||||
|
||||
mirrorEl.classList.add('fc-event-dragging')
|
||||
|
||||
applyStyle(mirrorEl, {
|
||||
position: 'fixed',
|
||||
zIndex: this.zIndex,
|
||||
visibility: '', // in case original element was hidden by the drag effect
|
||||
boxSizing: 'border-box', // for easy width/height
|
||||
width: sourceElRect.right - sourceElRect.left, // explicit height in case there was a 'right' value
|
||||
height: sourceElRect.bottom - sourceElRect.top, // explicit width in case there was a 'bottom' value
|
||||
right: 'auto', // erase and set width instead
|
||||
bottom: 'auto', // erase and set height instead
|
||||
margin: 0,
|
||||
})
|
||||
|
||||
this.parentNode.appendChild(mirrorEl)
|
||||
}
|
||||
|
||||
return mirrorEl
|
||||
}
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
import {
|
||||
PointerDragEvent,
|
||||
preventSelection,
|
||||
allowSelection,
|
||||
preventContextMenu,
|
||||
allowContextMenu,
|
||||
ElementDragging,
|
||||
} from '@fullcalendar/common'
|
||||
import { PointerDragging } from './PointerDragging'
|
||||
import { ElementMirror } from './ElementMirror'
|
||||
import { AutoScroller } from './AutoScroller'
|
||||
|
||||
/*
|
||||
Monitors dragging on an element. Has a number of high-level features:
|
||||
- minimum distance required before dragging
|
||||
- minimum wait time ("delay") before dragging
|
||||
- a mirror element that follows the pointer
|
||||
*/
|
||||
export class FeaturefulElementDragging extends ElementDragging {
|
||||
pointer: PointerDragging
|
||||
mirror: ElementMirror
|
||||
autoScroller: AutoScroller
|
||||
|
||||
// options that can be directly set by caller
|
||||
// the caller can also set the PointerDragging's options as well
|
||||
delay: number | null = null
|
||||
minDistance: number = 0
|
||||
touchScrollAllowed: boolean = true // prevents drag from starting and blocks scrolling during drag
|
||||
|
||||
mirrorNeedsRevert: boolean = false
|
||||
isInteracting: boolean = false // is the user validly moving the pointer? lasts until pointerup
|
||||
isDragging: boolean = false // is it INTENTFULLY dragging? lasts until after revert animation
|
||||
isDelayEnded: boolean = false
|
||||
isDistanceSurpassed: boolean = false
|
||||
delayTimeoutId: number | null = null
|
||||
|
||||
constructor(private containerEl: HTMLElement, selector?: string) {
|
||||
super(containerEl)
|
||||
|
||||
let pointer = this.pointer = new PointerDragging(containerEl)
|
||||
pointer.emitter.on('pointerdown', this.onPointerDown)
|
||||
pointer.emitter.on('pointermove', this.onPointerMove)
|
||||
pointer.emitter.on('pointerup', this.onPointerUp)
|
||||
|
||||
if (selector) {
|
||||
pointer.selector = selector
|
||||
}
|
||||
|
||||
this.mirror = new ElementMirror()
|
||||
this.autoScroller = new AutoScroller()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.pointer.destroy()
|
||||
|
||||
// HACK: simulate a pointer-up to end the current drag
|
||||
// TODO: fire 'dragend' directly and stop interaction. discourage use of pointerup event (b/c might not fire)
|
||||
this.onPointerUp({} as any)
|
||||
}
|
||||
|
||||
onPointerDown = (ev: PointerDragEvent) => {
|
||||
if (!this.isDragging) { // so new drag doesn't happen while revert animation is going
|
||||
this.isInteracting = true
|
||||
this.isDelayEnded = false
|
||||
this.isDistanceSurpassed = false
|
||||
|
||||
preventSelection(document.body)
|
||||
preventContextMenu(document.body)
|
||||
|
||||
// prevent links from being visited if there's an eventual drag.
|
||||
// also prevents selection in older browsers (maybe?).
|
||||
// not necessary for touch, besides, browser would complain about passiveness.
|
||||
if (!ev.isTouch) {
|
||||
ev.origEvent.preventDefault()
|
||||
}
|
||||
|
||||
this.emitter.trigger('pointerdown', ev)
|
||||
|
||||
if (
|
||||
this.isInteracting && // not destroyed via pointerdown handler
|
||||
!this.pointer.shouldIgnoreMove
|
||||
) {
|
||||
// actions related to initiating dragstart+dragmove+dragend...
|
||||
|
||||
this.mirror.setIsVisible(false) // reset. caller must set-visible
|
||||
this.mirror.start(ev.subjectEl as HTMLElement, ev.pageX, ev.pageY) // must happen on first pointer down
|
||||
|
||||
this.startDelay(ev)
|
||||
|
||||
if (!this.minDistance) {
|
||||
this.handleDistanceSurpassed(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPointerMove = (ev: PointerDragEvent) => {
|
||||
if (this.isInteracting) {
|
||||
this.emitter.trigger('pointermove', ev)
|
||||
|
||||
if (!this.isDistanceSurpassed) {
|
||||
let minDistance = this.minDistance
|
||||
let distanceSq // current distance from the origin, squared
|
||||
let { deltaX, deltaY } = ev
|
||||
|
||||
distanceSq = deltaX * deltaX + deltaY * deltaY
|
||||
if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
|
||||
this.handleDistanceSurpassed(ev)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isDragging) {
|
||||
// a real pointer move? (not one simulated by scrolling)
|
||||
if (ev.origEvent.type !== 'scroll') {
|
||||
this.mirror.handleMove(ev.pageX, ev.pageY)
|
||||
this.autoScroller.handleMove(ev.pageX, ev.pageY)
|
||||
}
|
||||
|
||||
this.emitter.trigger('dragmove', ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPointerUp = (ev: PointerDragEvent) => {
|
||||
if (this.isInteracting) {
|
||||
this.isInteracting = false
|
||||
|
||||
allowSelection(document.body)
|
||||
allowContextMenu(document.body)
|
||||
|
||||
this.emitter.trigger('pointerup', ev) // can potentially set mirrorNeedsRevert
|
||||
|
||||
if (this.isDragging) {
|
||||
this.autoScroller.stop()
|
||||
this.tryStopDrag(ev) // which will stop the mirror
|
||||
}
|
||||
|
||||
if (this.delayTimeoutId) {
|
||||
clearTimeout(this.delayTimeoutId)
|
||||
this.delayTimeoutId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startDelay(ev: PointerDragEvent) {
|
||||
if (typeof this.delay === 'number') {
|
||||
this.delayTimeoutId = setTimeout(() => {
|
||||
this.delayTimeoutId = null
|
||||
this.handleDelayEnd(ev)
|
||||
}, this.delay) as any // not assignable to number!
|
||||
} else {
|
||||
this.handleDelayEnd(ev)
|
||||
}
|
||||
}
|
||||
|
||||
handleDelayEnd(ev: PointerDragEvent) {
|
||||
this.isDelayEnded = true
|
||||
this.tryStartDrag(ev)
|
||||
}
|
||||
|
||||
handleDistanceSurpassed(ev: PointerDragEvent) {
|
||||
this.isDistanceSurpassed = true
|
||||
this.tryStartDrag(ev)
|
||||
}
|
||||
|
||||
tryStartDrag(ev: PointerDragEvent) {
|
||||
if (this.isDelayEnded && this.isDistanceSurpassed) {
|
||||
if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) {
|
||||
this.isDragging = true
|
||||
this.mirrorNeedsRevert = false
|
||||
|
||||
this.autoScroller.start(ev.pageX, ev.pageY, this.containerEl)
|
||||
this.emitter.trigger('dragstart', ev)
|
||||
|
||||
if (this.touchScrollAllowed === false) {
|
||||
this.pointer.cancelTouchScroll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tryStopDrag(ev: PointerDragEvent) {
|
||||
// .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events
|
||||
// that come from the document to fire beforehand. much more convenient this way.
|
||||
this.mirror.stop(
|
||||
this.mirrorNeedsRevert,
|
||||
this.stopDrag.bind(this, ev), // bound with args
|
||||
)
|
||||
}
|
||||
|
||||
stopDrag(ev: PointerDragEvent) {
|
||||
this.isDragging = false
|
||||
this.emitter.trigger('dragend', ev)
|
||||
}
|
||||
|
||||
// fill in the implementations...
|
||||
|
||||
setIgnoreMove(bool: boolean) {
|
||||
this.pointer.shouldIgnoreMove = bool
|
||||
}
|
||||
|
||||
setMirrorIsVisible(bool: boolean) {
|
||||
this.mirror.setIsVisible(bool)
|
||||
}
|
||||
|
||||
setMirrorNeedsRevert(bool: boolean) {
|
||||
this.mirrorNeedsRevert = bool
|
||||
}
|
||||
|
||||
setAutoScrollEnabled(bool: boolean) {
|
||||
this.autoScroller.isEnabled = bool
|
||||
}
|
||||
}
|
|
@ -1,344 +0,0 @@
|
|||
import { config, elementClosest, Emitter, PointerDragEvent } from '@fullcalendar/common'
|
||||
|
||||
config.touchMouseIgnoreWait = 500
|
||||
|
||||
let ignoreMouseDepth = 0
|
||||
let listenerCnt = 0
|
||||
let isWindowTouchMoveCancelled = false
|
||||
|
||||
/*
|
||||
Uses a "pointer" abstraction, which monitors UI events for both mouse and touch.
|
||||
Tracks when the pointer "drags" on a certain element, meaning down+move+up.
|
||||
|
||||
Also, tracks if there was touch-scrolling.
|
||||
Also, can prevent touch-scrolling from happening.
|
||||
Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement.
|
||||
|
||||
emits:
|
||||
- pointerdown
|
||||
- pointermove
|
||||
- pointerup
|
||||
*/
|
||||
export class PointerDragging {
|
||||
containerEl: EventTarget
|
||||
subjectEl: HTMLElement | null = null
|
||||
emitter: Emitter<any>
|
||||
|
||||
// options that can be directly assigned by caller
|
||||
selector: string = '' // will cause subjectEl in all emitted events to be this element
|
||||
handleSelector: string = ''
|
||||
shouldIgnoreMove: boolean = false
|
||||
shouldWatchScroll: boolean = true // for simulating pointermove on scroll
|
||||
|
||||
// internal states
|
||||
isDragging: boolean = false
|
||||
isTouchDragging: boolean = false
|
||||
wasTouchScroll: boolean = false
|
||||
origPageX: number
|
||||
origPageY: number
|
||||
prevPageX: number
|
||||
prevPageY: number
|
||||
prevScrollX: number // at time of last pointer pageX/pageY capture
|
||||
prevScrollY: number // "
|
||||
|
||||
constructor(containerEl: EventTarget) {
|
||||
this.containerEl = containerEl
|
||||
this.emitter = new Emitter()
|
||||
containerEl.addEventListener('mousedown', this.handleMouseDown as EventListener)
|
||||
containerEl.addEventListener('touchstart', this.handleTouchStart as EventListener, { passive: true })
|
||||
listenerCreated()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.containerEl.removeEventListener('mousedown', this.handleMouseDown as EventListener)
|
||||
this.containerEl.removeEventListener('touchstart', this.handleTouchStart as EventListener, { passive: true } as AddEventListenerOptions)
|
||||
listenerDestroyed()
|
||||
}
|
||||
|
||||
tryStart(ev: UIEvent): boolean {
|
||||
let subjectEl = this.querySubjectEl(ev)
|
||||
let downEl = ev.target as HTMLElement
|
||||
|
||||
if (
|
||||
subjectEl &&
|
||||
(!this.handleSelector || elementClosest(downEl, this.handleSelector))
|
||||
) {
|
||||
this.subjectEl = subjectEl
|
||||
this.isDragging = true // do this first so cancelTouchScroll will work
|
||||
this.wasTouchScroll = false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
isWindowTouchMoveCancelled = false
|
||||
this.isDragging = false
|
||||
this.subjectEl = null
|
||||
// keep wasTouchScroll around for later access
|
||||
this.destroyScrollWatch()
|
||||
}
|
||||
|
||||
querySubjectEl(ev: UIEvent): HTMLElement {
|
||||
if (this.selector) {
|
||||
return elementClosest(ev.target as HTMLElement, this.selector)
|
||||
}
|
||||
return this.containerEl as HTMLElement
|
||||
}
|
||||
|
||||
// Mouse
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
|
||||
handleMouseDown = (ev: MouseEvent) => {
|
||||
if (
|
||||
!this.shouldIgnoreMouse() &&
|
||||
isPrimaryMouseButton(ev) &&
|
||||
this.tryStart(ev)
|
||||
) {
|
||||
let pev = this.createEventFromMouse(ev, true)
|
||||
this.emitter.trigger('pointerdown', pev)
|
||||
this.initScrollWatch(pev)
|
||||
|
||||
if (!this.shouldIgnoreMove) {
|
||||
document.addEventListener('mousemove', this.handleMouseMove)
|
||||
}
|
||||
|
||||
document.addEventListener('mouseup', this.handleMouseUp)
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove = (ev: MouseEvent) => {
|
||||
let pev = this.createEventFromMouse(ev)
|
||||
this.recordCoords(pev)
|
||||
this.emitter.trigger('pointermove', pev)
|
||||
}
|
||||
|
||||
handleMouseUp = (ev: MouseEvent) => {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove)
|
||||
document.removeEventListener('mouseup', this.handleMouseUp)
|
||||
|
||||
this.emitter.trigger('pointerup', this.createEventFromMouse(ev))
|
||||
|
||||
this.cleanup() // call last so that pointerup has access to props
|
||||
}
|
||||
|
||||
shouldIgnoreMouse() {
|
||||
return ignoreMouseDepth || this.isTouchDragging
|
||||
}
|
||||
|
||||
// Touch
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
|
||||
handleTouchStart = (ev: TouchEvent) => {
|
||||
if (this.tryStart(ev)) {
|
||||
this.isTouchDragging = true
|
||||
|
||||
let pev = this.createEventFromTouch(ev, true)
|
||||
this.emitter.trigger('pointerdown', pev)
|
||||
this.initScrollWatch(pev)
|
||||
|
||||
// unlike mouse, need to attach to target, not document
|
||||
// https://stackoverflow.com/a/45760014
|
||||
let targetEl = ev.target as HTMLElement
|
||||
|
||||
if (!this.shouldIgnoreMove) {
|
||||
targetEl.addEventListener('touchmove', this.handleTouchMove)
|
||||
}
|
||||
|
||||
targetEl.addEventListener('touchend', this.handleTouchEnd)
|
||||
targetEl.addEventListener('touchcancel', this.handleTouchEnd) // treat it as a touch end
|
||||
|
||||
// attach a handler to get called when ANY scroll action happens on the page.
|
||||
// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
|
||||
// http://stackoverflow.com/a/32954565/96342
|
||||
window.addEventListener(
|
||||
'scroll',
|
||||
this.handleTouchScroll,
|
||||
true, // useCapture
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchMove = (ev: TouchEvent) => {
|
||||
let pev = this.createEventFromTouch(ev)
|
||||
this.recordCoords(pev)
|
||||
this.emitter.trigger('pointermove', pev)
|
||||
}
|
||||
|
||||
handleTouchEnd = (ev: TouchEvent) => {
|
||||
if (this.isDragging) { // done to guard against touchend followed by touchcancel
|
||||
let targetEl = ev.target as HTMLElement
|
||||
|
||||
targetEl.removeEventListener('touchmove', this.handleTouchMove)
|
||||
targetEl.removeEventListener('touchend', this.handleTouchEnd)
|
||||
targetEl.removeEventListener('touchcancel', this.handleTouchEnd)
|
||||
window.removeEventListener('scroll', this.handleTouchScroll, true) // useCaptured=true
|
||||
|
||||
this.emitter.trigger('pointerup', this.createEventFromTouch(ev))
|
||||
|
||||
this.cleanup() // call last so that pointerup has access to props
|
||||
this.isTouchDragging = false
|
||||
startIgnoringMouse()
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchScroll = () => {
|
||||
this.wasTouchScroll = true
|
||||
}
|
||||
|
||||
// can be called by user of this class, to cancel touch-based scrolling for the current drag
|
||||
cancelTouchScroll() {
|
||||
if (this.isDragging) {
|
||||
isWindowTouchMoveCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolling that simulates pointermoves
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
|
||||
initScrollWatch(ev: PointerDragEvent) {
|
||||
if (this.shouldWatchScroll) {
|
||||
this.recordCoords(ev)
|
||||
window.addEventListener('scroll', this.handleScroll, true) // useCapture=true
|
||||
}
|
||||
}
|
||||
|
||||
recordCoords(ev: PointerDragEvent) {
|
||||
if (this.shouldWatchScroll) {
|
||||
this.prevPageX = (ev as any).pageX
|
||||
this.prevPageY = (ev as any).pageY
|
||||
this.prevScrollX = window.pageXOffset
|
||||
this.prevScrollY = window.pageYOffset
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll = (ev: UIEvent) => {
|
||||
if (!this.shouldIgnoreMove) {
|
||||
let pageX = (window.pageXOffset - this.prevScrollX) + this.prevPageX
|
||||
let pageY = (window.pageYOffset - this.prevScrollY) + this.prevPageY
|
||||
|
||||
this.emitter.trigger('pointermove', {
|
||||
origEvent: ev,
|
||||
isTouch: this.isTouchDragging,
|
||||
subjectEl: this.subjectEl,
|
||||
pageX,
|
||||
pageY,
|
||||
deltaX: pageX - this.origPageX,
|
||||
deltaY: pageY - this.origPageY,
|
||||
} as PointerDragEvent)
|
||||
}
|
||||
}
|
||||
|
||||
destroyScrollWatch() {
|
||||
if (this.shouldWatchScroll) {
|
||||
window.removeEventListener('scroll', this.handleScroll, true) // useCaptured=true
|
||||
}
|
||||
}
|
||||
|
||||
// Event Normalization
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
|
||||
createEventFromMouse(ev: MouseEvent, isFirst?: boolean): PointerDragEvent {
|
||||
let deltaX = 0
|
||||
let deltaY = 0
|
||||
|
||||
// TODO: repeat code
|
||||
if (isFirst) {
|
||||
this.origPageX = ev.pageX
|
||||
this.origPageY = ev.pageY
|
||||
} else {
|
||||
deltaX = ev.pageX - this.origPageX
|
||||
deltaY = ev.pageY - this.origPageY
|
||||
}
|
||||
|
||||
return {
|
||||
origEvent: ev,
|
||||
isTouch: false,
|
||||
subjectEl: this.subjectEl,
|
||||
pageX: ev.pageX,
|
||||
pageY: ev.pageY,
|
||||
deltaX,
|
||||
deltaY,
|
||||
}
|
||||
}
|
||||
|
||||
createEventFromTouch(ev: TouchEvent, isFirst?: boolean): PointerDragEvent {
|
||||
let touches = ev.touches
|
||||
let pageX
|
||||
let pageY
|
||||
let deltaX = 0
|
||||
let deltaY = 0
|
||||
|
||||
// if touch coords available, prefer,
|
||||
// because FF would give bad ev.pageX ev.pageY
|
||||
if (touches && touches.length) {
|
||||
pageX = touches[0].pageX
|
||||
pageY = touches[0].pageY
|
||||
} else {
|
||||
pageX = (ev as any).pageX
|
||||
pageY = (ev as any).pageY
|
||||
}
|
||||
|
||||
// TODO: repeat code
|
||||
if (isFirst) {
|
||||
this.origPageX = pageX
|
||||
this.origPageY = pageY
|
||||
} else {
|
||||
deltaX = pageX - this.origPageX
|
||||
deltaY = pageY - this.origPageY
|
||||
}
|
||||
|
||||
return {
|
||||
origEvent: ev,
|
||||
isTouch: true,
|
||||
subjectEl: this.subjectEl,
|
||||
pageX,
|
||||
pageY,
|
||||
deltaX,
|
||||
deltaY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
|
||||
function isPrimaryMouseButton(ev: MouseEvent) {
|
||||
return ev.button === 0 && !ev.ctrlKey
|
||||
}
|
||||
|
||||
// Ignoring fake mouse events generated by touch
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
|
||||
function startIgnoringMouse() { // can be made non-class function
|
||||
ignoreMouseDepth += 1
|
||||
|
||||
setTimeout(() => {
|
||||
ignoreMouseDepth -= 1
|
||||
}, config.touchMouseIgnoreWait)
|
||||
}
|
||||
|
||||
// We want to attach touchmove as early as possible for Safari
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
|
||||
function listenerCreated() {
|
||||
listenerCnt += 1
|
||||
|
||||
if (listenerCnt === 1) {
|
||||
window.addEventListener('touchmove', onWindowTouchMove, { passive: false })
|
||||
}
|
||||
}
|
||||
|
||||
function listenerDestroyed() {
|
||||
listenerCnt -= 1
|
||||
|
||||
if (!listenerCnt) {
|
||||
window.removeEventListener('touchmove', onWindowTouchMove, { passive: false } as AddEventListenerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowTouchMove(ev: UIEvent) {
|
||||
if (isWindowTouchMoveCancelled) {
|
||||
ev.preventDefault()
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import { BASE_OPTION_DEFAULTS, PointerDragEvent } from '@fullcalendar/common'
|
||||
import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging'
|
||||
import { ExternalElementDragging, DragMetaGenerator } from './ExternalElementDragging'
|
||||
|
||||
export interface ExternalDraggableSettings {
|
||||
eventData?: DragMetaGenerator
|
||||
itemSelector?: string
|
||||
minDistance?: number
|
||||
longPressDelay?: number
|
||||
appendTo?: HTMLElement
|
||||
}
|
||||
|
||||
/*
|
||||
Makes an element (that is *external* to any calendar) draggable.
|
||||
Can pass in data that determines how an event will be created when dropped onto a calendar.
|
||||
Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system.
|
||||
*/
|
||||
export class ExternalDraggable {
|
||||
dragging: FeaturefulElementDragging
|
||||
settings: ExternalDraggableSettings
|
||||
|
||||
constructor(el: HTMLElement, settings: ExternalDraggableSettings = {}) {
|
||||
this.settings = settings
|
||||
|
||||
let dragging = this.dragging = new FeaturefulElementDragging(el)
|
||||
dragging.touchScrollAllowed = false
|
||||
|
||||
if (settings.itemSelector != null) {
|
||||
dragging.pointer.selector = settings.itemSelector
|
||||
}
|
||||
|
||||
if (settings.appendTo != null) {
|
||||
dragging.mirror.parentNode = settings.appendTo // TODO: write tests
|
||||
}
|
||||
|
||||
dragging.emitter.on('pointerdown', this.handlePointerDown)
|
||||
dragging.emitter.on('dragstart', this.handleDragStart)
|
||||
|
||||
new ExternalElementDragging(dragging, settings.eventData) // eslint-disable-line no-new
|
||||
}
|
||||
|
||||
handlePointerDown = (ev: PointerDragEvent) => {
|
||||
let { dragging } = this
|
||||
let { minDistance, longPressDelay } = this.settings
|
||||
|
||||
dragging.minDistance =
|
||||
minDistance != null ?
|
||||
minDistance :
|
||||
(ev.isTouch ? 0 : BASE_OPTION_DEFAULTS.eventDragMinDistance)
|
||||
|
||||
dragging.delay =
|
||||
ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
|
||||
(longPressDelay != null ? longPressDelay : BASE_OPTION_DEFAULTS.longPressDelay) :
|
||||
0
|
||||
}
|
||||
|
||||
handleDragStart = (ev: PointerDragEvent) => {
|
||||
if (
|
||||
ev.isTouch &&
|
||||
this.dragging.delay &&
|
||||
(ev.subjectEl as HTMLElement).classList.contains('fc-event')
|
||||
) {
|
||||
this.dragging.mirror.getMirrorEl().classList.add('fc-event-selected')
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dragging.destroy()
|
||||
}
|
||||
}
|
|
@ -1,268 +0,0 @@
|
|||
import {
|
||||
Hit,
|
||||
interactionSettingsStore,
|
||||
PointerDragEvent,
|
||||
parseEventDef, createEventInstance, EventTuple,
|
||||
createEmptyEventStore, eventTupleToStore,
|
||||
config,
|
||||
DateSpan, DatePointApi,
|
||||
EventInteractionState,
|
||||
DragMetaInput, DragMeta, parseDragMeta,
|
||||
EventApi,
|
||||
elementMatches,
|
||||
enableCursor, disableCursor,
|
||||
isInteractionValid,
|
||||
ElementDragging,
|
||||
ViewApi,
|
||||
CalendarContext,
|
||||
getDefaultEventEnd,
|
||||
refineEventDef,
|
||||
} from '@fullcalendar/common'
|
||||
import { __assign } from 'tslib'
|
||||
import { HitDragging } from '../interactions/HitDragging'
|
||||
import { buildDatePointApiWithContext } from '../utils'
|
||||
|
||||
export type DragMetaGenerator = DragMetaInput | ((el: HTMLElement) => DragMetaInput)
|
||||
|
||||
export interface ExternalDropApi extends DatePointApi {
|
||||
draggedEl: HTMLElement
|
||||
jsEvent: UIEvent
|
||||
view: ViewApi
|
||||
}
|
||||
|
||||
/*
|
||||
Given an already instantiated draggable object for one-or-more elements,
|
||||
Interprets any dragging as an attempt to drag an events that lives outside
|
||||
of a calendar onto a calendar.
|
||||
*/
|
||||
export class ExternalElementDragging {
|
||||
hitDragging: HitDragging
|
||||
receivingContext: CalendarContext | null = null
|
||||
droppableEvent: EventTuple | null = null // will exist for all drags, even if create:false
|
||||
suppliedDragMeta: DragMetaGenerator | null = null
|
||||
dragMeta: DragMeta | null = null
|
||||
|
||||
constructor(dragging: ElementDragging, suppliedDragMeta?: DragMetaGenerator) {
|
||||
let hitDragging = this.hitDragging = new HitDragging(dragging, interactionSettingsStore)
|
||||
hitDragging.requireInitial = false // will start outside of a component
|
||||
hitDragging.emitter.on('dragstart', this.handleDragStart)
|
||||
hitDragging.emitter.on('hitupdate', this.handleHitUpdate)
|
||||
hitDragging.emitter.on('dragend', this.handleDragEnd)
|
||||
|
||||
this.suppliedDragMeta = suppliedDragMeta
|
||||
}
|
||||
|
||||
handleDragStart = (ev: PointerDragEvent) => {
|
||||
this.dragMeta = this.buildDragMeta(ev.subjectEl as HTMLElement)
|
||||
}
|
||||
|
||||
buildDragMeta(subjectEl: HTMLElement) {
|
||||
if (typeof this.suppliedDragMeta === 'object') {
|
||||
return parseDragMeta(this.suppliedDragMeta)
|
||||
}
|
||||
if (typeof this.suppliedDragMeta === 'function') {
|
||||
return parseDragMeta(this.suppliedDragMeta(subjectEl))
|
||||
}
|
||||
return getDragMetaFromEl(subjectEl)
|
||||
}
|
||||
|
||||
handleHitUpdate = (hit: Hit | null, isFinal: boolean, ev: PointerDragEvent) => {
|
||||
let { dragging } = this.hitDragging
|
||||
let receivingContext: CalendarContext | null = null
|
||||
let droppableEvent: EventTuple | null = null
|
||||
let isInvalid = false
|
||||
let interaction: EventInteractionState = {
|
||||
affectedEvents: createEmptyEventStore(),
|
||||
mutatedEvents: createEmptyEventStore(),
|
||||
isEvent: this.dragMeta!.create,
|
||||
}
|
||||
|
||||
if (hit) {
|
||||
receivingContext = hit.context
|
||||
|
||||
if (this.canDropElOnCalendar(ev.subjectEl as HTMLElement, receivingContext)) {
|
||||
droppableEvent = computeEventForDateSpan(
|
||||
hit.dateSpan,
|
||||
this.dragMeta!,
|
||||
receivingContext,
|
||||
)
|
||||
|
||||
interaction.mutatedEvents = eventTupleToStore(droppableEvent)
|
||||
isInvalid = !isInteractionValid(interaction, hit.dateProfile, receivingContext)
|
||||
|
||||
if (isInvalid) {
|
||||
interaction.mutatedEvents = createEmptyEventStore()
|
||||
droppableEvent = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.displayDrag(receivingContext, interaction)
|
||||
|
||||
// show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?)
|
||||
// TODO: wish we could somehow wait for dispatch to guarantee render
|
||||
dragging.setMirrorIsVisible(
|
||||
isFinal || !droppableEvent || !document.querySelector('.fc-event-mirror'), // TODO: turn className into constant
|
||||
// TODO: somehow query FullCalendars WITHIN shadow-roots for existing event-mirror els
|
||||
)
|
||||
|
||||
if (!isInvalid) {
|
||||
enableCursor()
|
||||
} else {
|
||||
disableCursor()
|
||||
}
|
||||
|
||||
if (!isFinal) {
|
||||
dragging.setMirrorNeedsRevert(!droppableEvent)
|
||||
|
||||
this.receivingContext = receivingContext
|
||||
this.droppableEvent = droppableEvent
|
||||
}
|
||||
}
|
||||
|
||||
handleDragEnd = (pev: PointerDragEvent) => {
|
||||
let { receivingContext, droppableEvent } = this
|
||||
|
||||
this.clearDrag()
|
||||
|
||||
if (receivingContext && droppableEvent) {
|
||||
let finalHit = this.hitDragging.finalHit!
|
||||
let finalView = finalHit.context.viewApi
|
||||
let dragMeta = this.dragMeta!
|
||||
|
||||
receivingContext.emitter.trigger('drop', {
|
||||
...buildDatePointApiWithContext(finalHit.dateSpan, receivingContext),
|
||||
draggedEl: pev.subjectEl as HTMLElement,
|
||||
jsEvent: pev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
|
||||
view: finalView,
|
||||
})
|
||||
|
||||
if (dragMeta.create) {
|
||||
let addingEvents = eventTupleToStore(droppableEvent)
|
||||
|
||||
receivingContext.dispatch({
|
||||
type: 'MERGE_EVENTS',
|
||||
eventStore: addingEvents,
|
||||
})
|
||||
|
||||
if (pev.isTouch) {
|
||||
receivingContext.dispatch({
|
||||
type: 'SELECT_EVENT',
|
||||
eventInstanceId: droppableEvent.instance.instanceId,
|
||||
})
|
||||
}
|
||||
|
||||
// signal that an external event landed
|
||||
receivingContext.emitter.trigger('eventReceive', {
|
||||
event: new EventApi(
|
||||
receivingContext,
|
||||
droppableEvent.def,
|
||||
droppableEvent.instance,
|
||||
),
|
||||
relatedEvents: [],
|
||||
revert() {
|
||||
receivingContext.dispatch({
|
||||
type: 'REMOVE_EVENTS',
|
||||
eventStore: addingEvents,
|
||||
})
|
||||
},
|
||||
draggedEl: pev.subjectEl as HTMLElement,
|
||||
view: finalView,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.receivingContext = null
|
||||
this.droppableEvent = null
|
||||
}
|
||||
|
||||
displayDrag(nextContext: CalendarContext | null, state: EventInteractionState) {
|
||||
let prevContext = this.receivingContext
|
||||
|
||||
if (prevContext && prevContext !== nextContext) {
|
||||
prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' })
|
||||
}
|
||||
|
||||
if (nextContext) {
|
||||
nextContext.dispatch({ type: 'SET_EVENT_DRAG', state })
|
||||
}
|
||||
}
|
||||
|
||||
clearDrag() {
|
||||
if (this.receivingContext) {
|
||||
this.receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' })
|
||||
}
|
||||
}
|
||||
|
||||
canDropElOnCalendar(el: HTMLElement, receivingContext: CalendarContext): boolean {
|
||||
let dropAccept = receivingContext.options.dropAccept
|
||||
|
||||
if (typeof dropAccept === 'function') {
|
||||
return dropAccept.call(receivingContext.calendarApi, el)
|
||||
}
|
||||
|
||||
if (typeof dropAccept === 'string' && dropAccept) {
|
||||
return Boolean(elementMatches(el, dropAccept))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Utils for computing event store from the DragMeta
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
|
||||
function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, context: CalendarContext): EventTuple {
|
||||
let defProps = { ...dragMeta.leftoverProps }
|
||||
|
||||
for (let transform of context.pluginHooks.externalDefTransforms) {
|
||||
__assign(defProps, transform(dateSpan, dragMeta))
|
||||
}
|
||||
|
||||
let { refined, extra } = refineEventDef(defProps, context)
|
||||
let def = parseEventDef(
|
||||
refined,
|
||||
extra,
|
||||
dragMeta.sourceId,
|
||||
dateSpan.allDay,
|
||||
context.options.forceEventDuration || Boolean(dragMeta.duration), // hasEnd
|
||||
context,
|
||||
)
|
||||
|
||||
let start = dateSpan.range.start
|
||||
|
||||
// only rely on time info if drop zone is all-day,
|
||||
// otherwise, we already know the time
|
||||
if (dateSpan.allDay && dragMeta.startTime) {
|
||||
start = context.dateEnv.add(start, dragMeta.startTime)
|
||||
}
|
||||
|
||||
let end = dragMeta.duration ?
|
||||
context.dateEnv.add(start, dragMeta.duration) :
|
||||
getDefaultEventEnd(dateSpan.allDay, start, context)
|
||||
|
||||
let instance = createEventInstance(def.defId, { start, end })
|
||||
|
||||
return { def, instance }
|
||||
}
|
||||
|
||||
// Utils for extracting data from element
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
|
||||
function getDragMetaFromEl(el: HTMLElement): DragMeta {
|
||||
let str = getEmbeddedElData(el, 'event')
|
||||
let obj = str ?
|
||||
JSON.parse(str) :
|
||||
{ create: false } // if no embedded data, assume no event creation
|
||||
|
||||
return parseDragMeta(obj)
|
||||
}
|
||||
|
||||
config.dataAttrPrefix = ''
|
||||
|
||||
function getEmbeddedElData(el: HTMLElement, name: string): string {
|
||||
let prefix = config.dataAttrPrefix
|
||||
let prefixedName = (prefix ? prefix + '-' : '') + name
|
||||
|
||||
return el.getAttribute('data-' + prefixedName) || ''
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
import { PointerDragEvent, ElementDragging } from '@fullcalendar/common'
|
||||
import { PointerDragging } from '../dnd/PointerDragging'
|
||||
|
||||
/*
|
||||
Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements.
|
||||
The third-party system is responsible for drawing the visuals effects of the drag.
|
||||
This class simply monitors for pointer movements and fires events.
|
||||
It also has the ability to hide the moving element (the "mirror") during the drag.
|
||||
*/
|
||||
export class InferredElementDragging extends ElementDragging {
|
||||
pointer: PointerDragging
|
||||
shouldIgnoreMove: boolean = false
|
||||
mirrorSelector: string = ''
|
||||
currentMirrorEl: HTMLElement | null = null
|
||||
|
||||
constructor(containerEl: HTMLElement) {
|
||||
super(containerEl)
|
||||
|
||||
let pointer = this.pointer = new PointerDragging(containerEl)
|
||||
pointer.emitter.on('pointerdown', this.handlePointerDown)
|
||||
pointer.emitter.on('pointermove', this.handlePointerMove)
|
||||
pointer.emitter.on('pointerup', this.handlePointerUp)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.pointer.destroy()
|
||||
}
|
||||
|
||||
handlePointerDown = (ev: PointerDragEvent) => {
|
||||
this.emitter.trigger('pointerdown', ev)
|
||||
|
||||
if (!this.shouldIgnoreMove) {
|
||||
// fire dragstart right away. does not support delay or min-distance
|
||||
this.emitter.trigger('dragstart', ev)
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerMove = (ev: PointerDragEvent) => {
|
||||
if (!this.shouldIgnoreMove) {
|
||||
this.emitter.trigger('dragmove', ev)
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerUp = (ev: PointerDragEvent) => {
|
||||
this.emitter.trigger('pointerup', ev)
|
||||
|
||||
if (!this.shouldIgnoreMove) {
|
||||
// fire dragend right away. does not support a revert animation
|
||||
this.emitter.trigger('dragend', ev)
|
||||
}
|
||||
}
|
||||
|
||||
setIgnoreMove(bool: boolean) {
|
||||
this.shouldIgnoreMove = bool
|
||||
}
|
||||
|
||||
setMirrorIsVisible(bool: boolean) {
|
||||
if (bool) {
|
||||
// restore a previously hidden element.
|
||||
// use the reference in case the selector class has already been removed.
|
||||
if (this.currentMirrorEl) {
|
||||
this.currentMirrorEl.style.visibility = ''
|
||||
this.currentMirrorEl = null
|
||||
}
|
||||
} else {
|
||||
let mirrorEl = this.mirrorSelector
|
||||
// TODO: somehow query FullCalendars WITHIN shadow-roots
|
||||
? document.querySelector(this.mirrorSelector) as HTMLElement
|
||||
: null
|
||||
|
||||
if (mirrorEl) {
|
||||
this.currentMirrorEl = mirrorEl
|
||||
mirrorEl.style.visibility = 'hidden'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import { ExternalElementDragging, DragMetaGenerator } from './ExternalElementDragging'
|
||||
import { InferredElementDragging } from './InferredElementDragging'
|
||||
|
||||
export interface ThirdPartyDraggableSettings {
|
||||
eventData?: DragMetaGenerator
|
||||
itemSelector?: string
|
||||
mirrorSelector?: string
|
||||
}
|
||||
|
||||
/*
|
||||
Bridges third-party drag-n-drop systems with FullCalendar.
|
||||
Must be instantiated and destroyed by caller.
|
||||
*/
|
||||
export class ThirdPartyDraggable {
|
||||
dragging: InferredElementDragging
|
||||
|
||||
constructor(
|
||||
containerOrSettings?: EventTarget | ThirdPartyDraggableSettings,
|
||||
settings?: ThirdPartyDraggableSettings,
|
||||
) {
|
||||
let containerEl: EventTarget = document
|
||||
|
||||
if (
|
||||
// wish we could just test instanceof EventTarget, but doesn't work in IE11
|
||||
containerOrSettings === document ||
|
||||
containerOrSettings instanceof Element
|
||||
) {
|
||||
containerEl = containerOrSettings as EventTarget
|
||||
settings = settings || {}
|
||||
} else {
|
||||
settings = (containerOrSettings || {}) as ThirdPartyDraggableSettings
|
||||
}
|
||||
|
||||
let dragging = this.dragging = new InferredElementDragging(containerEl as HTMLElement)
|
||||
|
||||
if (typeof settings.itemSelector === 'string') {
|
||||
dragging.pointer.selector = settings.itemSelector
|
||||
} else if (containerEl === document) {
|
||||
dragging.pointer.selector = '[data-event]'
|
||||
}
|
||||
|
||||
if (typeof settings.mirrorSelector === 'string') {
|
||||
dragging.mirrorSelector = settings.mirrorSelector
|
||||
}
|
||||
|
||||
new ExternalElementDragging(dragging, settings.eventData) // eslint-disable-line no-new
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dragging.destroy()
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import {
|
||||
PointerDragEvent, Interaction, InteractionSettings, interactionSettingsToStore,
|
||||
DatePointApi,
|
||||
ViewApi,
|
||||
} from '@fullcalendar/common'
|
||||
import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging'
|
||||
import { HitDragging, isHitsEqual } from './HitDragging'
|
||||
import { buildDatePointApiWithContext } from '../utils'
|
||||
|
||||
export interface DateClickArg extends DatePointApi {
|
||||
dayEl: HTMLElement
|
||||
jsEvent: MouseEvent
|
||||
view: ViewApi
|
||||
}
|
||||
|
||||
/*
|
||||
Monitors when the user clicks on a specific date/time of a component.
|
||||
A pointerdown+pointerup on the same "hit" constitutes a click.
|
||||
*/
|
||||
export class DateClicking extends Interaction {
|
||||
dragging: FeaturefulElementDragging
|
||||
hitDragging: HitDragging
|
||||
|
||||
constructor(settings: InteractionSettings) {
|
||||
super(settings)
|
||||
|
||||
// we DO want to watch pointer moves because otherwise finalHit won't get populated
|
||||
this.dragging = new FeaturefulElementDragging(settings.el)
|
||||
this.dragging.autoScroller.isEnabled = false
|
||||
|
||||
let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings))
|
||||
hitDragging.emitter.on('pointerdown', this.handlePointerDown)
|
||||
hitDragging.emitter.on('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dragging.destroy()
|
||||
}
|
||||
|
||||
handlePointerDown = (pev: PointerDragEvent) => {
|
||||
let { dragging } = this
|
||||
let downEl = pev.origEvent.target as HTMLElement
|
||||
|
||||
// do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired
|
||||
dragging.setIgnoreMove(
|
||||
!this.component.isValidDateDownEl(downEl),
|
||||
)
|
||||
}
|
||||
|
||||
// won't even fire if moving was ignored
|
||||
handleDragEnd = (ev: PointerDragEvent) => {
|
||||
let { component } = this
|
||||
let { pointer } = this.dragging
|
||||
|
||||
if (!pointer.wasTouchScroll) {
|
||||
let { initialHit, finalHit } = this.hitDragging
|
||||
|
||||
if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) {
|
||||
let { context } = component
|
||||
let arg: DateClickArg = {
|
||||
...buildDatePointApiWithContext(initialHit.dateSpan, context),
|
||||
dayEl: initialHit.dayEl,
|
||||
jsEvent: ev.origEvent as MouseEvent,
|
||||
view: context.viewApi || context.calendarApi.view,
|
||||
}
|
||||
|
||||
context.emitter.trigger('dateClick', arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
import {
|
||||
compareNumbers, enableCursor, disableCursor, DateComponent, Hit,
|
||||
DateSpan, PointerDragEvent, dateSelectionJoinTransformer,
|
||||
Interaction, InteractionSettings, interactionSettingsToStore,
|
||||
triggerDateSelect, isDateSelectionValid,
|
||||
} from '@fullcalendar/common'
|
||||
import { __assign } from 'tslib'
|
||||
import { HitDragging } from './HitDragging'
|
||||
import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging'
|
||||
|
||||
/*
|
||||
Tracks when the user selects a portion of time of a component,
|
||||
constituted by a drag over date cells, with a possible delay at the beginning of the drag.
|
||||
*/
|
||||
export class DateSelecting extends Interaction {
|
||||
dragging: FeaturefulElementDragging
|
||||
hitDragging: HitDragging
|
||||
dragSelection: DateSpan | null = null
|
||||
|
||||
constructor(settings: InteractionSettings) {
|
||||
super(settings)
|
||||
let { component } = settings
|
||||
let { options } = component.context
|
||||
|
||||
let dragging = this.dragging = new FeaturefulElementDragging(settings.el)
|
||||
dragging.touchScrollAllowed = false
|
||||
dragging.minDistance = options.selectMinDistance || 0
|
||||
dragging.autoScroller.isEnabled = options.dragScroll
|
||||
|
||||
let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings))
|
||||
hitDragging.emitter.on('pointerdown', this.handlePointerDown)
|
||||
hitDragging.emitter.on('dragstart', this.handleDragStart)
|
||||
hitDragging.emitter.on('hitupdate', this.handleHitUpdate)
|
||||
hitDragging.emitter.on('pointerup', this.handlePointerUp)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dragging.destroy()
|
||||
}
|
||||
|
||||
handlePointerDown = (ev: PointerDragEvent) => {
|
||||
let { component, dragging } = this
|
||||
let { options } = component.context
|
||||
|
||||
let canSelect = options.selectable &&
|
||||
component.isValidDateDownEl(ev.origEvent.target as HTMLElement)
|
||||
|
||||
// don't bother to watch expensive moves if component won't do selection
|
||||
dragging.setIgnoreMove(!canSelect)
|
||||
|
||||
// if touch, require user to hold down
|
||||
dragging.delay = ev.isTouch ? getComponentTouchDelay(component) : null
|
||||
}
|
||||
|
||||
handleDragStart = (ev: PointerDragEvent) => {
|
||||
this.component.context.calendarApi.unselect(ev) // unselect previous selections
|
||||
}
|
||||
|
||||
handleHitUpdate = (hit: Hit | null, isFinal: boolean) => {
|
||||
let { context } = this.component
|
||||
let dragSelection: DateSpan | null = null
|
||||
let isInvalid = false
|
||||
|
||||
if (hit) {
|
||||
let initialHit = this.hitDragging.initialHit!
|
||||
let disallowed = hit.componentId === initialHit.componentId
|
||||
&& this.isHitComboAllowed
|
||||
&& !this.isHitComboAllowed(initialHit, hit)
|
||||
|
||||
if (!disallowed) {
|
||||
dragSelection = joinHitsIntoSelection(
|
||||
initialHit,
|
||||
hit,
|
||||
context.pluginHooks.dateSelectionTransformers,
|
||||
)
|
||||
}
|
||||
|
||||
if (!dragSelection || !isDateSelectionValid(dragSelection, hit.dateProfile, context)) {
|
||||
isInvalid = true
|
||||
dragSelection = null
|
||||
}
|
||||
}
|
||||
|
||||
if (dragSelection) {
|
||||
context.dispatch({ type: 'SELECT_DATES', selection: dragSelection })
|
||||
} else if (!isFinal) { // only unselect if moved away while dragging
|
||||
context.dispatch({ type: 'UNSELECT_DATES' })
|
||||
}
|
||||
|
||||
if (!isInvalid) {
|
||||
enableCursor()
|
||||
} else {
|
||||
disableCursor()
|
||||
}
|
||||
|
||||
if (!isFinal) {
|
||||
this.dragSelection = dragSelection // only clear if moved away from all hits while dragging
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerUp = (pev: PointerDragEvent) => {
|
||||
if (this.dragSelection) {
|
||||
// selection is already rendered, so just need to report selection
|
||||
triggerDateSelect(this.dragSelection, pev, this.component.context)
|
||||
|
||||
this.dragSelection = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getComponentTouchDelay(component: DateComponent<any>): number {
|
||||
let { options } = component.context
|
||||
let delay = options.selectLongPressDelay
|
||||
|
||||
if (delay == null) {
|
||||
delay = options.longPressDelay
|
||||
}
|
||||
|
||||
return delay
|
||||
}
|
||||
|
||||
function joinHitsIntoSelection(hit0: Hit, hit1: Hit, dateSelectionTransformers: dateSelectionJoinTransformer[]): DateSpan {
|
||||
let dateSpan0 = hit0.dateSpan
|
||||
let dateSpan1 = hit1.dateSpan
|
||||
let ms = [
|
||||
dateSpan0.range.start,
|
||||
dateSpan0.range.end,
|
||||
dateSpan1.range.start,
|
||||
dateSpan1.range.end,
|
||||
]
|
||||
|
||||
ms.sort(compareNumbers)
|
||||
|
||||
let props = {} as DateSpan
|
||||
|
||||
for (let transformer of dateSelectionTransformers) {
|
||||
let res = transformer(hit0, hit1)
|
||||
|
||||
if (res === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (res) {
|
||||
__assign(props, res)
|
||||
}
|
||||
}
|
||||
|
||||
props.range = { start: ms[0], end: ms[3] }
|
||||
props.allDay = dateSpan0.allDay
|
||||
|
||||
return props
|
||||
}
|
|
@ -1,482 +0,0 @@
|
|||
import {
|
||||
DateComponent, Seg,
|
||||
PointerDragEvent, Hit,
|
||||
EventMutation, applyMutationToEventStore,
|
||||
startOfDay,
|
||||
elementClosest,
|
||||
EventStore, getRelevantEvents, createEmptyEventStore,
|
||||
EventInteractionState,
|
||||
diffDates, enableCursor, disableCursor,
|
||||
EventRenderRange, getElSeg,
|
||||
EventApi,
|
||||
eventDragMutationMassager,
|
||||
Interaction, InteractionSettings, interactionSettingsStore,
|
||||
EventDropTransformers,
|
||||
CalendarContext,
|
||||
ViewApi,
|
||||
EventChangeArg,
|
||||
buildEventApis,
|
||||
EventAddArg,
|
||||
EventRemoveArg,
|
||||
isInteractionValid,
|
||||
getElRoot,
|
||||
} from '@fullcalendar/common'
|
||||
import { __assign } from 'tslib'
|
||||
import { HitDragging, isHitsEqual } from './HitDragging'
|
||||
import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging'
|
||||
import { buildDatePointApiWithContext } from '../utils'
|
||||
|
||||
export type EventDragStopArg = EventDragArg
|
||||
export type EventDragStartArg = EventDragArg
|
||||
|
||||
export interface EventDragArg {
|
||||
el: HTMLElement
|
||||
event: EventApi
|
||||
jsEvent: MouseEvent
|
||||
view: ViewApi
|
||||
}
|
||||
|
||||
export class EventDragging extends Interaction { // TODO: rename to EventSelectingAndDragging
|
||||
// TODO: test this in IE11
|
||||
// QUESTION: why do we need it on the resizable???
|
||||
static SELECTOR = '.fc-event-draggable, .fc-event-resizable'
|
||||
|
||||
dragging: FeaturefulElementDragging
|
||||
hitDragging: HitDragging
|
||||
|
||||
// internal state
|
||||
subjectEl: HTMLElement | null = null
|
||||
subjectSeg: Seg | null = null // the seg being selected/dragged
|
||||
isDragging: boolean = false
|
||||
eventRange: EventRenderRange | null = null
|
||||
relevantEvents: EventStore | null = null // the events being dragged
|
||||
receivingContext: CalendarContext | null = null
|
||||
validMutation: EventMutation | null = null
|
||||
mutatedRelevantEvents: EventStore | null = null
|
||||
|
||||
constructor(settings: InteractionSettings) {
|
||||
super(settings)
|
||||
let { component } = this
|
||||
let { options } = component.context
|
||||
|
||||
let dragging = this.dragging = new FeaturefulElementDragging(settings.el)
|
||||
dragging.pointer.selector = EventDragging.SELECTOR
|
||||
dragging.touchScrollAllowed = false
|
||||
dragging.autoScroller.isEnabled = options.dragScroll
|
||||
|
||||
let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsStore)
|
||||
hitDragging.useSubjectCenter = settings.useEventCenter
|
||||
hitDragging.emitter.on('pointerdown', this.handlePointerDown)
|
||||
hitDragging.emitter.on('dragstart', this.handleDragStart)
|
||||
hitDragging.emitter.on('hitupdate', this.handleHitUpdate)
|
||||
hitDragging.emitter.on('pointerup', this.handlePointerUp)
|
||||
hitDragging.emitter.on('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dragging.destroy()
|
||||
}
|
||||
|
||||
handlePointerDown = (ev: PointerDragEvent) => {
|
||||
let origTarget = ev.origEvent.target as HTMLElement
|
||||
let { component, dragging } = this
|
||||
let { mirror } = dragging
|
||||
let { options } = component.context
|
||||
let initialContext = component.context
|
||||
this.subjectEl = ev.subjectEl as HTMLElement
|
||||
let subjectSeg = this.subjectSeg = getElSeg(ev.subjectEl as HTMLElement)!
|
||||
let eventRange = this.eventRange = subjectSeg.eventRange!
|
||||
let eventInstanceId = eventRange.instance!.instanceId
|
||||
|
||||
this.relevantEvents = getRelevantEvents(
|
||||
initialContext.getCurrentData().eventStore,
|
||||
eventInstanceId,
|
||||
)
|
||||
|
||||
dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance
|
||||
dragging.delay =
|
||||
// only do a touch delay if touch and this event hasn't been selected yet
|
||||
(ev.isTouch && eventInstanceId !== component.props.eventSelection) ?
|
||||
getComponentTouchDelay(component) :
|
||||
null
|
||||
|
||||
if (options.fixedMirrorParent) {
|
||||
mirror.parentNode = options.fixedMirrorParent
|
||||
} else {
|
||||
mirror.parentNode = elementClosest(origTarget, '.fc')
|
||||
}
|
||||
|
||||
mirror.revertDuration = options.dragRevertDuration
|
||||
|
||||
let isValid =
|
||||
component.isValidSegDownEl(origTarget) &&
|
||||
!elementClosest(origTarget, '.fc-event-resizer') // NOT on a resizer
|
||||
|
||||
dragging.setIgnoreMove(!isValid)
|
||||
|
||||
// disable dragging for elements that are resizable (ie, selectable)
|
||||
// but are not draggable
|
||||
this.isDragging = isValid &&
|
||||
(ev.subjectEl as HTMLElement).classList.contains('fc-event-draggable')
|
||||
}
|
||||
|
||||
handleDragStart = (ev: PointerDragEvent) => {
|
||||
let initialContext = this.component.context
|
||||
let eventRange = this.eventRange!
|
||||
let eventInstanceId = eventRange.instance.instanceId
|
||||
|
||||
if (ev.isTouch) {
|
||||
// need to select a different event?
|
||||
if (eventInstanceId !== this.component.props.eventSelection) {
|
||||
initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId })
|
||||
}
|
||||
} else {
|
||||
// if now using mouse, but was previous touch interaction, clear selected event
|
||||
initialContext.dispatch({ type: 'UNSELECT_EVENT' })
|
||||
}
|
||||
|
||||
if (this.isDragging) {
|
||||
initialContext.calendarApi.unselect(ev) // unselect *date* selection
|
||||
initialContext.emitter.trigger('eventDragStart', {
|
||||
el: this.subjectEl,
|
||||
event: new EventApi(initialContext, eventRange.def, eventRange.instance),
|
||||
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
|
||||
view: initialContext.viewApi,
|
||||
} as EventDragStartArg)
|
||||
}
|
||||
}
|
||||
|
||||
handleHitUpdate = (hit: Hit | null, isFinal: boolean) => {
|
||||
if (!this.isDragging) {
|
||||
return
|
||||
}
|
||||
|
||||
let relevantEvents = this.relevantEvents!
|
||||
let initialHit = this.hitDragging.initialHit!
|
||||
let initialContext = this.component.context
|
||||
|
||||
// states based on new hit
|
||||
let receivingContext: CalendarContext | null = null
|
||||
let mutation: EventMutation | null = null
|
||||
let mutatedRelevantEvents: EventStore | null = null
|
||||
let isInvalid = false
|
||||
let interaction: EventInteractionState = {
|
||||
affectedEvents: relevantEvents,
|
||||
mutatedEvents: createEmptyEventStore(),
|
||||
isEvent: true,
|
||||
}
|
||||
|
||||
if (hit) {
|
||||
receivingContext = hit.context
|
||||
let receivingOptions = receivingContext.options
|
||||
|
||||
if (
|
||||
initialContext === receivingContext ||
|
||||
(receivingOptions.editable && receivingOptions.droppable)
|
||||
) {
|
||||
mutation = computeEventMutation(initialHit, hit, receivingContext.getCurrentData().pluginHooks.eventDragMutationMassagers)
|
||||
|
||||
if (mutation) {
|
||||
mutatedRelevantEvents = applyMutationToEventStore(
|
||||
relevantEvents,
|
||||
receivingContext.getCurrentData().eventUiBases,
|
||||
mutation,
|
||||
receivingContext,
|
||||
)
|
||||
interaction.mutatedEvents = mutatedRelevantEvents
|
||||
|
||||
if (!isInteractionValid(interaction, hit.dateProfile, receivingContext)) {
|
||||
isInvalid = true
|
||||
mutation = null
|
||||
mutatedRelevantEvents = null
|
||||
interaction.mutatedEvents = createEmptyEventStore()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
receivingContext = null
|
||||
}
|
||||
}
|
||||
|
||||
this.displayDrag(receivingContext, interaction)
|
||||
|
||||
if (!isInvalid) {
|
||||
enableCursor()
|
||||
} else {
|
||||
disableCursor()
|
||||
}
|
||||
|
||||
if (!isFinal) {
|
||||
if (
|
||||
initialContext === receivingContext && // TODO: write test for this
|
||||
isHitsEqual(initialHit, hit)
|
||||
) {
|
||||
mutation = null
|
||||
}
|
||||
|
||||
this.dragging.setMirrorNeedsRevert(!mutation)
|
||||
|
||||
// render the mirror if no already-rendered mirror
|
||||
// TODO: wish we could somehow wait for dispatch to guarantee render
|
||||
this.dragging.setMirrorIsVisible(
|
||||
!hit || !getElRoot(this.subjectEl).querySelector('.fc-event-mirror'), // TODO: turn className into constant
|
||||
)
|
||||
|
||||
// assign states based on new hit
|
||||
this.receivingContext = receivingContext
|
||||
this.validMutation = mutation
|
||||
this.mutatedRelevantEvents = mutatedRelevantEvents
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerUp = () => {
|
||||
if (!this.isDragging) {
|
||||
this.cleanup() // because handleDragEnd won't fire
|
||||
}
|
||||
}
|
||||
|
||||
handleDragEnd = (ev: PointerDragEvent) => {
|
||||
if (this.isDragging) {
|
||||
let initialContext = this.component.context
|
||||
let initialView = initialContext.viewApi
|
||||
let { receivingContext, validMutation } = this
|
||||
let eventDef = this.eventRange!.def
|
||||
let eventInstance = this.eventRange!.instance
|
||||
let eventApi = new EventApi(initialContext, eventDef, eventInstance)
|
||||
let relevantEvents = this.relevantEvents!
|
||||
let mutatedRelevantEvents = this.mutatedRelevantEvents!
|
||||
let { finalHit } = this.hitDragging
|
||||
|
||||
this.clearDrag() // must happen after revert animation
|
||||
|
||||
initialContext.emitter.trigger('eventDragStop', {
|
||||
el: this.subjectEl,
|
||||
event: eventApi,
|
||||
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
|
||||
view: initialView,
|
||||
} as EventDragStopArg)
|
||||
|
||||
if (validMutation) {
|
||||
// dropped within same calendar
|
||||
if (receivingContext === initialContext) {
|
||||
let updatedEventApi = new EventApi(
|
||||
initialContext,
|
||||
mutatedRelevantEvents.defs[eventDef.defId],
|
||||
eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null,
|
||||
)
|
||||
|
||||
initialContext.dispatch({
|
||||
type: 'MERGE_EVENTS',
|
||||
eventStore: mutatedRelevantEvents,
|
||||
})
|
||||
|
||||
let eventChangeArg: EventChangeArg = {
|
||||
oldEvent: eventApi,
|
||||
event: updatedEventApi,
|
||||
relatedEvents: buildEventApis(mutatedRelevantEvents, initialContext, eventInstance),
|
||||
revert() {
|
||||
initialContext.dispatch({
|
||||
type: 'MERGE_EVENTS',
|
||||
eventStore: relevantEvents, // the pre-change data
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
let transformed: ReturnType<EventDropTransformers> = {}
|
||||
for (let transformer of initialContext.getCurrentData().pluginHooks.eventDropTransformers) {
|
||||
__assign(transformed, transformer(validMutation, initialContext))
|
||||
}
|
||||
|
||||
initialContext.emitter.trigger('eventDrop', {
|
||||
...eventChangeArg,
|
||||
...transformed,
|
||||
el: ev.subjectEl as HTMLElement,
|
||||
delta: validMutation.datesDelta!,
|
||||
jsEvent: ev.origEvent as MouseEvent, // bad
|
||||
view: initialView,
|
||||
})
|
||||
|
||||
initialContext.emitter.trigger('eventChange', eventChangeArg)
|
||||
|
||||
// dropped in different calendar
|
||||
} else if (receivingContext) {
|
||||
let eventRemoveArg: EventRemoveArg = {
|
||||
event: eventApi,
|
||||
relatedEvents: buildEventApis(relevantEvents, initialContext, eventInstance),
|
||||
revert() {
|
||||
initialContext.dispatch({
|
||||
type: 'MERGE_EVENTS',
|
||||
eventStore: relevantEvents,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
initialContext.emitter.trigger('eventLeave', {
|
||||
...eventRemoveArg,
|
||||
draggedEl: ev.subjectEl as HTMLElement,
|
||||
view: initialView,
|
||||
})
|
||||
|
||||
initialContext.dispatch({
|
||||
type: 'REMOVE_EVENTS',
|
||||
eventStore: relevantEvents,
|
||||
})
|
||||
|
||||
initialContext.emitter.trigger('eventRemove', eventRemoveArg)
|
||||
|
||||
let addedEventDef = mutatedRelevantEvents.defs[eventDef.defId]
|
||||
let addedEventInstance = mutatedRelevantEvents.instances[eventInstance.instanceId]
|
||||
let addedEventApi = new EventApi(receivingContext, addedEventDef, addedEventInstance)
|
||||
|
||||
receivingContext.dispatch({
|
||||
type: 'MERGE_EVENTS',
|
||||
eventStore: mutatedRelevantEvents,
|
||||
})
|
||||
|
||||
let eventAddArg: EventAddArg = {
|
||||
event: addedEventApi,
|
||||
relatedEvents: buildEventApis(mutatedRelevantEvents, receivingContext, addedEventInstance),
|
||||
revert() {
|
||||
receivingContext.dispatch({
|
||||
type: 'REMOVE_EVENTS',
|
||||
eventStore: mutatedRelevantEvents,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
receivingContext.emitter.trigger('eventAdd', eventAddArg)
|
||||
|
||||
if (ev.isTouch) {
|
||||
receivingContext.dispatch({
|
||||
type: 'SELECT_EVENT',
|
||||
eventInstanceId: eventInstance.instanceId,
|
||||
})
|
||||
}
|
||||
|
||||
receivingContext.emitter.trigger('drop', {
|
||||
...buildDatePointApiWithContext(finalHit.dateSpan, receivingContext),
|
||||
draggedEl: ev.subjectEl as HTMLElement,
|
||||
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
|
||||
view: finalHit.context.viewApi,
|
||||
})
|
||||
|
||||
receivingContext.emitter.trigger('eventReceive', {
|
||||
...eventAddArg,
|
||||
draggedEl: ev.subjectEl as HTMLElement,
|
||||
view: finalHit.context.viewApi,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
initialContext.emitter.trigger('_noEventDrop')
|
||||
}
|
||||
}
|
||||
|
||||
this.cleanup()
|
||||
}
|
||||
|
||||
// render a drag state on the next receivingCalendar
|
||||
displayDrag(nextContext: CalendarContext | null, state: EventInteractionState) {
|
||||
let initialContext = this.component.context
|
||||
let prevContext = this.receivingContext
|
||||
|
||||
// does the previous calendar need to be cleared?
|
||||
if (prevContext && prevContext !== nextContext) {
|
||||
// does the initial calendar need to be cleared?
|
||||
// if so, don't clear all the way. we still need to to hide the affectedEvents
|
||||
if (prevContext === initialContext) {
|
||||
prevContext.dispatch({
|
||||
type: 'SET_EVENT_DRAG',
|
||||
state: {
|
||||
affectedEvents: state.affectedEvents,
|
||||
mutatedEvents: createEmptyEventStore(),
|
||||
isEvent: true,
|
||||
},
|
||||
})
|
||||
|
||||
// completely clear the old calendar if it wasn't the initial
|
||||
} else {
|
||||
prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' })
|
||||
}
|
||||
}
|
||||
|
||||
if (nextContext) {
|
||||
nextContext.dispatch({ type: 'SET_EVENT_DRAG', state })
|
||||
}
|
||||
}
|
||||
|
||||
clearDrag() {
|
||||
let initialCalendar = this.component.context
|
||||
let { receivingContext } = this
|
||||
|
||||
if (receivingContext) {
|
||||
receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' })
|
||||
}
|
||||
|
||||
// the initial calendar might have an dummy drag state from displayDrag
|
||||
if (initialCalendar !== receivingContext) {
|
||||
initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' })
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() { // reset all internal state
|
||||
this.subjectSeg = null
|
||||
this.isDragging = false
|
||||
this.eventRange = null
|
||||
this.relevantEvents = null
|
||||
this.receivingContext = null
|
||||
this.validMutation = null
|
||||
this.mutatedRelevantEvents = null
|
||||
}
|
||||
}
|
||||
|
||||
function computeEventMutation(hit0: Hit, hit1: Hit, massagers: eventDragMutationMassager[]): EventMutation {
|
||||
let dateSpan0 = hit0.dateSpan
|
||||
let dateSpan1 = hit1.dateSpan
|
||||
let date0 = dateSpan0.range.start
|
||||
let date1 = dateSpan1.range.start
|
||||
let standardProps = {} as any
|
||||
|
||||
if (dateSpan0.allDay !== dateSpan1.allDay) {
|
||||
standardProps.allDay = dateSpan1.allDay
|
||||
standardProps.hasEnd = hit1.context.options.allDayMaintainDuration
|
||||
|
||||
if (dateSpan1.allDay) {
|
||||
// means date1 is already start-of-day,
|
||||
// but date0 needs to be converted
|
||||
date0 = startOfDay(date0)
|
||||
}
|
||||
}
|
||||
|
||||
let delta = diffDates(
|
||||
date0, date1,
|
||||
hit0.context.dateEnv,
|
||||
hit0.componentId === hit1.componentId ?
|
||||
hit0.largeUnit :
|
||||
null,
|
||||
)
|
||||
|
||||
if (delta.milliseconds) { // has hours/minutes/seconds
|
||||
standardProps.allDay = false
|
||||
}
|
||||
|
||||
let mutation: EventMutation = {
|
||||
datesDelta: delta,
|
||||
standardProps,
|
||||
}
|
||||
|
||||
for (let massager of massagers) {
|
||||
massager(mutation, hit0, hit1)
|
||||
}
|
||||
|
||||
return mutation
|
||||
}
|
||||
|
||||
function getComponentTouchDelay(component: DateComponent<any>): number | null {
|
||||
let { options } = component.context
|
||||
let delay = options.eventLongPressDelay
|
||||
|
||||
if (delay == null) {
|
||||
delay = options.longPressDelay
|
||||
}
|
||||
|
||||
return delay
|
||||
}
|
|
@ -1,263 +0,0 @@
|
|||
import {
|
||||
Seg, Hit,
|
||||
EventMutation, applyMutationToEventStore,
|
||||
elementClosest,
|
||||
PointerDragEvent,
|
||||
EventStore, getRelevantEvents, createEmptyEventStore,
|
||||
diffDates, enableCursor, disableCursor,
|
||||
DateRange,
|
||||
EventApi,
|
||||
EventRenderRange, getElSeg,
|
||||
createDuration,
|
||||
EventInteractionState,
|
||||
Interaction, InteractionSettings, interactionSettingsToStore, ViewApi, Duration, EventChangeArg, buildEventApis, isInteractionValid,
|
||||
} from '@fullcalendar/common'
|
||||
import { __assign } from 'tslib'
|
||||
import { HitDragging, isHitsEqual } from './HitDragging'
|
||||
import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging'
|
||||
|
||||
export type EventResizeStartArg = EventResizeStartStopArg
|
||||
export type EventResizeStopArg = EventResizeStartStopArg
|
||||
|
||||
export interface EventResizeStartStopArg {
|
||||
el: HTMLElement
|
||||
event: EventApi
|
||||
jsEvent: MouseEvent
|
||||
view: ViewApi
|
||||
}
|
||||
|
||||
export interface EventResizeDoneArg extends EventChangeArg {
|
||||
el: HTMLElement
|
||||
startDelta: Duration
|
||||
endDelta: Duration
|
||||
jsEvent: MouseEvent
|
||||
view: ViewApi
|
||||
}
|
||||
|
||||
export class EventResizing extends Interaction {
|
||||
dragging: FeaturefulElementDragging
|
||||
hitDragging: HitDragging
|
||||
|
||||
// internal state
|
||||
draggingSegEl: HTMLElement | null = null
|
||||
draggingSeg: Seg | null = null // TODO: rename to resizingSeg? subjectSeg?
|
||||
eventRange: EventRenderRange | null = null
|
||||
relevantEvents: EventStore | null = null
|
||||
validMutation: EventMutation | null = null
|
||||
mutatedRelevantEvents: EventStore | null = null
|
||||
|
||||
constructor(settings: InteractionSettings) {
|
||||
super(settings)
|
||||
let { component } = settings
|
||||
|
||||
let dragging = this.dragging = new FeaturefulElementDragging(settings.el)
|
||||
dragging.pointer.selector = '.fc-event-resizer'
|
||||
dragging.touchScrollAllowed = false
|
||||
dragging.autoScroller.isEnabled = component.context.options.dragScroll
|
||||
|
||||
let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings))
|
||||
hitDragging.emitter.on('pointerdown', this.handlePointerDown)
|
||||
hitDragging.emitter.on('dragstart', this.handleDragStart)
|
||||
hitDragging.emitter.on('hitupdate', this.handleHitUpdate)
|
||||
hitDragging.emitter.on('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dragging.destroy()
|
||||
}
|
||||
|
||||
handlePointerDown = (ev: PointerDragEvent) => {
|
||||
let { component } = this
|
||||
let segEl = this.querySegEl(ev)
|
||||
let seg = getElSeg(segEl)
|
||||
let eventRange = this.eventRange = seg.eventRange!
|
||||
|
||||
this.dragging.minDistance = component.context.options.eventDragMinDistance
|
||||
|
||||
// if touch, need to be working with a selected event
|
||||
this.dragging.setIgnoreMove(
|
||||
!this.component.isValidSegDownEl(ev.origEvent.target as HTMLElement) ||
|
||||
(ev.isTouch && this.component.props.eventSelection !== eventRange.instance!.instanceId),
|
||||
)
|
||||
}
|
||||
|
||||
handleDragStart = (ev: PointerDragEvent) => {
|
||||
let { context } = this.component
|
||||
let eventRange = this.eventRange!
|
||||
|
||||
this.relevantEvents = getRelevantEvents(
|
||||
context.getCurrentData().eventStore,
|
||||
this.eventRange.instance!.instanceId,
|
||||
)
|
||||
|
||||
let segEl = this.querySegEl(ev)
|
||||
this.draggingSegEl = segEl
|
||||
this.draggingSeg = getElSeg(segEl)
|
||||
|
||||
context.calendarApi.unselect()
|
||||
context.emitter.trigger('eventResizeStart', {
|
||||
el: segEl,
|
||||
event: new EventApi(context, eventRange.def, eventRange.instance),
|
||||
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
|
||||
view: context.viewApi,
|
||||
} as EventResizeStartArg)
|
||||
}
|
||||
|
||||
handleHitUpdate = (hit: Hit | null, isFinal: boolean, ev: PointerDragEvent) => {
|
||||
let { context } = this.component
|
||||
let relevantEvents = this.relevantEvents!
|
||||
let initialHit = this.hitDragging.initialHit!
|
||||
let eventInstance = this.eventRange.instance!
|
||||
let mutation: EventMutation | null = null
|
||||
let mutatedRelevantEvents: EventStore | null = null
|
||||
let isInvalid = false
|
||||
let interaction: EventInteractionState = {
|
||||
affectedEvents: relevantEvents,
|
||||
mutatedEvents: createEmptyEventStore(),
|
||||
isEvent: true,
|
||||
}
|
||||
|
||||
if (hit) {
|
||||
let disallowed = hit.componentId === initialHit.componentId
|
||||
&& this.isHitComboAllowed
|
||||
&& !this.isHitComboAllowed(initialHit, hit)
|
||||
|
||||
if (!disallowed) {
|
||||
mutation = computeMutation(
|
||||
initialHit,
|
||||
hit,
|
||||
(ev.subjectEl as HTMLElement).classList.contains('fc-event-resizer-start'),
|
||||
eventInstance.range,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (mutation) {
|
||||
mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.getCurrentData().eventUiBases, mutation, context)
|
||||
interaction.mutatedEvents = mutatedRelevantEvents
|
||||
|
||||
if (!isInteractionValid(interaction, hit.dateProfile, context)) {
|
||||
isInvalid = true
|
||||
mutation = null
|
||||
mutatedRelevantEvents = null
|
||||
interaction.mutatedEvents = null
|
||||
}
|
||||
}
|
||||
|
||||
if (mutatedRelevantEvents) {
|
||||
context.dispatch({
|
||||
type: 'SET_EVENT_RESIZE',
|
||||
state: interaction,
|
||||
})
|
||||
} else {
|
||||
context.dispatch({ type: 'UNSET_EVENT_RESIZE' })
|
||||
}
|
||||
|
||||
if (!isInvalid) {
|
||||
enableCursor()
|
||||
} else {
|
||||
disableCursor()
|
||||
}
|
||||
|
||||
if (!isFinal) {
|
||||
if (mutation && isHitsEqual(initialHit, hit)) {
|
||||
mutation = null
|
||||
}
|
||||
|
||||
this.validMutation = mutation
|
||||
this.mutatedRelevantEvents = mutatedRelevantEvents
|
||||
}
|
||||
}
|
||||
|
||||
handleDragEnd = (ev: PointerDragEvent) => {
|
||||
let { context } = this.component
|
||||
let eventDef = this.eventRange!.def
|
||||
let eventInstance = this.eventRange!.instance
|
||||
let eventApi = new EventApi(context, eventDef, eventInstance)
|
||||
let relevantEvents = this.relevantEvents!
|
||||
let mutatedRelevantEvents = this.mutatedRelevantEvents!
|
||||
|
||||
context.emitter.trigger('eventResizeStop', {
|
||||
el: this.draggingSegEl,
|
||||
event: eventApi,
|
||||
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
|
||||
view: context.viewApi,
|
||||
} as EventResizeStopArg)
|
||||
|
||||
if (this.validMutation) {
|
||||
let updatedEventApi = new EventApi(
|
||||
context,
|
||||
mutatedRelevantEvents.defs[eventDef.defId],
|
||||
eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null,
|
||||
)
|
||||
|
||||
context.dispatch({
|
||||
type: 'MERGE_EVENTS',
|
||||
eventStore: mutatedRelevantEvents,
|
||||
})
|
||||
|
||||
let eventChangeArg: EventChangeArg = {
|
||||
oldEvent: eventApi,
|
||||
event: updatedEventApi,
|
||||
relatedEvents: buildEventApis(mutatedRelevantEvents, context, eventInstance),
|
||||
revert() {
|
||||
context.dispatch({
|
||||
type: 'MERGE_EVENTS',
|
||||
eventStore: relevantEvents, // the pre-change events
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
context.emitter.trigger('eventResize', {
|
||||
...eventChangeArg,
|
||||
el: this.draggingSegEl,
|
||||
startDelta: this.validMutation.startDelta || createDuration(0),
|
||||
endDelta: this.validMutation.endDelta || createDuration(0),
|
||||
jsEvent: ev.origEvent as MouseEvent,
|
||||
view: context.viewApi,
|
||||
})
|
||||
|
||||
context.emitter.trigger('eventChange', eventChangeArg)
|
||||
} else {
|
||||
context.emitter.trigger('_noEventResize')
|
||||
}
|
||||
|
||||
// reset all internal state
|
||||
this.draggingSeg = null
|
||||
this.relevantEvents = null
|
||||
this.validMutation = null
|
||||
|
||||
// okay to keep eventInstance around. useful to set it in handlePointerDown
|
||||
}
|
||||
|
||||
querySegEl(ev: PointerDragEvent) {
|
||||
return elementClosest(ev.subjectEl as HTMLElement, '.fc-event')
|
||||
}
|
||||
}
|
||||
|
||||
function computeMutation(
|
||||
hit0: Hit,
|
||||
hit1: Hit,
|
||||
isFromStart: boolean,
|
||||
instanceRange: DateRange,
|
||||
): EventMutation | null {
|
||||
let dateEnv = hit0.context.dateEnv
|
||||
let date0 = hit0.dateSpan.range.start
|
||||
let date1 = hit1.dateSpan.range.start
|
||||
|
||||
let delta = diffDates(
|
||||
date0, date1,
|
||||
dateEnv,
|
||||
hit0.largeUnit,
|
||||
)
|
||||
|
||||
if (isFromStart) {
|
||||
if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) {
|
||||
return { startDelta: delta }
|
||||
}
|
||||
} else if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) {
|
||||
return { endDelta: delta }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
import {
|
||||
Emitter, PointerDragEvent,
|
||||
isDateSpansEqual,
|
||||
computeRect,
|
||||
constrainPoint, intersectRects, getRectCenter, diffPoints, Point,
|
||||
rangeContainsRange,
|
||||
Hit,
|
||||
InteractionSettingsStore,
|
||||
mapHash,
|
||||
ElementDragging,
|
||||
} from '@fullcalendar/common'
|
||||
import { OffsetTracker } from '../OffsetTracker'
|
||||
|
||||
/*
|
||||
Tracks movement over multiple droppable areas (aka "hits")
|
||||
that exist in one or more DateComponents.
|
||||
Relies on an existing draggable.
|
||||
|
||||
emits:
|
||||
- pointerdown
|
||||
- dragstart
|
||||
- hitchange - fires initially, even if not over a hit
|
||||
- pointerup
|
||||
- (hitchange - again, to null, if ended over a hit)
|
||||
- dragend
|
||||
*/
|
||||
export class HitDragging {
|
||||
droppableStore: InteractionSettingsStore
|
||||
dragging: ElementDragging
|
||||
emitter: Emitter<any>
|
||||
|
||||
// options that can be set by caller
|
||||
useSubjectCenter: boolean = false
|
||||
requireInitial: boolean = true // if doesn't start out on a hit, won't emit any events
|
||||
|
||||
// internal state
|
||||
offsetTrackers: { [componentUid: string]: OffsetTracker }
|
||||
initialHit: Hit | null = null
|
||||
movingHit: Hit | null = null
|
||||
finalHit: Hit | null = null // won't ever be populated if shouldIgnoreMove
|
||||
coordAdjust?: Point
|
||||
|
||||
constructor(dragging: ElementDragging, droppableStore: InteractionSettingsStore) {
|
||||
this.droppableStore = droppableStore
|
||||
|
||||
dragging.emitter.on('pointerdown', this.handlePointerDown)
|
||||
dragging.emitter.on('dragstart', this.handleDragStart)
|
||||
dragging.emitter.on('dragmove', this.handleDragMove)
|
||||
dragging.emitter.on('pointerup', this.handlePointerUp)
|
||||
dragging.emitter.on('dragend', this.handleDragEnd)
|
||||
|
||||
this.dragging = dragging
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
handlePointerDown = (ev: PointerDragEvent) => {
|
||||
let { dragging } = this
|
||||
|
||||
this.initialHit = null
|
||||
this.movingHit = null
|
||||
this.finalHit = null
|
||||
|
||||
this.prepareHits()
|
||||
this.processFirstCoord(ev)
|
||||
|
||||
if (this.initialHit || !this.requireInitial) {
|
||||
dragging.setIgnoreMove(false)
|
||||
|
||||
// TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :(
|
||||
this.emitter.trigger('pointerdown', ev)
|
||||
} else {
|
||||
dragging.setIgnoreMove(true)
|
||||
}
|
||||
}
|
||||
|
||||
// sets initialHit
|
||||
// sets coordAdjust
|
||||
processFirstCoord(ev: PointerDragEvent) {
|
||||
let origPoint = { left: ev.pageX, top: ev.pageY }
|
||||
let adjustedPoint = origPoint
|
||||
let subjectEl = ev.subjectEl
|
||||
let subjectRect
|
||||
|
||||
if (subjectEl instanceof HTMLElement) { // i.e. not a Document/ShadowRoot
|
||||
subjectRect = computeRect(subjectEl)
|
||||
adjustedPoint = constrainPoint(adjustedPoint, subjectRect)
|
||||
}
|
||||
|
||||
let initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top)
|
||||
if (initialHit) {
|
||||
if (this.useSubjectCenter && subjectRect) {
|
||||
let slicedSubjectRect = intersectRects(subjectRect, initialHit.rect)
|
||||
if (slicedSubjectRect) {
|
||||
adjustedPoint = getRectCenter(slicedSubjectRect)
|
||||
}
|
||||
}
|
||||
|
||||
this.coordAdjust = diffPoints(adjustedPoint, origPoint)
|
||||
} else {
|
||||
this.coordAdjust = { left: 0, top: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
handleDragStart = (ev: PointerDragEvent) => {
|
||||
this.emitter.trigger('dragstart', ev)
|
||||
this.handleMove(ev, true) // force = fire even if initially null
|
||||
}
|
||||
|
||||
handleDragMove = (ev: PointerDragEvent) => {
|
||||
this.emitter.trigger('dragmove', ev)
|
||||
this.handleMove(ev)
|
||||
}
|
||||
|
||||
handlePointerUp = (ev: PointerDragEvent) => {
|
||||
this.releaseHits()
|
||||
this.emitter.trigger('pointerup', ev)
|
||||
}
|
||||
|
||||
handleDragEnd = (ev: PointerDragEvent) => {
|
||||
if (this.movingHit) {
|
||||
this.emitter.trigger('hitupdate', null, true, ev)
|
||||
}
|
||||
|
||||
this.finalHit = this.movingHit
|
||||
this.movingHit = null
|
||||
this.emitter.trigger('dragend', ev)
|
||||
}
|
||||
|
||||
handleMove(ev: PointerDragEvent, forceHandle?: boolean) {
|
||||
let hit = this.queryHitForOffset(
|
||||
ev.pageX + this.coordAdjust!.left,
|
||||
ev.pageY + this.coordAdjust!.top,
|
||||
)
|
||||
|
||||
if (forceHandle || !isHitsEqual(this.movingHit, hit)) {
|
||||
this.movingHit = hit
|
||||
this.emitter.trigger('hitupdate', hit, false, ev)
|
||||
}
|
||||
}
|
||||
|
||||
prepareHits() {
|
||||
this.offsetTrackers = mapHash(this.droppableStore, (interactionSettings) => {
|
||||
interactionSettings.component.prepareHits()
|
||||
return new OffsetTracker(interactionSettings.el)
|
||||
})
|
||||
}
|
||||
|
||||
releaseHits() {
|
||||
let { offsetTrackers } = this
|
||||
|
||||
for (let id in offsetTrackers) {
|
||||
offsetTrackers[id].destroy()
|
||||
}
|
||||
|
||||
this.offsetTrackers = {}
|
||||
}
|
||||
|
||||
queryHitForOffset(offsetLeft: number, offsetTop: number): Hit | null {
|
||||
let { droppableStore, offsetTrackers } = this
|
||||
let bestHit: Hit | null = null
|
||||
|
||||
for (let id in droppableStore) {
|
||||
let component = droppableStore[id].component
|
||||
let offsetTracker = offsetTrackers[id]
|
||||
|
||||
if (
|
||||
offsetTracker && // wasn't destroyed mid-drag
|
||||
offsetTracker.isWithinClipping(offsetLeft, offsetTop)
|
||||
) {
|
||||
let originLeft = offsetTracker.computeLeft()
|
||||
let originTop = offsetTracker.computeTop()
|
||||
let positionLeft = offsetLeft - originLeft
|
||||
let positionTop = offsetTop - originTop
|
||||
let { origRect } = offsetTracker
|
||||
let width = origRect.right - origRect.left
|
||||
let height = origRect.bottom - origRect.top
|
||||
|
||||
if (
|
||||
// must be within the element's bounds
|
||||
positionLeft >= 0 && positionLeft < width &&
|
||||
positionTop >= 0 && positionTop < height
|
||||
) {
|
||||
let hit = component.queryHit(positionLeft, positionTop, width, height)
|
||||
if (
|
||||
hit && (
|
||||
// make sure the hit is within activeRange, meaning it's not a dead cell
|
||||
rangeContainsRange(hit.dateProfile.activeRange, hit.dateSpan.range)
|
||||
) &&
|
||||
(!bestHit || hit.layer > bestHit.layer)
|
||||
) {
|
||||
hit.componentId = id
|
||||
hit.context = component.context
|
||||
|
||||
// TODO: better way to re-orient rectangle
|
||||
hit.rect.left += originLeft
|
||||
hit.rect.right += originLeft
|
||||
hit.rect.top += originTop
|
||||
hit.rect.bottom += originTop
|
||||
|
||||
bestHit = hit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestHit
|
||||
}
|
||||
}
|
||||
|
||||
export function isHitsEqual(hit0: Hit | null, hit1: Hit | null): boolean {
|
||||
if (!hit0 && !hit1) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Boolean(hit0) !== Boolean(hit1)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isDateSpansEqual(hit0!.dateSpan, hit1!.dateSpan)
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
import {
|
||||
DateSelectionApi,
|
||||
PointerDragEvent,
|
||||
elementClosest,
|
||||
CalendarContext,
|
||||
getEventTargetViaRoot,
|
||||
} from '@fullcalendar/common'
|
||||
import { PointerDragging } from '../dnd/PointerDragging'
|
||||
import { EventDragging } from './EventDragging'
|
||||
|
||||
export class UnselectAuto {
|
||||
documentPointer: PointerDragging // for unfocusing
|
||||
isRecentPointerDateSelect = false // wish we could use a selector to detect date selection, but uses hit system
|
||||
matchesCancel = false
|
||||
matchesEvent = false
|
||||
|
||||
constructor(private context: CalendarContext) {
|
||||
let documentPointer = this.documentPointer = new PointerDragging(document)
|
||||
documentPointer.shouldIgnoreMove = true
|
||||
documentPointer.shouldWatchScroll = false
|
||||
documentPointer.emitter.on('pointerdown', this.onDocumentPointerDown)
|
||||
documentPointer.emitter.on('pointerup', this.onDocumentPointerUp)
|
||||
|
||||
/*
|
||||
TODO: better way to know about whether there was a selection with the pointer
|
||||
*/
|
||||
context.emitter.on('select', this.onSelect)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.context.emitter.off('select', this.onSelect)
|
||||
this.documentPointer.destroy()
|
||||
}
|
||||
|
||||
onSelect = (selectInfo: DateSelectionApi) => {
|
||||
if (selectInfo.jsEvent) {
|
||||
this.isRecentPointerDateSelect = true
|
||||
}
|
||||
}
|
||||
|
||||
onDocumentPointerDown = (pev: PointerDragEvent) => {
|
||||
let unselectCancel = this.context.options.unselectCancel
|
||||
let downEl = getEventTargetViaRoot(pev.origEvent) as HTMLElement
|
||||
|
||||
this.matchesCancel = !!elementClosest(downEl, unselectCancel)
|
||||
this.matchesEvent = !!elementClosest(downEl, EventDragging.SELECTOR) // interaction started on an event?
|
||||
}
|
||||
|
||||
onDocumentPointerUp = (pev: PointerDragEvent) => {
|
||||
let { context } = this
|
||||
let { documentPointer } = this
|
||||
let calendarState = context.getCurrentData()
|
||||
|
||||
// touch-scrolling should never unfocus any type of selection
|
||||
if (!documentPointer.wasTouchScroll) {
|
||||
if (
|
||||
calendarState.dateSelection && // an existing date selection?
|
||||
!this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp?
|
||||
) {
|
||||
let unselectAuto = context.options.unselectAuto
|
||||
|
||||
if (unselectAuto && (!unselectAuto || !this.matchesCancel)) {
|
||||
context.calendarApi.unselect(pev)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
calendarState.eventSelection && // an existing event selected?
|
||||
!this.matchesEvent // interaction DIDN'T start on an event
|
||||
) {
|
||||
context.dispatch({ type: 'UNSELECT_EVENT' })
|
||||
}
|
||||
}
|
||||
|
||||
this.isRecentPointerDateSelect = false
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { globalPlugins } from '@fullcalendar/common'
|
||||
import plugin from './main'
|
||||
|
||||
globalPlugins.push(plugin)
|
||||
|
||||
export default plugin
|
||||
export * from './main'
|
|
@ -1,23 +0,0 @@
|
|||
import { createPlugin } from '@fullcalendar/common'
|
||||
import { DateClicking } from './interactions/DateClicking'
|
||||
import { DateSelecting } from './interactions/DateSelecting'
|
||||
import { EventDragging } from './interactions/EventDragging'
|
||||
import { EventResizing } from './interactions/EventResizing'
|
||||
import { UnselectAuto } from './interactions/UnselectAuto'
|
||||
import { FeaturefulElementDragging } from './dnd/FeaturefulElementDragging'
|
||||
import { OPTION_REFINERS, LISTENER_REFINERS } from './options'
|
||||
import './options-declare'
|
||||
|
||||
export default createPlugin({
|
||||
componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing],
|
||||
calendarInteractions: [UnselectAuto],
|
||||
elementDraggingImpl: FeaturefulElementDragging,
|
||||
optionRefiners: OPTION_REFINERS,
|
||||
listenerRefiners: LISTENER_REFINERS,
|
||||
})
|
||||
|
||||
export * from './api-type-deps'
|
||||
export { FeaturefulElementDragging }
|
||||
export { PointerDragging } from './dnd/PointerDragging'
|
||||
export { ExternalDraggable as Draggable } from './interactions-external/ExternalDraggable'
|
||||
export { ThirdPartyDraggable } from './interactions-external/ThirdPartyDraggable'
|
|
@ -1,9 +0,0 @@
|
|||
import { OPTION_REFINERS, LISTENER_REFINERS } from './options'
|
||||
|
||||
type ExtraOptionRefiners = typeof OPTION_REFINERS
|
||||
type ExtraListenerRefiners = typeof LISTENER_REFINERS
|
||||
|
||||
declare module '@fullcalendar/common' {
|
||||
interface BaseOptionRefiners extends ExtraOptionRefiners {}
|
||||
interface CalendarListenerRefiners extends ExtraListenerRefiners {}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { identity, Identity, EventDropArg } from '@fullcalendar/common'
|
||||
|
||||
// public
|
||||
import {
|
||||
DateClickArg,
|
||||
EventDragStartArg, EventDragStopArg,
|
||||
EventResizeStartArg, EventResizeStopArg, EventResizeDoneArg,
|
||||
DropArg, EventReceiveArg, EventLeaveArg,
|
||||
} from './api-type-deps'
|
||||
|
||||
export const OPTION_REFINERS = {
|
||||
fixedMirrorParent: identity as Identity<HTMLElement>,
|
||||
}
|
||||
|
||||
export const LISTENER_REFINERS = {
|
||||
dateClick: identity as Identity<(arg: DateClickArg) => void>,
|
||||
eventDragStart: identity as Identity<(arg: EventDragStartArg) => void>,
|
||||
eventDragStop: identity as Identity<(arg: EventDragStopArg) => void>,
|
||||
eventDrop: identity as Identity<(arg: EventDropArg) => void>,
|
||||
eventResizeStart: identity as Identity<(arg: EventResizeStartArg) => void>,
|
||||
eventResizeStop: identity as Identity<(arg: EventResizeStopArg) => void>,
|
||||
eventResize: identity as Identity<(arg: EventResizeDoneArg) => void>,
|
||||
drop: identity as Identity<(arg: DropArg) => void>,
|
||||
eventReceive: identity as Identity<(arg: EventReceiveArg) => void>,
|
||||
eventLeave: identity as Identity<(arg: EventLeaveArg) => void>,
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { DateSpan, CalendarContext, DatePointApi, DateEnv, ViewApi, EventApi } from '@fullcalendar/common'
|
||||
import { __assign } from 'tslib'
|
||||
|
||||
export interface DropArg extends DatePointApi {
|
||||
draggedEl: HTMLElement
|
||||
jsEvent: MouseEvent
|
||||
view: ViewApi
|
||||
}
|
||||
|
||||
export type EventReceiveArg = EventReceiveLeaveArg
|
||||
export type EventLeaveArg = EventReceiveLeaveArg
|
||||
export interface EventReceiveLeaveArg { // will this become public?
|
||||
draggedEl: HTMLElement
|
||||
event: EventApi
|
||||
relatedEvents: EventApi[]
|
||||
revert: () => void
|
||||
view: ViewApi
|
||||
}
|
||||
|
||||
export function buildDatePointApiWithContext(dateSpan: DateSpan, context: CalendarContext) {
|
||||
let props = {} as DatePointApi
|
||||
|
||||
for (let transform of context.pluginHooks.datePointTransforms) {
|
||||
__assign(props, transform(dateSpan, context))
|
||||
}
|
||||
|
||||
__assign(props, buildDatePointApi(dateSpan, context.dateEnv))
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
export function buildDatePointApi(span: DateSpan, dateEnv: DateEnv): DatePointApi {
|
||||
return {
|
||||
date: dateEnv.toDate(span.range.start),
|
||||
dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
|
||||
allDay: span.allDay,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue