foundation.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. import { format,
  3. getWeeksInMonth,
  4. getWeekOfMonth,
  5. isSameMonth,
  6. startOfMonth,
  7. endOfMonth,
  8. isBefore,
  9. isAfter,
  10. addDays,
  11. startOfWeek,
  12. differenceInCalendarDays,
  13. isSameDay,
  14. startOfDay,
  15. isSameWeek,
  16. Locale } from 'date-fns';
  17. import {
  18. parseEvent,
  19. parseAllDayEvent,
  20. calcWeekData,
  21. getCurrDate,
  22. parseWeeklyAllDayEvent,
  23. sortDate,
  24. collectDailyEvents,
  25. round,
  26. getPos,
  27. convertEventsArrToMap,
  28. filterWeeklyEvents,
  29. renderDailyEvent,
  30. calcRangeData,
  31. filterEvents,
  32. parseRangeAllDayEvent,
  33. DateObj,
  34. checkWeekend
  35. } from './eventUtil';
  36. export interface EventObject {
  37. [x: string]: any;
  38. key: string;
  39. allDay?: boolean;
  40. start?: Date;
  41. end?: Date;
  42. children?: React.ReactNode;
  43. }
  44. export interface ParsedEventsWithArray {
  45. day: Array<any>;
  46. allDay: Array<any>;
  47. }
  48. export interface ParsedEvents {
  49. day?: Map<string, Array<EventObject>>;
  50. allDay?: Map<string, Array<EventObject>>;
  51. }
  52. export interface ParsedRangeEvent extends EventObject {
  53. leftPos?: number;
  54. width?: number;
  55. topInd?: number;
  56. }
  57. export interface MonthlyEvent {
  58. day: ParsedRangeEvent[][];
  59. display: ParsedRangeEvent[];
  60. }
  61. export interface RangeData {
  62. month: string;
  63. week: any[];
  64. }
  65. export interface WeeklyData {
  66. month: string;
  67. week: DateObj[];
  68. }
  69. export type MonthData = Record<number, DateObj[]>;
  70. // export interface CalendarAdapter extends DefaultAdapter {
  71. // updateScrollHeight: (scrollHeight: number) => void;
  72. // setParsedEvents: (parsedEvents: ParsedEvents) => void;
  73. // cacheEventKeys: (cachedKeys: Array<string>) => void;
  74. // }
  75. export interface CalendarAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  76. updateCurrPos?: (currPos: number) => void;
  77. updateShowCurrTime?: () => void;
  78. updateScrollHeight?: (scrollHeight: number) => void;
  79. setParsedEvents?: (parsedEvents: ParsedEvents | ParsedEventsWithArray | MonthlyEvent) => void;
  80. cacheEventKeys?: (cachedKeys: Array<string>) => void;
  81. setRangeData?: (data: RangeData) => void;
  82. getRangeData?: () => RangeData;
  83. setWeeklyData?: (data: WeeklyData) => void;
  84. getWeeklyData?: () => WeeklyData;
  85. registerClickOutsideHandler?: (key: string, cb: () => void) => void;
  86. unregisterClickOutsideHandler?: () => void;
  87. setMonthlyData?: (data: MonthData) => void;
  88. getMonthlyData?: () => MonthData;
  89. notifyClose?: (e: any, key: string) => void;
  90. openCard?: (key: string, spacing: boolean) => void;
  91. setItemLimit?: (itemLimit: number) => void;
  92. }
  93. export default class CalendarFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<CalendarAdapter<P, S>, P, S> {
  94. raf: number;
  95. constructor(adapter: CalendarAdapter<P, S>) {
  96. super({ ...adapter });
  97. }
  98. // eslint-disable-next-line @typescript-eslint/no-empty-function
  99. init() {
  100. }
  101. destroy() {
  102. this.raf && cancelAnimationFrame(this.raf);
  103. }
  104. initCurrTime() {
  105. const { showCurrTime, displayValue } = this.getProps();
  106. if (showCurrTime && isSameDay(displayValue, getCurrDate())) {
  107. this._adapter.updateShowCurrTime();
  108. this.getCurrLocation();
  109. }
  110. }
  111. notifyScrollHeight(height: number) {
  112. this._adapter.updateScrollHeight(height);
  113. }
  114. closeCard(e: any, key: string) {
  115. this._adapter.unregisterClickOutsideHandler();
  116. this._adapter.notifyClose(e, key);
  117. }
  118. _getDate() {
  119. const { displayValue } = this.getProps();
  120. return displayValue || getCurrDate();
  121. }
  122. showCard(e: any, key: string) {
  123. this._adapter.unregisterClickOutsideHandler();
  124. const bodyWidth = document.querySelector('body').clientWidth;
  125. const popoverWidth = 110;
  126. const spacing = bodyWidth - e.target.getBoundingClientRect().right - popoverWidth;
  127. this._adapter.openCard(key, spacing > 0);
  128. this._adapter.registerClickOutsideHandler(key, () => {
  129. this.closeCard(null, key);
  130. });
  131. }
  132. formatCbValue(val: [Date, number, number, number] | [Date]) {
  133. const date = val.shift() as Date;
  134. const dateArr = [date.getFullYear(), date.getMonth(), date.getDate(), ...val];
  135. // @ts-ignore skip
  136. return new Date(...dateArr);
  137. }
  138. /**
  139. *
  140. * find the location of showCurrTime red line
  141. */
  142. getCurrLocation() {
  143. let startTime: number = null;
  144. let pos = getPos(getCurrDate());
  145. this._adapter.updateCurrPos(round(pos));
  146. const frameFunc = () => {
  147. const timestamp = Date.now();
  148. if (!startTime) {
  149. startTime = timestamp;
  150. }
  151. const time = timestamp - startTime;
  152. if (time > 30000) {
  153. pos = getPos(getCurrDate());
  154. this._adapter.updateCurrPos(round(pos));
  155. startTime = timestamp;
  156. }
  157. this.raf = requestAnimationFrame(frameFunc);
  158. };
  159. this.raf = requestAnimationFrame(frameFunc);
  160. }
  161. getWeeklyData(value: Date, dateFnsLocale: Locale) {
  162. const data = {} as WeeklyData;
  163. data.month = format(value, 'LLL', { locale: dateFnsLocale });
  164. data.week = calcWeekData(value, 'week', dateFnsLocale);
  165. this._adapter.setWeeklyData(data);
  166. return data;
  167. }
  168. getRangeData(value: Date, dateFnsLocale: Locale) {
  169. const data = {} as { month: string; week: Array<DateObj> };
  170. const { range } = this.getProps();
  171. const len = differenceInCalendarDays(range[1], range[0]);
  172. data.month = format(value, 'LLL', { locale: dateFnsLocale });
  173. data.week = calcRangeData(value, range[0], len, 'week', dateFnsLocale);
  174. this._adapter.setRangeData(data);
  175. return data;
  176. }
  177. getMonthlyData(value: Date, dateFnsLocale: Locale) {
  178. const monthStart = startOfMonth(value);
  179. const data = {} as MonthData;
  180. const numberOfWeek = getWeeksInMonth(value);
  181. [...Array(numberOfWeek).keys()].map(ind => {
  182. data[ind] = calcWeekData(addDays(monthStart, ind * 7), 'month', dateFnsLocale);
  183. });
  184. this._adapter.setMonthlyData(data);
  185. return data;
  186. }
  187. // ================== Daily Event ==================
  188. _parseEvents(events: EventObject[]) {
  189. const parsed: ParsedEventsWithArray = {
  190. allDay: [],
  191. day: []
  192. };
  193. events.map(event => parseEvent(event)).forEach(item => {
  194. item.forEach(i => {
  195. i.allDay ? parsed.allDay.push(i) : parsed.day.push(i);
  196. });
  197. });
  198. return parsed;
  199. }
  200. getParseDailyEvents(events: EventObject[], date: Date) {
  201. if (!date) {
  202. date = this._getDate();
  203. }
  204. const parsed = this._parseEvents(events);
  205. const { displayValue } = this.getProps();
  206. const key = startOfDay(date).toString();
  207. parsed.allDay = convertEventsArrToMap(parsed.allDay, 'date', startOfDay, displayValue).get(key);
  208. parsed.day = convertEventsArrToMap(parsed.day, 'date', null, displayValue).get(key);
  209. if (!parsed.allDay) {
  210. parsed.allDay = [];
  211. }
  212. if (!parsed.day) {
  213. parsed.day = [];
  214. }
  215. parsed.day = parsed.day.map(item => renderDailyEvent(item));
  216. return parsed;
  217. }
  218. parseDailyEvents() {
  219. const { events, displayValue } = this.getProps();
  220. const parsed = this.getParseDailyEvents(events, displayValue);
  221. this._adapter.setParsedEvents(parsed);
  222. this._adapter.cacheEventKeys((events as EventObject[]).map(i => i.key));
  223. }
  224. // ================== Weekly Event ==================
  225. _parseWeeklyEvents(events: ParsedEvents['allDay'], weekStart: Date) {
  226. let parsed = [[]] as ParsedRangeEvent[][];
  227. const filtered = filterWeeklyEvents(events, weekStart);
  228. [...filtered.keys()].sort((a, b) => sortDate(a, b)).forEach(item => {
  229. const startDate = new Date(item);
  230. const curr = filtered.get(item).filter(event => isSameDay(event.date, startDate));
  231. parsed = parseWeeklyAllDayEvent(curr, startDate, weekStart, parsed);
  232. });
  233. return parsed;
  234. }
  235. _renderWeeklyAllDayEvent(events: ParsedRangeEvent[][]) {
  236. const res = [] as ParsedRangeEvent[];
  237. events.forEach(row => {
  238. const event = row.filter(item => 'leftPos' in item);
  239. res.push(...event);
  240. });
  241. return res;
  242. }
  243. // return parsed weekly allday events
  244. parseWeeklyAllDayEvents(events: ParsedEvents['allDay']) {
  245. const { week } = this._adapter.getWeeklyData();
  246. const weekStart = week[0].date;
  247. const parsed = this._parseWeeklyEvents(events, weekStart);
  248. const res = this._renderWeeklyAllDayEvent(parsed);
  249. return res;
  250. }
  251. getParsedWeeklyEvents(events: EventObject[]) {
  252. const parsed = this._parseEvents(events);
  253. const { displayValue } = this.getProps();
  254. const result: ParsedEvents = {};
  255. result.allDay = convertEventsArrToMap(parsed.allDay, 'start', startOfDay, displayValue);
  256. result.day = convertEventsArrToMap(parsed.day, 'date', null, displayValue);
  257. return result;
  258. }
  259. // return parsed weekly allday events
  260. parseWeeklyEvents() {
  261. const { events } = this.getProps();
  262. const parsed = this.getParsedWeeklyEvents(events);
  263. this._adapter.setParsedEvents(parsed);
  264. this._adapter.cacheEventKeys((events as EventObject[]).map(i => i.key));
  265. }
  266. // ================== Monthly Event ==================
  267. pushDayEventIntoWeekMap(item: EventObject, index: number, map: Record<string, EventObject[]>) {
  268. if (index in map) {
  269. map[index].push(item);
  270. } else {
  271. map[index] = [item];
  272. }
  273. }
  274. convertMapToArray(weekMap: Map<string, EventObject[]>, weekStart: Date) {
  275. const eventArray = [];
  276. for (const entry of weekMap.entries()) {
  277. const [key, value] = entry;
  278. const map = new Map();
  279. map.set(key, value);
  280. const weekEvents = this._parseWeeklyEvents(map, weekStart);
  281. eventArray.push(...weekEvents);
  282. }
  283. return eventArray;
  284. }
  285. getParseMonthlyEvents(itemLimit: number) {
  286. const parsed: any = {};
  287. const { displayValue, events } = this.getProps();
  288. const currDate = this._getDate();
  289. const firstDayOfMonth = startOfMonth(displayValue);
  290. const lastDayOfMonth = endOfMonth(displayValue);
  291. const res: EventObject[] = [];
  292. (events as EventObject[]).sort((prev, next) => {
  293. if (isBefore(prev.start, next.start)) {
  294. return -1;
  295. }
  296. if (isAfter(prev.start, next.start)) {
  297. return 1;
  298. }
  299. return 0;
  300. }).forEach(event => {
  301. const parsedEvent = parseAllDayEvent(event, event.allDay, currDate);
  302. res.push(...parsedEvent);
  303. });
  304. res.filter(item => isSameMonth(item.date, displayValue));
  305. res.forEach(item => {
  306. // WeekInd calculation error, need to consider the boundary situation at the beginning/end of the month
  307. // When the date falls within the month
  308. if (isSameMonth(item.date, displayValue)) {
  309. const weekInd = getWeekOfMonth(item.date) - 1;
  310. this.pushDayEventIntoWeekMap(item, weekInd, parsed);
  311. return;
  312. }
  313. // When the date is within the previous month
  314. if (isBefore(item.date, firstDayOfMonth)) {
  315. if (isSameWeek(item.date, firstDayOfMonth)) {
  316. this.pushDayEventIntoWeekMap(item, 0, parsed);
  317. }
  318. return;
  319. }
  320. // When the date is within the next month
  321. if (isAfter(item.date, lastDayOfMonth)) {
  322. if (isSameWeek(item.date, lastDayOfMonth)) {
  323. const weekInd = getWeekOfMonth(lastDayOfMonth) - 1;
  324. this.pushDayEventIntoWeekMap(item, weekInd, parsed);
  325. }
  326. return;
  327. }
  328. });
  329. Object.keys(parsed).forEach(key => {
  330. const week = parsed[key];
  331. parsed[key] = {};
  332. const weekStart = startOfWeek(week[0].date);
  333. const weekMap = convertEventsArrToMap(week, 'start', startOfDay);
  334. // When there are multiple events in a week, multiple events should be parsed
  335. // const oldParsedWeeklyEvent = this._parseWeeklyEvents(weekMap, weekStart);
  336. const parsedWeeklyEvent = this.convertMapToArray(weekMap, weekStart);
  337. parsed[key].day = collectDailyEvents(parsedWeeklyEvent);
  338. parsed[key].display = this._renderDisplayEvents(parsedWeeklyEvent);
  339. });
  340. return parsed as MonthlyEvent;
  341. }
  342. parseMonthlyEvents(itemLimit: number) {
  343. const { events } = this.getProps();
  344. const parsed = this.getParseMonthlyEvents(itemLimit);
  345. this._adapter.setParsedEvents(parsed);
  346. this._adapter.setItemLimit(itemLimit);
  347. this._adapter.cacheEventKeys((events as EventObject[]).map(i => i.key));
  348. }
  349. _renderDisplayEvents(events: ParsedRangeEvent[][]) {
  350. // Limits should not be added when calculating the relative position of each event, because there will be calculations that separate two events in the middle of the week
  351. let displayEvents: ParsedRangeEvent[] | ParsedRangeEvent[][] = events.slice();
  352. if (displayEvents.length) {
  353. displayEvents = this._renderWeeklyAllDayEvent(displayEvents);
  354. }
  355. return displayEvents;
  356. }
  357. // ================== Range Event ==================
  358. _parseRangeEvents(events: Map<string, EventObject[]>) {
  359. let parsed: Array<Array<ParsedRangeEvent>> = [[]];
  360. const [start, end] = this.getProp('range');
  361. const filtered = filterEvents(events, start, end);
  362. [...filtered.keys()].sort((a, b) => sortDate(a, b)).forEach(item => {
  363. const startDate = new Date(item);
  364. const curr = filtered.get(item).filter(event => isSameDay(event.date, startDate));
  365. parsed = parseRangeAllDayEvent(curr, startDate, start, end, parsed);
  366. });
  367. return parsed;
  368. }
  369. _renderRangeAllDayEvent(events: Array<Array<ParsedRangeEvent>>) {
  370. let res: Array<ParsedRangeEvent> = [];
  371. events.forEach(row => {
  372. const event = row.filter(item => 'leftPos' in item);
  373. res = [...res, ...event];
  374. });
  375. return res;
  376. }
  377. // return parsed weekly allday events
  378. parseRangeAllDayEvents(events: ParsedEvents['allDay']) {
  379. const parsed = this._parseRangeEvents(events);
  380. const res = this._renderRangeAllDayEvent(parsed);
  381. return res;
  382. }
  383. getParsedRangeEvents(events: EventObject[]) {
  384. const parsed = this._parseEvents(events);
  385. const [start] = this.getProp('range');
  386. (parsed as unknown as ParsedEvents).allDay = convertEventsArrToMap(parsed.allDay, 'start', startOfDay, start);
  387. ((parsed as unknown as ParsedEvents)).day = convertEventsArrToMap(parsed.day, 'date', null, start);
  388. return parsed as unknown as ParsedEvents;
  389. }
  390. // return parsed weekly allday events
  391. parseRangeEvents() {
  392. const { events } = this.getProps() as { events: EventObject[] };
  393. const parsed = this.getParsedRangeEvents(events);
  394. this._adapter.setParsedEvents(parsed);
  395. this._adapter.cacheEventKeys(events.map(i => i.key));
  396. }
  397. checkWeekend(val: Date) {
  398. return checkWeekend(val);
  399. }
  400. }