1
0
Fork 0

Delete apps/schoolCalendar/fullcalendar/interaction/src directory

master
Ronin0000 2021-11-22 09:06:52 -08:00 committed by GitHub
parent 1bf5132a5c
commit 3863be1830
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 0 additions and 2987 deletions

View File

@ -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)
}
}

View File

@ -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'
}

View File

@ -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() {
}
}

View File

@ -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()
}
}

View File

@ -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'

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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) || ''
}

View File

@ -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'
}
}
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -1,7 +0,0 @@
import { globalPlugins } from '@fullcalendar/common'
import plugin from './main'
globalPlugins.push(plugin)
export default plugin
export * from './main'

View File

@ -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'

View File

@ -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 {}
}

View File

@ -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>,
}

View File

@ -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,
}
}