internal.js 60 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145
  1. import { Splitter, hasBgRendering, createFormatter, ViewContextType, ContentContainer, BaseComponent, DateComponent, diffDays, buildNavLinkAttrs, WeekNumberContainer, getStickyHeaderDates, ViewContainer, SimpleScrollGrid, getStickyFooterScrollbar, NowTimer, NowIndicatorContainer, renderScrollShim, rangeContainsMarker, startOfDay, asRoughMs, createDuration, RefMap, PositionCache, MoreLinkContainer, SegHierarchy, groupIntersectingEntries, binarySearch, getEntrySpanEnd, buildEntryKey, StandardEvent, memoize, sortEventSegs, DayCellContainer, hasCustomDayCellContent, getSegMeta, buildIsoString, computeEarliestSegStart, buildEventRangeKey, BgEvent, renderFill, addDurations, multiplyDuration, wholeDivideDurations, Slicer, intersectRanges, formatIsoTimeString, DayHeader, DaySeriesModel, DayTableModel, injectStyles } from '@fullcalendar/core/internal.js';
  2. import { createElement, createRef, Fragment } from '@fullcalendar/core/preact.js';
  3. import { DayTable } from '@fullcalendar/daygrid/internal.js';
  4. class AllDaySplitter extends Splitter {
  5. getKeyInfo() {
  6. return {
  7. allDay: {},
  8. timed: {},
  9. };
  10. }
  11. getKeysForDateSpan(dateSpan) {
  12. if (dateSpan.allDay) {
  13. return ['allDay'];
  14. }
  15. return ['timed'];
  16. }
  17. getKeysForEventDef(eventDef) {
  18. if (!eventDef.allDay) {
  19. return ['timed'];
  20. }
  21. if (hasBgRendering(eventDef)) {
  22. return ['timed', 'allDay'];
  23. }
  24. return ['allDay'];
  25. }
  26. }
  27. const DEFAULT_SLAT_LABEL_FORMAT = createFormatter({
  28. hour: 'numeric',
  29. minute: '2-digit',
  30. omitZeroMinute: true,
  31. meridiem: 'short',
  32. });
  33. function TimeColsAxisCell(props) {
  34. let classNames = [
  35. 'fc-timegrid-slot',
  36. 'fc-timegrid-slot-label',
  37. props.isLabeled ? 'fc-scrollgrid-shrink' : 'fc-timegrid-slot-minor',
  38. ];
  39. return (createElement(ViewContextType.Consumer, null, (context) => {
  40. if (!props.isLabeled) {
  41. return (createElement("td", { className: classNames.join(' '), "data-time": props.isoTimeStr }));
  42. }
  43. let { dateEnv, options, viewApi } = context;
  44. let labelFormat = // TODO: fully pre-parse
  45. options.slotLabelFormat == null ? DEFAULT_SLAT_LABEL_FORMAT :
  46. Array.isArray(options.slotLabelFormat) ? createFormatter(options.slotLabelFormat[0]) :
  47. createFormatter(options.slotLabelFormat);
  48. let renderProps = {
  49. level: 0,
  50. time: props.time,
  51. date: dateEnv.toDate(props.date),
  52. view: viewApi,
  53. text: dateEnv.format(props.date, labelFormat),
  54. };
  55. return (createElement(ContentContainer, { elTag: "td", elClasses: classNames, elAttrs: {
  56. 'data-time': props.isoTimeStr,
  57. }, renderProps: renderProps, generatorName: "slotLabelContent", customGenerator: options.slotLabelContent, defaultGenerator: renderInnerContent, classNameGenerator: options.slotLabelClassNames, didMount: options.slotLabelDidMount, willUnmount: options.slotLabelWillUnmount }, (InnerContent) => (createElement("div", { className: "fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame" },
  58. createElement(InnerContent, { elTag: "div", elClasses: [
  59. 'fc-timegrid-slot-label-cushion',
  60. 'fc-scrollgrid-shrink-cushion',
  61. ] })))));
  62. }));
  63. }
  64. function renderInnerContent(props) {
  65. return props.text;
  66. }
  67. class TimeBodyAxis extends BaseComponent {
  68. render() {
  69. return this.props.slatMetas.map((slatMeta) => (createElement("tr", { key: slatMeta.key },
  70. createElement(TimeColsAxisCell, Object.assign({}, slatMeta)))));
  71. }
  72. }
  73. const DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'short' });
  74. const AUTO_ALL_DAY_MAX_EVENT_ROWS = 5;
  75. class TimeColsView extends DateComponent {
  76. constructor() {
  77. super(...arguments);
  78. this.allDaySplitter = new AllDaySplitter(); // for use by subclasses
  79. this.headerElRef = createRef();
  80. this.rootElRef = createRef();
  81. this.scrollerElRef = createRef();
  82. this.state = {
  83. slatCoords: null,
  84. };
  85. this.handleScrollTopRequest = (scrollTop) => {
  86. let scrollerEl = this.scrollerElRef.current;
  87. if (scrollerEl) { // TODO: not sure how this could ever be null. weirdness with the reducer
  88. scrollerEl.scrollTop = scrollTop;
  89. }
  90. };
  91. /* Header Render Methods
  92. ------------------------------------------------------------------------------------------------------------------*/
  93. this.renderHeadAxis = (rowKey, frameHeight = '') => {
  94. let { options } = this.context;
  95. let { dateProfile } = this.props;
  96. let range = dateProfile.renderRange;
  97. let dayCnt = diffDays(range.start, range.end);
  98. // only do in day views (to avoid doing in week views that dont need it)
  99. let navLinkAttrs = (dayCnt === 1)
  100. ? buildNavLinkAttrs(this.context, range.start, 'week')
  101. : {};
  102. if (options.weekNumbers && rowKey === 'day') {
  103. return (createElement(WeekNumberContainer, { elTag: "th", elClasses: [
  104. 'fc-timegrid-axis',
  105. 'fc-scrollgrid-shrink',
  106. ], elAttrs: {
  107. 'aria-hidden': true,
  108. }, date: range.start, defaultFormat: DEFAULT_WEEK_NUM_FORMAT }, (InnerContent) => (createElement("div", { className: [
  109. 'fc-timegrid-axis-frame',
  110. 'fc-scrollgrid-shrink-frame',
  111. 'fc-timegrid-axis-frame-liquid',
  112. ].join(' '), style: { height: frameHeight } },
  113. createElement(InnerContent, { elTag: "a", elClasses: [
  114. 'fc-timegrid-axis-cushion',
  115. 'fc-scrollgrid-shrink-cushion',
  116. 'fc-scrollgrid-sync-inner',
  117. ], elAttrs: navLinkAttrs })))));
  118. }
  119. return (createElement("th", { "aria-hidden": true, className: "fc-timegrid-axis" },
  120. createElement("div", { className: "fc-timegrid-axis-frame", style: { height: frameHeight } })));
  121. };
  122. /* Table Component Render Methods
  123. ------------------------------------------------------------------------------------------------------------------*/
  124. // only a one-way height sync. we don't send the axis inner-content height to the DayGrid,
  125. // but DayGrid still needs to have classNames on inner elements in order to measure.
  126. this.renderTableRowAxis = (rowHeight) => {
  127. let { options, viewApi } = this.context;
  128. let renderProps = {
  129. text: options.allDayText,
  130. view: viewApi,
  131. };
  132. return (
  133. // TODO: make reusable hook. used in list view too
  134. createElement(ContentContainer, { elTag: "td", elClasses: [
  135. 'fc-timegrid-axis',
  136. 'fc-scrollgrid-shrink',
  137. ], elAttrs: {
  138. 'aria-hidden': true,
  139. }, renderProps: renderProps, generatorName: "allDayContent", customGenerator: options.allDayContent, defaultGenerator: renderAllDayInner, classNameGenerator: options.allDayClassNames, didMount: options.allDayDidMount, willUnmount: options.allDayWillUnmount }, (InnerContent) => (createElement("div", { className: [
  140. 'fc-timegrid-axis-frame',
  141. 'fc-scrollgrid-shrink-frame',
  142. rowHeight == null ? ' fc-timegrid-axis-frame-liquid' : '',
  143. ].join(' '), style: { height: rowHeight } },
  144. createElement(InnerContent, { elTag: "span", elClasses: [
  145. 'fc-timegrid-axis-cushion',
  146. 'fc-scrollgrid-shrink-cushion',
  147. 'fc-scrollgrid-sync-inner',
  148. ] })))));
  149. };
  150. this.handleSlatCoords = (slatCoords) => {
  151. this.setState({ slatCoords });
  152. };
  153. }
  154. // rendering
  155. // ----------------------------------------------------------------------------------------------------
  156. renderSimpleLayout(headerRowContent, allDayContent, timeContent) {
  157. let { context, props } = this;
  158. let sections = [];
  159. let stickyHeaderDates = getStickyHeaderDates(context.options);
  160. if (headerRowContent) {
  161. sections.push({
  162. type: 'header',
  163. key: 'header',
  164. isSticky: stickyHeaderDates,
  165. chunk: {
  166. elRef: this.headerElRef,
  167. tableClassName: 'fc-col-header',
  168. rowContent: headerRowContent,
  169. },
  170. });
  171. }
  172. if (allDayContent) {
  173. sections.push({
  174. type: 'body',
  175. key: 'all-day',
  176. chunk: { content: allDayContent },
  177. });
  178. sections.push({
  179. type: 'body',
  180. key: 'all-day-divider',
  181. outerContent: ( // TODO: rename to cellContent so don't need to define <tr>?
  182. createElement("tr", { role: "presentation", className: "fc-scrollgrid-section" },
  183. createElement("td", { className: 'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded') }))),
  184. });
  185. }
  186. sections.push({
  187. type: 'body',
  188. key: 'body',
  189. liquid: true,
  190. expandRows: Boolean(context.options.expandRows),
  191. chunk: {
  192. scrollerElRef: this.scrollerElRef,
  193. content: timeContent,
  194. },
  195. });
  196. return (createElement(ViewContainer, { elRef: this.rootElRef, elClasses: ['fc-timegrid'], viewSpec: context.viewSpec },
  197. createElement(SimpleScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, collapsibleWidth: props.forPrint, cols: [{ width: 'shrink' }], sections: sections })));
  198. }
  199. renderHScrollLayout(headerRowContent, allDayContent, timeContent, colCnt, dayMinWidth, slatMetas, slatCoords) {
  200. let ScrollGrid = this.context.pluginHooks.scrollGridImpl;
  201. if (!ScrollGrid) {
  202. throw new Error('No ScrollGrid implementation');
  203. }
  204. let { context, props } = this;
  205. let stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options);
  206. let stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options);
  207. let sections = [];
  208. if (headerRowContent) {
  209. sections.push({
  210. type: 'header',
  211. key: 'header',
  212. isSticky: stickyHeaderDates,
  213. syncRowHeights: true,
  214. chunks: [
  215. {
  216. key: 'axis',
  217. rowContent: (arg) => (createElement("tr", { role: "presentation" }, this.renderHeadAxis('day', arg.rowSyncHeights[0]))),
  218. },
  219. {
  220. key: 'cols',
  221. elRef: this.headerElRef,
  222. tableClassName: 'fc-col-header',
  223. rowContent: headerRowContent,
  224. },
  225. ],
  226. });
  227. }
  228. if (allDayContent) {
  229. sections.push({
  230. type: 'body',
  231. key: 'all-day',
  232. syncRowHeights: true,
  233. chunks: [
  234. {
  235. key: 'axis',
  236. rowContent: (contentArg) => (createElement("tr", { role: "presentation" }, this.renderTableRowAxis(contentArg.rowSyncHeights[0]))),
  237. },
  238. {
  239. key: 'cols',
  240. content: allDayContent,
  241. },
  242. ],
  243. });
  244. sections.push({
  245. key: 'all-day-divider',
  246. type: 'body',
  247. outerContent: ( // TODO: rename to cellContent so don't need to define <tr>?
  248. createElement("tr", { role: "presentation", className: "fc-scrollgrid-section" },
  249. createElement("td", { colSpan: 2, className: 'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded') }))),
  250. });
  251. }
  252. let isNowIndicator = context.options.nowIndicator;
  253. sections.push({
  254. type: 'body',
  255. key: 'body',
  256. liquid: true,
  257. expandRows: Boolean(context.options.expandRows),
  258. chunks: [
  259. {
  260. key: 'axis',
  261. content: (arg) => (
  262. // TODO: make this now-indicator arrow more DRY with TimeColsContent
  263. createElement("div", { className: "fc-timegrid-axis-chunk" },
  264. createElement("table", { "aria-hidden": true, style: { height: arg.expandRows ? arg.clientHeight : '' } },
  265. arg.tableColGroupNode,
  266. createElement("tbody", null,
  267. createElement(TimeBodyAxis, { slatMetas: slatMetas }))),
  268. createElement("div", { className: "fc-timegrid-now-indicator-container" },
  269. createElement(NowTimer, { unit: isNowIndicator ? 'minute' : 'day' /* hacky */ }, (nowDate) => {
  270. let nowIndicatorTop = isNowIndicator &&
  271. slatCoords &&
  272. slatCoords.safeComputeTop(nowDate); // might return void
  273. if (typeof nowIndicatorTop === 'number') {
  274. return (createElement(NowIndicatorContainer, { elClasses: ['fc-timegrid-now-indicator-arrow'], elStyle: { top: nowIndicatorTop }, isAxis: true, date: nowDate }));
  275. }
  276. return null;
  277. })))),
  278. },
  279. {
  280. key: 'cols',
  281. scrollerElRef: this.scrollerElRef,
  282. content: timeContent,
  283. },
  284. ],
  285. });
  286. if (stickyFooterScrollbar) {
  287. sections.push({
  288. key: 'footer',
  289. type: 'footer',
  290. isSticky: true,
  291. chunks: [
  292. {
  293. key: 'axis',
  294. content: renderScrollShim,
  295. },
  296. {
  297. key: 'cols',
  298. content: renderScrollShim,
  299. },
  300. ],
  301. });
  302. }
  303. return (createElement(ViewContainer, { elRef: this.rootElRef, elClasses: ['fc-timegrid'], viewSpec: context.viewSpec },
  304. createElement(ScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, forPrint: props.forPrint, collapsibleWidth: false, colGroups: [
  305. { width: 'shrink', cols: [{ width: 'shrink' }] },
  306. { cols: [{ span: colCnt, minWidth: dayMinWidth }] },
  307. ], sections: sections })));
  308. }
  309. /* Dimensions
  310. ------------------------------------------------------------------------------------------------------------------*/
  311. getAllDayMaxEventProps() {
  312. let { dayMaxEvents, dayMaxEventRows } = this.context.options;
  313. if (dayMaxEvents === true || dayMaxEventRows === true) { // is auto?
  314. dayMaxEvents = undefined;
  315. dayMaxEventRows = AUTO_ALL_DAY_MAX_EVENT_ROWS; // make sure "auto" goes to a real number
  316. }
  317. return { dayMaxEvents, dayMaxEventRows };
  318. }
  319. }
  320. function renderAllDayInner(renderProps) {
  321. return renderProps.text;
  322. }
  323. class TimeColsSlatsCoords {
  324. constructor(positions, dateProfile, slotDuration) {
  325. this.positions = positions;
  326. this.dateProfile = dateProfile;
  327. this.slotDuration = slotDuration;
  328. }
  329. safeComputeTop(date) {
  330. let { dateProfile } = this;
  331. if (rangeContainsMarker(dateProfile.currentRange, date)) {
  332. let startOfDayDate = startOfDay(date);
  333. let timeMs = date.valueOf() - startOfDayDate.valueOf();
  334. if (timeMs >= asRoughMs(dateProfile.slotMinTime) &&
  335. timeMs < asRoughMs(dateProfile.slotMaxTime)) {
  336. return this.computeTimeTop(createDuration(timeMs));
  337. }
  338. }
  339. return null;
  340. }
  341. // Computes the top coordinate, relative to the bounds of the grid, of the given date.
  342. // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
  343. computeDateTop(when, startOfDayDate) {
  344. if (!startOfDayDate) {
  345. startOfDayDate = startOfDay(when);
  346. }
  347. return this.computeTimeTop(createDuration(when.valueOf() - startOfDayDate.valueOf()));
  348. }
  349. // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
  350. // This is a makeshify way to compute the time-top. Assumes all slatMetas dates are uniform.
  351. // Eventually allow computation with arbirary slat dates.
  352. computeTimeTop(duration) {
  353. let { positions, dateProfile } = this;
  354. let len = positions.els.length;
  355. // floating-point value of # of slots covered
  356. let slatCoverage = (duration.milliseconds - asRoughMs(dateProfile.slotMinTime)) / asRoughMs(this.slotDuration);
  357. let slatIndex;
  358. let slatRemainder;
  359. // compute a floating-point number for how many slats should be progressed through.
  360. // from 0 to number of slats (inclusive)
  361. // constrained because slotMinTime/slotMaxTime might be customized.
  362. slatCoverage = Math.max(0, slatCoverage);
  363. slatCoverage = Math.min(len, slatCoverage);
  364. // an integer index of the furthest whole slat
  365. // from 0 to number slats (*exclusive*, so len-1)
  366. slatIndex = Math.floor(slatCoverage);
  367. slatIndex = Math.min(slatIndex, len - 1);
  368. // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
  369. // could be 1.0 if slatCoverage is covering *all* the slots
  370. slatRemainder = slatCoverage - slatIndex;
  371. return positions.tops[slatIndex] +
  372. positions.getHeight(slatIndex) * slatRemainder;
  373. }
  374. }
  375. class TimeColsSlatsBody extends BaseComponent {
  376. render() {
  377. let { props, context } = this;
  378. let { options } = context;
  379. let { slatElRefs } = props;
  380. return (createElement("tbody", null, props.slatMetas.map((slatMeta, i) => {
  381. let renderProps = {
  382. time: slatMeta.time,
  383. date: context.dateEnv.toDate(slatMeta.date),
  384. view: context.viewApi,
  385. };
  386. return (createElement("tr", { key: slatMeta.key, ref: slatElRefs.createRef(slatMeta.key) },
  387. props.axis && (createElement(TimeColsAxisCell, Object.assign({}, slatMeta))),
  388. createElement(ContentContainer, { elTag: "td", elClasses: [
  389. 'fc-timegrid-slot',
  390. 'fc-timegrid-slot-lane',
  391. !slatMeta.isLabeled && 'fc-timegrid-slot-minor',
  392. ], elAttrs: {
  393. 'data-time': slatMeta.isoTimeStr,
  394. }, renderProps: renderProps, generatorName: "slotLaneContent", customGenerator: options.slotLaneContent, classNameGenerator: options.slotLaneClassNames, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount })));
  395. })));
  396. }
  397. }
  398. /*
  399. for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
  400. */
  401. class TimeColsSlats extends BaseComponent {
  402. constructor() {
  403. super(...arguments);
  404. this.rootElRef = createRef();
  405. this.slatElRefs = new RefMap();
  406. }
  407. render() {
  408. let { props, context } = this;
  409. return (createElement("div", { ref: this.rootElRef, className: "fc-timegrid-slots" },
  410. createElement("table", { "aria-hidden": true, className: context.theme.getClass('table'), style: {
  411. minWidth: props.tableMinWidth,
  412. width: props.clientWidth,
  413. height: props.minHeight,
  414. } },
  415. props.tableColGroupNode /* relies on there only being a single <col> for the axis */,
  416. createElement(TimeColsSlatsBody, { slatElRefs: this.slatElRefs, axis: props.axis, slatMetas: props.slatMetas }))));
  417. }
  418. componentDidMount() {
  419. this.updateSizing();
  420. }
  421. componentDidUpdate() {
  422. this.updateSizing();
  423. }
  424. componentWillUnmount() {
  425. if (this.props.onCoords) {
  426. this.props.onCoords(null);
  427. }
  428. }
  429. updateSizing() {
  430. let { context, props } = this;
  431. if (props.onCoords &&
  432. props.clientWidth !== null // means sizing has stabilized
  433. ) {
  434. let rootEl = this.rootElRef.current;
  435. if (rootEl.offsetHeight) { // not hidden by css
  436. props.onCoords(new TimeColsSlatsCoords(new PositionCache(this.rootElRef.current, collectSlatEls(this.slatElRefs.currentMap, props.slatMetas), false, true), this.props.dateProfile, context.options.slotDuration));
  437. }
  438. }
  439. }
  440. }
  441. function collectSlatEls(elMap, slatMetas) {
  442. return slatMetas.map((slatMeta) => elMap[slatMeta.key]);
  443. }
  444. function splitSegsByCol(segs, colCnt) {
  445. let segsByCol = [];
  446. let i;
  447. for (i = 0; i < colCnt; i += 1) {
  448. segsByCol.push([]);
  449. }
  450. if (segs) {
  451. for (i = 0; i < segs.length; i += 1) {
  452. segsByCol[segs[i].col].push(segs[i]);
  453. }
  454. }
  455. return segsByCol;
  456. }
  457. function splitInteractionByCol(ui, colCnt) {
  458. let byRow = [];
  459. if (!ui) {
  460. for (let i = 0; i < colCnt; i += 1) {
  461. byRow[i] = null;
  462. }
  463. }
  464. else {
  465. for (let i = 0; i < colCnt; i += 1) {
  466. byRow[i] = {
  467. affectedInstances: ui.affectedInstances,
  468. isEvent: ui.isEvent,
  469. segs: [],
  470. };
  471. }
  472. for (let seg of ui.segs) {
  473. byRow[seg.col].segs.push(seg);
  474. }
  475. }
  476. return byRow;
  477. }
  478. class TimeColMoreLink extends BaseComponent {
  479. render() {
  480. let { props } = this;
  481. return (createElement(MoreLinkContainer, { elClasses: ['fc-timegrid-more-link'], elStyle: {
  482. top: props.top,
  483. bottom: props.bottom,
  484. }, allDayDate: null, moreCnt: props.hiddenSegs.length, allSegs: props.hiddenSegs, hiddenSegs: props.hiddenSegs, extraDateSpan: props.extraDateSpan, dateProfile: props.dateProfile, todayRange: props.todayRange, popoverContent: () => renderPlainFgSegs(props.hiddenSegs, props), defaultGenerator: renderMoreLinkInner, forceTimed: true }, (InnerContent) => (createElement(InnerContent, { elTag: "div", elClasses: ['fc-timegrid-more-link-inner', 'fc-sticky'] }))));
  485. }
  486. }
  487. function renderMoreLinkInner(props) {
  488. return props.shortText;
  489. }
  490. // segInputs assumed sorted
  491. function buildPositioning(segInputs, strictOrder, maxStackCnt) {
  492. let hierarchy = new SegHierarchy();
  493. if (strictOrder != null) {
  494. hierarchy.strictOrder = strictOrder;
  495. }
  496. if (maxStackCnt != null) {
  497. hierarchy.maxStackCnt = maxStackCnt;
  498. }
  499. let hiddenEntries = hierarchy.addSegs(segInputs);
  500. let hiddenGroups = groupIntersectingEntries(hiddenEntries);
  501. let web = buildWeb(hierarchy);
  502. web = stretchWeb(web, 1); // all levelCoords/thickness will have 0.0-1.0
  503. let segRects = webToRects(web);
  504. return { segRects, hiddenGroups };
  505. }
  506. function buildWeb(hierarchy) {
  507. const { entriesByLevel } = hierarchy;
  508. const buildNode = cacheable((level, lateral) => level + ':' + lateral, (level, lateral) => {
  509. let siblingRange = findNextLevelSegs(hierarchy, level, lateral);
  510. let nextLevelRes = buildNodes(siblingRange, buildNode);
  511. let entry = entriesByLevel[level][lateral];
  512. return [
  513. Object.assign(Object.assign({}, entry), { nextLevelNodes: nextLevelRes[0] }),
  514. entry.thickness + nextLevelRes[1], // the pressure builds
  515. ];
  516. });
  517. return buildNodes(entriesByLevel.length
  518. ? { level: 0, lateralStart: 0, lateralEnd: entriesByLevel[0].length }
  519. : null, buildNode)[0];
  520. }
  521. function buildNodes(siblingRange, buildNode) {
  522. if (!siblingRange) {
  523. return [[], 0];
  524. }
  525. let { level, lateralStart, lateralEnd } = siblingRange;
  526. let lateral = lateralStart;
  527. let pairs = [];
  528. while (lateral < lateralEnd) {
  529. pairs.push(buildNode(level, lateral));
  530. lateral += 1;
  531. }
  532. pairs.sort(cmpDescPressures);
  533. return [
  534. pairs.map(extractNode),
  535. pairs[0][1], // first item's pressure
  536. ];
  537. }
  538. function cmpDescPressures(a, b) {
  539. return b[1] - a[1];
  540. }
  541. function extractNode(a) {
  542. return a[0];
  543. }
  544. function findNextLevelSegs(hierarchy, subjectLevel, subjectLateral) {
  545. let { levelCoords, entriesByLevel } = hierarchy;
  546. let subjectEntry = entriesByLevel[subjectLevel][subjectLateral];
  547. let afterSubject = levelCoords[subjectLevel] + subjectEntry.thickness;
  548. let levelCnt = levelCoords.length;
  549. let level = subjectLevel;
  550. // skip past levels that are too high up
  551. for (; level < levelCnt && levelCoords[level] < afterSubject; level += 1)
  552. ; // do nothing
  553. for (; level < levelCnt; level += 1) {
  554. let entries = entriesByLevel[level];
  555. let entry;
  556. let searchIndex = binarySearch(entries, subjectEntry.span.start, getEntrySpanEnd);
  557. let lateralStart = searchIndex[0] + searchIndex[1]; // if exact match (which doesn't collide), go to next one
  558. let lateralEnd = lateralStart;
  559. while ( // loop through entries that horizontally intersect
  560. (entry = entries[lateralEnd]) && // but not past the whole seg list
  561. entry.span.start < subjectEntry.span.end) {
  562. lateralEnd += 1;
  563. }
  564. if (lateralStart < lateralEnd) {
  565. return { level, lateralStart, lateralEnd };
  566. }
  567. }
  568. return null;
  569. }
  570. function stretchWeb(topLevelNodes, totalThickness) {
  571. const stretchNode = cacheable((node, startCoord, prevThickness) => buildEntryKey(node), (node, startCoord, prevThickness) => {
  572. let { nextLevelNodes, thickness } = node;
  573. let allThickness = thickness + prevThickness;
  574. let thicknessFraction = thickness / allThickness;
  575. let endCoord;
  576. let newChildren = [];
  577. if (!nextLevelNodes.length) {
  578. endCoord = totalThickness;
  579. }
  580. else {
  581. for (let childNode of nextLevelNodes) {
  582. if (endCoord === undefined) {
  583. let res = stretchNode(childNode, startCoord, allThickness);
  584. endCoord = res[0];
  585. newChildren.push(res[1]);
  586. }
  587. else {
  588. let res = stretchNode(childNode, endCoord, 0);
  589. newChildren.push(res[1]);
  590. }
  591. }
  592. }
  593. let newThickness = (endCoord - startCoord) * thicknessFraction;
  594. return [endCoord - newThickness, Object.assign(Object.assign({}, node), { thickness: newThickness, nextLevelNodes: newChildren })];
  595. });
  596. return topLevelNodes.map((node) => stretchNode(node, 0, 0)[1]);
  597. }
  598. // not sorted in any particular order
  599. function webToRects(topLevelNodes) {
  600. let rects = [];
  601. const processNode = cacheable((node, levelCoord, stackDepth) => buildEntryKey(node), (node, levelCoord, stackDepth) => {
  602. let rect = Object.assign(Object.assign({}, node), { levelCoord,
  603. stackDepth, stackForward: 0 });
  604. rects.push(rect);
  605. return (rect.stackForward = processNodes(node.nextLevelNodes, levelCoord + node.thickness, stackDepth + 1) + 1);
  606. });
  607. function processNodes(nodes, levelCoord, stackDepth) {
  608. let stackForward = 0;
  609. for (let node of nodes) {
  610. stackForward = Math.max(processNode(node, levelCoord, stackDepth), stackForward);
  611. }
  612. return stackForward;
  613. }
  614. processNodes(topLevelNodes, 0, 0);
  615. return rects; // TODO: sort rects by levelCoord to be consistent with toRects?
  616. }
  617. // TODO: move to general util
  618. function cacheable(keyFunc, workFunc) {
  619. const cache = {};
  620. return (...args) => {
  621. let key = keyFunc(...args);
  622. return (key in cache)
  623. ? cache[key]
  624. : (cache[key] = workFunc(...args));
  625. };
  626. }
  627. function computeSegVCoords(segs, colDate, slatCoords = null, eventMinHeight = 0) {
  628. let vcoords = [];
  629. if (slatCoords) {
  630. for (let i = 0; i < segs.length; i += 1) {
  631. let seg = segs[i];
  632. let spanStart = slatCoords.computeDateTop(seg.start, colDate);
  633. let spanEnd = Math.max(spanStart + (eventMinHeight || 0), // :(
  634. slatCoords.computeDateTop(seg.end, colDate));
  635. vcoords.push({
  636. start: Math.round(spanStart),
  637. end: Math.round(spanEnd), //
  638. });
  639. }
  640. }
  641. return vcoords;
  642. }
  643. function computeFgSegPlacements(segs, segVCoords, // might not have for every seg
  644. eventOrderStrict, eventMaxStack) {
  645. let segInputs = [];
  646. let dumbSegs = []; // segs without coords
  647. for (let i = 0; i < segs.length; i += 1) {
  648. let vcoords = segVCoords[i];
  649. if (vcoords) {
  650. segInputs.push({
  651. index: i,
  652. thickness: 1,
  653. span: vcoords,
  654. });
  655. }
  656. else {
  657. dumbSegs.push(segs[i]);
  658. }
  659. }
  660. let { segRects, hiddenGroups } = buildPositioning(segInputs, eventOrderStrict, eventMaxStack);
  661. let segPlacements = [];
  662. for (let segRect of segRects) {
  663. segPlacements.push({
  664. seg: segs[segRect.index],
  665. rect: segRect,
  666. });
  667. }
  668. for (let dumbSeg of dumbSegs) {
  669. segPlacements.push({ seg: dumbSeg, rect: null });
  670. }
  671. return { segPlacements, hiddenGroups };
  672. }
  673. const DEFAULT_TIME_FORMAT = createFormatter({
  674. hour: 'numeric',
  675. minute: '2-digit',
  676. meridiem: false,
  677. });
  678. class TimeColEvent extends BaseComponent {
  679. render() {
  680. return (createElement(StandardEvent, Object.assign({}, this.props, { elClasses: [
  681. 'fc-timegrid-event',
  682. 'fc-v-event',
  683. this.props.isShort && 'fc-timegrid-event-short',
  684. ], defaultTimeFormat: DEFAULT_TIME_FORMAT })));
  685. }
  686. }
  687. class TimeCol extends BaseComponent {
  688. constructor() {
  689. super(...arguments);
  690. this.sortEventSegs = memoize(sortEventSegs);
  691. }
  692. // TODO: memoize event-placement?
  693. render() {
  694. let { props, context } = this;
  695. let { options } = context;
  696. let isSelectMirror = options.selectMirror;
  697. let mirrorSegs = // yuck
  698. (props.eventDrag && props.eventDrag.segs) ||
  699. (props.eventResize && props.eventResize.segs) ||
  700. (isSelectMirror && props.dateSelectionSegs) ||
  701. [];
  702. let interactionAffectedInstances = // TODO: messy way to compute this
  703. (props.eventDrag && props.eventDrag.affectedInstances) ||
  704. (props.eventResize && props.eventResize.affectedInstances) ||
  705. {};
  706. let sortedFgSegs = this.sortEventSegs(props.fgEventSegs, options.eventOrder);
  707. return (createElement(DayCellContainer, { elTag: "td", elRef: props.elRef, elClasses: [
  708. 'fc-timegrid-col',
  709. ...(props.extraClassNames || []),
  710. ], elAttrs: Object.assign({ role: 'gridcell' }, props.extraDataAttrs), date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, extraRenderProps: props.extraRenderProps }, (InnerContent) => (createElement("div", { className: "fc-timegrid-col-frame" },
  711. createElement("div", { className: "fc-timegrid-col-bg" },
  712. this.renderFillSegs(props.businessHourSegs, 'non-business'),
  713. this.renderFillSegs(props.bgEventSegs, 'bg-event'),
  714. this.renderFillSegs(props.dateSelectionSegs, 'highlight')),
  715. createElement("div", { className: "fc-timegrid-col-events" }, this.renderFgSegs(sortedFgSegs, interactionAffectedInstances, false, false, false)),
  716. createElement("div", { className: "fc-timegrid-col-events" }, this.renderFgSegs(mirrorSegs, {}, Boolean(props.eventDrag), Boolean(props.eventResize), Boolean(isSelectMirror), 'mirror')),
  717. createElement("div", { className: "fc-timegrid-now-indicator-container" }, this.renderNowIndicator(props.nowIndicatorSegs)),
  718. hasCustomDayCellContent(options) && (createElement(InnerContent, { elTag: "div", elClasses: ['fc-timegrid-col-misc'] }))))));
  719. }
  720. renderFgSegs(sortedFgSegs, segIsInvisible, isDragging, isResizing, isDateSelecting, forcedKey) {
  721. let { props } = this;
  722. if (props.forPrint) {
  723. return renderPlainFgSegs(sortedFgSegs, props);
  724. }
  725. return this.renderPositionedFgSegs(sortedFgSegs, segIsInvisible, isDragging, isResizing, isDateSelecting, forcedKey);
  726. }
  727. renderPositionedFgSegs(segs, // if not mirror, needs to be sorted
  728. segIsInvisible, isDragging, isResizing, isDateSelecting, forcedKey) {
  729. let { eventMaxStack, eventShortHeight, eventOrderStrict, eventMinHeight } = this.context.options;
  730. let { date, slatCoords, eventSelection, todayRange, nowDate } = this.props;
  731. let isMirror = isDragging || isResizing || isDateSelecting;
  732. let segVCoords = computeSegVCoords(segs, date, slatCoords, eventMinHeight);
  733. let { segPlacements, hiddenGroups } = computeFgSegPlacements(segs, segVCoords, eventOrderStrict, eventMaxStack);
  734. return (createElement(Fragment, null,
  735. this.renderHiddenGroups(hiddenGroups, segs),
  736. segPlacements.map((segPlacement) => {
  737. let { seg, rect } = segPlacement;
  738. let instanceId = seg.eventRange.instance.instanceId;
  739. let isVisible = isMirror || Boolean(!segIsInvisible[instanceId] && rect);
  740. let vStyle = computeSegVStyle(rect && rect.span);
  741. let hStyle = (!isMirror && rect) ? this.computeSegHStyle(rect) : { left: 0, right: 0 };
  742. let isInset = Boolean(rect) && rect.stackForward > 0;
  743. let isShort = Boolean(rect) && (rect.span.end - rect.span.start) < eventShortHeight; // look at other places for this problem
  744. return (createElement("div", { className: 'fc-timegrid-event-harness' +
  745. (isInset ? ' fc-timegrid-event-harness-inset' : ''), key: forcedKey || instanceId, style: Object.assign(Object.assign({ visibility: isVisible ? '' : 'hidden' }, vStyle), hStyle) },
  746. createElement(TimeColEvent, Object.assign({ seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === eventSelection, isShort: isShort }, getSegMeta(seg, todayRange, nowDate)))));
  747. })));
  748. }
  749. // will already have eventMinHeight applied because segInputs already had it
  750. renderHiddenGroups(hiddenGroups, segs) {
  751. let { extraDateSpan, dateProfile, todayRange, nowDate, eventSelection, eventDrag, eventResize } = this.props;
  752. return (createElement(Fragment, null, hiddenGroups.map((hiddenGroup) => {
  753. let positionCss = computeSegVStyle(hiddenGroup.span);
  754. let hiddenSegs = compileSegsFromEntries(hiddenGroup.entries, segs);
  755. return (createElement(TimeColMoreLink, { key: buildIsoString(computeEarliestSegStart(hiddenSegs)), hiddenSegs: hiddenSegs, top: positionCss.top, bottom: positionCss.bottom, extraDateSpan: extraDateSpan, dateProfile: dateProfile, todayRange: todayRange, nowDate: nowDate, eventSelection: eventSelection, eventDrag: eventDrag, eventResize: eventResize }));
  756. })));
  757. }
  758. renderFillSegs(segs, fillType) {
  759. let { props, context } = this;
  760. let segVCoords = computeSegVCoords(segs, props.date, props.slatCoords, context.options.eventMinHeight); // don't assume all populated
  761. let children = segVCoords.map((vcoords, i) => {
  762. let seg = segs[i];
  763. return (createElement("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-timegrid-bg-harness", style: computeSegVStyle(vcoords) }, fillType === 'bg-event' ?
  764. createElement(BgEvent, Object.assign({ seg: seg }, getSegMeta(seg, props.todayRange, props.nowDate))) :
  765. renderFill(fillType)));
  766. });
  767. return createElement(Fragment, null, children);
  768. }
  769. renderNowIndicator(segs) {
  770. let { slatCoords, date } = this.props;
  771. if (!slatCoords) {
  772. return null;
  773. }
  774. return segs.map((seg, i) => (createElement(NowIndicatorContainer
  775. // key doesn't matter. will only ever be one
  776. , {
  777. // key doesn't matter. will only ever be one
  778. key: i, elClasses: ['fc-timegrid-now-indicator-line'], elStyle: {
  779. top: slatCoords.computeDateTop(seg.start, date),
  780. }, isAxis: false, date: date })));
  781. }
  782. computeSegHStyle(segHCoords) {
  783. let { isRtl, options } = this.context;
  784. let shouldOverlap = options.slotEventOverlap;
  785. let nearCoord = segHCoords.levelCoord; // the left side if LTR. the right side if RTL. floating-point
  786. let farCoord = segHCoords.levelCoord + segHCoords.thickness; // the right side if LTR. the left side if RTL. floating-point
  787. let left; // amount of space from left edge, a fraction of the total width
  788. let right; // amount of space from right edge, a fraction of the total width
  789. if (shouldOverlap) {
  790. // double the width, but don't go beyond the maximum forward coordinate (1.0)
  791. farCoord = Math.min(1, nearCoord + (farCoord - nearCoord) * 2);
  792. }
  793. if (isRtl) {
  794. left = 1 - farCoord;
  795. right = nearCoord;
  796. }
  797. else {
  798. left = nearCoord;
  799. right = 1 - farCoord;
  800. }
  801. let props = {
  802. zIndex: segHCoords.stackDepth + 1,
  803. left: left * 100 + '%',
  804. right: right * 100 + '%',
  805. };
  806. if (shouldOverlap && !segHCoords.stackForward) {
  807. // add padding to the edge so that forward stacked events don't cover the resizer's icon
  808. props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
  809. }
  810. return props;
  811. }
  812. }
  813. function renderPlainFgSegs(sortedFgSegs, { todayRange, nowDate, eventSelection, eventDrag, eventResize }) {
  814. let hiddenInstances = (eventDrag ? eventDrag.affectedInstances : null) ||
  815. (eventResize ? eventResize.affectedInstances : null) ||
  816. {};
  817. return (createElement(Fragment, null, sortedFgSegs.map((seg) => {
  818. let instanceId = seg.eventRange.instance.instanceId;
  819. return (createElement("div", { key: instanceId, style: { visibility: hiddenInstances[instanceId] ? 'hidden' : '' } },
  820. createElement(TimeColEvent, Object.assign({ seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === eventSelection, isShort: false }, getSegMeta(seg, todayRange, nowDate)))));
  821. })));
  822. }
  823. function computeSegVStyle(segVCoords) {
  824. if (!segVCoords) {
  825. return { top: '', bottom: '' };
  826. }
  827. return {
  828. top: segVCoords.start,
  829. bottom: -segVCoords.end,
  830. };
  831. }
  832. function compileSegsFromEntries(segEntries, allSegs) {
  833. return segEntries.map((segEntry) => allSegs[segEntry.index]);
  834. }
  835. class TimeColsContent extends BaseComponent {
  836. constructor() {
  837. super(...arguments);
  838. this.splitFgEventSegs = memoize(splitSegsByCol);
  839. this.splitBgEventSegs = memoize(splitSegsByCol);
  840. this.splitBusinessHourSegs = memoize(splitSegsByCol);
  841. this.splitNowIndicatorSegs = memoize(splitSegsByCol);
  842. this.splitDateSelectionSegs = memoize(splitSegsByCol);
  843. this.splitEventDrag = memoize(splitInteractionByCol);
  844. this.splitEventResize = memoize(splitInteractionByCol);
  845. this.rootElRef = createRef();
  846. this.cellElRefs = new RefMap();
  847. }
  848. render() {
  849. let { props, context } = this;
  850. let nowIndicatorTop = context.options.nowIndicator &&
  851. props.slatCoords &&
  852. props.slatCoords.safeComputeTop(props.nowDate); // might return void
  853. let colCnt = props.cells.length;
  854. let fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, colCnt);
  855. let bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, colCnt);
  856. let businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, colCnt);
  857. let nowIndicatorSegsByRow = this.splitNowIndicatorSegs(props.nowIndicatorSegs, colCnt);
  858. let dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, colCnt);
  859. let eventDragByRow = this.splitEventDrag(props.eventDrag, colCnt);
  860. let eventResizeByRow = this.splitEventResize(props.eventResize, colCnt);
  861. return (createElement("div", { className: "fc-timegrid-cols", ref: this.rootElRef },
  862. createElement("table", { role: "presentation", style: {
  863. minWidth: props.tableMinWidth,
  864. width: props.clientWidth,
  865. } },
  866. props.tableColGroupNode,
  867. createElement("tbody", { role: "presentation" },
  868. createElement("tr", { role: "row" },
  869. props.axis && (createElement("td", { "aria-hidden": true, className: "fc-timegrid-col fc-timegrid-axis" },
  870. createElement("div", { className: "fc-timegrid-col-frame" },
  871. createElement("div", { className: "fc-timegrid-now-indicator-container" }, typeof nowIndicatorTop === 'number' && (createElement(NowIndicatorContainer, { elClasses: ['fc-timegrid-now-indicator-arrow'], elStyle: { top: nowIndicatorTop }, isAxis: true, date: props.nowDate })))))),
  872. props.cells.map((cell, i) => (createElement(TimeCol, { key: cell.key, elRef: this.cellElRefs.createRef(cell.key), dateProfile: props.dateProfile, date: cell.date, nowDate: props.nowDate, todayRange: props.todayRange, extraRenderProps: cell.extraRenderProps, extraDataAttrs: cell.extraDataAttrs, extraClassNames: cell.extraClassNames, extraDateSpan: cell.extraDateSpan, fgEventSegs: fgEventSegsByRow[i], bgEventSegs: bgEventSegsByRow[i], businessHourSegs: businessHourSegsByRow[i], nowIndicatorSegs: nowIndicatorSegsByRow[i], dateSelectionSegs: dateSelectionSegsByRow[i], eventDrag: eventDragByRow[i], eventResize: eventResizeByRow[i], slatCoords: props.slatCoords, eventSelection: props.eventSelection, forPrint: props.forPrint }))))))));
  873. }
  874. componentDidMount() {
  875. this.updateCoords();
  876. }
  877. componentDidUpdate() {
  878. this.updateCoords();
  879. }
  880. updateCoords() {
  881. let { props } = this;
  882. if (props.onColCoords &&
  883. props.clientWidth !== null // means sizing has stabilized
  884. ) {
  885. props.onColCoords(new PositionCache(this.rootElRef.current, collectCellEls(this.cellElRefs.currentMap, props.cells), true, // horizontal
  886. false));
  887. }
  888. }
  889. }
  890. function collectCellEls(elMap, cells) {
  891. return cells.map((cell) => elMap[cell.key]);
  892. }
  893. /* A component that renders one or more columns of vertical time slots
  894. ----------------------------------------------------------------------------------------------------------------------*/
  895. class TimeCols extends DateComponent {
  896. constructor() {
  897. super(...arguments);
  898. this.processSlotOptions = memoize(processSlotOptions);
  899. this.state = {
  900. slatCoords: null,
  901. };
  902. this.handleRootEl = (el) => {
  903. if (el) {
  904. this.context.registerInteractiveComponent(this, {
  905. el,
  906. isHitComboAllowed: this.props.isHitComboAllowed,
  907. });
  908. }
  909. else {
  910. this.context.unregisterInteractiveComponent(this);
  911. }
  912. };
  913. this.handleScrollRequest = (request) => {
  914. let { onScrollTopRequest } = this.props;
  915. let { slatCoords } = this.state;
  916. if (onScrollTopRequest && slatCoords) {
  917. if (request.time) {
  918. let top = slatCoords.computeTimeTop(request.time);
  919. top = Math.ceil(top); // zoom can give weird floating-point values. rather scroll a little bit further
  920. if (top) {
  921. top += 1; // to overcome top border that slots beyond the first have. looks better
  922. }
  923. onScrollTopRequest(top);
  924. }
  925. return true;
  926. }
  927. return false;
  928. };
  929. this.handleColCoords = (colCoords) => {
  930. this.colCoords = colCoords;
  931. };
  932. this.handleSlatCoords = (slatCoords) => {
  933. this.setState({ slatCoords });
  934. if (this.props.onSlatCoords) {
  935. this.props.onSlatCoords(slatCoords);
  936. }
  937. };
  938. }
  939. render() {
  940. let { props, state } = this;
  941. return (createElement("div", { className: "fc-timegrid-body", ref: this.handleRootEl, style: {
  942. // these props are important to give this wrapper correct dimensions for interactions
  943. // TODO: if we set it here, can we avoid giving to inner tables?
  944. width: props.clientWidth,
  945. minWidth: props.tableMinWidth,
  946. } },
  947. createElement(TimeColsSlats, { axis: props.axis, dateProfile: props.dateProfile, slatMetas: props.slatMetas, clientWidth: props.clientWidth, minHeight: props.expandRows ? props.clientHeight : '', tableMinWidth: props.tableMinWidth, tableColGroupNode: props.axis ? props.tableColGroupNode : null /* axis depends on the colgroup's shrinking */, onCoords: this.handleSlatCoords }),
  948. createElement(TimeColsContent, { cells: props.cells, axis: props.axis, dateProfile: props.dateProfile, businessHourSegs: props.businessHourSegs, bgEventSegs: props.bgEventSegs, fgEventSegs: props.fgEventSegs, dateSelectionSegs: props.dateSelectionSegs, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, todayRange: props.todayRange, nowDate: props.nowDate, nowIndicatorSegs: props.nowIndicatorSegs, clientWidth: props.clientWidth, tableMinWidth: props.tableMinWidth, tableColGroupNode: props.tableColGroupNode, slatCoords: state.slatCoords, onColCoords: this.handleColCoords, forPrint: props.forPrint })));
  949. }
  950. componentDidMount() {
  951. this.scrollResponder = this.context.createScrollResponder(this.handleScrollRequest);
  952. }
  953. componentDidUpdate(prevProps) {
  954. this.scrollResponder.update(prevProps.dateProfile !== this.props.dateProfile);
  955. }
  956. componentWillUnmount() {
  957. this.scrollResponder.detach();
  958. }
  959. queryHit(positionLeft, positionTop) {
  960. let { dateEnv, options } = this.context;
  961. let { colCoords } = this;
  962. let { dateProfile } = this.props;
  963. let { slatCoords } = this.state;
  964. let { snapDuration, snapsPerSlot } = this.processSlotOptions(this.props.slotDuration, options.snapDuration);
  965. let colIndex = colCoords.leftToIndex(positionLeft);
  966. let slatIndex = slatCoords.positions.topToIndex(positionTop);
  967. if (colIndex != null && slatIndex != null) {
  968. let cell = this.props.cells[colIndex];
  969. let slatTop = slatCoords.positions.tops[slatIndex];
  970. let slatHeight = slatCoords.positions.getHeight(slatIndex);
  971. let partial = (positionTop - slatTop) / slatHeight; // floating point number between 0 and 1
  972. let localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
  973. let snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
  974. let dayDate = this.props.cells[colIndex].date;
  975. let time = addDurations(dateProfile.slotMinTime, multiplyDuration(snapDuration, snapIndex));
  976. let start = dateEnv.add(dayDate, time);
  977. let end = dateEnv.add(start, snapDuration);
  978. return {
  979. dateProfile,
  980. dateSpan: Object.assign({ range: { start, end }, allDay: false }, cell.extraDateSpan),
  981. dayEl: colCoords.els[colIndex],
  982. rect: {
  983. left: colCoords.lefts[colIndex],
  984. right: colCoords.rights[colIndex],
  985. top: slatTop,
  986. bottom: slatTop + slatHeight,
  987. },
  988. layer: 0,
  989. };
  990. }
  991. return null;
  992. }
  993. }
  994. function processSlotOptions(slotDuration, snapDurationOverride) {
  995. let snapDuration = snapDurationOverride || slotDuration;
  996. let snapsPerSlot = wholeDivideDurations(slotDuration, snapDuration);
  997. if (snapsPerSlot === null) {
  998. snapDuration = slotDuration;
  999. snapsPerSlot = 1;
  1000. // TODO: say warning?
  1001. }
  1002. return { snapDuration, snapsPerSlot };
  1003. }
  1004. class DayTimeColsSlicer extends Slicer {
  1005. sliceRange(range, dayRanges) {
  1006. let segs = [];
  1007. for (let col = 0; col < dayRanges.length; col += 1) {
  1008. let segRange = intersectRanges(range, dayRanges[col]);
  1009. if (segRange) {
  1010. segs.push({
  1011. start: segRange.start,
  1012. end: segRange.end,
  1013. isStart: segRange.start.valueOf() === range.start.valueOf(),
  1014. isEnd: segRange.end.valueOf() === range.end.valueOf(),
  1015. col,
  1016. });
  1017. }
  1018. }
  1019. return segs;
  1020. }
  1021. }
  1022. class DayTimeCols extends DateComponent {
  1023. constructor() {
  1024. super(...arguments);
  1025. this.buildDayRanges = memoize(buildDayRanges);
  1026. this.slicer = new DayTimeColsSlicer();
  1027. this.timeColsRef = createRef();
  1028. }
  1029. render() {
  1030. let { props, context } = this;
  1031. let { dateProfile, dayTableModel } = props;
  1032. let { nowIndicator, nextDayThreshold } = context.options;
  1033. let dayRanges = this.buildDayRanges(dayTableModel, dateProfile, context.dateEnv);
  1034. // give it the first row of cells
  1035. // TODO: would move this further down hierarchy, but sliceNowDate needs it
  1036. return (createElement(NowTimer, { unit: nowIndicator ? 'minute' : 'day' }, (nowDate, todayRange) => (createElement(TimeCols, Object.assign({ ref: this.timeColsRef }, this.slicer.sliceProps(props, dateProfile, null, context, dayRanges), { forPrint: props.forPrint, axis: props.axis, dateProfile: dateProfile, slatMetas: props.slatMetas, slotDuration: props.slotDuration, cells: dayTableModel.cells[0], tableColGroupNode: props.tableColGroupNode, tableMinWidth: props.tableMinWidth, clientWidth: props.clientWidth, clientHeight: props.clientHeight, expandRows: props.expandRows, nowDate: nowDate, nowIndicatorSegs: nowIndicator && this.slicer.sliceNowDate(nowDate, dateProfile, nextDayThreshold, context, dayRanges), todayRange: todayRange, onScrollTopRequest: props.onScrollTopRequest, onSlatCoords: props.onSlatCoords })))));
  1037. }
  1038. }
  1039. function buildDayRanges(dayTableModel, dateProfile, dateEnv) {
  1040. let ranges = [];
  1041. for (let date of dayTableModel.headerDates) {
  1042. ranges.push({
  1043. start: dateEnv.add(date, dateProfile.slotMinTime),
  1044. end: dateEnv.add(date, dateProfile.slotMaxTime),
  1045. });
  1046. }
  1047. return ranges;
  1048. }
  1049. // potential nice values for the slot-duration and interval-duration
  1050. // from largest to smallest
  1051. const STOCK_SUB_DURATIONS = [
  1052. { hours: 1 },
  1053. { minutes: 30 },
  1054. { minutes: 15 },
  1055. { seconds: 30 },
  1056. { seconds: 15 },
  1057. ];
  1058. function buildSlatMetas(slotMinTime, slotMaxTime, explicitLabelInterval, slotDuration, dateEnv) {
  1059. let dayStart = new Date(0);
  1060. let slatTime = slotMinTime;
  1061. let slatIterator = createDuration(0);
  1062. let labelInterval = explicitLabelInterval || computeLabelInterval(slotDuration);
  1063. let metas = [];
  1064. while (asRoughMs(slatTime) < asRoughMs(slotMaxTime)) {
  1065. let date = dateEnv.add(dayStart, slatTime);
  1066. let isLabeled = wholeDivideDurations(slatIterator, labelInterval) !== null;
  1067. metas.push({
  1068. date,
  1069. time: slatTime,
  1070. key: date.toISOString(),
  1071. isoTimeStr: formatIsoTimeString(date),
  1072. isLabeled,
  1073. });
  1074. slatTime = addDurations(slatTime, slotDuration);
  1075. slatIterator = addDurations(slatIterator, slotDuration);
  1076. }
  1077. return metas;
  1078. }
  1079. // Computes an automatic value for slotLabelInterval
  1080. function computeLabelInterval(slotDuration) {
  1081. let i;
  1082. let labelInterval;
  1083. let slotsPerLabel;
  1084. // find the smallest stock label interval that results in more than one slots-per-label
  1085. for (i = STOCK_SUB_DURATIONS.length - 1; i >= 0; i -= 1) {
  1086. labelInterval = createDuration(STOCK_SUB_DURATIONS[i]);
  1087. slotsPerLabel = wholeDivideDurations(labelInterval, slotDuration);
  1088. if (slotsPerLabel !== null && slotsPerLabel > 1) {
  1089. return labelInterval;
  1090. }
  1091. }
  1092. return slotDuration; // fall back
  1093. }
  1094. class DayTimeColsView extends TimeColsView {
  1095. constructor() {
  1096. super(...arguments);
  1097. this.buildTimeColsModel = memoize(buildTimeColsModel);
  1098. this.buildSlatMetas = memoize(buildSlatMetas);
  1099. }
  1100. render() {
  1101. let { options, dateEnv, dateProfileGenerator } = this.context;
  1102. let { props } = this;
  1103. let { dateProfile } = props;
  1104. let dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator);
  1105. let splitProps = this.allDaySplitter.splitProps(props);
  1106. let slatMetas = this.buildSlatMetas(dateProfile.slotMinTime, dateProfile.slotMaxTime, options.slotLabelInterval, options.slotDuration, dateEnv);
  1107. let { dayMinWidth } = options;
  1108. let hasAttachedAxis = !dayMinWidth;
  1109. let hasDetachedAxis = dayMinWidth;
  1110. let headerContent = options.dayHeaders && (createElement(DayHeader, { dates: dayTableModel.headerDates, dateProfile: dateProfile, datesRepDistinctDays: true, renderIntro: hasAttachedAxis ? this.renderHeadAxis : null }));
  1111. let allDayContent = (options.allDaySlot !== false) && ((contentArg) => (createElement(DayTable, Object.assign({}, splitProps.allDay, { dateProfile: dateProfile, dayTableModel: dayTableModel, nextDayThreshold: options.nextDayThreshold, tableMinWidth: contentArg.tableMinWidth, colGroupNode: contentArg.tableColGroupNode, renderRowIntro: hasAttachedAxis ? this.renderTableRowAxis : null, showWeekNumbers: false, expandRows: false, headerAlignElRef: this.headerElRef, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, forPrint: props.forPrint }, this.getAllDayMaxEventProps()))));
  1112. let timeGridContent = (contentArg) => (createElement(DayTimeCols, Object.assign({}, splitProps.timed, { dayTableModel: dayTableModel, dateProfile: dateProfile, axis: hasAttachedAxis, slotDuration: options.slotDuration, slatMetas: slatMetas, forPrint: props.forPrint, tableColGroupNode: contentArg.tableColGroupNode, tableMinWidth: contentArg.tableMinWidth, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, onSlatCoords: this.handleSlatCoords, expandRows: contentArg.expandRows, onScrollTopRequest: this.handleScrollTopRequest })));
  1113. return hasDetachedAxis
  1114. ? this.renderHScrollLayout(headerContent, allDayContent, timeGridContent, dayTableModel.colCnt, dayMinWidth, slatMetas, this.state.slatCoords)
  1115. : this.renderSimpleLayout(headerContent, allDayContent, timeGridContent);
  1116. }
  1117. }
  1118. function buildTimeColsModel(dateProfile, dateProfileGenerator) {
  1119. let daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator);
  1120. return new DayTableModel(daySeries, false);
  1121. }
  1122. var css_248z = ".fc-v-event{background-color:var(--fc-event-bg-color);border:1px solid var(--fc-event-border-color);display:block}.fc-v-event .fc-event-main{color:var(--fc-event-text-color);height:100%}.fc-v-event .fc-event-main-frame{display:flex;flex-direction:column;height:100%}.fc-v-event .fc-event-time{flex-grow:0;flex-shrink:0;max-height:100%;overflow:hidden}.fc-v-event .fc-event-title-container{flex-grow:1;flex-shrink:1;min-height:0}.fc-v-event .fc-event-title{bottom:0;max-height:100%;overflow:hidden;top:0}.fc-v-event:not(.fc-event-start){border-top-left-radius:0;border-top-right-radius:0;border-top-width:0}.fc-v-event:not(.fc-event-end){border-bottom-left-radius:0;border-bottom-right-radius:0;border-bottom-width:0}.fc-v-event.fc-event-selected:before{left:-10px;right:-10px}.fc-v-event .fc-event-resizer-start{cursor:n-resize}.fc-v-event .fc-event-resizer-end{cursor:s-resize}.fc-v-event:not(.fc-event-selected) .fc-event-resizer{height:var(--fc-event-resizer-thickness);left:0;right:0}.fc-v-event:not(.fc-event-selected) .fc-event-resizer-start{top:calc(var(--fc-event-resizer-thickness)/-2)}.fc-v-event:not(.fc-event-selected) .fc-event-resizer-end{bottom:calc(var(--fc-event-resizer-thickness)/-2)}.fc-v-event.fc-event-selected .fc-event-resizer{left:50%;margin-left:calc(var(--fc-event-resizer-dot-total-width)/-2)}.fc-v-event.fc-event-selected .fc-event-resizer-start{top:calc(var(--fc-event-resizer-dot-total-width)/-2)}.fc-v-event.fc-event-selected .fc-event-resizer-end{bottom:calc(var(--fc-event-resizer-dot-total-width)/-2)}.fc .fc-timegrid .fc-daygrid-body{z-index:2}.fc .fc-timegrid-divider{padding:0 0 2px}.fc .fc-timegrid-body{min-height:100%;position:relative;z-index:1}.fc .fc-timegrid-axis-chunk{position:relative}.fc .fc-timegrid-axis-chunk>table,.fc .fc-timegrid-slots{position:relative;z-index:1}.fc .fc-timegrid-slot{border-bottom:0;height:1.5em}.fc .fc-timegrid-slot:empty:before{content:\"\\00a0\"}.fc .fc-timegrid-slot-minor{border-top-style:dotted}.fc .fc-timegrid-slot-label-cushion{display:inline-block;white-space:nowrap}.fc .fc-timegrid-slot-label{vertical-align:middle}.fc .fc-timegrid-axis-cushion,.fc .fc-timegrid-slot-label-cushion{padding:0 4px}.fc .fc-timegrid-axis-frame-liquid{height:100%}.fc .fc-timegrid-axis-frame{align-items:center;display:flex;justify-content:flex-end;overflow:hidden}.fc .fc-timegrid-axis-cushion{flex-shrink:0;max-width:60px}.fc-direction-ltr .fc-timegrid-slot-label-frame{text-align:right}.fc-direction-rtl .fc-timegrid-slot-label-frame{text-align:left}.fc-liquid-hack .fc-timegrid-axis-frame-liquid{bottom:0;height:auto;left:0;position:absolute;right:0;top:0}.fc .fc-timegrid-col.fc-day-today{background-color:var(--fc-today-bg-color)}.fc .fc-timegrid-col-frame{min-height:100%;position:relative}.fc-media-screen.fc-liquid-hack .fc-timegrid-col-frame{bottom:0;height:auto;left:0;position:absolute;right:0;top:0}.fc-media-screen .fc-timegrid-cols{bottom:0;left:0;position:absolute;right:0;top:0}.fc-media-screen .fc-timegrid-cols>table{height:100%}.fc-media-screen .fc-timegrid-col-bg,.fc-media-screen .fc-timegrid-col-events,.fc-media-screen .fc-timegrid-now-indicator-container{left:0;position:absolute;right:0;top:0}.fc .fc-timegrid-col-bg{z-index:2}.fc .fc-timegrid-col-bg .fc-non-business{z-index:1}.fc .fc-timegrid-col-bg .fc-bg-event{z-index:2}.fc .fc-timegrid-col-bg .fc-highlight{z-index:3}.fc .fc-timegrid-bg-harness{left:0;position:absolute;right:0}.fc .fc-timegrid-col-events{z-index:3}.fc .fc-timegrid-now-indicator-container{bottom:0;overflow:hidden}.fc-direction-ltr .fc-timegrid-col-events{margin:0 2.5% 0 2px}.fc-direction-rtl .fc-timegrid-col-events{margin:0 2px 0 2.5%}.fc-timegrid-event-harness{position:absolute}.fc-timegrid-event-harness>.fc-timegrid-event{bottom:0;left:0;position:absolute;right:0;top:0}.fc-timegrid-event-harness-inset .fc-timegrid-event,.fc-timegrid-event.fc-event-mirror,.fc-timegrid-more-link{box-shadow:0 0 0 1px var(--fc-page-bg-color)}.fc-timegrid-event,.fc-timegrid-more-link{border-radius:3px;font-size:var(--fc-small-font-size)}.fc-timegrid-event{margin-bottom:1px}.fc-timegrid-event .fc-event-main{padding:1px 1px 0}.fc-timegrid-event .fc-event-time{font-size:var(--fc-small-font-size);margin-bottom:1px;white-space:nowrap}.fc-timegrid-event-short .fc-event-main-frame{flex-direction:row;overflow:hidden}.fc-timegrid-event-short .fc-event-time:after{content:\"\\00a0-\\00a0\"}.fc-timegrid-event-short .fc-event-title{font-size:var(--fc-small-font-size)}.fc-timegrid-more-link{background:var(--fc-more-link-bg-color);color:var(--fc-more-link-text-color);cursor:pointer;margin-bottom:1px;position:absolute;z-index:9999}.fc-timegrid-more-link-inner{padding:3px 2px;top:0}.fc-direction-ltr .fc-timegrid-more-link{right:0}.fc-direction-rtl .fc-timegrid-more-link{left:0}.fc .fc-timegrid-now-indicator-line{border-color:var(--fc-now-indicator-color);border-style:solid;border-width:1px 0 0;left:0;position:absolute;right:0;z-index:4}.fc .fc-timegrid-now-indicator-arrow{border-color:var(--fc-now-indicator-color);border-style:solid;margin-top:-5px;position:absolute;z-index:4}.fc-direction-ltr .fc-timegrid-now-indicator-arrow{border-bottom-color:transparent;border-top-color:transparent;border-width:5px 0 5px 6px;left:0}.fc-direction-rtl .fc-timegrid-now-indicator-arrow{border-bottom-color:transparent;border-top-color:transparent;border-width:5px 6px 5px 0;right:0}";
  1123. injectStyles(css_248z);
  1124. export { DayTimeCols, DayTimeColsSlicer, DayTimeColsView, TimeCols, TimeColsSlatsCoords, TimeColsView, buildDayRanges, buildSlatMetas, buildTimeColsModel };