View.js 26 KB

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