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