View.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. /* An abstract class from which other views inherit from
  2. ----------------------------------------------------------------------------------------------------------------------*/
  3. // Newer methods should be written as prototype methods, not in the monster `View` function at the bottom.
  4. View.prototype = {
  5. calendar: null, // owner Calendar object
  6. coordMap: null, // a CoordMap object for converting pixel regions to dates
  7. el: null, // the view's containing element. set by Calendar
  8. // important Moments
  9. start: null, // the date of the very first cell
  10. end: null, // the date after the very last cell
  11. intervalStart: null, // the start of the interval of time the view represents (1st of month for month view)
  12. intervalEnd: null, // the exclusive end of the interval of time the view represents
  13. // used for cell-to-date and date-to-cell calculations
  14. rowCnt: null, // # of weeks
  15. colCnt: null, // # of days displayed in a week
  16. segs: null, // array of rendered event segment objects
  17. isSelected: false, // boolean whether cells are user-selected or not
  18. // subclasses can optionally use a scroll container
  19. scrollerEl: null, // the element that will most likely scroll when content is too tall
  20. scrollTop: null, // cached vertical scroll value
  21. // classNames styled by jqui themes
  22. widgetHeaderClass: null,
  23. widgetContentClass: null,
  24. highlightStateClass: null,
  25. dayRowThemeClass: null, // sets the theme className applied to DayGrid rows (none by default)
  26. // document handlers, bound to `this` object
  27. documentMousedownProxy: null,
  28. documentDragStartProxy: null,
  29. // Serves as a "constructor" to suppliment the monster `View` constructor below
  30. init: function() {
  31. var tm = this.opt('theme') ? 'ui' : 'fc';
  32. this.widgetHeaderClass = tm + '-widget-header';
  33. this.widgetContentClass = tm + '-widget-content';
  34. this.highlightStateClass = tm + '-state-highlight';
  35. // save reference to `this`-bound handlers and attach to document
  36. $(document)
  37. .on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown'))
  38. .on('dragstart', this.documentDragStartProxy = $.proxy(this, 'documentDragStart')); // jqui drag
  39. },
  40. // Renders the view inside an already-defined `this.el`.
  41. // Subclasses should override this and then call the super method afterwards.
  42. render: function() {
  43. this.updateHeight();
  44. this.updateWidth();
  45. this.trigger('viewRender', this, this, this.el);
  46. },
  47. // Clears all view rendering, event elements, and unregisters handlers
  48. destroy: function() {
  49. this.unselect();
  50. this.trigger('viewDestroy', this, this, this.el);
  51. this.destroyEvents();
  52. this.el.empty(); // removes inner contents but leaves the element intact
  53. $(document)
  54. .off('mousedown', this.documentMousedownProxy)
  55. .off('dragstart', this.documentDragStartProxy);
  56. },
  57. // Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next.
  58. // Should apply the delta to `date` (a Moment) and return it.
  59. incrementDate: function(date, delta) {
  60. // subclasses should implement
  61. },
  62. /* Dimensions
  63. ------------------------------------------------------------------------------------------------------------------*/
  64. // Refreshes the horizontal dimensions of the calendar
  65. updateWidth: function() {
  66. // subclasses should implement
  67. },
  68. // Refreshes the vertical dimensions of the calendar
  69. updateHeight: function() {
  70. var calendar = this.calendar; // we poll the calendar for height information
  71. this.setHeight(
  72. calendar.getSuggestedViewHeight(),
  73. calendar.isHeightAuto()
  74. );
  75. },
  76. // Updates the vertical dimensions of the calendar to the specified height.
  77. // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  78. setHeight: function(height, isAuto) {
  79. // subclasses should implement
  80. },
  81. // Given the total height of the view, return the number of pixels that should be used for the scroller.
  82. // Utility for subclasses.
  83. computeScrollerHeight: function(totalHeight) {
  84. // `otherHeight` is the cumulative height of everything that is not the scrollerEl in the view (header+borders)
  85. var otherHeight = this.el.outerHeight() - this.scrollerEl.height();
  86. return totalHeight - otherHeight;
  87. },
  88. // Called for remembering the current scroll value of the scroller.
  89. // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently
  90. // change the scroll of the container.
  91. recordScroll: function() {
  92. this.scrollTop = this.scrollerEl.scrollTop();
  93. },
  94. // Set the scroll value of the scroller to the previously recorded value.
  95. // Should be called after we know the view's dimensions have been restored following some type of destructive
  96. // operation (like temporarily removing DOM elements).
  97. restoreScroll: function() {
  98. if (this.scrollTop !== null) {
  99. this.scrollerEl.scrollTop(this.scrollTop);
  100. }
  101. },
  102. /* Events
  103. ------------------------------------------------------------------------------------------------------------------*/
  104. // Renders the events onto the view.
  105. // Should be overriden by subclasses. Subclasses should assign `this.segs` and call the super-method afterwards.
  106. renderEvents: function(events) {
  107. this.segEach(function(seg) {
  108. seg.el.data('fc-seg', seg); // store info about the segment object. used by handlers
  109. this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
  110. });
  111. this.trigger('eventAfterAllRender');
  112. },
  113. // Removes event elements from the view.
  114. // Should be overridden by subclasses. Actual element destruction should happen first, then call super-method.
  115. destroyEvents: function() {
  116. this.segEach(function(seg) {
  117. this.trigger('eventDestroy', seg.event, seg.event, seg.el);
  118. });
  119. this.segs = [];
  120. },
  121. // Given an event and the default element used for rendering, returns the element that should actually be used.
  122. // Basically runs events and elements through the eventRender hook.
  123. resolveEventEl: function(event, el) {
  124. var custom = this.trigger('eventRender', event, event, el);
  125. if (custom === false) { // means don't render at all
  126. el = null;
  127. }
  128. else if (custom && custom !== true) {
  129. el = $(custom);
  130. }
  131. return el;
  132. },
  133. // Hides all rendered event segments linked to the given event
  134. showEvent: function(event) {
  135. this.segEach(function(seg) {
  136. seg.el.css('visibility', '');
  137. }, event);
  138. },
  139. // Shows all rendered event segments linked to the given event
  140. hideEvent: function(event) {
  141. this.segEach(function(seg) {
  142. seg.el.css('visibility', 'hidden');
  143. }, event);
  144. },
  145. // Iterates through event segments. Goes through all by default.
  146. // If the optional `event` argument is specified, only iterates through segments linked to that event.
  147. // The `this` value of the callback function will be the view.
  148. segEach: function(func, event) {
  149. var segs = this.segs || [];
  150. var i;
  151. for (i = 0; i < segs.length; i++) {
  152. if (!event || segs[i].event._id === event._id) {
  153. func.call(this, segs[i]);
  154. }
  155. }
  156. },
  157. /* Event Drag Visualization
  158. ------------------------------------------------------------------------------------------------------------------*/
  159. // Renders a visual indication of an event hovering over the specified date.
  160. // `end` is a Moment and might be null.
  161. // `seg` might be null. if specified, it is the segment object of the event being dragged.
  162. // otherwise, an external event from outside the calendar is being dragged.
  163. renderDrag: function(start, end, seg) {
  164. // subclasses should implement
  165. },
  166. // Unrenders a visual indication of event hovering
  167. destroyDrag: function() {
  168. // subclasses should implement
  169. },
  170. // Handler for accepting externally dragged events being dropped in the view.
  171. // Gets called when jqui's 'dragstart' is fired.
  172. documentDragStart: function(ev, ui) {
  173. var _this = this;
  174. var dropDate = null;
  175. var dragListener;
  176. if (this.opt('droppable')) { // only listen if this setting is on
  177. // listener that tracks mouse movement over date-associated pixel regions
  178. dragListener = new DragListener(this.coordMap, {
  179. cellOver: function(cell, date) {
  180. dropDate = date;
  181. _this.renderDrag(date);
  182. },
  183. cellOut: function() {
  184. dropDate = null;
  185. _this.destroyDrag();
  186. }
  187. });
  188. // gets called, only once, when jqui drag is finished
  189. $(document).one('dragstop', function(ev, ui) {
  190. _this.destroyDrag();
  191. if (dropDate) {
  192. _this.trigger('drop', ev.target, dropDate, ev, ui);
  193. }
  194. });
  195. dragListener.startDrag(ev); // start listening immediately
  196. }
  197. },
  198. /* Selection
  199. ------------------------------------------------------------------------------------------------------------------*/
  200. // Selects a date range on the view. `start` and `end` are both Moments.
  201. // `ev` is the native mouse event that begin the interaction.
  202. select: function(start, end, ev) {
  203. this.unselect(ev);
  204. this.renderSelection(start, end);
  205. this.reportSelection(start, end, ev);
  206. },
  207. // Renders a visual indication of the selection
  208. renderSelection: function(start, end) {
  209. // subclasses should implement
  210. },
  211. // Called when a new selection is made. Updates internal state and triggers handlers.
  212. reportSelection: function(start, end, ev) {
  213. this.isSelected = true;
  214. this.trigger('select', null, start, end, ev);
  215. },
  216. // Undoes a selection. updates in the internal state and triggers handlers.
  217. // `ev` is the native mouse event that began the interaction.
  218. unselect: function(ev) {
  219. if (this.isSelected) {
  220. this.isSelected = false;
  221. this.destroySelection();
  222. this.trigger('unselect', null, ev);
  223. }
  224. },
  225. // Unrenders a visual indication of selection
  226. destroySelection: function() {
  227. // subclasses should implement
  228. },
  229. // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
  230. documentMousedown: function(ev) {
  231. var ignore;
  232. // is there a selection, and has the user made a proper left click?
  233. if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
  234. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  235. ignore = this.opt('unselectCancel');
  236. if (!ignore || !$(ev.target).closest(ignore).length) {
  237. this.unselect(ev);
  238. }
  239. }
  240. }
  241. };
  242. // We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the
  243. // constructor. Going forward, methods should be part of the prototype.
  244. function View(calendar) {
  245. var t = this;
  246. // exports
  247. t.calendar = calendar;
  248. t.opt = opt;
  249. t.trigger = trigger;
  250. t.isEventDraggable = isEventDraggable;
  251. t.isEventResizable = isEventResizable;
  252. t.eventDrop = eventDrop;
  253. t.eventResize = eventResize;
  254. // imports
  255. var reportEventChange = calendar.reportEventChange;
  256. // locals
  257. var options = calendar.options;
  258. var nextDayThreshold = moment.duration(options.nextDayThreshold);
  259. t.init(); // the "constructor" that concerns the prototype methods
  260. function opt(name) {
  261. var v = options[name];
  262. if ($.isPlainObject(v) && !isForcedAtomicOption(name)) {
  263. return smartProperty(v, t.name);
  264. }
  265. return v;
  266. }
  267. function trigger(name, thisObj) {
  268. return calendar.trigger.apply(
  269. calendar,
  270. [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
  271. );
  272. }
  273. /* Event Editable Boolean Calculations
  274. ------------------------------------------------------------------------------*/
  275. function isEventDraggable(event) {
  276. var source = event.source || {};
  277. return firstDefined(
  278. event.startEditable,
  279. source.startEditable,
  280. opt('eventStartEditable'),
  281. event.editable,
  282. source.editable,
  283. opt('editable')
  284. );
  285. }
  286. function isEventResizable(event) {
  287. var source = event.source || {};
  288. return firstDefined(
  289. event.durationEditable,
  290. source.durationEditable,
  291. opt('eventDurationEditable'),
  292. event.editable,
  293. source.editable,
  294. opt('editable')
  295. );
  296. }
  297. /* Event Elements
  298. ------------------------------------------------------------------------------*/
  299. // Compute the text that should be displayed on an event's element.
  300. // Based off the settings of the view. Possible signatures:
  301. // .getEventTimeText(event, formatStr)
  302. // .getEventTimeText(startMoment, endMoment, formatStr)
  303. // .getEventTimeText(startMoment, null, formatStr)
  304. // `timeFormat` is used but the `formatStr` argument can be used to override.
  305. t.getEventTimeText = function(event, formatStr) {
  306. var start;
  307. var end;
  308. if (typeof event === 'object' && typeof formatStr === 'object') {
  309. // first two arguments are actually moments (or null). shift arguments.
  310. start = event;
  311. end = formatStr;
  312. formatStr = arguments[2];
  313. }
  314. else {
  315. // otherwise, an event object was the first argument
  316. start = event.start;
  317. end = event.end;
  318. }
  319. formatStr = formatStr || opt('timeFormat');
  320. if (end && opt('displayEventEnd')) {
  321. return calendar.formatRange(start, end, formatStr);
  322. }
  323. else {
  324. return calendar.formatDate(start, formatStr);
  325. }
  326. };
  327. /* Event Modification Reporting
  328. ---------------------------------------------------------------------------------*/
  329. function eventDrop(el, event, newStart, ev) {
  330. var mutateResult = calendar.mutateEvent(event, newStart, null);
  331. trigger(
  332. 'eventDrop',
  333. el,
  334. event,
  335. mutateResult.dateDelta,
  336. function() {
  337. mutateResult.undo();
  338. reportEventChange();
  339. },
  340. ev,
  341. {} // jqui dummy
  342. );
  343. reportEventChange();
  344. }
  345. function eventResize(el, event, newEnd, ev) {
  346. var mutateResult = calendar.mutateEvent(event, null, newEnd);
  347. trigger(
  348. 'eventResize',
  349. el,
  350. event,
  351. mutateResult.durationDelta,
  352. function() {
  353. mutateResult.undo();
  354. reportEventChange();
  355. },
  356. ev,
  357. {} // jqui dummy
  358. );
  359. reportEventChange();
  360. }
  361. // ====================================================================================================
  362. // Utilities for day "cells"
  363. // ====================================================================================================
  364. // The "basic" views are completely made up of day cells.
  365. // The "agenda" views have day cells at the top "all day" slot.
  366. // This was the obvious common place to put these utilities, but they should be abstracted out into
  367. // a more meaningful class (like DayEventRenderer).
  368. // ====================================================================================================
  369. // For determining how a given "cell" translates into a "date":
  370. //
  371. // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
  372. // Keep in mind that column indices are inverted with isRTL. This is taken into account.
  373. //
  374. // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
  375. //
  376. // 3. Convert the "day offset" into a "date" (a Moment).
  377. //
  378. // The reverse transformation happens when transforming a date into a cell.
  379. // exports
  380. t.isHiddenDay = isHiddenDay;
  381. t.skipHiddenDays = skipHiddenDays;
  382. t.getCellsPerWeek = getCellsPerWeek;
  383. t.dateToCell = dateToCell;
  384. t.dateToDayOffset = dateToDayOffset;
  385. t.dayOffsetToCellOffset = dayOffsetToCellOffset;
  386. t.cellOffsetToCell = cellOffsetToCell;
  387. t.cellToDate = cellToDate;
  388. t.cellToCellOffset = cellToCellOffset;
  389. t.cellOffsetToDayOffset = cellOffsetToDayOffset;
  390. t.dayOffsetToDate = dayOffsetToDate;
  391. t.rangeToSegments = rangeToSegments;
  392. t.isMultiDayEvent = isMultiDayEvent;
  393. // internals
  394. var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  395. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  396. var cellsPerWeek;
  397. var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
  398. var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
  399. var isRTL = opt('isRTL');
  400. // initialize important internal variables
  401. (function() {
  402. if (opt('weekends') === false) {
  403. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  404. }
  405. // Loop through a hypothetical week and determine which
  406. // days-of-week are hidden. Record in both hashes (one is the reverse of the other).
  407. for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
  408. dayToCellMap[dayIndex] = cellIndex;
  409. isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
  410. if (!isHiddenDayHash[dayIndex]) {
  411. cellToDayMap[cellIndex] = dayIndex;
  412. cellIndex++;
  413. }
  414. }
  415. cellsPerWeek = cellIndex;
  416. if (!cellsPerWeek) {
  417. throw 'invalid hiddenDays'; // all days were hidden? bad.
  418. }
  419. })();
  420. // Is the current day hidden?
  421. // `day` is a day-of-week index (0-6), or a Moment
  422. function isHiddenDay(day) {
  423. if (moment.isMoment(day)) {
  424. day = day.day();
  425. }
  426. return isHiddenDayHash[day];
  427. }
  428. function getCellsPerWeek() {
  429. return cellsPerWeek;
  430. }
  431. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  432. // If the initial value of `date` is not a hidden day, don't do anything.
  433. // Pass `isExclusive` as `true` if you are dealing with an end date.
  434. // `inc` defaults to `1` (increment one day forward each time)
  435. function skipHiddenDays(date, inc, isExclusive) {
  436. var out = date.clone();
  437. inc = inc || 1;
  438. while (
  439. isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  440. ) {
  441. out.add('days', inc);
  442. }
  443. return out;
  444. }
  445. //
  446. // TRANSFORMATIONS: cell -> cell offset -> day offset -> date
  447. //
  448. // cell -> date (combines all transformations)
  449. // Possible arguments:
  450. // - row, col
  451. // - { row:#, col: # }
  452. function cellToDate() {
  453. var cellOffset = cellToCellOffset.apply(null, arguments);
  454. var dayOffset = cellOffsetToDayOffset(cellOffset);
  455. var date = dayOffsetToDate(dayOffset);
  456. return date;
  457. }
  458. // cell -> cell offset
  459. // Possible arguments:
  460. // - row, col
  461. // - { row:#, col:# }
  462. function cellToCellOffset(row, col) {
  463. var colCnt = t.colCnt;
  464. // rtl variables. wish we could pre-populate these. but where?
  465. var dis = isRTL ? -1 : 1;
  466. var dit = isRTL ? colCnt - 1 : 0;
  467. if (typeof row == 'object') {
  468. col = row.col;
  469. row = row.row;
  470. }
  471. var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
  472. return cellOffset;
  473. }
  474. // cell offset -> day offset
  475. function cellOffsetToDayOffset(cellOffset) {
  476. var day0 = t.start.day(); // first date's day of week
  477. cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
  478. return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks
  479. cellToDayMap[ // # of days from partial last week
  480. (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
  481. ] -
  482. day0; // adjustment for beginning-of-week normalization
  483. }
  484. // day offset -> date
  485. function dayOffsetToDate(dayOffset) {
  486. return t.start.clone().add('days', dayOffset);
  487. }
  488. //
  489. // TRANSFORMATIONS: date -> day offset -> cell offset -> cell
  490. //
  491. // date -> cell (combines all transformations)
  492. function dateToCell(date) {
  493. var dayOffset = dateToDayOffset(date);
  494. var cellOffset = dayOffsetToCellOffset(dayOffset);
  495. var cell = cellOffsetToCell(cellOffset);
  496. return cell;
  497. }
  498. // date -> day offset
  499. function dateToDayOffset(date) {
  500. return date.clone().stripTime().diff(t.start, 'days');
  501. }
  502. // day offset -> cell offset
  503. function dayOffsetToCellOffset(dayOffset) {
  504. var day0 = t.start.day(); // first date's day of week
  505. dayOffset += day0; // normalize dayOffset to beginning-of-week
  506. return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks
  507. dayToCellMap[ // # of cells from partial last week
  508. (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
  509. ] -
  510. dayToCellMap[day0]; // adjustment for beginning-of-week normalization
  511. }
  512. // cell offset -> cell (object with row & col keys)
  513. function cellOffsetToCell(cellOffset) {
  514. var colCnt = t.colCnt;
  515. // rtl variables. wish we could pre-populate these. but where?
  516. var dis = isRTL ? -1 : 1;
  517. var dit = isRTL ? colCnt - 1 : 0;
  518. var row = Math.floor(cellOffset / colCnt);
  519. var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
  520. return {
  521. row: row,
  522. col: col
  523. };
  524. }
  525. //
  526. // Converts a date range into an array of segment objects.
  527. // "Segments" are horizontal stretches of time, sliced up by row.
  528. // A segment object has the following properties:
  529. // - row
  530. // - cols
  531. // - isStart
  532. // - isEnd
  533. //
  534. function rangeToSegments(start, end) {
  535. var rowCnt = t.rowCnt;
  536. var colCnt = t.colCnt;
  537. var segments = []; // array of segments to return
  538. // day offset for given date range
  539. var dayRange = computeDayRange(start, end); // convert to a whole-day range
  540. var rangeDayOffsetStart = dateToDayOffset(dayRange.start);
  541. var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value
  542. // first and last cell offset for the given date range
  543. // "last" implies inclusivity
  544. var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
  545. var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
  546. // loop through all the rows in the view
  547. for (var row=0; row<rowCnt; row++) {
  548. // first and last cell offset for the row
  549. var rowCellOffsetFirst = row * colCnt;
  550. var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
  551. // get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
  552. var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
  553. var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
  554. // make sure segment's offsets are valid and in view
  555. if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
  556. // translate to cells
  557. var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
  558. var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
  559. // view might be RTL, so order by leftmost column
  560. var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
  561. // Determine if segment's first/last cell is the beginning/end of the date range.
  562. // We need to compare "day offset" because "cell offsets" are often ambiguous and
  563. // can translate to multiple days, and an edge case reveals itself when we the
  564. // range's first cell is hidden (we don't want isStart to be true).
  565. var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
  566. var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd;
  567. // +1 for comparing exclusively
  568. segments.push({
  569. row: row,
  570. leftCol: cols[0],
  571. rightCol: cols[1],
  572. isStart: isStart,
  573. isEnd: isEnd
  574. });
  575. }
  576. }
  577. return segments;
  578. }
  579. // Returns the date range of the full days the given range visually appears to occupy.
  580. // Returns object with properties `start` (moment) and `end` (moment, exclusive end).
  581. function computeDayRange(start, end) {
  582. var startDay = start.clone().stripTime(); // the beginning of the day the range starts
  583. var endDay;
  584. var endTimeMS;
  585. if (end) {
  586. endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  587. endTimeMS = +end.time(); // # of milliseconds into `endDay`
  588. // If the end time is actually inclusively part of the next day and is equal to or
  589. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  590. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  591. if (endTimeMS && endTimeMS >= nextDayThreshold) {
  592. endDay.add('days', 1);
  593. }
  594. }
  595. // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  596. // assign the default duration of one day.
  597. if (!end || endDay <= startDay) {
  598. endDay = startDay.clone().add('days', 1);
  599. }
  600. return { start: startDay, end: endDay };
  601. }
  602. // Does the given event visually appear to occupy more than one day?
  603. function isMultiDayEvent(event) {
  604. var range = computeDayRange(event.start, event.end);
  605. return range.end.diff(range.start, 'days') > 1;
  606. }
  607. }