UNPKG

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