View.js 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  1. /* An abstract class from which other views inherit from
  2. ----------------------------------------------------------------------------------------------------------------------*/
  3. var View = FC.View = InteractiveDateComponent.extend({
  4. type: null, // subclass' view name (string)
  5. name: null, // deprecated. use `type` instead
  6. title: null, // the text that will be displayed in the header's title
  7. calendar: null, // owner Calendar object
  8. viewSpec: null,
  9. options: null, // hash containing all options. already merged with view-specific-options
  10. renderQueue: null,
  11. batchRenderDepth: 0,
  12. isDatesRendered: false,
  13. isEventsRendered: false,
  14. isBaseRendered: false, // related to viewRender/viewDestroy triggers
  15. queuedScroll: null,
  16. isSelected: false, // boolean whether a range of time is user-selected or not
  17. selectedEventInstance: null,
  18. eventOrderSpecs: null, // criteria for ordering events when they have same date/time
  19. // for date utils, computed from options
  20. isHiddenDayHash: null,
  21. // now indicator
  22. isNowIndicatorRendered: null,
  23. initialNowDate: null, // result first getNow call
  24. initialNowQueriedMs: null, // ms time the getNow was called
  25. nowIndicatorTimeoutID: null, // for refresh timing of now indicator
  26. nowIndicatorIntervalID: null, // "
  27. constructor: function(calendar, viewSpec) {
  28. this.calendar = calendar;
  29. this.viewSpec = viewSpec;
  30. // shortcuts
  31. this.type = viewSpec.type;
  32. this.options = viewSpec.options;
  33. // .name is deprecated
  34. this.name = this.type;
  35. InteractiveDateComponent.call(this);
  36. this.initHiddenDays();
  37. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
  38. this.renderQueue = this.buildRenderQueue();
  39. this.initAutoBatchRender();
  40. // legacy
  41. if (this.initialize) {
  42. this.initialize();
  43. }
  44. },
  45. _getView: function() {
  46. return this;
  47. },
  48. buildRenderQueue: function() {
  49. var _this = this;
  50. var renderQueue = new RenderQueue({
  51. event: this.opt('eventRenderWait')
  52. });
  53. renderQueue.on('start', function() {
  54. _this.freezeHeight();
  55. _this.addScroll(_this.queryScroll());
  56. });
  57. renderQueue.on('stop', function() {
  58. _this.thawHeight();
  59. _this.popScroll();
  60. });
  61. return renderQueue;
  62. },
  63. initAutoBatchRender: function() {
  64. var _this = this;
  65. this.on('before:change', function() {
  66. _this.startBatchRender();
  67. });
  68. this.on('change', function() {
  69. _this.stopBatchRender();
  70. });
  71. },
  72. startBatchRender: function() {
  73. if (!(this.batchRenderDepth++)) {
  74. this.renderQueue.pause();
  75. }
  76. },
  77. stopBatchRender: function() {
  78. if (!(--this.batchRenderDepth)) {
  79. this.renderQueue.resume();
  80. }
  81. },
  82. // Retrieves an option with the given name
  83. opt: function(name) {
  84. return this.options[name];
  85. },
  86. /* Title and Date Formatting
  87. ------------------------------------------------------------------------------------------------------------------*/
  88. // Computes what the title at the top of the calendar should be for this view
  89. computeTitle: function(dateProfile) {
  90. var unzonedRange;
  91. // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
  92. if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
  93. unzonedRange = dateProfile.currentUnzonedRange;
  94. }
  95. else { // for day units or smaller, use the actual day range
  96. unzonedRange = dateProfile.activeUnzonedRange;
  97. }
  98. return this.formatRange(
  99. {
  100. start: this.calendar.msToMoment(unzonedRange.startMs, dateProfile.isRangeAllDay),
  101. end: this.calendar.msToMoment(unzonedRange.endMs, dateProfile.isRangeAllDay)
  102. },
  103. dateProfile.isRangeAllDay,
  104. this.opt('titleFormat') || this.computeTitleFormat(),
  105. this.opt('titleRangeSeparator')
  106. );
  107. },
  108. // Generates the format string that should be used to generate the title for the current date range.
  109. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  110. computeTitleFormat: function() {
  111. var currentRangeUnit = this.get('dateProfile').currentRangeUnit;
  112. if (currentRangeUnit == 'year') {
  113. return 'YYYY';
  114. }
  115. else if (currentRangeUnit == 'month') {
  116. return this.opt('monthYearFormat'); // like "September 2014"
  117. }
  118. else if (this.currentRangeAs('days') > 1) {
  119. return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
  120. }
  121. else {
  122. return 'LL'; // one day. longer, like "September 9 2014"
  123. }
  124. },
  125. // Element
  126. // -----------------------------------------------------------------------------------------------------------------
  127. setElement: function(el) {
  128. var _this = this;
  129. InteractiveDateComponent.prototype.setElement.apply(this, arguments);
  130. this.bindBaseRenderHandlers();
  131. // TODO: not best place for this
  132. // TODO: better way of forwarding options from calendar -> view
  133. this.calendar.optionsModel.watch('viewRawBusinessHours', [ 'businessHours' ], function(deps) {
  134. _this.set('rawBusinessHours', deps.businessHours);
  135. }, function() {
  136. _this.unset('rawBusinessHours');
  137. });
  138. },
  139. removeElement: function() {
  140. this.unsetDate();
  141. this.unbindBaseRenderHandlers();
  142. this.calendar.optionsModel.unwatch('viewRawBusinessHours');
  143. InteractiveDateComponent.prototype.removeElement.apply(this, arguments);
  144. },
  145. // Date Setting/Unsetting
  146. // -----------------------------------------------------------------------------------------------------------------
  147. setDate: function(date) {
  148. var currentDateProfile = this.get('dateProfile');
  149. var newDateProfile = this.buildDateProfile(date, null, true); // forceToValid=true
  150. if (
  151. !currentDateProfile ||
  152. !currentDateProfile.activeUnzonedRange.equals(newDateProfile.activeUnzonedRange)
  153. ) {
  154. this.set('dateProfile', newDateProfile);
  155. }
  156. },
  157. unsetDate: function() {
  158. this.unset('dateProfile');
  159. },
  160. handleDateProfileSet: function(dateProfile) {
  161. InteractiveDateComponent.prototype.handleDateProfileSet.apply(this, arguments);
  162. var calendar = this.calendar;
  163. // DEPRECATED, but we need to keep it updated...
  164. this.start = calendar.msToMoment(dateProfile.activeUnzonedRange.startMs, dateProfile.isRangeAllDay);
  165. this.end = calendar.msToMoment(dateProfile.activeUnzonedRange.endMs, dateProfile.isRangeAllDay);
  166. this.intervalStart = calendar.msToMoment(dateProfile.currentUnzonedRange.startMs, dateProfile.isRangeAllDay);
  167. this.intervalEnd = calendar.msToMoment(dateProfile.currentUnzonedRange.endMs, dateProfile.isRangeAllDay);
  168. this.title = this.computeTitle(dateProfile);
  169. calendar.reportViewDatesChanged(this, dateProfile); // TODO: reverse the pubsub
  170. },
  171. // Event Data
  172. // -----------------------------------------------------------------------------------------------------------------
  173. fetchInitialEvents: function(dateProfile) {
  174. var calendar = this.calendar;
  175. var forceAllDay = dateProfile.isRangeAllDay && !this.usesMinMaxTime;
  176. return calendar.requestEvents(
  177. calendar.msToMoment(dateProfile.activeUnzonedRange.startMs, forceAllDay),
  178. calendar.msToMoment(dateProfile.activeUnzonedRange.endMs, forceAllDay)
  179. );
  180. },
  181. bindEventChanges: function() {
  182. // can write this shorter?
  183. this.listenTo(this.calendar, 'eventsReset', this.handleEventsReset);
  184. //this.listenTo(this.calendar, 'eventAdd', this.handleEventAddOrUpdate);
  185. //this.listenTo(this.calendar, 'eventUpdate', this.handleEventAddOrUpdate);
  186. //this.listenTo(this.calendar, 'eventRemove', this.handleEventRemove);
  187. },
  188. unbindEventChanges: function() {
  189. // can write this shorter?
  190. this.stopListeningTo(this.calendar, 'eventsReset');
  191. //this.stopListeningTo(this.calendar, 'eventAdd');
  192. //this.stopListeningTo(this.calendar, 'eventUpdate');
  193. //this.stopListeningTo(this.calendar, 'eventRemove');
  194. },
  195. // Date High-level Rendering
  196. // -----------------------------------------------------------------------------------------------------------------
  197. // if dateProfile not specified, uses current
  198. executeDateRender: function(dateProfile, skipScroll) {
  199. if (this.render) {
  200. this.render(); // TODO: deprecate
  201. }
  202. this.renderDates();
  203. this.updateSize();
  204. this.startNowIndicator();
  205. if (!skipScroll) {
  206. this.addScroll(this.computeInitialDateScroll());
  207. }
  208. this.isDatesRendered = true;
  209. this.trigger('datesRendered');
  210. },
  211. executeDateUnrender: function() {
  212. this.unselect();
  213. this.stopNowIndicator();
  214. this.trigger('before:datesUnrendered');
  215. this.unrenderDates();
  216. if (this.destroy) {
  217. this.destroy(); // TODO: deprecate
  218. }
  219. this.isDatesRendered = false;
  220. },
  221. // Determing when the "meat" of the view is rendered (aka the base)
  222. // -----------------------------------------------------------------------------------------------------------------
  223. bindBaseRenderHandlers: function() {
  224. var _this = this;
  225. this.on('datesRendered.baseHandler', function() {
  226. _this.onBaseRender();
  227. });
  228. this.on('before:datesUnrendered.baseHandler', function() {
  229. _this.onBeforeBaseUnrender();
  230. });
  231. },
  232. unbindBaseRenderHandlers: function() {
  233. this.off('.baseHandler');
  234. },
  235. onBaseRender: function() {
  236. this.applyScreenState();
  237. this.publiclyTrigger('viewRender', {
  238. context: this,
  239. args: [ this, this.el ]
  240. });
  241. },
  242. onBeforeBaseUnrender: function() {
  243. this.applyScreenState();
  244. this.publiclyTrigger('viewDestroy', {
  245. context: this,
  246. args: [ this, this.el ]
  247. });
  248. },
  249. // Misc view rendering utils
  250. // -----------------------------------------------------------------------------------------------------------------
  251. // Binds DOM handlers to elements that reside outside the view container, such as the document
  252. bindGlobalHandlers: function() {
  253. InteractiveDateComponent.prototype.bindGlobalHandlers.apply(this, arguments);
  254. this.listenTo(GlobalEmitter.get(), {
  255. touchstart: this.processUnselect,
  256. mousedown: this.handleDocumentMousedown
  257. });
  258. },
  259. // Unbinds DOM handlers from elements that reside outside the view container
  260. unbindGlobalHandlers: function() {
  261. InteractiveDateComponent.prototype.unbindGlobalHandlers.apply(this, arguments);
  262. this.stopListeningTo(GlobalEmitter.get());
  263. },
  264. /* Now Indicator
  265. ------------------------------------------------------------------------------------------------------------------*/
  266. // Immediately render the current time indicator and begins re-rendering it at an interval,
  267. // which is defined by this.getNowIndicatorUnit().
  268. // TODO: somehow do this for the current whole day's background too
  269. startNowIndicator: function() {
  270. var _this = this;
  271. var unit;
  272. var update;
  273. var delay; // ms wait value
  274. if (this.opt('nowIndicator')) {
  275. unit = this.getNowIndicatorUnit();
  276. if (unit) {
  277. update = proxy(this, 'updateNowIndicator'); // bind to `this`
  278. this.initialNowDate = this.calendar.getNow();
  279. this.initialNowQueriedMs = +new Date();
  280. this.renderNowIndicator(this.initialNowDate);
  281. this.isNowIndicatorRendered = true;
  282. // wait until the beginning of the next interval
  283. delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate;
  284. this.nowIndicatorTimeoutID = setTimeout(function() {
  285. _this.nowIndicatorTimeoutID = null;
  286. update();
  287. delay = +moment.duration(1, unit);
  288. delay = Math.max(100, delay); // prevent too frequent
  289. _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
  290. }, delay);
  291. }
  292. }
  293. },
  294. // rerenders the now indicator, computing the new current time from the amount of time that has passed
  295. // since the initial getNow call.
  296. updateNowIndicator: function() {
  297. if (this.isNowIndicatorRendered) {
  298. this.unrenderNowIndicator();
  299. this.renderNowIndicator(
  300. this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms
  301. );
  302. }
  303. },
  304. // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
  305. // Won't cause side effects if indicator isn't rendered.
  306. stopNowIndicator: function() {
  307. if (this.isNowIndicatorRendered) {
  308. if (this.nowIndicatorTimeoutID) {
  309. clearTimeout(this.nowIndicatorTimeoutID);
  310. this.nowIndicatorTimeoutID = null;
  311. }
  312. if (this.nowIndicatorIntervalID) {
  313. clearTimeout(this.nowIndicatorIntervalID);
  314. this.nowIndicatorIntervalID = null;
  315. }
  316. this.unrenderNowIndicator();
  317. this.isNowIndicatorRendered = false;
  318. }
  319. },
  320. /* Dimensions
  321. ------------------------------------------------------------------------------------------------------------------*/
  322. // TODO: move some of these to DateComponent
  323. // Refreshes anything dependant upon sizing of the container element of the grid
  324. updateSize: function(isResize) {
  325. var scroll;
  326. if (isResize) {
  327. scroll = this.queryScroll();
  328. }
  329. this.updateHeight(isResize);
  330. this.updateWidth(isResize);
  331. this.updateNowIndicator();
  332. if (isResize) {
  333. this.applyScroll(scroll);
  334. }
  335. },
  336. // Refreshes the horizontal dimensions of the calendar
  337. updateWidth: function(isResize) {
  338. // subclasses should implement
  339. },
  340. // Refreshes the vertical dimensions of the calendar
  341. updateHeight: function(isResize) {
  342. var calendar = this.calendar; // we poll the calendar for height information
  343. this.setHeight(
  344. calendar.getSuggestedViewHeight(),
  345. calendar.isHeightAuto()
  346. );
  347. },
  348. // Updates the vertical dimensions of the calendar to the specified height.
  349. // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  350. setHeight: function(height, isAuto) {
  351. // subclasses should implement
  352. },
  353. /* Scroller
  354. ------------------------------------------------------------------------------------------------------------------*/
  355. addForcedScroll: function(scroll) {
  356. this.addScroll(
  357. $.extend(scroll, { isForced: true })
  358. );
  359. },
  360. addScroll: function(scroll) {
  361. var queuedScroll = this.queuedScroll || (this.queuedScroll = {});
  362. if (!queuedScroll.isForced) {
  363. $.extend(queuedScroll, scroll);
  364. }
  365. },
  366. popScroll: function() {
  367. this.applyQueuedScroll();
  368. this.queuedScroll = null;
  369. },
  370. applyQueuedScroll: function() {
  371. if (this.queuedScroll) {
  372. this.applyScroll(this.queuedScroll);
  373. }
  374. },
  375. queryScroll: function() {
  376. var scroll = {};
  377. if (this.isDatesRendered) {
  378. $.extend(scroll, this.queryDateScroll());
  379. }
  380. return scroll;
  381. },
  382. applyScroll: function(scroll) {
  383. if (this.isDatesRendered) {
  384. this.applyDateScroll(scroll);
  385. }
  386. },
  387. computeInitialDateScroll: function() {
  388. return {}; // subclasses must implement
  389. },
  390. queryDateScroll: function() {
  391. return {}; // subclasses must implement
  392. },
  393. applyDateScroll: function(scroll) {
  394. ; // subclasses must implement
  395. },
  396. /* Height Freezing
  397. ------------------------------------------------------------------------------------------------------------------*/
  398. freezeHeight: function() {
  399. this.calendar.freezeContentHeight();
  400. },
  401. thawHeight: function() {
  402. this.calendar.thawContentHeight();
  403. },
  404. // Event High-level Rendering
  405. // -----------------------------------------------------------------------------------------------------------------
  406. executeEventsRender: function(eventsPayload) {
  407. if (this.renderEvents) { // for legacy custom views
  408. this.renderEvents(convertEventsPayloadToLegacyArray(eventsPayload));
  409. }
  410. else {
  411. this.renderEventsPayload(eventsPayload);
  412. }
  413. this.isEventsRendered = true;
  414. this.onEventsRender();
  415. },
  416. executeEventsUnrender: function() {
  417. this.onBeforeEventsUnrender();
  418. if (this.destroyEvents) {
  419. this.destroyEvents(); // TODO: deprecate
  420. }
  421. this.unrenderEvents();
  422. this.isEventsRendered = false;
  423. },
  424. // Event Rendering Triggers
  425. // -----------------------------------------------------------------------------------------------------------------
  426. // Signals that all events have been rendered
  427. onEventsRender: function() {
  428. var _this = this;
  429. var hasSingleHandlers = this.hasPublicHandlers('eventAfterRender');
  430. if (hasSingleHandlers || this.hasPublicHandlers('eventAfterAllRender')) {
  431. this.applyScreenState();
  432. }
  433. if (hasSingleHandlers) {
  434. this.getEventSegs().forEach(function(seg) {
  435. var legacy;
  436. if (seg.el) { // necessary?
  437. legacy = seg.footprint.getEventLegacy();
  438. _this.publiclyTrigger('eventAfterRender', {
  439. context: legacy,
  440. args: [ legacy, seg.el, _this ]
  441. });
  442. }
  443. });
  444. }
  445. this.publiclyTrigger('eventAfterAllRender', {
  446. context: this,
  447. args: [ this ]
  448. });
  449. },
  450. // Signals that all event elements are about to be removed
  451. onBeforeEventsUnrender: function() {
  452. var _this = this;
  453. if (this.hasPublicHandlers('eventDestroy')) {
  454. this.applyScreenState();
  455. this.getEventSegs().forEach(function(seg) {
  456. var legacy;
  457. if (seg.el) { // necessary?
  458. legacy = seg.footprint.getEventLegacy();
  459. _this.publiclyTrigger('eventDestroy', {
  460. context: legacy,
  461. args: [ legacy, seg.el, _this ]
  462. });
  463. }
  464. });
  465. }
  466. },
  467. applyScreenState: function() {
  468. this.thawHeight();
  469. this.freezeHeight();
  470. this.applyQueuedScroll();
  471. },
  472. // Event Rendering Utils
  473. // -----------------------------------------------------------------------------------------------------------------
  474. // TODO: move this to DateComponent
  475. // Hides all rendered event segments linked to the given event
  476. showEventsWithId: function(eventDefId) {
  477. this.getEventSegs().forEach(function(seg) {
  478. if (
  479. seg.footprint.eventDef.id === eventDefId &&
  480. seg.el // necessary?
  481. ) {
  482. seg.el.css('visibility', '');
  483. }
  484. });
  485. },
  486. // Shows all rendered event segments linked to the given event
  487. hideEventsWithId: function(eventDefId) {
  488. this.getEventSegs().forEach(function(seg) {
  489. if (
  490. seg.footprint.eventDef.id === eventDefId &&
  491. seg.el // necessary?
  492. ) {
  493. seg.el.css('visibility', 'hidden');
  494. }
  495. });
  496. },
  497. /* Event Drag-n-Drop
  498. ------------------------------------------------------------------------------------------------------------------*/
  499. reportEventDrop: function(eventInstance, eventMutation, el, ev) {
  500. var eventManager = this.calendar.eventManager;
  501. var undoFunc = eventManager.mutateEventsWithId(
  502. eventInstance.def.id,
  503. eventMutation,
  504. this.calendar
  505. );
  506. var dateMutation = eventMutation.dateMutation;
  507. // update the EventInstance, for handlers
  508. if (dateMutation) {
  509. eventInstance.dateProfile = dateMutation.buildNewDateProfile(
  510. eventInstance.dateProfile,
  511. this.calendar
  512. );
  513. }
  514. this.triggerEventDrop(
  515. eventInstance,
  516. // a drop doesn't necessarily mean a date mutation (ex: resource change)
  517. (dateMutation && dateMutation.dateDelta) || moment.duration(),
  518. undoFunc,
  519. el, ev
  520. );
  521. },
  522. // Triggers event-drop handlers that have subscribed via the API
  523. triggerEventDrop: function(eventInstance, dateDelta, undoFunc, el, ev) {
  524. this.publiclyTrigger('eventDrop', {
  525. context: el[0],
  526. args: [
  527. eventInstance.toLegacy(),
  528. dateDelta,
  529. undoFunc,
  530. ev,
  531. {}, // {} = jqui dummy
  532. this
  533. ]
  534. });
  535. },
  536. /* External Element Drag-n-Drop
  537. ------------------------------------------------------------------------------------------------------------------*/
  538. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  539. // `meta` is the parsed data that has been embedded into the dragging event.
  540. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  541. reportExternalDrop: function(singleEventDef, isEvent, isSticky, el, ev, ui) {
  542. if (isEvent) {
  543. this.calendar.eventManager.addEventDef(singleEventDef, isSticky);
  544. }
  545. this.triggerExternalDrop(singleEventDef, isEvent, el, ev, ui);
  546. },
  547. // Triggers external-drop handlers that have subscribed via the API
  548. triggerExternalDrop: function(singleEventDef, isEvent, el, ev, ui) {
  549. // trigger 'drop' regardless of whether element represents an event
  550. this.publiclyTrigger('drop', {
  551. context: el[0],
  552. args: [
  553. singleEventDef.dateProfile.start.clone(),
  554. ev,
  555. ui,
  556. this
  557. ]
  558. });
  559. if (isEvent) {
  560. // signal an external event landed
  561. this.publiclyTrigger('eventReceive', {
  562. context: this,
  563. args: [
  564. singleEventDef.buildInstance().toLegacy(),
  565. this
  566. ]
  567. });
  568. }
  569. },
  570. /* Event Resizing
  571. ------------------------------------------------------------------------------------------------------------------*/
  572. // Must be called when an event in the view has been resized to a new length
  573. reportEventResize: function(eventInstance, eventMutation, el, ev) {
  574. var eventManager = this.calendar.eventManager;
  575. var undoFunc = eventManager.mutateEventsWithId(
  576. eventInstance.def.id,
  577. eventMutation,
  578. this.calendar
  579. );
  580. // update the EventInstance, for handlers
  581. eventInstance.dateProfile = eventMutation.dateMutation.buildNewDateProfile(
  582. eventInstance.dateProfile,
  583. this.calendar
  584. );
  585. this.triggerEventResize(
  586. eventInstance,
  587. eventMutation.dateMutation.endDelta,
  588. undoFunc,
  589. el, ev
  590. );
  591. },
  592. // Triggers event-resize handlers that have subscribed via the API
  593. triggerEventResize: function(eventInstance, durationDelta, undoFunc, el, ev) {
  594. this.publiclyTrigger('eventResize', {
  595. context: el[0],
  596. args: [
  597. eventInstance.toLegacy(),
  598. durationDelta,
  599. undoFunc,
  600. ev,
  601. {}, // {} = jqui dummy
  602. this
  603. ]
  604. });
  605. },
  606. /* Selection (time range)
  607. ------------------------------------------------------------------------------------------------------------------*/
  608. // Selects a date span on the view. `start` and `end` are both Moments.
  609. // `ev` is the native mouse event that begin the interaction.
  610. select: function(footprint, ev) {
  611. this.unselect(ev);
  612. this.renderSelectionFootprint(footprint);
  613. this.reportSelection(footprint, ev);
  614. },
  615. renderSelectionFootprint: function(footprint, ev) {
  616. if (this.renderSelection) { // legacy method in custom view classes
  617. this.renderSelection(
  618. footprint.toLegacy(this.calendar)
  619. );
  620. }
  621. else {
  622. InteractiveDateComponent.prototype.renderSelectionFootprint.apply(this, arguments);
  623. }
  624. },
  625. // Called when a new selection is made. Updates internal state and triggers handlers.
  626. reportSelection: function(footprint, ev) {
  627. this.isSelected = true;
  628. this.triggerSelect(footprint, ev);
  629. },
  630. // Triggers handlers to 'select'
  631. triggerSelect: function(footprint, ev) {
  632. var dateProfile = this.calendar.footprintToDateProfile(footprint); // abuse of "Event"DateProfile?
  633. this.publiclyTrigger('select', {
  634. context: this,
  635. args: [
  636. dateProfile.start,
  637. dateProfile.end,
  638. ev,
  639. this
  640. ]
  641. });
  642. },
  643. // Undoes a selection. updates in the internal state and triggers handlers.
  644. // `ev` is the native mouse event that began the interaction.
  645. unselect: function(ev) {
  646. if (this.isSelected) {
  647. this.isSelected = false;
  648. if (this.destroySelection) {
  649. this.destroySelection(); // TODO: deprecate
  650. }
  651. this.unrenderSelection();
  652. this.publiclyTrigger('unselect', {
  653. context: this,
  654. args: [ ev, this ]
  655. });
  656. }
  657. },
  658. /* Event Selection
  659. ------------------------------------------------------------------------------------------------------------------*/
  660. selectEventInstance: function(eventInstance) {
  661. if (
  662. !this.selectedEventInstance ||
  663. this.selectedEventInstance !== eventInstance
  664. ) {
  665. this.unselectEventInstance();
  666. this.getEventSegs().forEach(function(seg) {
  667. if (
  668. seg.footprint.eventInstance === eventInstance &&
  669. seg.el // necessary?
  670. ) {
  671. seg.el.addClass('fc-selected');
  672. }
  673. });
  674. this.selectedEventInstance = eventInstance;
  675. }
  676. },
  677. unselectEventInstance: function() {
  678. if (this.selectedEventInstance) {
  679. this.getEventSegs().forEach(function(seg) {
  680. if (seg.el) { // necessary?
  681. seg.el.removeClass('fc-selected');
  682. }
  683. });
  684. this.selectedEventInstance = null;
  685. }
  686. },
  687. isEventDefSelected: function(eventDef) {
  688. // event references might change on refetchEvents(), while selectedEventInstance doesn't,
  689. // so compare IDs
  690. return this.selectedEventInstance && this.selectedEventInstance.def.id === eventDef.id;
  691. },
  692. /* Mouse / Touch Unselecting (time range & event unselection)
  693. ------------------------------------------------------------------------------------------------------------------*/
  694. // TODO: move consistently to down/start or up/end?
  695. // TODO: don't kill previous selection if touch scrolling
  696. handleDocumentMousedown: function(ev) {
  697. if (isPrimaryMouseButton(ev)) {
  698. this.processUnselect(ev);
  699. }
  700. },
  701. processUnselect: function(ev) {
  702. this.processRangeUnselect(ev);
  703. this.processEventUnselect(ev);
  704. },
  705. processRangeUnselect: function(ev) {
  706. var ignore;
  707. // is there a time-range selection?
  708. if (this.isSelected && this.opt('unselectAuto')) {
  709. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  710. ignore = this.opt('unselectCancel');
  711. if (!ignore || !$(ev.target).closest(ignore).length) {
  712. this.unselect(ev);
  713. }
  714. }
  715. },
  716. processEventUnselect: function(ev) {
  717. if (this.selectedEventInstance) {
  718. if (!$(ev.target).closest('.fc-selected').length) {
  719. this.unselectEventInstance();
  720. }
  721. }
  722. },
  723. /* Day Click
  724. ------------------------------------------------------------------------------------------------------------------*/
  725. // Triggers handlers to 'dayClick'
  726. // Span has start/end of the clicked area. Only the start is useful.
  727. triggerDayClick: function(footprint, dayEl, ev) {
  728. var dateProfile = this.calendar.footprintToDateProfile(footprint); // abuse of "Event"DateProfile?
  729. this.publiclyTrigger('dayClick', {
  730. context: dayEl,
  731. args: [ dateProfile.start, ev, this ]
  732. });
  733. }
  734. });
  735. // responsible for populating data that DateComponent relies on
  736. View.watch('businessHours', [ 'rawBusinessHours', 'dateProfile' ], function(deps) {
  737. return new BusinessHours(
  738. deps.rawBusinessHours,
  739. deps.dateProfile.activeUnzonedRange,
  740. this.calendar
  741. );
  742. });
  743. View.watch('initialEvents', [ 'dateProfile' ], function(deps) {
  744. return this.fetchInitialEvents(deps.dateProfile);
  745. });
  746. View.watch('bindingEvents', [ 'initialEvents' ], function(deps) {
  747. this.handleEventsSet(deps.initialEvents);
  748. this.bindEventChanges();
  749. }, function() {
  750. this.unbindEventChanges();
  751. this.handleEventsUnset();
  752. });
  753. // legacy
  754. function convertEventsPayloadToLegacyArray(eventsPayload) {
  755. var legacyEvents = [];
  756. var id;
  757. var eventInstances;
  758. var i;
  759. for (id in eventsPayload) {
  760. eventInstances = eventsPayload[id].eventInstances;
  761. for (i = 0; i < eventInstances.length; i++) {
  762. legacyEvents.push(
  763. eventInstances[i].toLegacy()
  764. );
  765. }
  766. }
  767. return legacyEvents;
  768. }