eventUtil.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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 weekStartsOnEnum = 0 | 1 | 2 | 3 | 4 | 5 | 6;
  87. export const calcRangeData = (value: Date, start: Date, rangeLen: number, mode: string, locale: Locale, weekStartsOn: weekStartsOnEnum) => {
  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, weekStartsOn });
  96. dateObj.weekday = format(date, 'EEE', { locale, weekStartsOn });
  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 {Date} date
  110. * @param {Date} monthStart current month start date, using for month mode
  111. * @param {string} mode
  112. * @param {string} locale
  113. * @returns {object[]} { date: Date, dayString: string, ind: number, isToday: boolean, isWeekend: boolean, weekday: string }
  114. * create weekly object array
  115. */
  116. export const calcWeekData = (value: Date, monthStart: Date | null, mode = 'week', locale: Locale, weekStartsOn: weekStartsOnEnum) => {
  117. const start = startOfWeek(value, { weekStartsOn });
  118. const realValue = monthStart || value;
  119. return calcRangeData(realValue, start, 7, mode, locale, weekStartsOn);
  120. };
  121. /**
  122. *
  123. * @param {object} event
  124. * @param {boolean} allDay
  125. * @returns {object[]} { allDay: boolean, data: Date, start: Date, end: Date, children: ReactNode }
  126. * parsed a spanned all-day event into multiple dates
  127. */
  128. export const parseAllDayEvent = (event: EventObject, allDay = true, currDate: Date = undefined) => {
  129. const res = [];
  130. const { start, end } = event;
  131. if (start && end) {
  132. const diff = differenceInCalendarDays(end, start);
  133. [...Array(diff + 1).keys()].map(day => {
  134. res.push(copyEvent(event, addDays(start, day), null, null, allDay));
  135. });
  136. } else {
  137. const date = start || end || currDate;
  138. res.push(copyEvent(event, startOfDay(date), null, null, allDay));
  139. }
  140. return res;
  141. };
  142. /**
  143. *
  144. * @param {object} event
  145. * @returns {object[]} { allDay: boolean, data: Date, start: Date, end: Date, children: ReactNode }
  146. * parsed events
  147. */
  148. export const parseEvent = (event: EventObject) => {
  149. const { start, end } = event;
  150. let res: EventObject[] = [];
  151. if (isAllDayEvent(event)) {
  152. return parseAllDayEvent(event);
  153. }
  154. if (start && end) {
  155. if (!isBefore(start, end)) {
  156. [event.start, event.end] = [event.end, event.start];
  157. }
  158. if (isSameDay(start, end)) {
  159. res.push(copyEvent(event, startOfDay(start)));
  160. } else if (Math.abs(differenceInHours(start, end)) < 24) {
  161. res.push(copyEvent(event, startOfDay(start), null, endOfDay(start)));
  162. res.push(copyEvent(event, startOfDay(end), startOfDay(end)));
  163. } else {
  164. res = res.concat(parseAllDayEvent(event));
  165. }
  166. } else {
  167. const amend = amendEvent(event);
  168. res.push(copyEvent(amend, startOfDay(amend.start)));
  169. }
  170. return res;
  171. };
  172. /**
  173. *
  174. * @param {arr} arr
  175. * @param {key}
  176. * @param {function} func callback function
  177. * @returns {map}
  178. * convert events array to may, use datestring as key
  179. */
  180. export const convertEventsArrToMap = (
  181. arr: EventObject[],
  182. key: 'start' | 'date',
  183. func: (val: Date) => Date,
  184. displayValue?: Date
  185. ) => {
  186. const res = new Map();
  187. arr.forEach(item => {
  188. let val;
  189. if (key in item) {
  190. val = item[key];
  191. } else {
  192. val = startOfDay(displayValue);
  193. }
  194. const k = func ? func(val).toString() : val.toString();
  195. if (res.has(k)) {
  196. res.get(k).push(item);
  197. } else {
  198. res.set(k, [item]);
  199. }
  200. });
  201. return res;
  202. };
  203. /**
  204. * @returns {arr}
  205. * filter out event that is not in the date range
  206. */
  207. export const filterEvents = (events: Map<string, EventObject[]>, start: Date, end: Date) => {
  208. const res = new Map<string, EventObject[]>();
  209. [...events.keys()].map(day => {
  210. const item = events.get(day);
  211. const date = new Date(day);
  212. if (isDateInRange(date, start, end)) {
  213. res.set(day, item);
  214. } else if (isBefore(end, date)) {
  215. // do nothing
  216. } else {
  217. const filtered = item.filter(i => !i.end || !isBefore(i.end, start));
  218. const key = start.toString();
  219. if (res.has(key)) {
  220. res.set(key, [...res.get(key), ...filtered]);
  221. } else {
  222. res.set(key, item);
  223. }
  224. }
  225. });
  226. return res;
  227. };
  228. /**
  229. * @returns {arr}
  230. * filter out event that is not in the week range
  231. */
  232. // eslint-disable-next-line max-len
  233. export const filterWeeklyEvents = (events: Map<string, EventObject[]>, weekStart: Date, weekStartsOn: weekStartsOnEnum ) => filterEvents(events, weekStart, addDays(endOfWeek(weekStart, { weekStartsOn }), 1));
  234. /**
  235. * @returns {arr}
  236. * arrange and sort all day event for a range
  237. */
  238. export const parseRangeAllDayEvent = (
  239. event: EventObject[],
  240. startDate: Date,
  241. rangeStart: Date,
  242. rangeEnd: Date,
  243. parsed: Array<Array<ParsedRangeEvent>>
  244. ) => {
  245. const dateRangeLen = differenceInCalendarDays(rangeEnd, rangeStart);
  246. event.sort((a, b) => sortDate(a.start, b.start)).forEach(item => {
  247. const itemInfo = { ...item };
  248. const { end } = item;
  249. let dateLength;
  250. const j = differenceInCalendarDays(startDate, rangeStart);
  251. let i = 0;
  252. while (Boolean(parsed[i]) && Boolean(parsed[i][j])) {
  253. i++;
  254. }
  255. if (!end) {
  256. dateLength = 0;
  257. } else {
  258. dateLength = isDateInRange(end, rangeStart, rangeEnd) ?
  259. differenceInCalendarDays(end, startDate) :
  260. differenceInCalendarDays(rangeEnd, startDate);
  261. }
  262. itemInfo.leftPos = round(Number(j) / dateRangeLen);
  263. itemInfo.width = Math.min(1 - round(Number(j) / dateRangeLen), round((dateLength + 1) * 1 / dateRangeLen));
  264. itemInfo.topInd = i;
  265. [...Array(dateLength + 1).keys()].forEach(dist => {
  266. if (!parsed[i]) {
  267. parsed[i] = [];
  268. }
  269. if (dist > 0) {
  270. parsed[i][j + dist] = item;
  271. } else {
  272. parsed[i][j + dist] = itemInfo;
  273. }
  274. });
  275. });
  276. return parsed;
  277. };
  278. /**
  279. * @returns {arr}
  280. * arrange and sort weekly all day event
  281. */
  282. export const parseWeeklyAllDayEvent = (
  283. event: EventObject[],
  284. startDate: Date,
  285. weekStart: Date,
  286. parsed: Array<Array<ParsedRangeEvent>>,
  287. weekStartsOn: weekStartsOnEnum
  288. ) => parseRangeAllDayEvent(event, startDate, weekStart, addDays(endOfWeek(startDate, { weekStartsOn }), 1), parsed);
  289. export const collectDailyEvents = (events: ParsedRangeEvent[][]) => {
  290. const collections = {} as ParsedRangeEvent[][];
  291. events.forEach((row, rowInd) => {
  292. row.forEach((event, ind) => {
  293. if (collections[ind]) {
  294. collections[ind][rowInd] = event;
  295. } else {
  296. collections[ind] = [];
  297. collections[ind][rowInd] = event;
  298. }
  299. });
  300. });
  301. return collections;
  302. };
  303. export const renderDailyEvent = (event: EventObject) => {
  304. // eslint-disable-next-line prefer-const
  305. let { start, end, allDay, children } = event;
  306. let startPos,
  307. endPos;
  308. if (isAllDayEvent(event)) {
  309. startPos = 0;
  310. endPos = 0;
  311. } else if (!start || !end) {
  312. const amend = amendEvent(event);
  313. endPos = getPos(amend.end);
  314. startPos = getPos(amend.start);
  315. } else {
  316. if (!isBefore(start, end)) {
  317. [start, end] = [end, start];
  318. }
  319. startPos = getPos(start);
  320. endPos = getPos(end);
  321. }
  322. const parsed = {
  323. startPos: round(startPos),
  324. endPos: round(endPos),
  325. children,
  326. allDay: Boolean(allDay),
  327. };
  328. return parsed;
  329. };