12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126 |
- import { createPlugin } from '@fullcalendar/core/index.js';
- import { config, Emitter, elementClosest, applyStyle, whenTransitionDone, removeElement, ScrollController, ElementScrollController, computeInnerRect, WindowScrollController, ElementDragging, preventSelection, preventContextMenu, allowSelection, allowContextMenu, computeRect, getClippingParents, pointInsideRect, constrainPoint, intersectRects, getRectCenter, diffPoints, mapHash, rangeContainsRange, isDateSpansEqual, Interaction, interactionSettingsToStore, isDateSelectionValid, enableCursor, disableCursor, triggerDateSelect, compareNumbers, getElSeg, getRelevantEvents, EventImpl, createEmptyEventStore, applyMutationToEventStore, isInteractionValid, buildEventApis, interactionSettingsStore, startOfDay, diffDates, createDuration, getEventTargetViaRoot, identity, eventTupleToStore, parseDragMeta, elementMatches, refineEventDef, parseEventDef, getDefaultEventEnd, createEventInstance, BASE_OPTION_DEFAULTS } from '@fullcalendar/core/internal.js';
- 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
- */
- class PointerDragging {
- constructor(containerEl) {
- this.subjectEl = null;
- // options that can be directly assigned by caller
- this.selector = ''; // will cause subjectEl in all emitted events to be this element
- this.handleSelector = '';
- this.shouldIgnoreMove = false;
- this.shouldWatchScroll = true; // for simulating pointermove on scroll
- // internal states
- this.isDragging = false;
- this.isTouchDragging = false;
- this.wasTouchScroll = false;
- // Mouse
- // ----------------------------------------------------------------------------------------------------
- this.handleMouseDown = (ev) => {
- 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);
- }
- };
- this.handleMouseMove = (ev) => {
- let pev = this.createEventFromMouse(ev);
- this.recordCoords(pev);
- this.emitter.trigger('pointermove', pev);
- };
- this.handleMouseUp = (ev) => {
- 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
- };
- // Touch
- // ----------------------------------------------------------------------------------------------------
- this.handleTouchStart = (ev) => {
- 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;
- 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);
- }
- };
- this.handleTouchMove = (ev) => {
- let pev = this.createEventFromTouch(ev);
- this.recordCoords(pev);
- this.emitter.trigger('pointermove', pev);
- };
- this.handleTouchEnd = (ev) => {
- if (this.isDragging) { // done to guard against touchend followed by touchcancel
- let targetEl = ev.target;
- 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();
- }
- };
- this.handleTouchScroll = () => {
- this.wasTouchScroll = true;
- };
- this.handleScroll = (ev) => {
- if (!this.shouldIgnoreMove) {
- let pageX = (window.scrollX - this.prevScrollX) + this.prevPageX;
- let pageY = (window.scrollY - 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,
- });
- }
- };
- this.containerEl = containerEl;
- this.emitter = new Emitter();
- containerEl.addEventListener('mousedown', this.handleMouseDown);
- containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true });
- listenerCreated();
- }
- destroy() {
- this.containerEl.removeEventListener('mousedown', this.handleMouseDown);
- this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
- listenerDestroyed();
- }
- tryStart(ev) {
- let subjectEl = this.querySubjectEl(ev);
- let downEl = ev.target;
- 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) {
- if (this.selector) {
- return elementClosest(ev.target, this.selector);
- }
- return this.containerEl;
- }
- shouldIgnoreMouse() {
- return ignoreMouseDepth || this.isTouchDragging;
- }
- // 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) {
- if (this.shouldWatchScroll) {
- this.recordCoords(ev);
- window.addEventListener('scroll', this.handleScroll, true); // useCapture=true
- }
- }
- recordCoords(ev) {
- if (this.shouldWatchScroll) {
- this.prevPageX = ev.pageX;
- this.prevPageY = ev.pageY;
- this.prevScrollX = window.scrollX;
- this.prevScrollY = window.scrollY;
- }
- }
- destroyScrollWatch() {
- if (this.shouldWatchScroll) {
- window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true
- }
- }
- // Event Normalization
- // ----------------------------------------------------------------------------------------------------
- createEventFromMouse(ev, isFirst) {
- 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, isFirst) {
- 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.pageX;
- pageY = ev.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) {
- return ev.button === 0 && !ev.ctrlKey;
- }
- // Ignoring fake mouse events generated by touch
- // ----------------------------------------------------------------------------------------------------
- function startIgnoringMouse() {
- 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 });
- }
- }
- function onWindowTouchMove(ev) {
- if (isWindowTouchMoveCancelled) {
- ev.preventDefault();
- }
- }
- /*
- 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.
- */
- class ElementMirror {
- constructor() {
- this.isVisible = false; // must be explicitly enabled
- this.sourceEl = null;
- this.mirrorEl = null;
- this.sourceElRect = null; // screen coords relative to viewport
- // options that can be set directly by caller
- this.parentNode = document.body; // HIGHLY SUGGESTED to set this to sidestep ShadowDOM issues
- this.zIndex = 9999;
- this.revertDuration = 0;
- }
- start(sourceEl, pageX, pageY) {
- this.sourceEl = sourceEl;
- this.sourceElRect = this.sourceEl.getBoundingClientRect();
- this.origScreenX = pageX - window.scrollX;
- this.origScreenY = pageY - window.scrollY;
- this.deltaX = 0;
- this.deltaY = 0;
- this.updateElPosition();
- }
- handleMove(pageX, pageY) {
- this.deltaX = (pageX - window.scrollX) - this.origScreenX;
- this.deltaY = (pageY - window.scrollY) - this.origScreenY;
- this.updateElPosition();
- }
- // can be called before start
- setIsVisible(bool) {
- 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, callback) {
- 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, revertDuration) {
- 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() {
- let sourceElRect = this.sourceElRect;
- let mirrorEl = this.mirrorEl;
- if (!mirrorEl) {
- mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // 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.style.userSelect = 'none';
- mirrorEl.style.webkitUserSelect = 'none';
- mirrorEl.style.pointerEvents = 'none';
- mirrorEl.classList.add('fc-event-dragging');
- applyStyle(mirrorEl, {
- position: 'fixed',
- zIndex: this.zIndex,
- visibility: '',
- boxSizing: 'border-box',
- width: sourceElRect.right - sourceElRect.left,
- height: sourceElRect.bottom - sourceElRect.top,
- right: 'auto',
- bottom: 'auto',
- margin: 0,
- });
- this.parentNode.appendChild(mirrorEl);
- }
- return mirrorEl;
- }
- }
- /*
- 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
- */
- class ScrollGeomCache extends ScrollController {
- constructor(scrollController, doesListening) {
- super();
- this.handleScroll = () => {
- this.scrollTop = this.scrollController.getScrollTop();
- this.scrollLeft = this.scrollController.getScrollLeft();
- this.handleScrollChange();
- };
- 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);
- }
- }
- destroy() {
- if (this.doesListening) {
- this.getEventTarget().removeEventListener('scroll', this.handleScroll);
- }
- }
- getScrollTop() {
- return this.scrollTop;
- }
- getScrollLeft() {
- return this.scrollLeft;
- }
- setScrollTop(top) {
- 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) {
- 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() {
- }
- }
- class ElementScrollGeomCache extends ScrollGeomCache {
- constructor(el, doesListening) {
- super(new ElementScrollController(el), doesListening);
- }
- getEventTarget() {
- return this.scrollController.el;
- }
- computeClientRect() {
- return computeInnerRect(this.scrollController.el);
- }
- }
- class WindowScrollGeomCache extends ScrollGeomCache {
- constructor(doesListening) {
- super(new WindowScrollController(), doesListening);
- }
- getEventTarget() {
- return window;
- }
- computeClientRect() {
- 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();
- }
- }
- // 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.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.
- */
- class AutoScroller {
- constructor() {
- // options that can be set by caller
- this.isEnabled = true;
- this.scrollQuery = [window, '.fc-scroller'];
- this.edgeThreshold = 50; // pixels
- this.maxVelocity = 300; // pixels per second
- // internal state
- this.pointerScreenX = null;
- this.pointerScreenY = null;
- this.isAnimating = false;
- this.scrollCaches = null;
- // protect against the initial pointerdown being too close to an edge and starting the scroll
- this.everMovedUp = false;
- this.everMovedDown = false;
- this.everMovedLeft = false;
- this.everMovedRight = false;
- this.animate = () => {
- if (this.isAnimating) { // wasn't cancelled between animation calls
- let edge = this.computeBestEdge(this.pointerScreenX + window.scrollX, this.pointerScreenY + window.scrollY);
- if (edge) {
- let now = getTime();
- this.handleSide(edge, (now - this.msSinceRequest) / 1000);
- this.requestAnimation(now);
- }
- else {
- this.isAnimating = false; // will stop animation
- }
- }
- };
- }
- start(pageX, pageY, scrollStartEl) {
- 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, pageY) {
- if (this.isEnabled) {
- let pointerScreenX = pageX - window.scrollX;
- let pointerScreenY = pageY - window.scrollY;
- 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) {
- this.msSinceRequest = now;
- requestAnimationFrame(this.animate);
- }
- handleSide(edge, seconds) {
- 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
- computeBestEdge(left, top) {
- let { edgeThreshold } = this;
- let bestSide = null;
- let scrollCaches = this.scrollCaches || [];
- for (let scrollCache of 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 };
- }
- /*
- TODO: fix broken RTL scrolling. canScrollLeft always returning false
- https://github.com/fullcalendar/fullcalendar/issues/4837
- */
- 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;
- }
- buildCaches(scrollStartEl) {
- 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
- });
- }
- queryScrollEls(scrollStartEl) {
- let els = [];
- for (let query of this.scrollQuery) {
- if (typeof query === 'object') {
- els.push(query);
- }
- else {
- /*
- TODO: in the future, always have auto-scroll happen on element where current Hit came from
- Ticket: https://github.com/fullcalendar/fullcalendar/issues/4593
- */
- els.push(...Array.prototype.slice.call(scrollStartEl.getRootNode().querySelectorAll(query)));
- }
- }
- return els;
- }
- }
- /*
- 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
- */
- class FeaturefulElementDragging extends ElementDragging {
- constructor(containerEl, selector) {
- super(containerEl);
- this.containerEl = containerEl;
- // options that can be directly set by caller
- // the caller can also set the PointerDragging's options as well
- this.delay = null;
- this.minDistance = 0;
- this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag
- this.mirrorNeedsRevert = false;
- this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup
- this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation
- this.isDelayEnded = false;
- this.isDistanceSurpassed = false;
- this.delayTimeoutId = null;
- this.onPointerDown = (ev) => {
- 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, ev.pageX, ev.pageY); // must happen on first pointer down
- this.startDelay(ev);
- if (!this.minDistance) {
- this.handleDistanceSurpassed(ev);
- }
- }
- }
- };
- this.onPointerMove = (ev) => {
- 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);
- }
- }
- };
- this.onPointerUp = (ev) => {
- 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;
- }
- }
- };
- 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({});
- }
- startDelay(ev) {
- if (typeof this.delay === 'number') {
- this.delayTimeoutId = setTimeout(() => {
- this.delayTimeoutId = null;
- this.handleDelayEnd(ev);
- }, this.delay); // not assignable to number!
- }
- else {
- this.handleDelayEnd(ev);
- }
- }
- handleDelayEnd(ev) {
- this.isDelayEnded = true;
- this.tryStartDrag(ev);
- }
- handleDistanceSurpassed(ev) {
- this.isDistanceSurpassed = true;
- this.tryStartDrag(ev);
- }
- tryStartDrag(ev) {
- 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) {
- // .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));
- }
- stopDrag(ev) {
- this.isDragging = false;
- this.emitter.trigger('dragend', ev);
- }
- // fill in the implementations...
- setIgnoreMove(bool) {
- this.pointer.shouldIgnoreMove = bool;
- }
- setMirrorIsVisible(bool) {
- this.mirror.setIsVisible(bool);
- }
- setMirrorNeedsRevert(bool) {
- this.mirrorNeedsRevert = bool;
- }
- setAutoScrollEnabled(bool) {
- this.autoScroller.isEnabled = bool;
- }
- }
- /*
- 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.
- */
- class OffsetTracker {
- constructor(el) {
- this.el = el;
- this.origRect = computeRect(el);
- // will work fine for divs that have overflow:hidden
- this.scrollCaches = getClippingParents(el).map((scrollEl) => new ElementScrollGeomCache(scrollEl, 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, pageY) {
- 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) {
- let tagName = node.tagName;
- return tagName === 'HTML' || tagName === 'BODY';
- }
- /*
- 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
- */
- class HitDragging {
- constructor(dragging, droppableStore) {
- // options that can be set by caller
- this.useSubjectCenter = false;
- this.requireInitial = true; // if doesn't start out on a hit, won't emit any events
- this.disablePointCheck = false;
- this.initialHit = null;
- this.movingHit = null;
- this.finalHit = null; // won't ever be populated if shouldIgnoreMove
- this.handlePointerDown = (ev) => {
- 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);
- }
- };
- this.handleDragStart = (ev) => {
- this.emitter.trigger('dragstart', ev);
- this.handleMove(ev, true); // force = fire even if initially null
- };
- this.handleDragMove = (ev) => {
- this.emitter.trigger('dragmove', ev);
- this.handleMove(ev);
- };
- this.handlePointerUp = (ev) => {
- this.releaseHits();
- this.emitter.trigger('pointerup', ev);
- };
- this.handleDragEnd = (ev) => {
- if (this.movingHit) {
- this.emitter.trigger('hitupdate', null, true, ev);
- }
- this.finalHit = this.movingHit;
- this.movingHit = null;
- this.emitter.trigger('dragend', ev);
- };
- 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();
- }
- // sets initialHit
- // sets coordAdjust
- processFirstCoord(ev) {
- 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 };
- }
- }
- handleMove(ev, forceHandle) {
- 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, offsetTop) {
- let { droppableStore, offsetTrackers } = this;
- let bestHit = 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)) &&
- // Ensure the component we are querying for the hit is accessibly my the pointer
- // Prevents obscured calendars (ex: under a modal dialog) from accepting hit
- // https://github.com/fullcalendar/fullcalendar/issues/5026
- (this.disablePointCheck ||
- offsetTracker.el.contains(document.elementFromPoint(
- // add-back origins to get coordinate relative to top-left of window viewport
- positionLeft + originLeft - window.scrollX, positionTop + originTop - window.scrollY))) &&
- (!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;
- }
- }
- function isHitsEqual(hit0, hit1) {
- if (!hit0 && !hit1) {
- return true;
- }
- if (Boolean(hit0) !== Boolean(hit1)) {
- return false;
- }
- return isDateSpansEqual(hit0.dateSpan, hit1.dateSpan);
- }
- function buildDatePointApiWithContext(dateSpan, context) {
- let props = {};
- for (let transform of context.pluginHooks.datePointTransforms) {
- Object.assign(props, transform(dateSpan, context));
- }
- Object.assign(props, buildDatePointApi(dateSpan, context.dateEnv));
- return props;
- }
- function buildDatePointApi(span, dateEnv) {
- return {
- date: dateEnv.toDate(span.range.start),
- dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
- allDay: span.allDay,
- };
- }
- /*
- Monitors when the user clicks on a specific date/time of a component.
- A pointerdown+pointerup on the same "hit" constitutes a click.
- */
- class DateClicking extends Interaction {
- constructor(settings) {
- super(settings);
- this.handlePointerDown = (pev) => {
- let { dragging } = this;
- let downEl = pev.origEvent.target;
- // 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
- this.handleDragEnd = (ev) => {
- 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 = Object.assign(Object.assign({}, buildDatePointApiWithContext(initialHit.dateSpan, context)), { dayEl: initialHit.dayEl, jsEvent: ev.origEvent, view: context.viewApi || context.calendarApi.view });
- context.emitter.trigger('dateClick', arg);
- }
- }
- };
- // 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();
- }
- }
- /*
- 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.
- */
- class DateSelecting extends Interaction {
- constructor(settings) {
- super(settings);
- this.dragSelection = null;
- this.handlePointerDown = (ev) => {
- let { component, dragging } = this;
- let { options } = component.context;
- let canSelect = options.selectable &&
- component.isValidDateDownEl(ev.origEvent.target);
- // 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$1(component) : null;
- };
- this.handleDragStart = (ev) => {
- this.component.context.calendarApi.unselect(ev); // unselect previous selections
- };
- this.handleHitUpdate = (hit, isFinal) => {
- let { context } = this.component;
- let dragSelection = 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
- }
- };
- this.handlePointerUp = (pev) => {
- if (this.dragSelection) {
- // selection is already rendered, so just need to report selection
- triggerDateSelect(this.dragSelection, pev, this.component.context);
- this.dragSelection = null;
- }
- };
- 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();
- }
- }
- function getComponentTouchDelay$1(component) {
- let { options } = component.context;
- let delay = options.selectLongPressDelay;
- if (delay == null) {
- delay = options.longPressDelay;
- }
- return delay;
- }
- function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) {
- 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 = {};
- for (let transformer of dateSelectionTransformers) {
- let res = transformer(hit0, hit1);
- if (res === false) {
- return null;
- }
- if (res) {
- Object.assign(props, res);
- }
- }
- props.range = { start: ms[0], end: ms[3] };
- props.allDay = dateSpan0.allDay;
- return props;
- }
- class EventDragging extends Interaction {
- constructor(settings) {
- super(settings);
- // internal state
- this.subjectEl = null;
- this.subjectSeg = null; // the seg being selected/dragged
- this.isDragging = false;
- this.eventRange = null;
- this.relevantEvents = null; // the events being dragged
- this.receivingContext = null;
- this.validMutation = null;
- this.mutatedRelevantEvents = null;
- this.handlePointerDown = (ev) => {
- let origTarget = ev.origEvent.target;
- let { component, dragging } = this;
- let { mirror } = dragging;
- let { options } = component.context;
- let initialContext = component.context;
- this.subjectEl = ev.subjectEl;
- let subjectSeg = this.subjectSeg = getElSeg(ev.subjectEl);
- 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.classList.contains('fc-event-draggable');
- };
- this.handleDragStart = (ev) => {
- 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 EventImpl(initialContext, eventRange.def, eventRange.instance),
- jsEvent: ev.origEvent,
- view: initialContext.viewApi,
- });
- }
- };
- this.handleHitUpdate = (hit, isFinal) => {
- 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 = null;
- let mutation = null;
- let mutatedRelevantEvents = null;
- let isInvalid = false;
- let interaction = {
- 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, this.eventRange.instance.range.start, 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 || !this.subjectEl.getRootNode().querySelector('.fc-event-mirror'));
- // assign states based on new hit
- this.receivingContext = receivingContext;
- this.validMutation = mutation;
- this.mutatedRelevantEvents = mutatedRelevantEvents;
- }
- };
- this.handlePointerUp = () => {
- if (!this.isDragging) {
- this.cleanup(); // because handleDragEnd won't fire
- }
- };
- this.handleDragEnd = (ev) => {
- 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 EventImpl(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,
- view: initialView,
- });
- if (validMutation) {
- // dropped within same calendar
- if (receivingContext === initialContext) {
- let updatedEventApi = new EventImpl(initialContext, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null);
- initialContext.dispatch({
- type: 'MERGE_EVENTS',
- eventStore: mutatedRelevantEvents,
- });
- let eventChangeArg = {
- oldEvent: eventApi,
- event: updatedEventApi,
- relatedEvents: buildEventApis(mutatedRelevantEvents, initialContext, eventInstance),
- revert() {
- initialContext.dispatch({
- type: 'MERGE_EVENTS',
- eventStore: relevantEvents, // the pre-change data
- });
- },
- };
- let transformed = {};
- for (let transformer of initialContext.getCurrentData().pluginHooks.eventDropTransformers) {
- Object.assign(transformed, transformer(validMutation, initialContext));
- }
- initialContext.emitter.trigger('eventDrop', Object.assign(Object.assign(Object.assign({}, eventChangeArg), transformed), { el: ev.subjectEl, delta: validMutation.datesDelta, jsEvent: ev.origEvent, view: initialView }));
- initialContext.emitter.trigger('eventChange', eventChangeArg);
- // dropped in different calendar
- }
- else if (receivingContext) {
- let eventRemoveArg = {
- event: eventApi,
- relatedEvents: buildEventApis(relevantEvents, initialContext, eventInstance),
- revert() {
- initialContext.dispatch({
- type: 'MERGE_EVENTS',
- eventStore: relevantEvents,
- });
- },
- };
- initialContext.emitter.trigger('eventLeave', Object.assign(Object.assign({}, eventRemoveArg), { draggedEl: ev.subjectEl, 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 EventImpl(receivingContext, addedEventDef, addedEventInstance);
- receivingContext.dispatch({
- type: 'MERGE_EVENTS',
- eventStore: mutatedRelevantEvents,
- });
- let 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', Object.assign(Object.assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.context.viewApi }));
- receivingContext.emitter.trigger('eventReceive', Object.assign(Object.assign({}, eventAddArg), { draggedEl: ev.subjectEl, view: finalHit.context.viewApi }));
- }
- }
- else {
- initialContext.emitter.trigger('_noEventDrop');
- }
- }
- this.cleanup();
- };
- 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();
- }
- // render a drag state on the next receivingCalendar
- displayDrag(nextContext, state) {
- 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() {
- this.subjectSeg = null;
- this.isDragging = false;
- this.eventRange = null;
- this.relevantEvents = null;
- this.receivingContext = null;
- this.validMutation = null;
- this.mutatedRelevantEvents = null;
- }
- }
- // TODO: test this in IE11
- // QUESTION: why do we need it on the resizable???
- EventDragging.SELECTOR = '.fc-event-draggable, .fc-event-resizable';
- function computeEventMutation(hit0, hit1, eventInstanceStart, massagers) {
- let dateSpan0 = hit0.dateSpan;
- let dateSpan1 = hit1.dateSpan;
- let date0 = dateSpan0.range.start;
- let date1 = dateSpan1.range.start;
- let standardProps = {};
- 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(eventInstanceStart);
- }
- else {
- // Moving from allDate->timed
- // Doesn't matter where on the event the drag began, mutate the event's start-date to date1
- date0 = eventInstanceStart;
- }
- }
- 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 = {
- datesDelta: delta,
- standardProps,
- };
- for (let massager of massagers) {
- massager(mutation, hit0, hit1);
- }
- return mutation;
- }
- function getComponentTouchDelay(component) {
- let { options } = component.context;
- let delay = options.eventLongPressDelay;
- if (delay == null) {
- delay = options.longPressDelay;
- }
- return delay;
- }
- class EventResizing extends Interaction {
- constructor(settings) {
- super(settings);
- // internal state
- this.draggingSegEl = null;
- this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg?
- this.eventRange = null;
- this.relevantEvents = null;
- this.validMutation = null;
- this.mutatedRelevantEvents = null;
- this.handlePointerDown = (ev) => {
- 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) ||
- (ev.isTouch && this.component.props.eventSelection !== eventRange.instance.instanceId));
- };
- this.handleDragStart = (ev) => {
- 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 EventImpl(context, eventRange.def, eventRange.instance),
- jsEvent: ev.origEvent,
- view: context.viewApi,
- });
- };
- this.handleHitUpdate = (hit, isFinal, ev) => {
- let { context } = this.component;
- let relevantEvents = this.relevantEvents;
- let initialHit = this.hitDragging.initialHit;
- let eventInstance = this.eventRange.instance;
- let mutation = null;
- let mutatedRelevantEvents = null;
- let isInvalid = false;
- let interaction = {
- 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.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;
- }
- };
- this.handleDragEnd = (ev) => {
- let { context } = this.component;
- let eventDef = this.eventRange.def;
- let eventInstance = this.eventRange.instance;
- let eventApi = new EventImpl(context, eventDef, eventInstance);
- let relevantEvents = this.relevantEvents;
- let mutatedRelevantEvents = this.mutatedRelevantEvents;
- context.emitter.trigger('eventResizeStop', {
- el: this.draggingSegEl,
- event: eventApi,
- jsEvent: ev.origEvent,
- view: context.viewApi,
- });
- if (this.validMutation) {
- let updatedEventApi = new EventImpl(context, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null);
- context.dispatch({
- type: 'MERGE_EVENTS',
- eventStore: mutatedRelevantEvents,
- });
- let 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', Object.assign(Object.assign({}, eventChangeArg), { el: this.draggingSegEl, startDelta: this.validMutation.startDelta || createDuration(0), endDelta: this.validMutation.endDelta || createDuration(0), jsEvent: ev.origEvent, 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
- };
- 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();
- }
- querySegEl(ev) {
- return elementClosest(ev.subjectEl, '.fc-event');
- }
- }
- function computeMutation(hit0, hit1, isFromStart, instanceRange) {
- 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;
- }
- class UnselectAuto {
- constructor(context) {
- this.context = context;
- this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system
- this.matchesCancel = false;
- this.matchesEvent = false;
- this.onSelect = (selectInfo) => {
- if (selectInfo.jsEvent) {
- this.isRecentPointerDateSelect = true;
- }
- };
- this.onDocumentPointerDown = (pev) => {
- let unselectCancel = this.context.options.unselectCancel;
- let downEl = getEventTargetViaRoot(pev.origEvent);
- this.matchesCancel = !!elementClosest(downEl, unselectCancel);
- this.matchesEvent = !!elementClosest(downEl, EventDragging.SELECTOR); // interaction started on an event?
- };
- this.onDocumentPointerUp = (pev) => {
- 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;
- };
- 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();
- }
- }
- const OPTION_REFINERS = {
- fixedMirrorParent: identity,
- };
- const LISTENER_REFINERS = {
- dateClick: identity,
- eventDragStart: identity,
- eventDragStop: identity,
- eventDrop: identity,
- eventResizeStart: identity,
- eventResizeStop: identity,
- eventResize: identity,
- drop: identity,
- eventReceive: identity,
- eventLeave: identity,
- };
- /*
- 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.
- */
- class ExternalElementDragging {
- constructor(dragging, suppliedDragMeta) {
- this.receivingContext = null;
- this.droppableEvent = null; // will exist for all drags, even if create:false
- this.suppliedDragMeta = null;
- this.dragMeta = null;
- this.handleDragStart = (ev) => {
- this.dragMeta = this.buildDragMeta(ev.subjectEl);
- };
- this.handleHitUpdate = (hit, isFinal, ev) => {
- let { dragging } = this.hitDragging;
- let receivingContext = null;
- let droppableEvent = null;
- let isInvalid = false;
- let interaction = {
- affectedEvents: createEmptyEventStore(),
- mutatedEvents: createEmptyEventStore(),
- isEvent: this.dragMeta.create,
- };
- if (hit) {
- receivingContext = hit.context;
- if (this.canDropElOnCalendar(ev.subjectEl, 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'));
- if (!isInvalid) {
- enableCursor();
- }
- else {
- disableCursor();
- }
- if (!isFinal) {
- dragging.setMirrorNeedsRevert(!droppableEvent);
- this.receivingContext = receivingContext;
- this.droppableEvent = droppableEvent;
- }
- };
- this.handleDragEnd = (pev) => {
- 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', Object.assign(Object.assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, 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 EventImpl(receivingContext, droppableEvent.def, droppableEvent.instance),
- relatedEvents: [],
- revert() {
- receivingContext.dispatch({
- type: 'REMOVE_EVENTS',
- eventStore: addingEvents,
- });
- },
- draggedEl: pev.subjectEl,
- view: finalView,
- });
- }
- }
- this.receivingContext = null;
- this.droppableEvent = null;
- };
- 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;
- }
- buildDragMeta(subjectEl) {
- if (typeof this.suppliedDragMeta === 'object') {
- return parseDragMeta(this.suppliedDragMeta);
- }
- if (typeof this.suppliedDragMeta === 'function') {
- return parseDragMeta(this.suppliedDragMeta(subjectEl));
- }
- return getDragMetaFromEl(subjectEl);
- }
- displayDrag(nextContext, state) {
- 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, receivingContext) {
- 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, dragMeta, context) {
- let defProps = Object.assign({}, dragMeta.leftoverProps);
- for (let transform of context.pluginHooks.externalDefTransforms) {
- Object.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) {
- 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, name) {
- let prefix = config.dataAttrPrefix;
- let prefixedName = (prefix ? prefix + '-' : '') + name;
- return el.getAttribute('data-' + prefixedName) || '';
- }
- /*
- 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.
- */
- class ExternalDraggable {
- constructor(el, settings = {}) {
- this.handlePointerDown = (ev) => {
- 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;
- };
- this.handleDragStart = (ev) => {
- if (ev.isTouch &&
- this.dragging.delay &&
- ev.subjectEl.classList.contains('fc-event')) {
- this.dragging.mirror.getMirrorEl().classList.add('fc-event-selected');
- }
- };
- 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
- }
- destroy() {
- this.dragging.destroy();
- }
- }
- /*
- 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.
- */
- class InferredElementDragging extends ElementDragging {
- constructor(containerEl) {
- super(containerEl);
- this.shouldIgnoreMove = false;
- this.mirrorSelector = '';
- this.currentMirrorEl = null;
- this.handlePointerDown = (ev) => {
- this.emitter.trigger('pointerdown', ev);
- if (!this.shouldIgnoreMove) {
- // fire dragstart right away. does not support delay or min-distance
- this.emitter.trigger('dragstart', ev);
- }
- };
- this.handlePointerMove = (ev) => {
- if (!this.shouldIgnoreMove) {
- this.emitter.trigger('dragmove', ev);
- }
- };
- this.handlePointerUp = (ev) => {
- this.emitter.trigger('pointerup', ev);
- if (!this.shouldIgnoreMove) {
- // fire dragend right away. does not support a revert animation
- this.emitter.trigger('dragend', ev);
- }
- };
- 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();
- }
- setIgnoreMove(bool) {
- this.shouldIgnoreMove = bool;
- }
- setMirrorIsVisible(bool) {
- 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)
- : null;
- if (mirrorEl) {
- this.currentMirrorEl = mirrorEl;
- mirrorEl.style.visibility = 'hidden';
- }
- }
- }
- }
- /*
- Bridges third-party drag-n-drop systems with FullCalendar.
- Must be instantiated and destroyed by caller.
- */
- class ThirdPartyDraggable {
- constructor(containerOrSettings, settings) {
- let containerEl = document;
- if (
- // wish we could just test instanceof EventTarget, but doesn't work in IE11
- containerOrSettings === document ||
- containerOrSettings instanceof Element) {
- containerEl = containerOrSettings;
- settings = settings || {};
- }
- else {
- settings = (containerOrSettings || {});
- }
- let dragging = this.dragging = new InferredElementDragging(containerEl);
- 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;
- }
- let externalDragging = new ExternalElementDragging(dragging, settings.eventData);
- // The hit-detection system requires that the dnd-mirror-element be pointer-events:none,
- // but this can't be guaranteed for third-party draggables, so disable
- externalDragging.hitDragging.disablePointCheck = true;
- }
- destroy() {
- this.dragging.destroy();
- }
- }
- var index = createPlugin({
- name: '@fullcalendar/interaction',
- componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing],
- calendarInteractions: [UnselectAuto],
- elementDraggingImpl: FeaturefulElementDragging,
- optionRefiners: OPTION_REFINERS,
- listenerRefiners: LISTENER_REFINERS,
- });
- export { ExternalDraggable as Draggable, ThirdPartyDraggable, index as default };
|