eventUtil.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import {
  2. format,
  3. isSameMonth,
  4. isWeekend,
  5. differenceInCalendarDays,
  6. isBefore,
  7. addDays,
  8. startOfWeek,
  9. endOfWeek,
  10. getSeconds,
  11. differenceInHours,
  12. getMinutes,
  13. getHours,
  14. addHours,
  15. isSameDay,
  16. endOfDay,
  17. startOfDay,
  18. toDate,
  19. Locale,
  20. } from 'date-fns';
  21. import { EventObject, ParsedRangeEvent } from './foundation';
  22. const copyEvent = (event: EventObject, date: Date, start?: Date, end?: Date, allDay = false) => {
  23. const copied = { ...event };
  24. copied.date = date;
  25. start ? copied.start = start : null;
  26. end ? copied.end = end : null;
  27. copied.allDay = allDay;
  28. return copied;
  29. };
  30. const isDateInRange = (dirtyDate: Date, dirtyStart: Date, dirtyEnd: Date) => {
  31. const date = toDate(dirtyDate);
  32. const start = toDate(dirtyStart);
  33. const end = toDate(dirtyEnd);
  34. return date.getTime() < end.getTime() && date.getTime() >= start.getTime();
  35. };
  36. export const sortDate = (a: Date | string, b: Date | string) => {
  37. const res = isBefore(new Date(a), new Date(b)) ? -1 : 1;
  38. return res;
  39. };
  40. export const checkWeekend = (val: Date) => isWeekend(val);
  41. export const getCurrDate = () => new Date();
  42. export const round = (value: number) => Math.round(value * 1000) / 1000;
  43. export const getPos = (value: Date | number) => {
  44. const currSec = (getHours(value) * 60 + getMinutes(value)) * 60 + getSeconds(value);
  45. const totalSec = 24 * 60 * 60;
  46. return currSec / totalSec;
  47. };
  48. export const isAllDayEvent = (event: EventObject) => 'allDay' in event && event.allDay;
  49. /**
  50. *
  51. * @param {object} event
  52. * normalize event object:
  53. * if event object does not have start time, add start time = end time - 1h; if not same day, then startday of the end
  54. * if event object does not have end time, add end time = start time + 1h; if not same day, then endday of the start
  55. */
  56. export const amendEvent = (event: EventObject) => {
  57. const { start, end } = event;
  58. if (!start && !end) {
  59. return undefined;
  60. } else if (!start) {
  61. event.start = isSameDay(end, addHours(end, -1)) ? addHours(end, -1) : startOfDay(end);
  62. } else {
  63. event.end = isSameDay(start, addHours(start, 1)) ? addHours(start, 1) : endOfDay(start);
  64. }
  65. return event;
  66. };
  67. /**
  68. *
  69. * @param {arr} events
  70. * find the max topInd and used as row height
  71. */
  72. export const calcRowHeight = (events: ParsedRangeEvent[]) => {
  73. const topIndArr = events.map(item => item.topInd);
  74. return topIndArr.length ? Math.max(...topIndArr) + 1 : 1;
  75. };
  76. export interface DateObj {
  77. ind: number;
  78. date: Date;
  79. dayString: string;
  80. weekday: string;
  81. isToday: boolean;
  82. isWeekend: boolean;
  83. isSameMonth: boolean;
  84. month: string;
  85. }
  86. export type weeekStartsOnEnum = 0 | 1 | 2 | 3 | 4 | 5 | 6;
  87. export const calcRangeData = (value: Date, start: Date, rangeLen: number, mode: string, locale: Locale) => {
  88. const today = getCurrDate();
  89. const arr: Array<DateObj> = [];
  90. [...Array(rangeLen).keys()].map(ind => {
  91. const dateObj = {} as DateObj;
  92. const date = addDays(start, ind);
  93. dateObj.ind = ind;
  94. dateObj.date = date;
  95. dateObj.dayString = format(date, 'd', { locale });
  96. dateObj.weekday = format(date, 'EEE', { locale });
  97. dateObj.isToday = isSameDay(date, today);
  98. dateObj.isWeekend = checkWeekend(date);
  99. if (mode === 'month') {
  100. dateObj.isSameMonth = isSameMonth(value, date);
  101. dateObj.month = format(date, 'LLL', { locale });
  102. }
  103. arr.push(dateObj);
  104. });
  105. return arr;
  106. };
  107. /**
  108. *
  109. * @param {value} date
  110. * @param {string} mode
  111. * @param {string} locale
  112. * @returns {object[]} { date: Date, dayString: string, ind: number, isToday: boolean, isWeekend: boolean, weekday: string }
  113. * create weekly object array
  114. */
  115. export const calcWeekData = (value: Date, mode = 'week', locale: Locale, weekStartsOn: weeekStartsOnEnum) => {
  116. const start = startOfWeek(value, { weekStartsOn });
  117. return calcRangeData(value, start, 7, mode, locale);
  118. };
  119. /**
  120. *
  121. * @param {object} event
  122. * @param {boolean} allDay
  123. * @returns {object[]} { allDay: boolean, data: Date, start: Date, end: Date, children: ReactNode }
  124. * parsed a spanned all-day event into multiple dates
  125. */
  126. export const parseAllDayEvent = (event: EventObject, allDay = true, currDate: Date = undefined) => {
  127. const res = [];
  128. const { start, end } = event;
  129. if (start && end) {
  130. const diff = differenceInCalendarDays(end, start);
  131. [...Array(diff + 1).keys()].map(day => {
  132. res.push(copyEvent(event, addDays(start, day), null, null, allDay));
  133. });
  134. } else {
  135. const date = start || end || currDate;
  136. res.push(copyEvent(event, startOfDay(date), null, null, allDay));
  137. }
  138. return res;
  139. };
  140. /**
  141. *
  142. * @param {object} event
  143. * @returns {object[]} { allDay: boolean, data: Date, start: Date, end: Date, children: ReactNode }
  144. * parsed events
  145. */
  146. export const parseEvent = (event: EventObject) => {
  147. const { start, end } = event;
  148. let res: EventObject[] = [];
  149. if (isAllDayEvent(event)) {
  150. return parseAllDayEvent(event);
  151. }
  152. if (start && end) {
  153. if (!isBefore(start, end)) {
  154. [event.start, event.end] = [event.end, event.start];
  155. }
  156. if (isSameDay(start, end)) {
  157. res.push(copyEvent(event, startOfDay(start)));
  158. } else if (Math.abs(differenceInHours(start, end)) < 24) {
  159. res.push(copyEvent(event, startOfDay(start), null, endOfDay(start)));
  160. res.push(copyEvent(event, startOfDay(end), startOfDay(end)));
  161. } else {
  162. res = res.concat(parseAllDayEvent(event));
  163. }
  164. } else {
  165. const amend = amendEvent(event);
  166. res.push(copyEvent(amend, startOfDay(amend.start)));
  167. }
  168. return res;
  169. };
  170. /**
  171. *
  172. * @param {arr} arr
  173. * @param {key}
  174. * @param {function} func callback function
  175. * @returns {map}
  176. * convert events array to may, use datestring as key
  177. */
  178. export const convertEventsArrToMap = (
  179. arr: EventObject[],
  180. key: 'start' | 'date',
  181. func: (val: Date) => Date,
  182. displayValue?: Date
  183. ) => {
  184. const res = new Map();
  185. arr.forEach(item => {
  186. let val;
  187. if (key in item) {
  188. val = item[key];
  189. } else {
  190. val = startOfDay(displayValue);
  191. }
  192. const k = func ? func(val).toString() : val.toString();
  193. if (res.has(k)) {
  194. res.get(k).push(item);
  195. } else {
  196. res.set(k, [item]);
  197. }
  198. });
  199. return res;
  200. };
  201. /**
  202. * @returns {arr}
  203. * filter out event that is not in the date range
  204. */
  205. export const filterEvents = (events: Map<string, EventObject[]>, start: Date, end: Date) => {
  206. const res = new Map<string, EventObject[]>();
  207. [...events.keys()].map(day => {
  208. const item = events.get(day);
  209. const date = new Date(day);
  210. if (isDateInRange(date, start, end)) {
  211. res.set(day, item);
  212. } else if (isBefore(end, date)) {
  213. // do nothing
  214. } else {
  215. const filtered = item.filter(i => !i.end || !isBefore(i.end, start));
  216. const key = start.toString();
  217. if (res.has(key)) {
  218. res.set(key, [...res.get(key), ...filtered]);
  219. } else {
  220. res.set(key, item);
  221. }
  222. }
  223. });
  224. return res;
  225. };
  226. /**
  227. * @returns {arr}
  228. * filter out event that is not in the week range
  229. */
  230. // eslint-disable-next-line max-len
  231. export const filterWeeklyEvents = (events: Map<string, EventObject[]>, weekStart: Date) => filterEvents(events, weekStart, addDays(endOfWeek(weekStart), 1));
  232. /**
  233. * @returns {arr}
  234. * arrange and sort all day event for a range
  235. */
  236. export const parseRangeAllDayEvent = (
  237. event: EventObject[],
  238. startDate: Date,
  239. rangeStart: Date,
  240. rangeEnd: Date,
  241. parsed: Array<Array<ParsedRangeEvent>>
  242. ) => {
  243. const dateRangeLen = differenceInCalendarDays(rangeEnd, rangeStart);
  244. event.sort((a, b) => sortDate(a.start, b.start)).forEach(item => {
  245. const itemInfo = { ...item };
  246. const { end } = item;
  247. let dateLength;
  248. const j = differenceInCalendarDays(startDate, rangeStart);
  249. let i = 0;
  250. while (Boolean(parsed[i]) && Boolean(parsed[i][j])) {
  251. i++;
  252. }
  253. if (!end) {
  254. dateLength = 0;
  255. } else {
  256. dateLength = isDateInRange(end, rangeStart, rangeEnd) ?
  257. differenceInCalendarDays(end, startDate) :
  258. differenceInCalendarDays(rangeEnd, startDate);
  259. }
  260. itemInfo.leftPos = round(Number(j) / dateRangeLen);
  261. itemInfo.width = Math.min(1 - round(Number(j) / dateRangeLen), round((dateLength + 1) * 1 / dateRangeLen));
  262. itemInfo.topInd = i;
  263. [...Array(dateLength + 1).keys()].forEach(dist => {
  264. if (!parsed[i]) {
  265. parsed[i] = [];
  266. }
  267. if (dist > 0) {
  268. parsed[i][j + dist] = item;
  269. } else {
  270. parsed[i][j + dist] = itemInfo;
  271. }
  272. });
  273. });
  274. return parsed;
  275. };
  276. /**
  277. * @returns {arr}
  278. * arrange and sort weekly all day event
  279. */
  280. export const parseWeeklyAllDayEvent = (
  281. event: EventObject[],
  282. startDate: Date,
  283. weekStart: Date,
  284. parsed: Array<Array<ParsedRangeEvent>>
  285. ) => parseRangeAllDayEvent(event, startDate, weekStart, addDays(endOfWeek(startDate), 1), parsed);
  286. export const collectDailyEvents = (events: ParsedRangeEvent[][]) => {
  287. const collections = {} as ParsedRangeEvent[][];
  288. events.forEach((row, rowInd) => {
  289. row.forEach((event, ind) => {
  290. if (collections[ind]) {
  291. collections[ind][rowInd] = event;
  292. } else {
  293. collections[ind] = [];
  294. collections[ind][rowInd] = event;
  295. }
  296. });
  297. });
  298. return collections;
  299. };
  300. export const renderDailyEvent = (event: EventObject) => {
  301. // eslint-disable-next-line prefer-const
  302. let { start, end, allDay, children } = event;
  303. let startPos,
  304. endPos;
  305. if (isAllDayEvent(event)) {
  306. startPos = 0;
  307. endPos = 0;
  308. } else if (!start || !end) {
  309. const amend = amendEvent(event);
  310. endPos = getPos(amend.end);
  311. startPos = getPos(amend.start);
  312. } else {
  313. if (!isBefore(start, end)) {
  314. [start, end] = [end, start];
  315. }
  316. startPos = getPos(start);
  317. endPos = getPos(end);
  318. }
  319. const parsed = {
  320. startPos: round(startPos),
  321. endPos: round(endPos),
  322. children,
  323. allDay: Boolean(allDay),
  324. };
  325. return parsed;
  326. };