View.js 24 KB

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