Grid.events.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210
  1. /* Event-rendering and event-interaction methods for the abstract Grid class
  2. ----------------------------------------------------------------------------------------------------------------------
  3. */
  4. Grid.mixin({
  5. // self-config, overridable by subclasses
  6. segSelector: '.fc-event-container > *', // what constitutes an event element?
  7. mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
  8. isDraggingSeg: false, // is a segment being dragged? boolean
  9. isResizingSeg: false, // is a segment being resized? boolean
  10. isDraggingExternal: false, // jqui-dragging an external element? boolean
  11. segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
  12. renderEventsPayload: function(eventsPayload) {
  13. var unzonedRange = new UnzonedRange(this.view.activeRange.start, this.view.activeRange.end);
  14. var id, eventInstanceGroup;
  15. var eventRenderRanges;
  16. var eventFootprints;
  17. var eventSegs;
  18. var bgSegs = [];
  19. var fgSegs = [];
  20. for (id in eventsPayload) {
  21. eventInstanceGroup = eventsPayload[id];
  22. eventRenderRanges = eventInstanceGroup.sliceRenderRanges(unzonedRange);
  23. eventFootprints = this.eventRangesToEventFootprints(eventRenderRanges);
  24. eventSegs = this.eventFootprintsToSegs(eventFootprints);
  25. if (eventInstanceGroup.getEventDef().hasBgRendering()) {
  26. bgSegs.push.apply(bgSegs, // append
  27. eventSegs
  28. );
  29. }
  30. else {
  31. fgSegs.push.apply(fgSegs, // append
  32. eventSegs
  33. );
  34. }
  35. }
  36. this.segs = [].concat( // record all segs
  37. this.renderBgSegs(bgSegs) || bgSegs,
  38. this.renderFgSegs(fgSegs) || fgSegs
  39. );
  40. },
  41. // Unrenders all events currently rendered on the grid
  42. unrenderEvents: function() {
  43. this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
  44. this.clearDragListeners();
  45. this.unrenderFgSegs();
  46. this.unrenderBgSegs();
  47. this.segs = null;
  48. },
  49. // Retrieves all rendered segment objects currently rendered on the grid
  50. getEventSegs: function() {
  51. return this.segs || [];
  52. },
  53. // Background Segment Rendering
  54. // ---------------------------------------------------------------------------------------------------------------
  55. // TODO: move this to ChronoComponent, but without fill
  56. // Renders the given background event segments onto the grid.
  57. // Returns a subset of the segs that were actually rendered.
  58. renderBgSegs: function(segs) {
  59. return this.renderFill('bgEvent', segs);
  60. },
  61. // Unrenders all the currently rendered background event segments
  62. unrenderBgSegs: function() {
  63. this.unrenderFill('bgEvent');
  64. },
  65. // Renders a background event element, given the default rendering. Called by the fill system.
  66. bgEventSegEl: function(seg, el) {
  67. return this.resolveEventEl(seg.event, el); // will filter through eventRender
  68. },
  69. // Generates an array of classNames to be used for the default rendering of a background event.
  70. // Called by fillSegHtml.
  71. bgEventSegClasses: function(seg) {
  72. var event = seg.event;
  73. var source = event.source || {};
  74. return [ 'fc-bgevent' ].concat(
  75. event.className,
  76. source.className || []
  77. );
  78. },
  79. // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
  80. // Called by fillSegHtml.
  81. bgEventSegCss: function(seg) {
  82. return {
  83. 'background-color': this.getSegSkinCss(seg)['background-color']
  84. };
  85. },
  86. // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
  87. // Called by fillSegHtml.
  88. businessHoursSegClasses: function(seg) {
  89. return [ 'fc-nonbusiness', 'fc-bgevent' ];
  90. },
  91. /* Business Hours
  92. ------------------------------------------------------------------------------------------------------------------*/
  93. // Compute business hour segs for the grid's current date range.
  94. // Caller must ask if whole-day business hours are needed.
  95. buildBusinessHourSegs: function(wholeDay) {
  96. return this.eventFootprintsToSegs(
  97. this.buildBusinessHourEventFootprints(wholeDay)
  98. );
  99. },
  100. // Compute business hour *events* for the grid's current date range.
  101. // Caller must ask if whole-day business hours are needed.
  102. // FOR RENDERING
  103. buildBusinessHourEventFootprints: function(wholeDay) {
  104. var calendar = this.view.calendar;
  105. return this._buildBusinessHourEventFootprints(wholeDay, calendar.opt('businessHours'));
  106. },
  107. _buildBusinessHourEventFootprints: function(wholeDay, businessHourDef) {
  108. var calendar = this.view.calendar;
  109. var eventInstanceGroup;
  110. var eventRanges;
  111. eventInstanceGroup = calendar.buildBusinessInstanceGroup(
  112. wholeDay,
  113. businessHourDef,
  114. this.start,
  115. this.end
  116. );
  117. if (eventInstanceGroup) {
  118. eventRanges = eventInstanceGroup.sliceRenderRanges(
  119. new UnzonedRange(this.start, this.end),
  120. calendar
  121. );
  122. }
  123. else {
  124. eventRanges = [];
  125. }
  126. return this.eventRangesToEventFootprints(eventRanges);
  127. },
  128. /* Handlers
  129. ------------------------------------------------------------------------------------------------------------------*/
  130. // Attaches event-element-related handlers for *all* rendered event segments of the view.
  131. bindSegHandlers: function() {
  132. this.bindSegHandlersToEl(this.el);
  133. },
  134. // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
  135. bindSegHandlersToEl: function(el) {
  136. this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
  137. this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
  138. this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
  139. this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
  140. this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
  141. },
  142. // Executes a handler for any a user-interaction on a segment.
  143. // Handler gets called with (seg, ev), and with the `this` context of the Grid
  144. bindSegHandlerToEl: function(el, name, handler) {
  145. var _this = this;
  146. el.on(name, this.segSelector, function(ev) {
  147. var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEventsPayload
  148. // only call the handlers if there is not a drag/resize in progress
  149. if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
  150. return handler.call(_this, seg, ev); // context will be the Grid
  151. }
  152. });
  153. },
  154. handleSegClick: function(seg, ev) {
  155. var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
  156. if (res === false) {
  157. ev.preventDefault();
  158. }
  159. },
  160. // Updates internal state and triggers handlers for when an event element is moused over
  161. handleSegMouseover: function(seg, ev) {
  162. if (
  163. !GlobalEmitter.get().shouldIgnoreMouse() &&
  164. !this.mousedOverSeg
  165. ) {
  166. this.mousedOverSeg = seg;
  167. if (this.view.isEventResizable(seg.event)) {
  168. seg.el.addClass('fc-allow-mouse-resize');
  169. }
  170. this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev);
  171. }
  172. },
  173. // Updates internal state and triggers handlers for when an event element is moused out.
  174. // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
  175. handleSegMouseout: function(seg, ev) {
  176. ev = ev || {}; // if given no args, make a mock mouse event
  177. if (this.mousedOverSeg) {
  178. seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
  179. this.mousedOverSeg = null;
  180. if (this.view.isEventResizable(seg.event)) {
  181. seg.el.removeClass('fc-allow-mouse-resize');
  182. }
  183. this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev);
  184. }
  185. },
  186. handleSegMousedown: function(seg, ev) {
  187. var isResizing = this.startSegResize(seg, ev, { distance: 5 });
  188. if (!isResizing && this.view.isEventDraggable(seg.event)) {
  189. this.buildSegDragListener(seg)
  190. .startInteraction(ev, {
  191. distance: 5
  192. });
  193. }
  194. },
  195. handleSegTouchStart: function(seg, ev) {
  196. var view = this.view;
  197. var event = seg.event;
  198. var isSelected = view.isEventSelected(event);
  199. var isDraggable = view.isEventDraggable(event);
  200. var isResizable = view.isEventResizable(event);
  201. var isResizing = false;
  202. var dragListener;
  203. var eventLongPressDelay;
  204. if (isSelected && isResizable) {
  205. // only allow resizing of the event is selected
  206. isResizing = this.startSegResize(seg, ev);
  207. }
  208. if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
  209. eventLongPressDelay = this.opt('eventLongPressDelay');
  210. if (eventLongPressDelay == null) {
  211. eventLongPressDelay = this.opt('longPressDelay'); // fallback
  212. }
  213. dragListener = isDraggable ?
  214. this.buildSegDragListener(seg) :
  215. this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
  216. dragListener.startInteraction(ev, { // won't start if already started
  217. delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
  218. });
  219. }
  220. },
  221. // returns boolean whether resizing actually started or not.
  222. // assumes the seg allows resizing.
  223. // `dragOptions` are optional.
  224. startSegResize: function(seg, ev, dragOptions) {
  225. if ($(ev.target).is('.fc-resizer')) {
  226. this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
  227. .startInteraction(ev, dragOptions);
  228. return true;
  229. }
  230. return false;
  231. },
  232. /* Event Dragging
  233. ------------------------------------------------------------------------------------------------------------------*/
  234. // Builds a listener that will track user-dragging on an event segment.
  235. // Generic enough to work with any type of Grid.
  236. // Has side effect of setting/unsetting `segDragListener`
  237. buildSegDragListener: function(seg) {
  238. var _this = this;
  239. var view = this.view;
  240. var calendar = view.calendar;
  241. var eventManager = calendar.eventManager;
  242. var el = seg.el;
  243. var event = seg.event; // is a legacy event
  244. var isDragging;
  245. var mouseFollower; // A clone of the original element that will move with the mouse
  246. var eventDefMutation;
  247. if (this.segDragListener) {
  248. return this.segDragListener;
  249. }
  250. // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
  251. // of the view.
  252. var dragListener = this.segDragListener = new HitDragListener(view, {
  253. scroll: this.opt('dragScroll'),
  254. subjectEl: el,
  255. subjectCenter: true,
  256. interactionStart: function(ev) {
  257. seg.component = _this; // for renderDrag
  258. isDragging = false;
  259. mouseFollower = new MouseFollower(seg.el, {
  260. additionalClass: 'fc-dragging',
  261. parentEl: view.el,
  262. opacity: dragListener.isTouch ? null : _this.opt('dragOpacity'),
  263. revertDuration: _this.opt('dragRevertDuration'),
  264. zIndex: 2 // one above the .fc-view
  265. });
  266. mouseFollower.hide(); // don't show until we know this is a real drag
  267. mouseFollower.start(ev);
  268. },
  269. dragStart: function(ev) {
  270. if (dragListener.isTouch && !view.isEventSelected(event)) {
  271. // if not previously selected, will fire after a delay. then, select the event
  272. view.selectEvent(event);
  273. }
  274. isDragging = true;
  275. _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  276. _this.segDragStart(seg, ev);
  277. view.hideEvent(event); // hide all event segments. our mouseFollower will take over
  278. },
  279. hitOver: function(hit, isOrig, origHit) {
  280. var isAllowed = true;
  281. var origFootprint;
  282. var footprint;
  283. var mutatedEventInstanceGroup;
  284. var dragHelperEls;
  285. // starting hit could be forced (DayGrid.limit)
  286. if (seg.hit) {
  287. origHit = seg.hit;
  288. }
  289. // hit might not belong to this grid, so query origin grid
  290. origFootprint = origHit.component.getSafeHitFootprint(origHit);
  291. footprint = hit.component.getSafeHitFootprint(hit);
  292. if (origFootprint && footprint) {
  293. eventDefMutation = _this.computeEventDropMutation(origFootprint, footprint);
  294. if (eventDefMutation) {
  295. mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
  296. eventManager.getEventDefByUid(event._id).id,
  297. eventDefMutation
  298. );
  299. isAllowed = _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
  300. }
  301. else {
  302. isAllowed = false;
  303. }
  304. }
  305. else {
  306. isAllowed = false;
  307. }
  308. if (!isAllowed) {
  309. eventDefMutation = null;
  310. disableCursor();
  311. }
  312. // if a valid drop location, have the subclass render a visual indication
  313. if (
  314. eventDefMutation &&
  315. (dragHelperEls = view.renderDrag(
  316. _this.eventRangesToEventFootprints(
  317. mutatedEventInstanceGroup.sliceRenderRanges(
  318. new UnzonedRange(_this.start, _this.end),
  319. calendar
  320. )
  321. ),
  322. seg
  323. ))
  324. ) {
  325. dragHelperEls.addClass('fc-dragging');
  326. if (!dragListener.isTouch) {
  327. _this.applyDragOpacity(dragHelperEls);
  328. }
  329. mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
  330. }
  331. else {
  332. mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
  333. }
  334. if (isOrig) {
  335. // needs to have moved hits to be a valid drop
  336. eventDefMutation = null;
  337. }
  338. },
  339. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  340. view.unrenderDrag(); // unrender whatever was done in renderDrag
  341. mouseFollower.show(); // show in case we are moving out of all hits
  342. eventDefMutation = null;
  343. },
  344. hitDone: function() { // Called after a hitOut OR before a dragEnd
  345. enableCursor();
  346. },
  347. interactionEnd: function(ev) {
  348. delete seg.component; // prevent side effects
  349. // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
  350. mouseFollower.stop(!eventDefMutation, function() {
  351. if (isDragging) {
  352. view.unrenderDrag();
  353. _this.segDragStop(seg, ev);
  354. }
  355. if (eventDefMutation) {
  356. // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
  357. view.reportEventDrop(event, eventDefMutation, el, ev);
  358. }
  359. else {
  360. view.showEvent(event);
  361. }
  362. });
  363. _this.segDragListener = null;
  364. }
  365. });
  366. return dragListener;
  367. },
  368. // seg isn't draggable, but let's use a generic DragListener
  369. // simply for the delay, so it can be selected.
  370. // Has side effect of setting/unsetting `segDragListener`
  371. buildSegSelectListener: function(seg) {
  372. var _this = this;
  373. var view = this.view;
  374. var event = seg.event;
  375. if (this.segDragListener) {
  376. return this.segDragListener;
  377. }
  378. var dragListener = this.segDragListener = new DragListener({
  379. dragStart: function(ev) {
  380. if (dragListener.isTouch && !view.isEventSelected(event)) {
  381. // if not previously selected, will fire after a delay. then, select the event
  382. view.selectEvent(event);
  383. }
  384. },
  385. interactionEnd: function(ev) {
  386. _this.segDragListener = null;
  387. }
  388. });
  389. return dragListener;
  390. },
  391. // Called before event segment dragging starts
  392. segDragStart: function(seg, ev) {
  393. this.isDraggingSeg = true;
  394. this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  395. },
  396. // Called after event segment dragging stops
  397. segDragStop: function(seg, ev) {
  398. this.isDraggingSeg = false;
  399. this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  400. },
  401. // DOES NOT consider overlap/constraint
  402. computeEventDropMutation: function(startFootprint, endFootprint) {
  403. var date0 = startFootprint.unzonedRange.getStart();
  404. var date1 = endFootprint.unzonedRange.getStart();
  405. var clearEnd = false;
  406. var forceTimed = false;
  407. var forceAllDay = false;
  408. var dateDelta;
  409. var dateMutation;
  410. var eventDefMutation;
  411. if (startFootprint.isAllDay !== endFootprint.isAllDay) {
  412. clearEnd = true;
  413. if (endFootprint.isAllDay) {
  414. forceAllDay = true;
  415. date0.stripTime();
  416. }
  417. else {
  418. forceTimed = true;
  419. }
  420. }
  421. dateDelta = this.diffDates(date1, date0);
  422. dateMutation = new EventDefDateMutation();
  423. dateMutation.clearEnd = clearEnd;
  424. dateMutation.forceTimed = forceTimed;
  425. dateMutation.forceAllDay = forceAllDay;
  426. dateMutation.setDateDelta(dateDelta);
  427. eventDefMutation = new EventDefMutation();
  428. eventDefMutation.setDateMutation(dateMutation);
  429. return eventDefMutation;
  430. },
  431. // Utility for apply dragOpacity to a jQuery set
  432. applyDragOpacity: function(els) {
  433. var opacity = this.opt('dragOpacity');
  434. if (opacity != null) {
  435. els.css('opacity', opacity);
  436. }
  437. },
  438. /* External Element Dragging
  439. ------------------------------------------------------------------------------------------------------------------*/
  440. // Called when a jQuery UI drag is initiated anywhere in the DOM
  441. externalDragStart: function(ev, ui) {
  442. var el;
  443. var accept;
  444. if (this.opt('droppable')) { // only listen if this setting is on
  445. el = $((ui ? ui.item : null) || ev.target);
  446. // Test that the dragged element passes the dropAccept selector or filter function.
  447. // FYI, the default is "*" (matches all)
  448. accept = this.opt('dropAccept');
  449. if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
  450. if (!this.isDraggingExternal) { // prevent double-listening if fired twice
  451. this.listenToExternalDrag(el, ev, ui);
  452. }
  453. }
  454. }
  455. },
  456. // Called when a jQuery UI drag starts and it needs to be monitored for dropping
  457. listenToExternalDrag: function(el, ev, ui) {
  458. var _this = this;
  459. var view = this.view;
  460. var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
  461. var singleEventDef; // a null value signals an unsuccessful drag
  462. // listener that tracks mouse movement over date-associated pixel regions
  463. var dragListener = _this.externalDragListener = new HitDragListener(this, {
  464. interactionStart: function() {
  465. _this.isDraggingExternal = true;
  466. },
  467. hitOver: function(hit) {
  468. var isAllowed = true;
  469. var hitFootprint = hit.component.getSafeHitFootprint(hit); // hit might not belong to this grid
  470. var mutatedEventInstanceGroup;
  471. if (hitFootprint) {
  472. singleEventDef = _this.computeExternalDrop(hitFootprint, meta);
  473. if (singleEventDef) {
  474. mutatedEventInstanceGroup = new EventInstanceGroup(
  475. singleEventDef.buildInstances()
  476. );
  477. isAllowed = meta.eventProps ? // isEvent?
  478. _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup) :
  479. _this.isExternalInstanceGroupAllowed(mutatedEventInstanceGroup);
  480. }
  481. else {
  482. isAllowed = false;
  483. }
  484. }
  485. else {
  486. isAllowed = false;
  487. }
  488. if (!isAllowed) {
  489. singleEventDef = null;
  490. disableCursor();
  491. }
  492. if (singleEventDef) {
  493. _this.renderDrag( // called without a seg parameter
  494. _this.eventRangesToEventFootprints(
  495. mutatedEventInstanceGroup.sliceRenderRanges(
  496. new UnzonedRange(_this.start, _this.end),
  497. view.calendar
  498. )
  499. )
  500. );
  501. }
  502. },
  503. hitOut: function() {
  504. singleEventDef = null; // signal unsuccessful
  505. },
  506. hitDone: function() { // Called after a hitOut OR before a dragEnd
  507. enableCursor();
  508. _this.unrenderDrag();
  509. },
  510. interactionEnd: function(ev) {
  511. if (singleEventDef) { // element was dropped on a valid hit
  512. view.reportExternalDrop(
  513. singleEventDef,
  514. Boolean(meta.eventProps), // isEvent
  515. Boolean(meta.stick), // isSticky
  516. el, ev, ui
  517. );
  518. }
  519. _this.isDraggingExternal = false;
  520. _this.externalDragListener = null;
  521. }
  522. });
  523. dragListener.startDrag(ev); // start listening immediately
  524. },
  525. // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
  526. // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
  527. // Returning a null value signals an invalid drop hit.
  528. // DOES NOT consider overlap/constraint.
  529. computeExternalDrop: function(componentFootprint, meta) {
  530. var calendar = this.view.calendar;
  531. var start = FC.moment.utc(componentFootprint.unzonedRange.startMs).stripZone();
  532. var end;
  533. var eventDef;
  534. if (componentFootprint.isAllDay) {
  535. // if dropped on an all-day span, and element's metadata specified a time, set it
  536. if (meta.startTime) {
  537. start.time(meta.startTime);
  538. }
  539. else {
  540. start.stripTime();
  541. }
  542. }
  543. if (meta.duration) {
  544. end = start.clone().add(meta.duration);
  545. }
  546. start = calendar.applyTimezone(start);
  547. if (end) {
  548. end = calendar.applyTimezone(end);
  549. }
  550. eventDef = SingleEventDef.parse(
  551. $.extend({}, meta.eventProps, {
  552. start: start,
  553. end: end
  554. }),
  555. new EventSource(calendar)
  556. );
  557. return eventDef;
  558. },
  559. /* Resizing
  560. ------------------------------------------------------------------------------------------------------------------*/
  561. // Creates a listener that tracks the user as they resize an event segment.
  562. // Generic enough to work with any type of Grid.
  563. buildSegResizeListener: function(seg, isStart) {
  564. var _this = this;
  565. var view = this.view;
  566. var calendar = view.calendar;
  567. var eventManager = calendar.eventManager;
  568. var el = seg.el;
  569. var event = seg.event; // legacy event
  570. var isDragging;
  571. var resizeMutation; // zoned event date properties. falsy if invalid resize
  572. // Tracks mouse movement over the *grid's* coordinate map
  573. var dragListener = this.segResizeListener = new HitDragListener(this, {
  574. scroll: this.opt('dragScroll'),
  575. subjectEl: el,
  576. interactionStart: function() {
  577. isDragging = false;
  578. },
  579. dragStart: function(ev) {
  580. isDragging = true;
  581. _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  582. _this.segResizeStart(seg, ev);
  583. },
  584. hitOver: function(hit, isOrig, origHit) {
  585. var isAllowed = true;
  586. var origHitFootprint = _this.getSafeHitFootprint(origHit);
  587. var hitFootprint = _this.getSafeHitFootprint(hit);
  588. var mutatedEventInstanceGroup;
  589. if (origHitFootprint && hitFootprint) {
  590. resizeMutation = isStart ?
  591. _this.computeEventStartResizeMutation(origHitFootprint, hitFootprint, event) :
  592. _this.computeEventEndResizeMutation(origHitFootprint, hitFootprint, event);
  593. if (resizeMutation) {
  594. mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
  595. eventManager.getEventDefByUid(event._id).id,
  596. resizeMutation
  597. );
  598. isAllowed = _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
  599. }
  600. else {
  601. isAllowed = false;
  602. }
  603. }
  604. else {
  605. isAllowed = false;
  606. }
  607. if (!isAllowed) {
  608. resizeMutation = null;
  609. disableCursor();
  610. }
  611. else if (resizeMutation.isEmpty()) {
  612. // no change. (FYI, event dates might have zones)
  613. resizeMutation = null;
  614. }
  615. if (resizeMutation) {
  616. view.hideEvent(event);
  617. _this.renderEventResize(
  618. _this.eventRangesToEventFootprints(
  619. mutatedEventInstanceGroup.sliceRenderRanges(
  620. new UnzonedRange(_this.start, _this.end),
  621. calendar
  622. )
  623. ),
  624. seg
  625. );
  626. }
  627. },
  628. hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
  629. resizeMutation = null;
  630. view.showEvent(event); // for when out-of-bounds. show original
  631. },
  632. hitDone: function() { // resets the rendering to show the original event
  633. _this.unrenderEventResize();
  634. enableCursor();
  635. },
  636. interactionEnd: function(ev) {
  637. if (isDragging) {
  638. _this.segResizeStop(seg, ev);
  639. }
  640. if (resizeMutation) { // valid date to resize to?
  641. // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
  642. view.reportEventResize(event, resizeMutation, el, ev);
  643. }
  644. else {
  645. view.showEvent(event);
  646. }
  647. _this.segResizeListener = null;
  648. }
  649. });
  650. return dragListener;
  651. },
  652. // Called before event segment resizing starts
  653. segResizeStart: function(seg, ev) {
  654. this.isResizingSeg = true;
  655. this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  656. },
  657. // Called after event segment resizing stops
  658. segResizeStop: function(seg, ev) {
  659. this.isResizingSeg = false;
  660. this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  661. },
  662. // Returns new date-information for an event segment being resized from its start
  663. computeEventStartResizeMutation: function(startFootprint, endFootprint, event) {
  664. var startDelta = this.diffDates(
  665. endFootprint.unzonedRange.getStart(),
  666. startFootprint.unzonedRange.getStart()
  667. );
  668. var eventEnd = this.view.calendar.getEventEnd(event);
  669. var dateMutation;
  670. var eventDefMutation;
  671. if (event.start.clone().add(startDelta) < eventEnd) {
  672. dateMutation = new EventDefDateMutation();
  673. dateMutation.setStartDelta(startDelta);
  674. eventDefMutation = new EventDefMutation();
  675. eventDefMutation.setDateMutation(dateMutation);
  676. return eventDefMutation;
  677. }
  678. return false;
  679. },
  680. // Returns new date-information for an event segment being resized from its end
  681. computeEventEndResizeMutation: function(startFootprint, endFootprint, event) {
  682. var endDelta = this.diffDates(
  683. endFootprint.unzonedRange.getEnd(),
  684. startFootprint.unzonedRange.getEnd()
  685. );
  686. var eventEnd = this.view.calendar.getEventEnd(event);
  687. var dateMutation;
  688. var eventDefMutation;
  689. if (eventEnd.add(endDelta) > event.start) {
  690. dateMutation = new EventDefDateMutation();
  691. dateMutation.setEndDelta(endDelta);
  692. eventDefMutation = new EventDefMutation();
  693. eventDefMutation.setDateMutation(dateMutation);
  694. return eventDefMutation;
  695. }
  696. return false;
  697. },
  698. // Renders a visual indication of an event being resized.
  699. // Must return elements used for any mock events.
  700. renderEventResize: function(eventFootprints, seg) {
  701. // subclasses must implement
  702. },
  703. // Unrenders a visual indication of an event being resized.
  704. unrenderEventResize: function() {
  705. // subclasses must implement
  706. },
  707. /* Rendering Utils
  708. ------------------------------------------------------------------------------------------------------------------*/
  709. // Compute the text that should be displayed on an event's element.
  710. // `range` can be the Event object itself, or something range-like, with at least a `start`.
  711. // If event times are disabled, or the event has no time, will return a blank string.
  712. // If not specified, formatStr will default to the eventTimeFormat setting,
  713. // and displayEnd will default to the displayEventEnd setting.
  714. getEventTimeText: function(range, formatStr, displayEnd) {
  715. if (formatStr == null) {
  716. formatStr = this.eventTimeFormat;
  717. }
  718. if (displayEnd == null) {
  719. displayEnd = this.displayEventEnd;
  720. }
  721. if (this.displayEventTime && range.start.hasTime()) {
  722. if (displayEnd && range.end) {
  723. return this.view.formatRange(range, formatStr);
  724. }
  725. else {
  726. return range.start.format(formatStr);
  727. }
  728. }
  729. return '';
  730. },
  731. // Generic utility for generating the HTML classNames for an event segment's element
  732. getSegClasses: function(seg, isDraggable, isResizable) {
  733. var view = this.view;
  734. var classes = [
  735. 'fc-event',
  736. seg.isStart ? 'fc-start' : 'fc-not-start',
  737. seg.isEnd ? 'fc-end' : 'fc-not-end'
  738. ].concat(this.getSegCustomClasses(seg));
  739. if (isDraggable) {
  740. classes.push('fc-draggable');
  741. }
  742. if (isResizable) {
  743. classes.push('fc-resizable');
  744. }
  745. // event is currently selected? attach a className.
  746. if (view.isEventSelected(seg.event)) {
  747. classes.push('fc-selected');
  748. }
  749. return classes;
  750. },
  751. // List of classes that were defined by the caller of the API in some way
  752. getSegCustomClasses: function(seg) {
  753. var event = seg.event;
  754. return [].concat(
  755. event.className, // guaranteed to be an array
  756. event.source ? event.source.className : []
  757. );
  758. },
  759. // Utility for generating event skin-related CSS properties
  760. getSegSkinCss: function(seg) {
  761. return {
  762. 'background-color': this.getSegBackgroundColor(seg),
  763. 'border-color': this.getSegBorderColor(seg),
  764. color: this.getSegTextColor(seg)
  765. };
  766. },
  767. // Queries for caller-specified color, then falls back to default
  768. getSegBackgroundColor: function(seg) {
  769. return seg.event.backgroundColor ||
  770. seg.event.color ||
  771. this.getSegDefaultBackgroundColor(seg);
  772. },
  773. getSegDefaultBackgroundColor: function(seg) {
  774. var source = seg.event.source || {};
  775. return source.backgroundColor ||
  776. source.color ||
  777. this.opt('eventBackgroundColor') ||
  778. this.opt('eventColor');
  779. },
  780. // Queries for caller-specified color, then falls back to default
  781. getSegBorderColor: function(seg) {
  782. return seg.event.borderColor ||
  783. seg.event.color ||
  784. this.getSegDefaultBorderColor(seg);
  785. },
  786. getSegDefaultBorderColor: function(seg) {
  787. var source = seg.event.source || {};
  788. return source.borderColor ||
  789. source.color ||
  790. this.opt('eventBorderColor') ||
  791. this.opt('eventColor');
  792. },
  793. // Queries for caller-specified color, then falls back to default
  794. getSegTextColor: function(seg) {
  795. return seg.event.textColor ||
  796. this.getSegDefaultTextColor(seg);
  797. },
  798. getSegDefaultTextColor: function(seg) {
  799. var source = seg.event.source || {};
  800. return source.textColor ||
  801. this.opt('eventTextColor');
  802. },
  803. /* Event Location Validation
  804. ------------------------------------------------------------------------------------------------------------------*/
  805. isEventInstanceGroupAllowed: function(eventInstanceGroup) {
  806. var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
  807. var i;
  808. for (i = 0; i < eventFootprints.length; i++) {
  809. if (
  810. !isRangeWithinRange(
  811. eventFootprints[i].componentFootprint.unzonedRange.getRange(),
  812. this.view.validRange
  813. )
  814. ) {
  815. return false;
  816. }
  817. }
  818. return this.view.calendar.isEventInstanceGroupAllowed(eventInstanceGroup);
  819. },
  820. // when it's a completely anonymous external drag, no event.
  821. isExternalInstanceGroupAllowed: function(eventInstanceGroup) {
  822. var calendar = this.view.calendar;
  823. var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
  824. var i;
  825. for (i = 0; i < eventFootprints.length; i++) {
  826. if (
  827. !isRangeWithinRange(
  828. eventFootprints[i].componentFootprint.unzonedRange.getRange(),
  829. this.view.validRange
  830. )
  831. ) {
  832. return false;
  833. }
  834. }
  835. for (i = 0; i < eventFootprints.length; i++) {
  836. // treat it as a selection
  837. if (!calendar.isSelectionFootprintAllowed(eventFootprints[i].componentFootprint)) {
  838. return false;
  839. }
  840. }
  841. return true;
  842. },
  843. /* Converting eventRange -> eventFootprint -> eventSegs
  844. ------------------------------------------------------------------------------------------------------------------*/
  845. eventRangesToEventFootprints: function(eventRanges) {
  846. var eventFootprints = [];
  847. var i;
  848. for (i = 0; i < eventRanges.length; i++) {
  849. eventFootprints.push.apply(eventFootprints,
  850. this.eventRangeToEventFootprints(eventRanges[i])
  851. );
  852. }
  853. return eventFootprints;
  854. },
  855. // Given an event's unzoned date range, return an array of eventSpan objects.
  856. // eventSpan - { start, end, isStart, isEnd, otherthings... }
  857. // Subclasses can override.
  858. // Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans.
  859. eventRangeToEventFootprints: function(eventRange) {
  860. return [
  861. new EventFootprint(
  862. new ComponentFootprint(
  863. eventRange.unzonedRange,
  864. eventRange.eventDef.isAllDay()
  865. ),
  866. eventRange.eventDef,
  867. eventRange.eventInstance // might not exist
  868. )
  869. ];
  870. },
  871. eventFootprintsToSegs: function(eventFootprints) {
  872. var segs = [];
  873. var i;
  874. for (i = 0; i < eventFootprints.length; i++) {
  875. segs.push.apply(segs,
  876. this.eventFootprintToSegs(eventFootprints[i])
  877. );
  878. }
  879. return segs;
  880. },
  881. // Given an event's span (unzoned start/end and other misc data), and the event itself,
  882. // slices into segments and attaches event-derived properties to them.
  883. // eventSpan - { start, end, isStart, isEnd, otherthings... }
  884. // constraintRange allow additional clipping. optional. eventually remove this.
  885. eventFootprintToSegs: function(eventFootprint, constraintRange) {
  886. var unzonedRange = eventFootprint.componentFootprint.unzonedRange;
  887. var segs;
  888. var i, seg;
  889. if (constraintRange) {
  890. unzonedRange = unzonedRange.constrainTo(constraintRange);
  891. }
  892. segs = this.componentFootprintToSegs(eventFootprint.componentFootprint);
  893. for (i = 0; i < segs.length; i++) {
  894. seg = segs[i];
  895. if (!unzonedRange.isStart) {
  896. seg.isStart = false;
  897. }
  898. if (!unzonedRange.isEnd) {
  899. seg.isEnd = false;
  900. }
  901. seg.event = eventFootprint.getEventLegacy();
  902. seg.footprint = eventFootprint;
  903. seg.footprintStartMs = unzonedRange.startMs;
  904. seg.footprintDurationMs = unzonedRange.endMs - unzonedRange.startMs;
  905. }
  906. return segs;
  907. },
  908. sortEventSegs: function(segs) {
  909. segs.sort(proxy(this, 'compareEventSegs'));
  910. },
  911. // A cmp function for determining which segments should take visual priority
  912. compareEventSegs: function(seg1, seg2) {
  913. return seg1.footprintStartMs - seg2.footprintStartMs || // earlier events go first
  914. seg2.footprintDurationMs - seg1.footprintDurationMs || // tie? longer events go first
  915. seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
  916. compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs);
  917. }
  918. });
  919. /* External-Dragging-Element Data
  920. ----------------------------------------------------------------------------------------------------------------------*/
  921. // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
  922. // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
  923. FC.dataAttrPrefix = '';
  924. // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
  925. // to be used for Event Object creation.
  926. // A defined `.eventProps`, even when empty, indicates that an event should be created.
  927. function getDraggedElMeta(el) {
  928. var prefix = FC.dataAttrPrefix;
  929. var eventProps; // properties for creating the event, not related to date/time
  930. var startTime; // a Duration
  931. var duration;
  932. var stick;
  933. if (prefix) { prefix += '-'; }
  934. eventProps = el.data(prefix + 'event') || null;
  935. if (eventProps) {
  936. if (typeof eventProps === 'object') {
  937. eventProps = $.extend({}, eventProps); // make a copy
  938. }
  939. else { // something like 1 or true. still signal event creation
  940. eventProps = {};
  941. }
  942. // pluck special-cased date/time properties
  943. startTime = eventProps.start;
  944. if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
  945. duration = eventProps.duration;
  946. stick = eventProps.stick;
  947. delete eventProps.start;
  948. delete eventProps.time;
  949. delete eventProps.duration;
  950. delete eventProps.stick;
  951. }
  952. // fallback to standalone attribute values for each of the date/time properties
  953. if (startTime == null) { startTime = el.data(prefix + 'start'); }
  954. if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
  955. if (duration == null) { duration = el.data(prefix + 'duration'); }
  956. if (stick == null) { stick = el.data(prefix + 'stick'); }
  957. // massage into correct data types
  958. startTime = startTime != null ? moment.duration(startTime) : null;
  959. duration = duration != null ? moment.duration(duration) : null;
  960. stick = Boolean(stick);
  961. return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
  962. }