EventDragging.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. var EventDragging = FC.EventDragging = Interaction.extend({
  2. eventPointing: null,
  3. dragListener: null,
  4. isDragging: false,
  5. /*
  6. component implements:
  7. - bindSegHandlerToEl
  8. - publiclyTrigger
  9. - diffDates
  10. - eventRangesToEventFootprints
  11. - isEventInstanceGroupAllowed
  12. */
  13. constructor: function(component, eventPointing) {
  14. Interaction.call(this, component);
  15. this.eventPointing = eventPointing;
  16. },
  17. end: function() {
  18. if (this.dragListener) {
  19. this.dragListener.endInteraction();
  20. }
  21. },
  22. getSelectionDelay: function() {
  23. var delay = this.opt('eventLongPressDelay');
  24. if (delay == null) {
  25. delay = this.opt('longPressDelay'); // fallback
  26. }
  27. return delay;
  28. },
  29. bindToEl: function(el) {
  30. var component = this.component;
  31. component.bindSegHandlerToEl(el, 'mousedown', this.handleMousedown.bind(this));
  32. component.bindSegHandlerToEl(el, 'touchstart', this.handleTouchStart.bind(this));
  33. },
  34. handleMousedown: function(seg, ev) {
  35. if (this.component.canStartDrag(seg, ev)) {
  36. this.buildDragListener(seg).startInteraction(ev, { distance: 5 });
  37. }
  38. },
  39. handleTouchStart: function(seg, ev) {
  40. var component = this.component;
  41. var settings = {
  42. delay: this.view.isEventDefSelected(seg.footprint.eventDef) ? // already selected?
  43. 0 : this.getSelectionDelay()
  44. };
  45. if (component.canStartDrag(seg, ev)) {
  46. this.buildDragListener(seg).startInteraction(ev, settings);
  47. }
  48. else if (component.canStartSelection(seg, ev)) {
  49. this.buildSelectListener(seg).startInteraction(ev, settings);
  50. }
  51. },
  52. // seg isn't draggable, but let's use a generic DragListener
  53. // simply for the delay, so it can be selected.
  54. // Has side effect of setting/unsetting `dragListener`
  55. buildSelectListener: function(seg) {
  56. var _this = this;
  57. var view = this.view;
  58. var eventDef = seg.footprint.eventDef;
  59. var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
  60. if (this.dragListener) {
  61. return this.dragListener;
  62. }
  63. var dragListener = this.dragListener = new DragListener({
  64. dragStart: function(ev) {
  65. if (
  66. dragListener.isTouch &&
  67. !view.isEventDefSelected(eventDef) &&
  68. eventInstance
  69. ) {
  70. // if not previously selected, will fire after a delay. then, select the event
  71. view.selectEventInstance(eventInstance);
  72. }
  73. },
  74. interactionEnd: function(ev) {
  75. _this.dragListener = null;
  76. }
  77. });
  78. return dragListener;
  79. },
  80. // Builds a listener that will track user-dragging on an event segment.
  81. // Generic enough to work with any type of Grid.
  82. // Has side effect of setting/unsetting `dragListener`
  83. buildDragListener: function(seg) {
  84. var _this = this;
  85. var component = this.component;
  86. var view = this.view;
  87. var calendar = view.calendar;
  88. var eventManager = calendar.eventManager;
  89. var el = seg.el;
  90. var eventDef = seg.footprint.eventDef;
  91. var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
  92. var isDragging;
  93. var mouseFollower; // A clone of the original element that will move with the mouse
  94. var eventDefMutation;
  95. if (this.dragListener) {
  96. return this.dragListener;
  97. }
  98. // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
  99. // of the view.
  100. var dragListener = this.dragListener = new HitDragListener(view, {
  101. scroll: this.opt('dragScroll'),
  102. subjectEl: el,
  103. subjectCenter: true,
  104. interactionStart: function(ev) {
  105. seg.component = component; // for renderDrag
  106. isDragging = false;
  107. mouseFollower = new MouseFollower(seg.el, {
  108. additionalClass: 'fc-dragging',
  109. parentEl: view.el,
  110. opacity: dragListener.isTouch ? null : _this.opt('dragOpacity'),
  111. revertDuration: _this.opt('dragRevertDuration'),
  112. zIndex: 2 // one above the .fc-view
  113. });
  114. mouseFollower.hide(); // don't show until we know this is a real drag
  115. mouseFollower.start(ev);
  116. },
  117. dragStart: function(ev) {
  118. if (
  119. dragListener.isTouch &&
  120. !view.isEventDefSelected(eventDef) &&
  121. eventInstance
  122. ) {
  123. // if not previously selected, will fire after a delay. then, select the event
  124. view.selectEventInstance(eventInstance);
  125. }
  126. isDragging = true;
  127. // ensure a mouseout on the manipulated event has been reported
  128. _this.eventPointing.handleMouseout(seg, ev);
  129. _this.segDragStart(seg, ev);
  130. view.hideEventsWithId(eventDef.id); // hide all event segments. our mouseFollower will take over
  131. },
  132. hitOver: function(hit, isOrig, origHit) {
  133. var isAllowed = true;
  134. var origFootprint;
  135. var footprint;
  136. var mutatedEventInstanceGroup;
  137. // starting hit could be forced (DayGrid.limit)
  138. if (seg.hit) {
  139. origHit = seg.hit;
  140. }
  141. // hit might not belong to this grid, so query origin grid
  142. origFootprint = origHit.component.getSafeHitFootprint(origHit);
  143. footprint = hit.component.getSafeHitFootprint(hit);
  144. if (origFootprint && footprint) {
  145. eventDefMutation = _this.computeEventDropMutation(origFootprint, footprint, eventDef);
  146. if (eventDefMutation) {
  147. mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
  148. eventDef.id,
  149. eventDefMutation
  150. );
  151. isAllowed = component.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
  152. }
  153. else {
  154. isAllowed = false;
  155. }
  156. }
  157. else {
  158. isAllowed = false;
  159. }
  160. if (!isAllowed) {
  161. eventDefMutation = null;
  162. disableCursor();
  163. }
  164. // if a valid drop location, have the subclass render a visual indication
  165. if (
  166. eventDefMutation &&
  167. view.renderDrag( // truthy if rendered something
  168. component.eventRangesToEventFootprints(
  169. mutatedEventInstanceGroup.sliceRenderRanges(component.get('dateProfile').renderUnzonedRange, calendar)
  170. ),
  171. seg,
  172. dragListener.isTouch
  173. )
  174. ) {
  175. mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
  176. }
  177. else {
  178. mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
  179. }
  180. if (isOrig) {
  181. // needs to have moved hits to be a valid drop
  182. eventDefMutation = null;
  183. }
  184. },
  185. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  186. view.unrenderDrag(); // unrender whatever was done in renderDrag
  187. mouseFollower.show(); // show in case we are moving out of all hits
  188. eventDefMutation = null;
  189. },
  190. hitDone: function() { // Called after a hitOut OR before a dragEnd
  191. enableCursor();
  192. },
  193. interactionEnd: function(ev) {
  194. delete seg.component; // prevent side effects
  195. // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
  196. mouseFollower.stop(!eventDefMutation, function() {
  197. if (isDragging) {
  198. view.unrenderDrag();
  199. _this.segDragStop(seg, ev);
  200. }
  201. if (eventDefMutation) {
  202. // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
  203. view.reportEventDrop(eventInstance, eventDefMutation, el, ev);
  204. }
  205. else {
  206. view.showEventsWithId(eventDef.id);
  207. }
  208. });
  209. _this.dragListener = null;
  210. }
  211. });
  212. return dragListener;
  213. },
  214. // Called before event segment dragging starts
  215. segDragStart: function(seg, ev) {
  216. this.isDragging = true;
  217. this.component.publiclyTrigger('eventDragStart', {
  218. context: seg.el[0],
  219. args: [
  220. seg.footprint.getEventLegacy(),
  221. ev,
  222. {}, // jqui dummy
  223. this.view
  224. ]
  225. });
  226. },
  227. // Called after event segment dragging stops
  228. segDragStop: function(seg, ev) {
  229. this.isDragging = false;
  230. this.component.publiclyTrigger('eventDragStop', {
  231. context: seg.el[0],
  232. args: [
  233. seg.footprint.getEventLegacy(),
  234. ev,
  235. {}, // jqui dummy
  236. this.view
  237. ]
  238. });
  239. },
  240. // DOES NOT consider overlap/constraint
  241. computeEventDropMutation: function(startFootprint, endFootprint, eventDef) {
  242. var eventDefMutation = new EventDefMutation();
  243. eventDefMutation.setDateMutation(
  244. this.computeEventDateMutation(startFootprint, endFootprint)
  245. );
  246. return eventDefMutation;
  247. },
  248. computeEventDateMutation: function(startFootprint, endFootprint) {
  249. var date0 = startFootprint.unzonedRange.getStart();
  250. var date1 = endFootprint.unzonedRange.getStart();
  251. var clearEnd = false;
  252. var forceTimed = false;
  253. var forceAllDay = false;
  254. var dateDelta;
  255. var dateMutation;
  256. if (startFootprint.isAllDay !== endFootprint.isAllDay) {
  257. clearEnd = true;
  258. if (endFootprint.isAllDay) {
  259. forceAllDay = true;
  260. date0.stripTime();
  261. }
  262. else {
  263. forceTimed = true;
  264. }
  265. }
  266. dateDelta = this.component.diffDates(date1, date0);
  267. dateMutation = new EventDefDateMutation();
  268. dateMutation.clearEnd = clearEnd;
  269. dateMutation.forceTimed = forceTimed;
  270. dateMutation.forceAllDay = forceAllDay;
  271. dateMutation.setDateDelta(dateDelta);
  272. return dateMutation;
  273. }
  274. });