2
0

View.date-range.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. View.mixin({
  2. usesMinMaxTime: false, // whether minTime/maxTime will affect the activeUnzonedRange. Views must opt-in.
  3. // DEPRECATED
  4. start: null, // use activeUnzonedRange
  5. end: null, // use activeUnzonedRange
  6. intervalStart: null, // use currentUnzonedRange
  7. intervalEnd: null, // use currentUnzonedRange
  8. /* Date Range Computation
  9. ------------------------------------------------------------------------------------------------------------------*/
  10. // Builds a structure with info about what the dates/ranges will be for the "prev" view.
  11. buildPrevDateProfile: function(date) {
  12. var dateProfile = this.get('dateProfile');
  13. var prevDate = date.clone().startOf(dateProfile.currentRangeUnit)
  14. .subtract(dateProfile.dateIncrement);
  15. return this.buildDateProfile(prevDate, -1);
  16. },
  17. // Builds a structure with info about what the dates/ranges will be for the "next" view.
  18. buildNextDateProfile: function(date) {
  19. var dateProfile = this.get('dateProfile');
  20. var nextDate = date.clone().startOf(dateProfile.currentRangeUnit)
  21. .add(dateProfile.dateIncrement);
  22. return this.buildDateProfile(nextDate, 1);
  23. },
  24. // Builds a structure holding dates/ranges for rendering around the given date.
  25. // Optional direction param indicates whether the date is being incremented/decremented
  26. // from its previous value. decremented = -1, incremented = 1 (default).
  27. buildDateProfile: function(date, direction, forceToValid) {
  28. var isDateAllDay = !date.hasTime();
  29. var validUnzonedRange = this.buildValidRange();
  30. var minTime = null;
  31. var maxTime = null;
  32. var currentInfo;
  33. var isRangeAllDay;
  34. var renderUnzonedRange;
  35. var activeUnzonedRange;
  36. var isValid;
  37. if (forceToValid) {
  38. date = this.calendar.msToUtcMoment(
  39. validUnzonedRange.constrainDate(date), // returns MS
  40. isDateAllDay
  41. );
  42. }
  43. currentInfo = this.buildCurrentRangeInfo(date, direction);
  44. isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit);
  45. renderUnzonedRange = this.buildRenderRange(currentInfo.unzonedRange, currentInfo.unit, isRangeAllDay);
  46. activeUnzonedRange = renderUnzonedRange.clone();
  47. if (!this.opt('showNonCurrentDates')) {
  48. activeUnzonedRange = activeUnzonedRange.intersect(currentInfo.unzonedRange);
  49. }
  50. minTime = moment.duration(this.opt('minTime'));
  51. maxTime = moment.duration(this.opt('maxTime'));
  52. activeUnzonedRange = this.adjustActiveRange(activeUnzonedRange, minTime, maxTime);
  53. activeUnzonedRange = activeUnzonedRange.intersect(validUnzonedRange);
  54. if (activeUnzonedRange) {
  55. date = this.calendar.msToUtcMoment(
  56. activeUnzonedRange.constrainDate(date), // returns MS
  57. isDateAllDay
  58. );
  59. }
  60. // it's invalid if the originally requested date is not contained,
  61. // or if the range is completely outside of the valid range.
  62. isValid = currentInfo.unzonedRange.intersectsWith(validUnzonedRange);
  63. return {
  64. // constraint for where prev/next operations can go and where events can be dragged/resized to.
  65. // an object with optional start and end properties.
  66. validUnzonedRange: validUnzonedRange,
  67. // range the view is formally responsible for.
  68. // for example, a month view might have 1st-31st, excluding padded dates
  69. currentUnzonedRange: currentInfo.unzonedRange,
  70. // name of largest unit being displayed, like "month" or "week"
  71. currentRangeUnit: currentInfo.unit,
  72. isRangeAllDay: isRangeAllDay,
  73. // dates that display events and accept drag-n-drop
  74. activeUnzonedRange: activeUnzonedRange,
  75. // date range with a rendered skeleton
  76. // includes not-active days that need some sort of DOM
  77. renderUnzonedRange: renderUnzonedRange,
  78. // Duration object that denotes the first visible time of any given day
  79. minTime: minTime,
  80. // Duration object that denotes the exclusive visible end time of any given day
  81. maxTime: maxTime,
  82. isValid: isValid,
  83. date: date,
  84. // how far the current date will move for a prev/next operation
  85. dateIncrement: this.buildDateIncrement(currentInfo.duration)
  86. // pass a fallback (might be null) ^
  87. };
  88. },
  89. // Builds an object with optional start/end properties.
  90. // Indicates the minimum/maximum dates to display.
  91. buildValidRange: function() {
  92. return this.getUnzonedRangeOption('validRange', this.calendar.getNow()) ||
  93. new UnzonedRange(); // completely open-ended
  94. },
  95. // Builds a structure with info about the "current" range, the range that is
  96. // highlighted as being the current month for example.
  97. // See buildDateProfile for a description of `direction`.
  98. // Guaranteed to have `range` and `unit` properties. `duration` is optional.
  99. // TODO: accept a MS-time instead of a moment `date`?
  100. buildCurrentRangeInfo: function(date, direction) {
  101. var duration = null;
  102. var unit = null;
  103. var unzonedRange = null;
  104. var dayCount;
  105. if (this.viewSpec.duration) {
  106. duration = this.viewSpec.duration;
  107. unit = this.viewSpec.durationUnit;
  108. unzonedRange = this.buildRangeFromDuration(date, direction, duration, unit);
  109. }
  110. else if ((dayCount = this.opt('dayCount'))) {
  111. unit = 'day';
  112. unzonedRange = this.buildRangeFromDayCount(date, direction, dayCount);
  113. }
  114. else if ((unzonedRange = this.buildCustomVisibleRange(date))) {
  115. unit = computeGreatestUnit(unzonedRange.getStart(), unzonedRange.getEnd());
  116. }
  117. else {
  118. duration = this.getFallbackDuration();
  119. unit = computeGreatestUnit(duration);
  120. unzonedRange = this.buildRangeFromDuration(date, direction, duration, unit);
  121. }
  122. return { duration: duration, unit: unit, unzonedRange: unzonedRange };
  123. },
  124. getFallbackDuration: function() {
  125. return moment.duration({ days: 1 });
  126. },
  127. // Returns a new activeUnzonedRange to have time values (un-ambiguate)
  128. // minTime or maxTime causes the range to expand.
  129. adjustActiveRange: function(unzonedRange, minTime, maxTime) {
  130. var start = unzonedRange.getStart();
  131. var end = unzonedRange.getEnd();
  132. if (this.usesMinMaxTime) {
  133. if (minTime < 0) {
  134. start.time(0).add(minTime);
  135. }
  136. if (maxTime > 24 * 60 * 60 * 1000) { // beyond 24 hours?
  137. end.time(maxTime - (24 * 60 * 60 * 1000));
  138. }
  139. }
  140. return new UnzonedRange(start, end);
  141. },
  142. // Builds the "current" range when it is specified as an explicit duration.
  143. // `unit` is the already-computed computeGreatestUnit value of duration.
  144. // TODO: accept a MS-time instead of a moment `date`?
  145. buildRangeFromDuration: function(date, direction, duration, unit) {
  146. var alignment = this.opt('dateAlignment');
  147. var start = date.clone();
  148. var end;
  149. var dateIncrementInput;
  150. var dateIncrementDuration;
  151. // if the view displays a single day or smaller
  152. if (duration.as('days') <= 1) {
  153. if (this.isHiddenDay(start)) {
  154. start = this.skipHiddenDays(start, direction);
  155. start.startOf('day');
  156. }
  157. }
  158. // compute what the alignment should be
  159. if (!alignment) {
  160. dateIncrementInput = this.opt('dateIncrement');
  161. if (dateIncrementInput) {
  162. dateIncrementDuration = moment.duration(dateIncrementInput);
  163. // use the smaller of the two units
  164. if (dateIncrementDuration < duration) {
  165. alignment = computeDurationGreatestUnit(dateIncrementDuration, dateIncrementInput);
  166. }
  167. else {
  168. alignment = unit;
  169. }
  170. }
  171. else {
  172. alignment = unit;
  173. }
  174. }
  175. start.startOf(alignment);
  176. end = start.clone().add(duration);
  177. return new UnzonedRange(start, end);
  178. },
  179. // Builds the "current" range when a dayCount is specified.
  180. // TODO: accept a MS-time instead of a moment `date`?
  181. buildRangeFromDayCount: function(date, direction, dayCount) {
  182. var customAlignment = this.opt('dateAlignment');
  183. var runningCount = 0;
  184. var start = date.clone();
  185. var end;
  186. if (customAlignment) {
  187. start.startOf(customAlignment);
  188. }
  189. start.startOf('day');
  190. start = this.skipHiddenDays(start, direction);
  191. end = start.clone();
  192. do {
  193. end.add(1, 'day');
  194. if (!this.isHiddenDay(end)) {
  195. runningCount++;
  196. }
  197. } while (runningCount < dayCount);
  198. return new UnzonedRange(start, end);
  199. },
  200. // Builds a normalized range object for the "visible" range,
  201. // which is a way to define the currentUnzonedRange and activeUnzonedRange at the same time.
  202. // TODO: accept a MS-time instead of a moment `date`?
  203. buildCustomVisibleRange: function(date) {
  204. var visibleUnzonedRange = this.getUnzonedRangeOption(
  205. 'visibleRange',
  206. this.calendar.applyTimezone(date) // correct zone. also generates new obj that avoids mutations
  207. );
  208. if (visibleUnzonedRange && (visibleUnzonedRange.startMs === null || visibleUnzonedRange.endMs === null)) {
  209. return null;
  210. }
  211. return visibleUnzonedRange;
  212. },
  213. // Computes the range that will represent the element/cells for *rendering*,
  214. // but which may have voided days/times.
  215. buildRenderRange: function(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
  216. // cut off days in the currentUnzonedRange that are hidden
  217. return this.trimHiddenDays(currentUnzonedRange);
  218. },
  219. // Compute the duration value that should be added/substracted to the current date
  220. // when a prev/next operation happens.
  221. buildDateIncrement: function(fallback) {
  222. var dateIncrementInput = this.opt('dateIncrement');
  223. var customAlignment;
  224. if (dateIncrementInput) {
  225. return moment.duration(dateIncrementInput);
  226. }
  227. else if ((customAlignment = this.opt('dateAlignment'))) {
  228. return moment.duration(1, customAlignment);
  229. }
  230. else if (fallback) {
  231. return fallback;
  232. }
  233. else {
  234. return moment.duration({ days: 1 });
  235. }
  236. },
  237. // Remove days from the beginning and end of the range that are computed as hidden.
  238. trimHiddenDays: function(inputUnzonedRange) {
  239. var start = inputUnzonedRange.getStart();
  240. var end = inputUnzonedRange.getEnd();
  241. start = this.skipHiddenDays(start);
  242. end = this.skipHiddenDays(end, -1, true);
  243. return new UnzonedRange(start, end);
  244. },
  245. // Compute the number of the give units in the "current" range.
  246. // Will return a floating-point number. Won't round.
  247. currentRangeAs: function(unit) {
  248. var currentUnzonedRange = this.get('dateProfile').currentUnzonedRange;
  249. return moment.utc(currentUnzonedRange.endMs).diff(
  250. moment.utc(currentUnzonedRange.startMs),
  251. unit,
  252. true
  253. );
  254. },
  255. // For DateComponent::getDayClasses
  256. isDateInOtherMonth: function(date) {
  257. return false;
  258. },
  259. // Arguments after name will be forwarded to a hypothetical function value
  260. // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects.
  261. // Always clone your objects if you fear mutation.
  262. getUnzonedRangeOption: function(name) {
  263. var val = this.opt(name);
  264. if (typeof val === 'function') {
  265. val = val.apply(
  266. null,
  267. Array.prototype.slice.call(arguments, 1)
  268. );
  269. }
  270. if (val) {
  271. return this.calendar.parseUnzonedRange(val);
  272. }
  273. },
  274. /* Hidden Days
  275. ------------------------------------------------------------------------------------------------------------------*/
  276. // Initializes internal variables related to calculating hidden days-of-week
  277. initHiddenDays: function() {
  278. var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  279. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  280. var dayCnt = 0;
  281. var i;
  282. if (this.opt('weekends') === false) {
  283. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  284. }
  285. for (i = 0; i < 7; i++) {
  286. if (
  287. !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
  288. ) {
  289. dayCnt++;
  290. }
  291. }
  292. if (!dayCnt) {
  293. throw 'invalid hiddenDays'; // all days were hidden? bad.
  294. }
  295. this.isHiddenDayHash = isHiddenDayHash;
  296. },
  297. // Is the current day hidden?
  298. // `day` is a day-of-week index (0-6), or a Moment
  299. isHiddenDay: function(day) {
  300. if (moment.isMoment(day)) {
  301. day = day.day();
  302. }
  303. return this.isHiddenDayHash[day];
  304. },
  305. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  306. // DOES NOT CONSIDER validUnzonedRange!
  307. // If the initial value of `date` is not a hidden day, don't do anything.
  308. // Pass `isExclusive` as `true` if you are dealing with an end date.
  309. // `inc` defaults to `1` (increment one day forward each time)
  310. skipHiddenDays: function(date, inc, isExclusive) {
  311. var out = date.clone();
  312. inc = inc || 1;
  313. while (
  314. this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  315. ) {
  316. out.add(inc, 'days');
  317. }
  318. return out;
  319. }
  320. });