@fullcalendar/timeline 6.1.15 → 7.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/internal.js CHANGED
@@ -1,6 +1,6 @@
1
- import { config, createFormatter, greatestDurationDenominator, asCleanDays, createDuration, wholeDivideDurations, asRoughMs, addDays, startOfDay, asRoughSeconds, asRoughMinutes, diffWholeDays, isInt, computeVisibleDayRange, padStart, BaseComponent, memoizeObjArg, memoize, getDateMeta, ContentContainer, getSlotClassNames, getDayClassNames, buildNavLinkAttrs, PositionCache, findDirectChildren, rangeContainsMarker, NowTimer, NowIndicatorContainer, findElements, RefMap, multiplyDuration, SegHierarchy, groupIntersectingEntries, buildIsoString, computeEarliestSegStart, buildEventRangeKey, BgEvent, getSegMeta, renderFill, Slicer, intersectRanges, addMs, StandardEvent, MoreLinkContainer, sortEventSegs, mapHash, isPropsEqual, DateComponent, getStickyHeaderDates, getStickyFooterScrollbar, ViewContainer, renderScrollShim, injectStyles } from '@fullcalendar/core/internal.js';
2
- import { createElement, Fragment, createRef } from '@fullcalendar/core/preact.js';
3
- import { ScrollGrid } from '@fullcalendar/scrollgrid/internal.js';
1
+ import { config, createFormatter, greatestDurationDenominator, asCleanDays, createDuration, wholeDivideDurations, asRoughMs, addDays, startOfDay, asRoughSeconds, asRoughMinutes, diffWholeDays, isInt, computeVisibleDayRange, padStart, BaseComponent, getDateMeta, ContentContainer, getDayClassNames, getSlotClassNames, watchWidth, setRef, RefMap, afterSize, SegHierarchy, groupIntersectingEntries, buildEventRangeKey, BgEvent, getEventRangeMeta, renderFill, Slicer, intersectRanges, addMs, StandardEvent, MoreLinkContainer, watchHeight, memoize, sortEventSegs, memoizeObjArg, watchSize, buildNavLinkAttrs, NowIndicatorContainer, DateComponent, getIsHeightAuto, getStickyHeaderDates, getStickyFooterScrollbar, NowTimer, rangeContainsMarker, ViewContainer, Scroller, multiplyDuration, injectStyles } from '@fullcalendar/core/internal.js';
2
+ import { createRef, createElement, Fragment, Component } from '@fullcalendar/core/preact.js';
3
+ import { ScrollerSyncer } from '@fullcalendar/scrollgrid/internal.js';
4
4
 
5
5
  const MIN_AUTO_LABELS = 18; // more than `12` months but less that `24` hours
6
6
  const MAX_AUTO_SLOTS_PER_LABEL = 6; // allows 6 10-min slots in an hour
@@ -440,152 +440,91 @@ function buildCellObject(date, text, rowUnit) {
440
440
  return { date, text, rowUnit, colspan: 1, isWeekStart: false };
441
441
  }
442
442
 
443
- class TimelineHeaderTh extends BaseComponent {
443
+ class TimelineSlatCell extends BaseComponent {
444
444
  constructor() {
445
445
  super(...arguments);
446
- this.refineRenderProps = memoizeObjArg(refineRenderProps);
447
- this.buildCellNavLinkAttrs = memoize(buildCellNavLinkAttrs);
446
+ // ref
447
+ this.innerElRef = createRef();
448
448
  }
449
449
  render() {
450
450
  let { props, context } = this;
451
- let { dateEnv, options } = context;
452
- let { cell, dateProfile, tDateProfile } = props;
453
- // the cell.rowUnit is f'd
454
- // giving 'month' for a 3-day view
455
- // workaround: to infer day, do NOT time
456
- let dateMeta = getDateMeta(cell.date, props.todayRange, props.nowDate, dateProfile);
457
- let renderProps = this.refineRenderProps({
458
- level: props.rowLevel,
459
- dateMarker: cell.date,
460
- text: cell.text,
461
- dateEnv: context.dateEnv,
462
- viewApi: context.viewApi,
463
- });
464
- return (createElement(ContentContainer, { elTag: "th", elClasses: [
451
+ let { dateEnv, options, theme } = context;
452
+ let { date, tDateProfile, isEm } = props;
453
+ let dateMeta = getDateMeta(props.date, props.todayRange, props.nowDate, props.dateProfile);
454
+ let renderProps = Object.assign(Object.assign({ date: dateEnv.toDate(props.date) }, dateMeta), { view: context.viewApi });
455
+ return (createElement(ContentContainer, { elTag: "div", elClasses: [
456
+ 'fc-flex-column',
457
+ 'fc-align-start',
458
+ 'fc-cell',
465
459
  'fc-timeline-slot',
466
- 'fc-timeline-slot-label',
467
- cell.isWeekStart && 'fc-timeline-slot-em',
468
- ...( // TODO: so slot classnames for week/month/bigger. see note above about rowUnit
469
- cell.rowUnit === 'time' ?
470
- getSlotClassNames(dateMeta, context.theme) :
471
- getDayClassNames(dateMeta, context.theme)),
460
+ 'fc-timeline-slot-lane',
461
+ isEm ? 'fc-timeline-slot-em' : '',
462
+ tDateProfile.isTimeScale ? (isInt(dateEnv.countDurationsBetween(// best to do this here?
463
+ tDateProfile.normalizedRange.start, props.date, tDateProfile.labelInterval)) ?
464
+ 'fc-timeline-slot-major' :
465
+ 'fc-timeline-slot-minor') : '',
466
+ ...(props.isDay ?
467
+ getDayClassNames(dateMeta, theme) :
468
+ getSlotClassNames(dateMeta, theme)),
472
469
  ], elAttrs: {
473
- colSpan: cell.colspan,
474
- 'data-date': dateEnv.formatIso(cell.date, {
475
- omitTime: !tDateProfile.isTimeScale,
470
+ 'data-date': dateEnv.formatIso(date, {
476
471
  omitTimeZoneOffset: true,
472
+ omitTime: !tDateProfile.isTimeScale,
477
473
  }),
478
- }, renderProps: renderProps, generatorName: "slotLabelContent", customGenerator: options.slotLabelContent, defaultGenerator: renderInnerContent, classNameGenerator: options.slotLabelClassNames, didMount: options.slotLabelDidMount, willUnmount: options.slotLabelWillUnmount }, (InnerContent) => (createElement("div", { className: "fc-timeline-slot-frame", style: { height: props.rowInnerHeight } },
479
- createElement(InnerContent, { elTag: "a", elClasses: [
480
- 'fc-timeline-slot-cushion',
481
- 'fc-scrollgrid-sync-inner',
482
- props.isSticky && 'fc-sticky',
483
- ], elAttrs: this.buildCellNavLinkAttrs(context, cell.date, cell.rowUnit) })))));
474
+ }, elStyle: {
475
+ width: props.width,
476
+ }, renderProps: renderProps, generatorName: "slotLaneContent", customGenerator: options.slotLaneContent, classNameGenerator: options.slotLaneClassNames, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount }, (InnerContent) => (createElement("div", { ref: this.innerElRef, className: 'fc-flex-column' },
477
+ createElement(InnerContent, { elTag: "div", elClasses: ['fc-cell-inner'] })))));
484
478
  }
485
- }
486
- function buildCellNavLinkAttrs(context, cellDate, rowUnit) {
487
- return (rowUnit && rowUnit !== 'time')
488
- ? buildNavLinkAttrs(context, cellDate, rowUnit)
489
- : {};
490
- }
491
- function renderInnerContent(renderProps) {
492
- return renderProps.text;
493
- }
494
- function refineRenderProps(input) {
495
- return {
496
- level: input.level,
497
- date: input.dateEnv.toDate(input.dateMarker),
498
- view: input.viewApi,
499
- text: input.text,
500
- };
501
- }
502
-
503
- class TimelineHeaderRows extends BaseComponent {
504
- render() {
505
- let { dateProfile, tDateProfile, rowInnerHeights, todayRange, nowDate } = this.props;
506
- let { cellRows } = tDateProfile;
507
- return (createElement(Fragment, null, cellRows.map((rowCells, rowLevel) => {
508
- let isLast = rowLevel === cellRows.length - 1;
509
- let isChrono = tDateProfile.isTimeScale && isLast; // the final row, with times?
510
- let classNames = [
511
- 'fc-timeline-header-row',
512
- isChrono ? 'fc-timeline-header-row-chrono' : '',
513
- ];
514
- return ( // eslint-disable-next-line react/no-array-index-key
515
- createElement("tr", { key: rowLevel, className: classNames.join(' ') }, rowCells.map((cell) => (createElement(TimelineHeaderTh, { key: cell.date.toISOString(), cell: cell, rowLevel: rowLevel, dateProfile: dateProfile, tDateProfile: tDateProfile, todayRange: todayRange, nowDate: nowDate, rowInnerHeight: rowInnerHeights && rowInnerHeights[rowLevel], isSticky: !isLast })))));
516
- })));
479
+ componentDidMount() {
480
+ const innerEl = this.innerElRef.current;
481
+ this.detachWidth = watchWidth(innerEl, (width) => {
482
+ setRef(this.props.innerWidthRef, width);
483
+ });
484
+ }
485
+ componentWillUnmount() {
486
+ this.detachWidth();
487
+ setRef(this.props.innerWidthRef, null);
517
488
  }
518
489
  }
519
490
 
520
- class TimelineCoords {
521
- constructor(slatRootEl, // okay to expose?
522
- slatEls, dateProfile, tDateProfile, dateEnv, isRtl) {
523
- this.slatRootEl = slatRootEl;
524
- this.dateProfile = dateProfile;
525
- this.tDateProfile = tDateProfile;
526
- this.dateEnv = dateEnv;
527
- this.isRtl = isRtl;
528
- this.outerCoordCache = new PositionCache(slatRootEl, slatEls, true, // isHorizontal
529
- false);
530
- // for the inner divs within the slats
531
- // used for event rendering and scrollTime, to disregard slat border
532
- this.innerCoordCache = new PositionCache(slatRootEl, findDirectChildren(slatEls, 'div'), true, // isHorizontal
533
- false);
534
- }
535
- isDateInRange(date) {
536
- return rangeContainsMarker(this.dateProfile.currentRange, date);
537
- }
538
- // results range from negative width of area to 0
539
- dateToCoord(date) {
540
- let { tDateProfile } = this;
541
- let snapCoverage = this.computeDateSnapCoverage(date);
542
- let slotCoverage = snapCoverage / tDateProfile.snapsPerSlot;
543
- let slotIndex = Math.floor(slotCoverage);
544
- slotIndex = Math.min(slotIndex, tDateProfile.slotCnt - 1);
545
- let partial = slotCoverage - slotIndex;
546
- let { innerCoordCache, outerCoordCache } = this;
547
- if (this.isRtl) {
548
- return outerCoordCache.originClientRect.width - (outerCoordCache.rights[slotIndex] -
549
- (innerCoordCache.getWidth(slotIndex) * partial));
550
- }
551
- return (outerCoordCache.lefts[slotIndex] +
552
- (innerCoordCache.getWidth(slotIndex) * partial));
553
- }
554
- rangeToCoords(range) {
555
- return {
556
- start: this.dateToCoord(range.start),
557
- end: this.dateToCoord(range.end),
558
- };
559
- }
560
- durationToCoord(duration) {
561
- let { dateProfile, tDateProfile, dateEnv, isRtl } = this;
562
- let coord = 0;
563
- if (dateProfile) {
564
- let date = dateEnv.add(dateProfile.activeRange.start, duration);
565
- if (!tDateProfile.isTimeScale) {
566
- date = startOfDay(date);
567
- }
568
- coord = this.dateToCoord(date);
569
- // hack to overcome the left borders of non-first slat
570
- if (!isRtl && coord) {
571
- coord += 1;
491
+ class TimelineSlats extends BaseComponent {
492
+ constructor() {
493
+ super(...arguments);
494
+ this.innerWidthRefMap = new RefMap(() => {
495
+ afterSize(this.handleInnerWidths);
496
+ });
497
+ this.handleInnerWidths = () => {
498
+ const innerWidthMap = this.innerWidthRefMap.current;
499
+ let max = 0;
500
+ for (const innerWidth of innerWidthMap.values()) {
501
+ max = Math.max(max, innerWidth);
572
502
  }
573
- }
574
- return coord;
575
- }
576
- coordFromLeft(coord) {
577
- if (this.isRtl) {
578
- return this.outerCoordCache.originClientRect.width - coord;
579
- }
580
- return coord;
503
+ // TODO: check to see if changed before firing ref!? YES. do in other places too
504
+ setRef(this.props.innerWidthRef, max);
505
+ };
581
506
  }
582
- // returned value is between 0 and the number of snaps
583
- computeDateSnapCoverage(date) {
584
- return computeDateSnapCoverage(date, this.tDateProfile, this.dateEnv);
507
+ render() {
508
+ let { props, innerWidthRefMap } = this;
509
+ let { tDateProfile, slotWidth } = props;
510
+ let { slotDates, isWeekStarts } = tDateProfile;
511
+ let isDay = !tDateProfile.isTimeScale && !tDateProfile.largeUnit;
512
+ return (createElement("div", { className: "fc-timeline-slots fc-fill fc-flex-row" }, slotDates.map((slotDate, i) => {
513
+ let key = slotDate.toISOString();
514
+ return (createElement(TimelineSlatCell, { key: key, date: slotDate, dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isEm: isWeekStarts[i], isDay: isDay,
515
+ // ref
516
+ innerWidthRef: innerWidthRefMap.createRef(key),
517
+ // dimensions
518
+ width: slotWidth }));
519
+ })));
585
520
  }
586
521
  }
522
+
523
+ /*
524
+ TODO: rename this file!
525
+ */
587
526
  // returned value is between 0 and the number of snaps
588
- function computeDateSnapCoverage(date, tDateProfile, dateEnv) {
527
+ function computeDateSnapCoverage$1(date, tDateProfile, dateEnv) {
589
528
  let snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration);
590
529
  if (snapDiff < 0) {
591
530
  return 0;
@@ -605,6 +544,9 @@ function computeDateSnapCoverage(date, tDateProfile, dateEnv) {
605
544
  }
606
545
  return snapCoverage;
607
546
  }
547
+ /*
548
+ TODO: audit!!!
549
+ */
608
550
  function coordToCss(hcoord, isRtl) {
609
551
  if (hcoord === null) {
610
552
  return { left: '', right: '' };
@@ -614,6 +556,9 @@ function coordToCss(hcoord, isRtl) {
614
556
  }
615
557
  return { left: hcoord, right: '' };
616
558
  }
559
+ /*
560
+ TODO: audit!!!
561
+ */
617
562
  function coordsToCss(hcoords, isRtl) {
618
563
  if (!hcoords) {
619
564
  return { left: '', right: '' };
@@ -623,216 +568,141 @@ function coordsToCss(hcoords, isRtl) {
623
568
  }
624
569
  return { left: hcoords.start, right: -hcoords.end };
625
570
  }
626
-
627
- class TimelineHeader extends BaseComponent {
628
- constructor() {
629
- super(...arguments);
630
- this.rootElRef = createRef();
631
- }
632
- render() {
633
- let { props, context } = this;
634
- // TODO: very repetitive
635
- // TODO: make part of tDateProfile?
636
- let timerUnit = greatestDurationDenominator(props.tDateProfile.slotDuration).unit;
637
- // WORKAROUND: make ignore slatCoords when out of sync with dateProfile
638
- let slatCoords = props.slatCoords && props.slatCoords.dateProfile === props.dateProfile ? props.slatCoords : null;
639
- return (createElement(NowTimer, { unit: timerUnit }, (nowDate, todayRange) => (createElement("div", { className: "fc-timeline-header", ref: this.rootElRef },
640
- createElement("table", { "aria-hidden": true, className: "fc-scrollgrid-sync-table", style: { minWidth: props.tableMinWidth, width: props.clientWidth } },
641
- props.tableColGroupNode,
642
- createElement("tbody", null,
643
- createElement(TimelineHeaderRows, { dateProfile: props.dateProfile, tDateProfile: props.tDateProfile, nowDate: nowDate, todayRange: todayRange, rowInnerHeights: props.rowInnerHeights }))),
644
- context.options.nowIndicator && (
645
- // need to have a container regardless of whether the current view has a visible now indicator
646
- // because apparently removal of the element resets the scroll for some reasons (issue #5351).
647
- // this issue doesn't happen for the timeline body however (
648
- createElement("div", { className: "fc-timeline-now-indicator-container" }, (slatCoords && slatCoords.isDateInRange(nowDate)) && (createElement(NowIndicatorContainer, { elClasses: ['fc-timeline-now-indicator-arrow'], elStyle: coordToCss(slatCoords.dateToCoord(nowDate), context.isRtl), isAxis: true, date: nowDate }))))))));
571
+ /*
572
+ TODO: DRY up with elsewhere?
573
+ */
574
+ function horizontalsToCss(hcoord, isRtl) {
575
+ if (!hcoord) {
576
+ return {};
649
577
  }
650
- componentDidMount() {
651
- this.updateSize();
578
+ if (isRtl) {
579
+ return { right: hcoord.start, width: hcoord.size };
652
580
  }
653
- componentDidUpdate() {
654
- this.updateSize();
581
+ else {
582
+ return { left: hcoord.start, width: hcoord.size };
655
583
  }
656
- updateSize() {
657
- if (this.props.onMaxCushionWidth) {
658
- this.props.onMaxCushionWidth(this.computeMaxCushionWidth());
659
- }
584
+ }
585
+ function horizontalCoordToCss(start, isRtl) {
586
+ if (isRtl) {
587
+ return { right: start };
660
588
  }
661
- computeMaxCushionWidth() {
662
- return Math.max(...findElements(this.rootElRef.current, '.fc-timeline-header-row:last-child .fc-timeline-slot-cushion').map((el) => el.getBoundingClientRect().width));
589
+ else {
590
+ return { left: start };
663
591
  }
664
592
  }
665
593
 
666
- class TimelineSlatCell extends BaseComponent {
667
- render() {
668
- let { props, context } = this;
669
- let { dateEnv, options, theme } = context;
670
- let { date, tDateProfile, isEm } = props;
671
- let dateMeta = getDateMeta(props.date, props.todayRange, props.nowDate, props.dateProfile);
672
- let renderProps = Object.assign(Object.assign({ date: dateEnv.toDate(props.date) }, dateMeta), { view: context.viewApi });
673
- return (createElement(ContentContainer, { elTag: "td", elRef: props.elRef, elClasses: [
674
- 'fc-timeline-slot',
675
- 'fc-timeline-slot-lane',
676
- isEm && 'fc-timeline-slot-em',
677
- tDateProfile.isTimeScale ? (isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, props.date, tDateProfile.labelInterval)) ?
678
- 'fc-timeline-slot-major' :
679
- 'fc-timeline-slot-minor') : '',
680
- ...(props.isDay ?
681
- getDayClassNames(dateMeta, theme) :
682
- getSlotClassNames(dateMeta, theme)),
683
- ], elAttrs: {
684
- 'data-date': dateEnv.formatIso(date, {
685
- omitTimeZoneOffset: true,
686
- omitTime: !tDateProfile.isTimeScale,
687
- }),
688
- }, renderProps: renderProps, generatorName: "slotLaneContent", customGenerator: options.slotLaneContent, classNameGenerator: options.slotLaneClassNames, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount }, (InnerContent) => (createElement(InnerContent, { elTag: "div" }))));
594
+ function createVerticalStyle(props) {
595
+ if (props) {
596
+ return {
597
+ top: props.start,
598
+ height: props.size,
599
+ };
689
600
  }
690
601
  }
691
-
692
- class TimelineSlatsBody extends BaseComponent {
693
- render() {
694
- let { props } = this;
695
- let { tDateProfile, cellElRefs } = props;
696
- let { slotDates, isWeekStarts } = tDateProfile;
697
- let isDay = !tDateProfile.isTimeScale && !tDateProfile.largeUnit;
698
- return (createElement("tbody", null,
699
- createElement("tr", null, slotDates.map((slotDate, i) => {
700
- let key = slotDate.toISOString();
701
- return (createElement(TimelineSlatCell, { key: key, elRef: cellElRefs.createRef(key), date: slotDate, dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isEm: isWeekStarts[i], isDay: isDay }));
702
- }))));
602
+ function createHorizontalStyle(props, isRtl) {
603
+ if (props) {
604
+ return {
605
+ [isRtl ? 'right' : 'left']: props.start,
606
+ width: props.size,
607
+ };
703
608
  }
704
609
  }
705
-
706
- class TimelineSlats extends BaseComponent {
707
- constructor() {
708
- super(...arguments);
709
- this.rootElRef = createRef();
710
- this.cellElRefs = new RefMap();
711
- this.handleScrollRequest = (request) => {
712
- let { onScrollLeftRequest } = this.props;
713
- let { coords } = this;
714
- if (onScrollLeftRequest && coords) {
715
- if (request.time) {
716
- let scrollLeft = coords.coordFromLeft(coords.durationToCoord(request.time));
717
- onScrollLeftRequest(scrollLeft);
718
- }
719
- return true;
720
- }
721
- return null; // best?
722
- };
610
+ // Timeline-specific
611
+ // -------------------------------------------------------------------------------------------------
612
+ const MIN_SLOT_WIDTH = 30; // for real
613
+ /*
614
+ TODO: DRY with computeSlatHeight?
615
+ */
616
+ function computeSlotWidth(slatCnt, slatsPerLabel, slatMinWidth, labelInnerWidth, viewportWidth) {
617
+ if (labelInnerWidth == null || viewportWidth == null) {
618
+ return [undefined, undefined, false];
619
+ }
620
+ slatMinWidth = Math.max(slatMinWidth || 0, (labelInnerWidth + 1) / slatsPerLabel, MIN_SLOT_WIDTH);
621
+ const slatTryWidth = viewportWidth / slatCnt;
622
+ let slatLiquid;
623
+ let slatWidth;
624
+ if (slatTryWidth >= slatMinWidth) {
625
+ slatLiquid = true;
626
+ slatWidth = slatTryWidth;
723
627
  }
724
- render() {
725
- let { props, context } = this;
726
- return (createElement("div", { className: "fc-timeline-slots", ref: this.rootElRef },
727
- createElement("table", { "aria-hidden": true, className: context.theme.getClass('table'), style: {
728
- minWidth: props.tableMinWidth,
729
- width: props.clientWidth,
730
- } },
731
- props.tableColGroupNode,
732
- createElement(TimelineSlatsBody, { cellElRefs: this.cellElRefs, dateProfile: props.dateProfile, tDateProfile: props.tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange }))));
628
+ else {
629
+ slatLiquid = false;
630
+ slatWidth = Math.max(slatMinWidth, slatTryWidth);
733
631
  }
734
- componentDidMount() {
735
- this.updateSizing();
736
- this.scrollResponder = this.context.createScrollResponder(this.handleScrollRequest);
632
+ return [slatWidth * slatCnt, slatWidth, slatLiquid];
633
+ }
634
+ function timeToCoord(// pixels
635
+ time, dateEnv, dateProfile, tDateProfile, slowWidth) {
636
+ let date = dateEnv.add(dateProfile.activeRange.start, time);
637
+ if (!tDateProfile.isTimeScale) {
638
+ date = startOfDay(date);
737
639
  }
738
- componentDidUpdate(prevProps) {
739
- this.updateSizing();
740
- this.scrollResponder.update(prevProps.dateProfile !== this.props.dateProfile);
640
+ return dateToCoord(date, dateEnv, tDateProfile, slowWidth);
641
+ }
642
+ function dateToCoord(// pixels
643
+ date, dateEnv, tDateProfile, slotWidth) {
644
+ let snapCoverage = computeDateSnapCoverage(date, tDateProfile, dateEnv);
645
+ let slotCoverage = snapCoverage / tDateProfile.snapsPerSlot;
646
+ return slotCoverage * slotWidth;
647
+ }
648
+ /*
649
+ returned value is between 0 and the number of snaps
650
+ */
651
+ function computeDateSnapCoverage(date, tDateProfile, dateEnv) {
652
+ let snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration);
653
+ if (snapDiff < 0) {
654
+ return 0;
741
655
  }
742
- componentWillUnmount() {
743
- this.scrollResponder.detach();
744
- if (this.props.onCoords) {
745
- this.props.onCoords(null);
746
- }
656
+ if (snapDiff >= tDateProfile.snapDiffToIndex.length) {
657
+ return tDateProfile.snapCnt;
747
658
  }
748
- updateSizing() {
749
- let { props, context } = this;
750
- if (props.clientWidth !== null && // is sizing stable?
751
- this.scrollResponder
752
- // ^it's possible to have clientWidth immediately after mount (when returning from print view), but w/o scrollResponder
753
- ) {
754
- let rootEl = this.rootElRef.current;
755
- if (rootEl.offsetWidth) { // not hidden by css
756
- this.coords = new TimelineCoords(this.rootElRef.current, collectCellEls(this.cellElRefs.currentMap, props.tDateProfile.slotDates), props.dateProfile, props.tDateProfile, context.dateEnv, context.isRtl);
757
- if (props.onCoords) {
758
- props.onCoords(this.coords);
759
- }
760
- this.scrollResponder.update(false); // TODO: wouldn't have to do this if coords were in state
761
- }
762
- }
659
+ let snapDiffInt = Math.floor(snapDiff);
660
+ let snapCoverage = tDateProfile.snapDiffToIndex[snapDiffInt];
661
+ if (isInt(snapCoverage)) { // not an in-between value
662
+ snapCoverage += snapDiff - snapDiffInt; // add the remainder
763
663
  }
764
- positionToHit(leftPosition) {
765
- let { outerCoordCache } = this.coords;
766
- let { dateEnv, isRtl } = this.context;
767
- let { tDateProfile } = this.props;
768
- let slatIndex = outerCoordCache.leftToIndex(leftPosition);
769
- if (slatIndex != null) {
770
- // somewhat similar to what TimeGrid does. consolidate?
771
- let slatWidth = outerCoordCache.getWidth(slatIndex);
772
- let partial = isRtl ?
773
- (outerCoordCache.rights[slatIndex] - leftPosition) / slatWidth :
774
- (leftPosition - outerCoordCache.lefts[slatIndex]) / slatWidth;
775
- let localSnapIndex = Math.floor(partial * tDateProfile.snapsPerSlot);
776
- let start = dateEnv.add(tDateProfile.slotDates[slatIndex], multiplyDuration(tDateProfile.snapDuration, localSnapIndex));
777
- let end = dateEnv.add(start, tDateProfile.snapDuration);
778
- return {
779
- dateSpan: {
780
- range: { start, end },
781
- allDay: !this.props.tDateProfile.isTimeScale,
782
- },
783
- dayEl: this.cellElRefs.currentMap[slatIndex],
784
- left: outerCoordCache.lefts[slatIndex],
785
- right: outerCoordCache.rights[slatIndex],
786
- };
787
- }
788
- return null;
664
+ else {
665
+ // a fractional value, meaning the date is not visible
666
+ // always round up in this case. works for start AND end dates in a range.
667
+ snapCoverage = Math.ceil(snapCoverage);
789
668
  }
790
- }
791
- function collectCellEls(elMap, slotDates) {
792
- return slotDates.map((slotDate) => {
793
- let key = slotDate.toISOString();
794
- return elMap[key];
795
- });
669
+ return snapCoverage;
796
670
  }
797
671
 
798
- function computeSegHCoords(segs, minWidth, timelineCoords) {
799
- let hcoords = [];
800
- if (timelineCoords) {
801
- for (let seg of segs) {
802
- let res = timelineCoords.rangeToCoords(seg);
803
- let start = Math.round(res.start); // for barely-overlapping collisions
804
- let end = Math.round(res.end); //
805
- if (end - start < minWidth) {
806
- end = start + minWidth;
807
- }
808
- hcoords.push({ start, end });
809
- }
672
+ function computeManySegHorizontals(segs, segMinWidth, dateEnv, tDateProfile, slotWidth) {
673
+ const res = {};
674
+ for (const seg of segs) {
675
+ res[seg.eventRange.instance.instanceId] = computeSegHorizontals(seg, segMinWidth, dateEnv, tDateProfile, slotWidth);
810
676
  }
811
- return hcoords;
677
+ return res;
678
+ }
679
+ function computeSegHorizontals(seg, segMinWidth, dateEnv, tDateProfile, slotWidth) {
680
+ const startCoord = dateToCoord(seg.start, dateEnv, tDateProfile, slotWidth);
681
+ const endCoord = dateToCoord(seg.end, dateEnv, tDateProfile, slotWidth);
682
+ let size = endCoord - startCoord;
683
+ if (segMinWidth) {
684
+ size = Math.max(size, segMinWidth);
685
+ }
686
+ return { start: startCoord, size };
812
687
  }
813
- function computeFgSegPlacements(segs, segHCoords, // might not have for every seg
814
- eventInstanceHeights, // might not have for every seg
815
- moreLinkHeights, // might not have for every more-link
688
+ function computeFgSegPlacements(// mostly horizontals
689
+ segs, segHorizontals, segHeights, // keyed by instanceId
816
690
  strictOrder, maxStackCnt) {
817
- let segInputs = [];
818
- let crudePlacements = []; // when we don't know dims
691
+ let segEntries = [];
819
692
  for (let i = 0; i < segs.length; i += 1) {
820
693
  let seg = segs[i];
821
694
  let instanceId = seg.eventRange.instance.instanceId;
822
- let height = eventInstanceHeights[instanceId];
823
- let hcoords = segHCoords[i];
824
- if (height && hcoords) {
825
- segInputs.push({
695
+ let height = segHeights.get(instanceId);
696
+ let hcoords = segHorizontals[instanceId];
697
+ if (height != null && hcoords != null) {
698
+ segEntries.push({
826
699
  index: i,
827
- span: hcoords,
828
- thickness: height,
829
- });
830
- }
831
- else {
832
- crudePlacements.push({
833
700
  seg,
834
- hcoords,
835
- top: null,
701
+ span: {
702
+ start: hcoords.start,
703
+ end: hcoords.start + hcoords.size,
704
+ },
705
+ thickness: height,
836
706
  });
837
707
  }
838
708
  }
@@ -843,80 +713,84 @@ strictOrder, maxStackCnt) {
843
713
  if (maxStackCnt != null) {
844
714
  hierarchy.maxStackCnt = maxStackCnt;
845
715
  }
846
- let hiddenEntries = hierarchy.addSegs(segInputs);
847
- let hiddenPlacements = hiddenEntries.map((entry) => ({
848
- seg: segs[entry.index],
849
- hcoords: entry.span,
850
- top: null,
851
- }));
716
+ let hiddenEntries = hierarchy.addSegs(segEntries);
852
717
  let hiddenGroups = groupIntersectingEntries(hiddenEntries);
853
- let moreLinkInputs = [];
854
- let moreLinkCrudePlacements = [];
855
- const extractSeg = (entry) => segs[entry.index];
856
- for (let i = 0; i < hiddenGroups.length; i += 1) {
857
- let hiddenGroup = hiddenGroups[i];
858
- let sortedSegs = hiddenGroup.entries.map(extractSeg);
859
- let height = moreLinkHeights[buildIsoString(computeEarliestSegStart(sortedSegs))]; // not optimal :(
860
- if (height != null) {
861
- // NOTE: the hiddenGroup's spanStart/spanEnd are already computed by rangeToCoords. computed during input.
862
- moreLinkInputs.push({
863
- index: segs.length + i,
864
- thickness: height,
865
- span: hiddenGroup.span,
866
- });
867
- }
868
- else {
869
- moreLinkCrudePlacements.push({
870
- seg: sortedSegs,
871
- hcoords: hiddenGroup.span,
872
- top: null,
873
- });
874
- }
875
- }
718
+ let hiddenGroupEntries = hiddenGroups.map((hiddenGroup, index) => ({
719
+ index: segs.length + index,
720
+ segGroup: hiddenGroup,
721
+ span: hiddenGroup.span,
722
+ thickness: 1, // HACK to ensure it's placed
723
+ }));
876
724
  // add more-links into the hierarchy, but don't limit
877
725
  hierarchy.maxStackCnt = -1;
878
- hierarchy.addSegs(moreLinkInputs);
726
+ hierarchy.addSegs(hiddenGroupEntries);
879
727
  let visibleRects = hierarchy.toRects();
880
- let visiblePlacements = [];
881
- let maxHeight = 0;
728
+ let segTops = {};
729
+ let hiddenGroupTops = {};
882
730
  for (let rect of visibleRects) {
883
- let segIndex = rect.index;
884
- visiblePlacements.push({
885
- seg: segIndex < segs.length
886
- ? segs[segIndex] // a real seg
887
- : hiddenGroups[segIndex - segs.length].entries.map(extractSeg),
888
- hcoords: rect.span,
889
- top: rect.levelCoord,
890
- });
891
- maxHeight = Math.max(maxHeight, rect.levelCoord + rect.thickness);
731
+ const { seg, segGroup } = rect;
732
+ if (seg) { // regular seg
733
+ segTops[seg.eventRange.instance.instanceId] = rect.levelCoord;
734
+ }
735
+ else { // hiddenGroup
736
+ hiddenGroupTops[segGroup.key] = rect.levelCoord;
737
+ }
892
738
  }
893
739
  return [
894
- visiblePlacements.concat(crudePlacements, hiddenPlacements, moreLinkCrudePlacements),
895
- maxHeight,
740
+ segTops,
741
+ computeMaxBottom(segs, segTops, segHeights),
742
+ hiddenGroups,
743
+ hiddenGroupTops,
896
744
  ];
897
745
  }
746
+ function computeMaxBottom(segs, segTops, segHeights) {
747
+ let max = 0;
748
+ for (const seg of segs) {
749
+ const { instanceId } = seg.eventRange.instance;
750
+ const top = segTops[instanceId];
751
+ const height = segHeights.get(instanceId);
752
+ if (top != null && height != null) {
753
+ max = Math.max(max, top + height);
754
+ }
755
+ }
756
+ return max;
757
+ }
758
+ /*
759
+ TODO: converge with computeMaxBottom, but keys are different
760
+ */
761
+ function computeMoreLinkMaxBottom(hiddenGroups, hiddenGroupTops, hiddenGroupHeights) {
762
+ let max = 0;
763
+ for (const hiddenGroup of hiddenGroups) {
764
+ const top = hiddenGroupTops[hiddenGroup.key];
765
+ const height = hiddenGroupHeights.get(hiddenGroup.key);
766
+ if (top != null && height != null) {
767
+ max = Math.max(max, top + height);
768
+ }
769
+ }
770
+ return max;
771
+ }
898
772
 
899
773
  class TimelineLaneBg extends BaseComponent {
900
774
  render() {
901
775
  let { props } = this;
902
776
  let highlightSeg = [].concat(props.eventResizeSegs, props.dateSelectionSegs);
903
- return props.timelineCoords && (createElement("div", { className: "fc-timeline-bg" },
904
- this.renderSegs(props.businessHourSegs || [], props.timelineCoords, 'non-business'),
905
- this.renderSegs(props.bgEventSegs || [], props.timelineCoords, 'bg-event'),
906
- this.renderSegs(highlightSeg, props.timelineCoords, 'highlight')));
907
- }
908
- renderSegs(segs, timelineCoords, fillType) {
909
- let { todayRange, nowDate } = this.props;
910
- let { isRtl } = this.context;
911
- let segHCoords = computeSegHCoords(segs, 0, timelineCoords);
912
- let children = segs.map((seg, i) => {
913
- let hcoords = segHCoords[i];
914
- let hStyle = coordsToCss(hcoords, isRtl);
777
+ return (createElement("div", { className: "fc-timeline-bg" },
778
+ this.renderSegs(props.businessHourSegs || [], 'non-business'),
779
+ this.renderSegs(props.bgEventSegs || [], 'bg-event'),
780
+ this.renderSegs(highlightSeg, 'highlight')));
781
+ }
782
+ renderSegs(segs, fillType) {
783
+ let { tDateProfile, todayRange, nowDate, slotWidth } = this.props;
784
+ let { dateEnv, isRtl } = this.context;
785
+ return (createElement(Fragment, null, segs.map((seg) => {
786
+ let hStyle; // TODO
787
+ if (slotWidth != null) {
788
+ let segHorizontal = computeSegHorizontals(seg, undefined, dateEnv, tDateProfile, slotWidth);
789
+ hStyle = horizontalsToCss(segHorizontal, isRtl);
790
+ }
915
791
  return (createElement("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-timeline-bg-harness", style: hStyle }, fillType === 'bg-event' ?
916
- createElement(BgEvent, Object.assign({ seg: seg }, getSegMeta(seg, todayRange, nowDate))) :
917
- renderFill(fillType)));
918
- });
919
- return createElement(Fragment, null, children);
792
+ createElement(BgEvent, Object.assign({ eventRange: seg.eventRange, isStart: seg.isStart, isEnd: seg.isEnd }, getEventRangeMeta(seg.eventRange, todayRange, nowDate))) : (renderFill(fillType))));
793
+ })));
920
794
  }
921
795
  }
922
796
 
@@ -925,8 +799,8 @@ class TimelineLaneSlicer extends Slicer {
925
799
  let normalRange = normalizeRange(origRange, tDateProfile, dateEnv);
926
800
  let segs = [];
927
801
  // protect against when the span is entirely in an invalid date region
928
- if (computeDateSnapCoverage(normalRange.start, tDateProfile, dateEnv)
929
- < computeDateSnapCoverage(normalRange.end, tDateProfile, dateEnv)) {
802
+ if (computeDateSnapCoverage$1(normalRange.start, tDateProfile, dateEnv)
803
+ < computeDateSnapCoverage$1(normalRange.end, tDateProfile, dateEnv)) {
930
804
  // intersect the footprint's range with the grid's range
931
805
  let slicedRange = intersectRanges(normalRange, tDateProfile.normalizedRange);
932
806
  if (slicedRange) {
@@ -952,48 +826,86 @@ const DEFAULT_TIME_FORMAT = createFormatter({
952
826
  });
953
827
  class TimelineEvent extends BaseComponent {
954
828
  render() {
955
- let { props } = this;
956
- return (createElement(StandardEvent, Object.assign({}, props, { elClasses: ['fc-timeline-event', 'fc-h-event'], defaultTimeFormat: DEFAULT_TIME_FORMAT, defaultDisplayEventTime: !props.isTimeScale })));
829
+ let { props, context } = this;
830
+ let { options } = context;
831
+ return (createElement(StandardEvent, Object.assign({}, props, { elClasses: [
832
+ 'fc-timeline-event',
833
+ 'fc-h-event',
834
+ options.eventOverlap === false // TODO: fix bad default
835
+ ? 'fc-timeline-event-spacious'
836
+ : ''
837
+ ], defaultTimeFormat: DEFAULT_TIME_FORMAT, defaultDisplayEventTime: !props.isTimeScale })));
957
838
  }
958
839
  }
959
840
 
960
841
  class TimelineLaneMoreLink extends BaseComponent {
961
842
  render() {
962
- let { props, context } = this;
963
- let { hiddenSegs, placement, resourceId } = props;
964
- let { top, hcoords } = placement;
965
- let isVisible = hcoords && top !== null;
966
- let hStyle = coordsToCss(hcoords, context.isRtl);
843
+ let { props } = this;
844
+ let { hiddenSegs, resourceId, forcedInvisibleMap } = props;
967
845
  let extraDateSpan = resourceId ? { resourceId } : {};
968
- return (createElement(MoreLinkContainer, { elRef: props.elRef, elClasses: ['fc-timeline-more-link'], elStyle: Object.assign({ visibility: isVisible ? '' : 'hidden', top: top || 0 }, hStyle), allDayDate: null, moreCnt: hiddenSegs.length, allSegs: hiddenSegs, hiddenSegs: hiddenSegs, dateProfile: props.dateProfile, todayRange: props.todayRange, extraDateSpan: extraDateSpan, popoverContent: () => (createElement(Fragment, null, hiddenSegs.map((seg) => {
969
- let instanceId = seg.eventRange.instance.instanceId;
970
- return (createElement("div", { key: instanceId, style: { visibility: props.isForcedInvisible[instanceId] ? 'hidden' : '' } },
971
- createElement(TimelineEvent, Object.assign({ isTimeScale: props.isTimeScale, seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === props.eventSelection }, getSegMeta(seg, props.todayRange, props.nowDate)))));
972
- }))) }, (InnerContent) => (createElement(InnerContent, { elTag: "div", elClasses: ['fc-timeline-more-link-inner', 'fc-sticky'] }))));
846
+ return (createElement(MoreLinkContainer, { elClasses: ['fc-timeline-more-link'], allDayDate: null, segs: hiddenSegs, hiddenSegs: hiddenSegs, dateProfile: props.dateProfile, todayRange: props.todayRange, extraDateSpan: extraDateSpan, popoverContent: () => (createElement(Fragment, null, hiddenSegs.map((seg) => {
847
+ let { eventRange } = seg;
848
+ let instanceId = eventRange.instance.instanceId;
849
+ return (createElement("div", { key: instanceId, style: { visibility: forcedInvisibleMap[instanceId] ? 'hidden' : '' } },
850
+ createElement(TimelineEvent, Object.assign({ isTimeScale: props.isTimeScale, eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === props.eventSelection }, getEventRangeMeta(eventRange, props.todayRange, props.nowDate)))));
851
+ }))) }, (InnerContent) => (createElement(InnerContent, { elTag: "div", elClasses: ['fc-timeline-more-link-inner', 'fc-sticky-x'] }))));
852
+ }
853
+ }
854
+
855
+ /*
856
+ TODO: make DRY with other Event Harnesses
857
+ */
858
+ class TimelineEventHarness extends Component {
859
+ constructor() {
860
+ super(...arguments);
861
+ // ref
862
+ this.rootElRef = createRef();
863
+ }
864
+ render() {
865
+ const { props } = this;
866
+ return (createElement("div", { className: "fc-abs", style: props.style, ref: this.rootElRef }, props.children));
867
+ }
868
+ componentDidMount() {
869
+ const rootEl = this.rootElRef.current; // TODO: make dynamic with useEffect
870
+ this.detachHeight = watchHeight(rootEl, (height) => {
871
+ setRef(this.props.heightRef, height);
872
+ });
873
+ }
874
+ componentWillUnmount() {
875
+ this.detachHeight();
876
+ setRef(this.props.heightRef, null);
973
877
  }
974
878
  }
975
879
 
880
+ /*
881
+ TODO: split TimelineLaneBg and TimelineLaneFg?
882
+ */
976
883
  class TimelineLane extends BaseComponent {
977
884
  constructor() {
978
885
  super(...arguments);
979
- this.slicer = new TimelineLaneSlicer();
886
+ // memo
980
887
  this.sortEventSegs = memoize(sortEventSegs);
981
- this.harnessElRefs = new RefMap();
982
- this.moreElRefs = new RefMap();
983
- this.innerElRef = createRef();
984
- // TODO: memoize event positioning
985
- this.state = {
986
- eventInstanceHeights: {},
987
- moreLinkHeights: {},
888
+ // refs
889
+ this.segHeightRefMap = new RefMap(() => {
890
+ afterSize(this.handleSegHeights);
891
+ });
892
+ this.moreLinkHeightRefMap = new RefMap(() => {
893
+ afterSize(this.handleMoreLinkHeights);
894
+ });
895
+ // internal
896
+ this.slicer = new TimelineLaneSlicer();
897
+ this.handleMoreLinkHeights = () => {
898
+ this.setState({ moreLinkHeightRev: this.moreLinkHeightRefMap.rev }); // will trigger rerender
988
899
  };
989
- this.handleResize = (isForced) => {
990
- if (isForced) {
991
- this.updateSize();
992
- }
900
+ this.handleSegHeights = () => {
901
+ this.setState({ segHeightRev: this.segHeightRefMap.rev }); // will trigger rerender
993
902
  };
994
903
  }
904
+ /*
905
+ TODO: lots of memoization needed here!
906
+ */
995
907
  render() {
996
- let { props, state, context } = this;
908
+ let { props, context, segHeightRefMap } = this;
997
909
  let { options } = context;
998
910
  let { dateProfile, tDateProfile } = props;
999
911
  let slicedProps = this.slicer.sliceProps(props, dateProfile, tDateProfile.isTimeScale ? null : props.nextDayThreshold, context, // wish we didn't have to pass in the rest of the args...
@@ -1002,106 +914,290 @@ class TimelineLane extends BaseComponent {
1002
914
  (slicedProps.eventResize ? slicedProps.eventResize.segs : null) ||
1003
915
  [];
1004
916
  let fgSegs = this.sortEventSegs(slicedProps.fgEventSegs, options.eventOrder);
1005
- let fgSegHCoords = computeSegHCoords(fgSegs, options.eventMinWidth, props.timelineCoords);
1006
- let [fgPlacements, fgHeight] = computeFgSegPlacements(fgSegs, fgSegHCoords, state.eventInstanceHeights, state.moreLinkHeights, options.eventOrderStrict, options.eventMaxStack);
1007
- let isForcedInvisible = // TODO: more convenient
917
+ let fgSegHorizontals = props.slotWidth != null
918
+ ? computeManySegHorizontals(fgSegs, options.eventMinWidth, context.dateEnv, tDateProfile, props.slotWidth)
919
+ : {};
920
+ let [fgSegTops, fgSegsBottom, hiddenGroups, hiddenGroupTops] = computeFgSegPlacements(// verticals
921
+ fgSegs, fgSegHorizontals, segHeightRefMap.current, options.eventOrderStrict, options.eventMaxStack);
922
+ let moreLinksBottom = computeMoreLinkMaxBottom(hiddenGroups, hiddenGroupTops, this.moreLinkHeightRefMap.current);
923
+ let innerHeight = Math.max(moreLinksBottom, fgSegsBottom);
924
+ let forcedInvisibleMap = // TODO: more convenient/DRY
1008
925
  (slicedProps.eventDrag ? slicedProps.eventDrag.affectedInstances : null) ||
1009
926
  (slicedProps.eventResize ? slicedProps.eventResize.affectedInstances : null) ||
1010
927
  {};
1011
928
  return (createElement(Fragment, null,
1012
- createElement(TimelineLaneBg, { businessHourSegs: slicedProps.businessHourSegs, bgEventSegs: slicedProps.bgEventSegs, timelineCoords: props.timelineCoords, eventResizeSegs: slicedProps.eventResize ? slicedProps.eventResize.segs : [] /* bad new empty array? */, dateSelectionSegs: slicedProps.dateSelectionSegs, nowDate: props.nowDate, todayRange: props.todayRange }),
1013
- createElement("div", { className: "fc-timeline-events fc-scrollgrid-sync-inner", ref: this.innerElRef, style: { height: fgHeight } },
1014
- this.renderFgSegs(fgPlacements, isForcedInvisible, false, false, false),
1015
- this.renderFgSegs(buildMirrorPlacements(mirrorSegs, props.timelineCoords, fgPlacements), {}, Boolean(slicedProps.eventDrag), Boolean(slicedProps.eventResize), false))));
929
+ createElement(TimelineLaneBg, { tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange,
930
+ // content
931
+ bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: slicedProps.businessHourSegs, dateSelectionSegs: slicedProps.dateSelectionSegs, eventResizeSegs: slicedProps.eventResize ? slicedProps.eventResize.segs : [] /* bad new empty array? */,
932
+ // dimensions
933
+ slotWidth: props.slotWidth }),
934
+ createElement("div", { className: [
935
+ 'fc-timeline-events',
936
+ 'fc-content-box',
937
+ options.eventOverlap === false // TODO: fix bad default
938
+ ? 'fc-timeline-events-overlap-disabled'
939
+ : 'fc-timeline-events-overlap-enabled'
940
+ ].join(' '), style: { height: innerHeight } },
941
+ this.renderFgSegs(fgSegs, fgSegHorizontals, fgSegTops, forcedInvisibleMap, hiddenGroups, hiddenGroupTops, false, // isDragging
942
+ false, // isResizing
943
+ false),
944
+ this.renderFgSegs(mirrorSegs, props.slotWidth // TODO: memoize
945
+ ? computeManySegHorizontals(mirrorSegs, options.eventMinWidth, context.dateEnv, tDateProfile, props.slotWidth)
946
+ : {}, fgSegTops, {}, // forcedInvisibleMap
947
+ [], {}, Boolean(slicedProps.eventDrag), Boolean(slicedProps.eventResize), false))));
948
+ }
949
+ renderFgSegs(segs, segHorizontals, segTops, forcedInvisibleMap, hiddenGroups, hiddenGroupTops, isDragging, isResizing, isDateSelecting) {
950
+ let { props, context, segHeightRefMap, moreLinkHeightRefMap } = this;
951
+ let isMirror = isDragging || isResizing || isDateSelecting;
952
+ return (createElement(Fragment, null,
953
+ segs.map((seg) => {
954
+ const { eventRange } = seg;
955
+ const { instanceId } = eventRange.instance;
956
+ const segTop = segTops[instanceId];
957
+ const segHorizontal = segHorizontals[instanceId];
958
+ const isVisible = isMirror ||
959
+ (segHorizontal && segTop != null && !forcedInvisibleMap[instanceId]);
960
+ return (createElement(TimelineEventHarness, { key: instanceId, style: Object.assign({ visibility: isVisible ? '' : 'hidden', top: segTop || 0 }, horizontalsToCss(segHorizontal, context.isRtl)), heightRef: isMirror ? undefined : segHeightRefMap.createRef(instanceId) },
961
+ createElement(TimelineEvent, Object.assign({ isTimeScale: props.tDateProfile.isTimeScale, eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === props.eventSelection /* TODO: bad for mirror? */ }, getEventRangeMeta(eventRange, props.todayRange, props.nowDate)))));
962
+ }),
963
+ hiddenGroups.map((hiddenGroup) => (createElement(TimelineEventHarness, { key: hiddenGroup.key, style: Object.assign({ top: hiddenGroupTops[hiddenGroup.key] || 0 }, horizontalsToCss({
964
+ start: hiddenGroup.span.start,
965
+ size: hiddenGroup.span.end - hiddenGroup.span.start
966
+ }, context.isRtl)), heightRef: moreLinkHeightRefMap.createRef(hiddenGroup.key) },
967
+ createElement(TimelineLaneMoreLink, { hiddenSegs: hiddenGroup.segs /* TODO: make SegGroup generic! */, dateProfile: props.dateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isTimeScale: props.tDateProfile.isTimeScale, eventSelection: props.eventSelection, resourceId: props.resourceId, forcedInvisibleMap: forcedInvisibleMap }))))));
968
+ }
969
+ }
970
+
971
+ class TimelineHeaderCell extends BaseComponent {
972
+ constructor() {
973
+ super(...arguments);
974
+ // memo
975
+ this.refineRenderProps = memoizeObjArg(refineRenderProps);
976
+ this.buildCellNavLinkAttrs = memoize(buildCellNavLinkAttrs);
977
+ // ref
978
+ this.innerElRef = createRef();
979
+ }
980
+ render() {
981
+ let { props, context } = this;
982
+ let { dateEnv, options } = context;
983
+ let { cell, dateProfile, tDateProfile } = props;
984
+ // the cell.rowUnit is f'd
985
+ // giving 'month' for a 3-day view
986
+ // workaround: to infer day, do NOT time
987
+ let dateMeta = getDateMeta(cell.date, props.todayRange, props.nowDate, dateProfile);
988
+ let renderProps = this.refineRenderProps({
989
+ level: props.rowLevel,
990
+ dateMarker: cell.date,
991
+ text: cell.text,
992
+ dateEnv: context.dateEnv,
993
+ viewApi: context.viewApi,
994
+ });
995
+ return (createElement(ContentContainer, { elTag: "div", elClasses: [
996
+ 'fc-timeline-slot-label',
997
+ 'fc-timeline-slot',
998
+ cell.isWeekStart ? 'fc-timeline-slot-em' : '',
999
+ 'fc-header-cell',
1000
+ 'fc-cell',
1001
+ 'fc-flex-column',
1002
+ 'fc-justify-center',
1003
+ props.isCentered ? 'fc-align-center' : 'fc-align-start',
1004
+ ...( // TODO: so slot classnames for week/month/bigger. see note above about rowUnit
1005
+ cell.rowUnit === 'time' ?
1006
+ getSlotClassNames(dateMeta, context.theme) :
1007
+ getDayClassNames(dateMeta, context.theme)),
1008
+ ], elAttrs: {
1009
+ 'data-date': dateEnv.formatIso(cell.date, {
1010
+ omitTime: !tDateProfile.isTimeScale,
1011
+ omitTimeZoneOffset: true,
1012
+ }),
1013
+ }, elStyle: {
1014
+ width: props.slotWidth != null
1015
+ ? props.slotWidth * cell.colspan
1016
+ : undefined,
1017
+ }, renderProps: renderProps, generatorName: "slotLabelContent", customGenerator: options.slotLabelContent, defaultGenerator: renderInnerContent, classNameGenerator: options.slotLabelClassNames, didMount: options.slotLabelDidMount, willUnmount: options.slotLabelWillUnmount }, (InnerContent) => (createElement("div", { ref: this.innerElRef, className: [
1018
+ 'fc-flex-column',
1019
+ props.isSticky ? 'fc-sticky-x' : '',
1020
+ ].join(' ') },
1021
+ createElement(InnerContent, { elTag: "a", elClasses: [
1022
+ 'fc-cell-inner',
1023
+ 'fc-padding-md',
1024
+ ], elAttrs: this.buildCellNavLinkAttrs(context, cell.date, cell.rowUnit) })))));
1016
1025
  }
1017
1026
  componentDidMount() {
1018
- this.updateSize();
1019
- this.context.addResizeHandler(this.handleResize);
1020
- }
1021
- componentDidUpdate(prevProps, prevState) {
1022
- if (prevProps.eventStore !== this.props.eventStore || // external thing changed?
1023
- prevProps.timelineCoords !== this.props.timelineCoords || // external thing changed?
1024
- prevState.moreLinkHeights !== this.state.moreLinkHeights // HACK. see addStateEquality
1025
- ) {
1026
- this.updateSize();
1027
- }
1027
+ const { props } = this;
1028
+ const innerEl = this.innerElRef.current; // TODO: make dynamic with useEffect
1029
+ this.detachSize = watchSize(innerEl, (width, height) => {
1030
+ setRef(props.innerWidthRef, width);
1031
+ setRef(props.innerHeightRef, height);
1032
+ // HACK for sticky-centering
1033
+ innerEl.style.left = innerEl.style.right =
1034
+ (props.isCentered && props.isSticky)
1035
+ ? `calc(50% - ${width / 2}px)`
1036
+ : '';
1037
+ });
1028
1038
  }
1029
1039
  componentWillUnmount() {
1030
- this.context.removeResizeHandler(this.handleResize);
1040
+ const { props } = this;
1041
+ this.detachSize();
1042
+ setRef(props.innerWidthRef, null);
1043
+ setRef(props.innerHeightRef, null);
1031
1044
  }
1032
- updateSize() {
1033
- let { props } = this;
1034
- let { timelineCoords } = props;
1035
- const innerEl = this.innerElRef.current;
1036
- if (props.onHeightChange) {
1037
- props.onHeightChange(innerEl, false);
1038
- }
1039
- if (timelineCoords) {
1040
- this.setState({
1041
- eventInstanceHeights: mapHash(this.harnessElRefs.currentMap, (harnessEl) => (Math.round(harnessEl.getBoundingClientRect().height))),
1042
- moreLinkHeights: mapHash(this.moreElRefs.currentMap, (moreEl) => (Math.round(moreEl.getBoundingClientRect().height))),
1043
- }, () => {
1044
- if (props.onHeightChange) {
1045
- props.onHeightChange(innerEl, true);
1046
- }
1047
- });
1048
- }
1049
- // hack
1050
- if (props.syncParentMinHeight) {
1051
- innerEl.parentElement.style.minHeight = innerEl.style.height;
1052
- }
1053
- }
1054
- renderFgSegs(segPlacements, isForcedInvisible, isDragging, isResizing, isDateSelecting) {
1055
- let { harnessElRefs, moreElRefs, props, context } = this;
1056
- let isMirror = isDragging || isResizing || isDateSelecting;
1057
- return (createElement(Fragment, null, segPlacements.map((segPlacement) => {
1058
- let { seg, hcoords, top } = segPlacement;
1059
- if (Array.isArray(seg)) { // a more-link
1060
- let isoStr = buildIsoString(computeEarliestSegStart(seg));
1061
- return (createElement(TimelineLaneMoreLink, { key: 'm:' + isoStr /* "m" for "more" */, elRef: moreElRefs.createRef(isoStr), hiddenSegs: seg, placement: segPlacement, dateProfile: props.dateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isTimeScale: props.tDateProfile.isTimeScale, eventSelection: props.eventSelection, resourceId: props.resourceId, isForcedInvisible: isForcedInvisible }));
1045
+ }
1046
+ // Utils
1047
+ // -------------------------------------------------------------------------------------------------
1048
+ function buildCellNavLinkAttrs(context, cellDate, rowUnit) {
1049
+ return (rowUnit && rowUnit !== 'time')
1050
+ ? buildNavLinkAttrs(context, cellDate, rowUnit)
1051
+ : {};
1052
+ }
1053
+ function renderInnerContent(renderProps) {
1054
+ return renderProps.text;
1055
+ }
1056
+ function refineRenderProps(input) {
1057
+ return {
1058
+ level: input.level,
1059
+ date: input.dateEnv.toDate(input.dateMarker),
1060
+ view: input.viewApi,
1061
+ text: input.text,
1062
+ };
1063
+ }
1064
+
1065
+ class TimelineHeaderRow extends BaseComponent {
1066
+ constructor() {
1067
+ super(...arguments);
1068
+ // refs
1069
+ this.innerWidthRefMap = new RefMap(() => {
1070
+ afterSize(this.handleInnerWidths);
1071
+ });
1072
+ this.innerHeightRefMap = new RefMap(() => {
1073
+ afterSize(this.handleInnerHeights);
1074
+ });
1075
+ this.handleInnerWidths = () => {
1076
+ const innerWidthMap = this.innerWidthRefMap.current;
1077
+ let max = 0;
1078
+ for (const innerWidth of innerWidthMap.values()) {
1079
+ max = Math.max(max, innerWidth);
1080
+ }
1081
+ // TODO: ensure not equal?
1082
+ setRef(this.props.innerWidthRef, max);
1083
+ };
1084
+ this.handleInnerHeights = () => {
1085
+ const innerHeightMap = this.innerHeightRefMap.current;
1086
+ let max = 0;
1087
+ for (const innerHeight of innerHeightMap.values()) {
1088
+ max = Math.max(max, innerHeight);
1062
1089
  }
1063
- let instanceId = seg.eventRange.instance.instanceId;
1064
- let isVisible = isMirror || Boolean(!isForcedInvisible[instanceId] && hcoords && top !== null);
1065
- let hStyle = coordsToCss(hcoords, context.isRtl);
1066
- return (createElement("div", { key: 'e:' + instanceId /* "e" for "event" */, ref: isMirror ? null : harnessElRefs.createRef(instanceId), className: "fc-timeline-event-harness", style: Object.assign({ visibility: isVisible ? '' : 'hidden', top: top || 0 }, hStyle) },
1067
- createElement(TimelineEvent, Object.assign({ isTimeScale: props.tDateProfile.isTimeScale, seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === props.eventSelection /* TODO: bad for mirror? */ }, getSegMeta(seg, props.todayRange, props.nowDate)))));
1090
+ // TODO: ensure not equal?
1091
+ setRef(this.props.innerHeighRef, max);
1092
+ };
1093
+ }
1094
+ render() {
1095
+ const { props, innerWidthRefMap, innerHeightRefMap } = this;
1096
+ const isCentered = !(props.tDateProfile.isTimeScale && props.isLastRow);
1097
+ const isSticky = !props.isLastRow;
1098
+ return (createElement("div", { className: 'fc-row', style: { height: props.height } }, props.cells.map((cell) => {
1099
+ // TODO: make this part of the cell obj?
1100
+ // TODO: rowUnit seems wrong sometimes. says 'month' when it should be day
1101
+ // TODO: rowUnit is relevant to whole row. put it on a row object, not the cells
1102
+ // TODO: use rowUnit to key the Row itself?
1103
+ const key = cell.rowUnit + ':' + cell.date.toISOString();
1104
+ return (createElement(TimelineHeaderCell, { key: key, cell: cell, rowLevel: props.rowLevel, dateProfile: props.dateProfile, tDateProfile: props.tDateProfile, todayRange: props.todayRange, nowDate: props.nowDate, isCentered: isCentered, isSticky: isSticky,
1105
+ // refs
1106
+ innerWidthRef: innerWidthRefMap.createRef(key), innerHeightRef: innerHeightRefMap.createRef(key),
1107
+ // dimensions
1108
+ slotWidth: props.slotWidth }));
1068
1109
  })));
1069
1110
  }
1111
+ componentWillUnmount() {
1112
+ setRef(this.props.innerWidthRef, null);
1113
+ setRef(this.props.innerHeighRef, null);
1114
+ }
1070
1115
  }
1071
- TimelineLane.addStateEquality({
1072
- eventInstanceHeights: isPropsEqual,
1073
- moreLinkHeights: isPropsEqual,
1074
- });
1075
- function buildMirrorPlacements(mirrorSegs, timelineCoords, fgPlacements) {
1076
- if (!mirrorSegs.length || !timelineCoords) {
1077
- return [];
1078
- }
1079
- let topsByInstanceId = buildAbsoluteTopHash(fgPlacements); // TODO: cache this at first render?
1080
- return mirrorSegs.map((seg) => ({
1081
- seg,
1082
- hcoords: timelineCoords.rangeToCoords(seg),
1083
- top: topsByInstanceId[seg.eventRange.instance.instanceId],
1084
- }));
1116
+
1117
+ class TimelineNowIndicatorLine extends BaseComponent {
1118
+ render() {
1119
+ const { props, context } = this;
1120
+ return (createElement("div", { className: "fc-timeline-now-indicator-container" },
1121
+ createElement(NowIndicatorContainer // TODO: make separate component?
1122
+ , { elClasses: ['fc-timeline-now-indicator-line'], elStyle: props.slotWidth != null
1123
+ ? horizontalCoordToCss(dateToCoord(props.nowDate, context.dateEnv, props.tDateProfile, props.slotWidth), context.isRtl)
1124
+ : {}, isAxis: false, date: props.nowDate })));
1125
+ }
1085
1126
  }
1086
- function buildAbsoluteTopHash(placements) {
1087
- let topsByInstanceId = {};
1088
- for (let placement of placements) {
1089
- let { seg } = placement;
1090
- if (!Array.isArray(seg)) { // doesn't represent a more-link
1091
- topsByInstanceId[seg.eventRange.instance.instanceId] = placement.top;
1092
- }
1127
+
1128
+ class TimelineNowIndicatorArrow extends BaseComponent {
1129
+ render() {
1130
+ const { props, context } = this;
1131
+ return (createElement("div", { className: "fc-timeline-now-indicator-container" },
1132
+ createElement(NowIndicatorContainer, { elClasses: ['fc-timeline-now-indicator-arrow'], elStyle: props.slotWidth != null
1133
+ ? horizontalCoordToCss(dateToCoord(props.nowDate, context.dateEnv, props.tDateProfile, props.slotWidth), context.isRtl)
1134
+ : {}, isAxis: true, date: props.nowDate })));
1093
1135
  }
1094
- return topsByInstanceId;
1095
1136
  }
1096
1137
 
1097
- class TimelineGrid extends DateComponent {
1138
+ class TimelineView extends DateComponent {
1098
1139
  constructor() {
1099
1140
  super(...arguments);
1100
- this.slatsRef = createRef();
1101
- this.state = {
1102
- coords: null,
1141
+ // memoized
1142
+ this.buildTimelineDateProfile = memoize(buildTimelineDateProfile);
1143
+ this.computeSlotWidth = memoize(computeSlotWidth);
1144
+ // refs
1145
+ this.headerScrollerRef = createRef();
1146
+ this.bodyScrollerRef = createRef();
1147
+ this.footerScrollerRef = createRef();
1148
+ this.headerRowInnerWidthMap = new RefMap(() => {
1149
+ afterSize(this.handleSlotInnerWidths);
1150
+ });
1151
+ this.scrollTime = null;
1152
+ // Sizing
1153
+ // -----------------------------------------------------------------------------------------------
1154
+ this.handleBodySlotInnerWidth = (innerWidth) => {
1155
+ this.bodySlotInnerWidth = innerWidth;
1156
+ afterSize(this.handleSlotInnerWidths);
1157
+ };
1158
+ this.handleSlotInnerWidths = () => {
1159
+ const { state } = this;
1160
+ const slotInnerWidth = Math.max(this.headerRowInnerWidthMap.current.get(this.tDateProfile.cellRows.length - 1) || 0, this.bodySlotInnerWidth);
1161
+ if (state.slotInnerWidth !== slotInnerWidth) {
1162
+ this.setState({ slotInnerWidth });
1163
+ }
1164
+ };
1165
+ this.handleScrollerWidth = (scrollerWidth) => {
1166
+ this.setState({
1167
+ scrollerWidth,
1168
+ });
1103
1169
  };
1104
- this.handeEl = (el) => {
1170
+ this.handleLeftScrollbarWidth = (leftScrollbarWidth) => {
1171
+ this.setState({
1172
+ leftScrollbarWidth
1173
+ });
1174
+ };
1175
+ this.handleRightScrollbarWidth = (rightScrollbarWidth) => {
1176
+ this.setState({
1177
+ rightScrollbarWidth
1178
+ });
1179
+ };
1180
+ this.handleTimeScroll = (scrollTime) => {
1181
+ this.scrollTime = scrollTime;
1182
+ this.updateScroll();
1183
+ };
1184
+ this.updateScroll = () => {
1185
+ const { props, context, tDateProfile, scrollTime, slotWidth } = this;
1186
+ if (scrollTime != null && slotWidth != null) {
1187
+ let x = timeToCoord(scrollTime, context.dateEnv, props.dateProfile, tDateProfile, slotWidth);
1188
+ if (x) {
1189
+ x += context.isRtl ? -1 : 1; // overcome border. TODO: DRY this up
1190
+ }
1191
+ this.syncedScroller.scrollTo({ x });
1192
+ }
1193
+ };
1194
+ this.clearScroll = () => {
1195
+ this.scrollTime = null;
1196
+ };
1197
+ // Hit System
1198
+ // -----------------------------------------------------------------------------------------------
1199
+ this.handeBodyEl = (el) => {
1200
+ this.bodyEl = el;
1105
1201
  if (el) {
1106
1202
  this.context.registerInteractiveComponent(this, { el });
1107
1203
  }
@@ -1109,45 +1205,143 @@ class TimelineGrid extends DateComponent {
1109
1205
  this.context.unregisterInteractiveComponent(this);
1110
1206
  }
1111
1207
  };
1112
- this.handleCoords = (coords) => {
1113
- this.setState({ coords });
1114
- if (this.props.onSlatCoords) {
1115
- this.props.onSlatCoords(coords);
1116
- }
1117
- };
1118
1208
  }
1119
1209
  render() {
1120
- let { props, state, context } = this;
1121
- let { options } = context;
1122
- let { dateProfile, tDateProfile } = props;
1123
- let timerUnit = greatestDurationDenominator(tDateProfile.slotDuration).unit;
1124
- return (createElement("div", { className: "fc-timeline-body", ref: this.handeEl, style: {
1125
- minWidth: props.tableMinWidth,
1126
- height: props.clientHeight,
1127
- width: props.clientWidth,
1128
- } },
1129
- createElement(NowTimer, { unit: timerUnit }, (nowDate, todayRange) => (createElement(Fragment, null,
1130
- createElement(TimelineSlats, { ref: this.slatsRef, dateProfile: dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, clientWidth: props.clientWidth, tableColGroupNode: props.tableColGroupNode, tableMinWidth: props.tableMinWidth, onCoords: this.handleCoords, onScrollLeftRequest: props.onScrollLeftRequest }),
1131
- createElement(TimelineLane, { dateProfile: dateProfile, tDateProfile: props.tDateProfile, nowDate: nowDate, todayRange: todayRange, nextDayThreshold: options.nextDayThreshold, businessHours: props.businessHours, eventStore: props.eventStore, eventUiBases: props.eventUiBases, dateSelection: props.dateSelection, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, timelineCoords: state.coords, syncParentMinHeight: true }),
1132
- (options.nowIndicator && state.coords && state.coords.isDateInRange(nowDate)) && (createElement("div", { className: "fc-timeline-now-indicator-container" },
1133
- createElement(NowIndicatorContainer, { elClasses: ['fc-timeline-now-indicator-line'], elStyle: coordToCss(state.coords.dateToCoord(nowDate), context.isRtl), isAxis: false, date: nowDate }))))))));
1134
- }
1135
- // Hit System
1136
- // ------------------------------------------------------------------------------------------
1210
+ const { props, state, context } = this;
1211
+ const { options } = context;
1212
+ /* date */
1213
+ const tDateProfile = this.tDateProfile = this.buildTimelineDateProfile(props.dateProfile, context.dateEnv, options, context.dateProfileGenerator);
1214
+ const { cellRows } = tDateProfile;
1215
+ const timerUnit = greatestDurationDenominator(tDateProfile.slotDuration).unit;
1216
+ /* table settings */
1217
+ const verticalScrolling = !props.forPrint && !getIsHeightAuto(options);
1218
+ const stickyHeaderDates = !props.forPrint && getStickyHeaderDates(options);
1219
+ const stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(options);
1220
+ /* table positions */
1221
+ const [canvasWidth, slotWidth] = this.computeSlotWidth(tDateProfile.slotCnt, tDateProfile.slotsPerLabel, options.slotMinWidth, state.slotInnerWidth, // is ACTUALLY the label width. rename?
1222
+ state.scrollerWidth);
1223
+ this.slotWidth = slotWidth;
1224
+ return (createElement(NowTimer, { unit: timerUnit }, (nowDate, todayRange) => {
1225
+ const enableNowIndicator = // TODO: DRY
1226
+ options.nowIndicator &&
1227
+ slotWidth != null &&
1228
+ rangeContainsMarker(props.dateProfile.currentRange, nowDate);
1229
+ return (createElement(ViewContainer, { viewSpec: context.viewSpec, elClasses: [
1230
+ 'fc-timeline-view',
1231
+ 'fc-flex-column',
1232
+ 'fc-border',
1233
+ ] },
1234
+ createElement(Scroller, { horizontal: true, hideScrollbars: true, elClassNames: [
1235
+ 'fc-timeline-header',
1236
+ 'fc-rowgroup',
1237
+ stickyHeaderDates ? 'fc-sticky-header' : '',
1238
+ ], ref: this.headerScrollerRef },
1239
+ createElement("div", { className: 'fc-rel fc-content-box' // origin for now-indicator
1240
+ , style: {
1241
+ width: canvasWidth,
1242
+ paddingLeft: state.leftScrollbarWidth,
1243
+ paddingRight: state.rightScrollbarWidth,
1244
+ } },
1245
+ createElement("div", null, cellRows.map((cells, rowLevel) => {
1246
+ const isLast = rowLevel === cellRows.length - 1;
1247
+ return (createElement(TimelineHeaderRow, { key: rowLevel, dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, rowLevel: rowLevel, isLastRow: isLast, cells: cells, slotWidth: slotWidth, innerWidthRef: this.headerRowInnerWidthMap.createRef(rowLevel) }));
1248
+ })),
1249
+ enableNowIndicator && (
1250
+ // TODO: make this positioned WITHIN padding?
1251
+ createElement(TimelineNowIndicatorArrow, { tDateProfile: tDateProfile, nowDate: nowDate, slotWidth: slotWidth })))),
1252
+ createElement(Scroller, { vertical: verticalScrolling, horizontal: true, elClassNames: [
1253
+ 'fc-timeline-body',
1254
+ 'fc-rowgroup',
1255
+ verticalScrolling ? 'fc-liquid' : '',
1256
+ ], ref: this.bodyScrollerRef, widthRef: this.handleScrollerWidth, leftScrollbarWidthRef: this.handleLeftScrollbarWidth, rightScrollbarWidthRef: this.handleRightScrollbarWidth },
1257
+ createElement("div", { className: "fc-rel fc-grow", style: {
1258
+ width: canvasWidth,
1259
+ }, ref: this.handeBodyEl },
1260
+ createElement(TimelineSlats, { dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange,
1261
+ // ref
1262
+ innerWidthRef: this.handleBodySlotInnerWidth,
1263
+ // dimensions
1264
+ slotWidth: slotWidth }),
1265
+ createElement(TimelineLane, { dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, nextDayThreshold: options.nextDayThreshold, eventStore: props.eventStore, eventUiBases: props.eventUiBases, businessHours: props.businessHours, dateSelection: props.dateSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection, slotWidth: slotWidth }),
1266
+ enableNowIndicator && (createElement(TimelineNowIndicatorLine, { tDateProfile: tDateProfile, nowDate: nowDate, slotWidth: slotWidth })))),
1267
+ stickyFooterScrollbar && (createElement(Scroller, { ref: this.footerScrollerRef, horizontal: true },
1268
+ createElement("div", { style: { width: canvasWidth } })))));
1269
+ }));
1270
+ }
1271
+ // Lifecycle
1272
+ // -----------------------------------------------------------------------------------------------
1273
+ componentDidMount() {
1274
+ this.syncedScroller = new ScrollerSyncer(true); // horizontal=true
1275
+ this.updateSyncedScroller();
1276
+ this.resetScroll();
1277
+ this.context.emitter.on('_timeScrollRequest', this.handleTimeScroll);
1278
+ this.syncedScroller.addScrollEndListener(this.clearScroll);
1279
+ }
1280
+ componentDidUpdate(prevProps) {
1281
+ this.updateSyncedScroller();
1282
+ if (prevProps.dateProfile !== this.props.dateProfile && this.context.options.scrollTimeReset) {
1283
+ this.resetScroll();
1284
+ }
1285
+ else {
1286
+ // TODO: inefficient to update so often
1287
+ this.updateScroll();
1288
+ }
1289
+ }
1290
+ componentWillUnmount() {
1291
+ this.syncedScroller.destroy();
1292
+ this.context.emitter.off('_timeScrollRequest', this.handleTimeScroll);
1293
+ this.syncedScroller.removeScrollEndListener(this.clearScroll);
1294
+ }
1295
+ // Scrolling
1296
+ // -----------------------------------------------------------------------------------------------
1297
+ updateSyncedScroller() {
1298
+ this.syncedScroller.handleChildren([
1299
+ this.headerScrollerRef.current,
1300
+ this.bodyScrollerRef.current,
1301
+ this.footerScrollerRef.current
1302
+ ]);
1303
+ }
1304
+ resetScroll() {
1305
+ this.handleTimeScroll(this.context.options.scrollTime);
1306
+ }
1137
1307
  queryHit(positionLeft, positionTop, elWidth, elHeight) {
1138
- let slats = this.slatsRef.current;
1139
- let slatHit = slats.positionToHit(positionLeft);
1140
- if (slatHit) {
1308
+ const { props, context, tDateProfile, slotWidth } = this;
1309
+ const { dateEnv } = context;
1310
+ if (slotWidth) {
1311
+ const x = context.isRtl ? elWidth - positionLeft : positionLeft;
1312
+ const slatIndex = Math.floor(x / slotWidth);
1313
+ const slatX = slatIndex * slotWidth;
1314
+ const partial = (x - slatX) / slotWidth; // floating point number between 0 and 1
1315
+ const localSnapIndex = Math.floor(partial * tDateProfile.snapsPerSlot); // the snap # relative to start of slat
1316
+ let startDate = dateEnv.add(tDateProfile.slotDates[slatIndex], multiplyDuration(tDateProfile.snapDuration, localSnapIndex));
1317
+ let endDate = dateEnv.add(startDate, tDateProfile.snapDuration);
1318
+ // TODO: generalize this coord stuff to TimeGrid?
1319
+ let snapWidth = slotWidth / tDateProfile.snapsPerSlot;
1320
+ let startCoord = slatIndex * slotWidth + (snapWidth * localSnapIndex);
1321
+ let endCoord = startCoord + snapWidth;
1322
+ let left, right;
1323
+ if (context.isRtl) {
1324
+ left = elWidth - endCoord;
1325
+ right = elWidth - startCoord;
1326
+ }
1327
+ else {
1328
+ left = startCoord;
1329
+ right = endCoord;
1330
+ }
1141
1331
  return {
1142
- dateProfile: this.props.dateProfile,
1143
- dateSpan: slatHit.dateSpan,
1332
+ dateProfile: props.dateProfile,
1333
+ dateSpan: {
1334
+ range: { start: startDate, end: endDate },
1335
+ allDay: !tDateProfile.isTimeScale,
1336
+ },
1144
1337
  rect: {
1145
- left: slatHit.left,
1146
- right: slatHit.right,
1338
+ left,
1339
+ right,
1147
1340
  top: 0,
1148
1341
  bottom: elHeight,
1149
1342
  },
1150
- dayEl: slatHit.dayEl,
1343
+ // HACK. TODO: This is expensive to do every hit-query
1344
+ dayEl: this.bodyEl.querySelectorAll('.fc-timeline-slot')[slatIndex],
1151
1345
  layer: 0,
1152
1346
  };
1153
1347
  }
@@ -1155,89 +1349,7 @@ class TimelineGrid extends DateComponent {
1155
1349
  }
1156
1350
  }
1157
1351
 
1158
- class TimelineView extends DateComponent {
1159
- constructor() {
1160
- super(...arguments);
1161
- this.buildTimelineDateProfile = memoize(buildTimelineDateProfile);
1162
- this.scrollGridRef = createRef();
1163
- this.state = {
1164
- slatCoords: null,
1165
- slotCushionMaxWidth: null,
1166
- };
1167
- this.handleSlatCoords = (slatCoords) => {
1168
- this.setState({ slatCoords });
1169
- };
1170
- this.handleScrollLeftRequest = (scrollLeft) => {
1171
- let scrollGrid = this.scrollGridRef.current;
1172
- scrollGrid.forceScrollLeft(0, scrollLeft);
1173
- };
1174
- this.handleMaxCushionWidth = (slotCushionMaxWidth) => {
1175
- this.setState({
1176
- slotCushionMaxWidth: Math.ceil(slotCushionMaxWidth), // for less rerendering TODO: DRY
1177
- });
1178
- };
1179
- }
1180
- render() {
1181
- let { props, state, context } = this;
1182
- let { options } = context;
1183
- let stickyHeaderDates = !props.forPrint && getStickyHeaderDates(options);
1184
- let stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(options);
1185
- let tDateProfile = this.buildTimelineDateProfile(props.dateProfile, context.dateEnv, options, context.dateProfileGenerator);
1186
- let { slotMinWidth } = options;
1187
- let slatCols = buildSlatCols(tDateProfile, slotMinWidth || this.computeFallbackSlotMinWidth(tDateProfile));
1188
- let sections = [
1189
- {
1190
- type: 'header',
1191
- key: 'header',
1192
- isSticky: stickyHeaderDates,
1193
- chunks: [{
1194
- key: 'timeline',
1195
- content: (contentArg) => (createElement(TimelineHeader, { dateProfile: props.dateProfile, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, tableMinWidth: contentArg.tableMinWidth, tableColGroupNode: contentArg.tableColGroupNode, tDateProfile: tDateProfile, slatCoords: state.slatCoords, onMaxCushionWidth: slotMinWidth ? null : this.handleMaxCushionWidth })),
1196
- }],
1197
- },
1198
- {
1199
- type: 'body',
1200
- key: 'body',
1201
- liquid: true,
1202
- chunks: [{
1203
- key: 'timeline',
1204
- content: (contentArg) => (createElement(TimelineGrid, Object.assign({}, props, { clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, tableMinWidth: contentArg.tableMinWidth, tableColGroupNode: contentArg.tableColGroupNode, tDateProfile: tDateProfile, onSlatCoords: this.handleSlatCoords, onScrollLeftRequest: this.handleScrollLeftRequest }))),
1205
- }],
1206
- },
1207
- ];
1208
- if (stickyFooterScrollbar) {
1209
- sections.push({
1210
- type: 'footer',
1211
- key: 'footer',
1212
- isSticky: true,
1213
- chunks: [{
1214
- key: 'timeline',
1215
- content: renderScrollShim,
1216
- }],
1217
- });
1218
- }
1219
- return (createElement(ViewContainer, { elClasses: [
1220
- 'fc-timeline',
1221
- options.eventOverlap === false ?
1222
- 'fc-timeline-overlap-disabled' :
1223
- '',
1224
- ], viewSpec: context.viewSpec },
1225
- createElement(ScrollGrid, { ref: this.scrollGridRef, liquid: !props.isHeightAuto && !props.forPrint, forPrint: props.forPrint, collapsibleWidth: false, colGroups: [
1226
- { cols: slatCols },
1227
- ], sections: sections })));
1228
- }
1229
- computeFallbackSlotMinWidth(tDateProfile) {
1230
- return Math.max(30, ((this.state.slotCushionMaxWidth || 0) / tDateProfile.slotsPerLabel));
1231
- }
1232
- }
1233
- function buildSlatCols(tDateProfile, slotMinWidth) {
1234
- return [{
1235
- span: tDateProfile.slotCnt,
1236
- minWidth: slotMinWidth || 1, // needs to be a non-zero number to trigger horizontal scrollbars!??????
1237
- }];
1238
- }
1239
-
1240
- var css_248z = ".fc .fc-timeline-body{min-height:100%;position:relative;z-index:1}.fc .fc-timeline-slots{bottom:0;position:absolute;top:0;z-index:1}.fc .fc-timeline-slots>table{height:100%}.fc .fc-timeline-slot-minor{border-style:dotted}.fc .fc-timeline-slot-frame{align-items:center;display:flex;justify-content:center}.fc .fc-timeline-header-row-chrono .fc-timeline-slot-frame{justify-content:flex-start}.fc .fc-timeline-header-row:last-child .fc-timeline-slot-frame{overflow:hidden}.fc .fc-timeline-slot-cushion{padding:4px 5px;white-space:nowrap}.fc-direction-ltr .fc-timeline-slot{border-right:0!important}.fc-direction-rtl .fc-timeline-slot{border-left:0!important}.fc .fc-timeline-now-indicator-container{bottom:0;left:0;position:absolute;right:0;top:0;width:0;z-index:4}.fc .fc-timeline-now-indicator-arrow,.fc .fc-timeline-now-indicator-line{border-color:var(--fc-now-indicator-color);border-style:solid;pointer-events:none;position:absolute;top:0}.fc .fc-timeline-now-indicator-arrow{border-left-color:transparent;border-right-color:transparent;border-width:6px 5px 0;margin:0 -6px}.fc .fc-timeline-now-indicator-line{border-width:0 0 0 1px;bottom:0;margin:0 -1px}.fc .fc-timeline-events{position:relative;width:0;z-index:3}.fc .fc-timeline-event-harness,.fc .fc-timeline-more-link{position:absolute;top:0}.fc-timeline-event{z-index:1}.fc-timeline-event.fc-event-mirror{z-index:2}.fc-timeline-event{align-items:center;border-radius:0;display:flex;font-size:var(--fc-small-font-size);margin-bottom:1px;padding:2px 1px;position:relative}.fc-timeline-event .fc-event-main{flex-grow:1;flex-shrink:1;min-width:0}.fc-timeline-event .fc-event-time{font-weight:700}.fc-timeline-event .fc-event-time,.fc-timeline-event .fc-event-title{padding:0 2px;white-space:nowrap}.fc-direction-ltr .fc-timeline-event.fc-event-end,.fc-direction-ltr .fc-timeline-more-link{margin-right:1px}.fc-direction-rtl .fc-timeline-event.fc-event-end,.fc-direction-rtl .fc-timeline-more-link{margin-left:1px}.fc-timeline-overlap-disabled .fc-timeline-event{margin-bottom:0;padding-bottom:5px;padding-top:5px}.fc-timeline-event:not(.fc-event-end):after,.fc-timeline-event:not(.fc-event-start):before{border-color:transparent #000;border-style:solid;border-width:5px;content:\"\";flex-grow:0;flex-shrink:0;height:0;margin:0 1px;opacity:.5;width:0}.fc-direction-ltr .fc-timeline-event:not(.fc-event-start):before,.fc-direction-rtl .fc-timeline-event:not(.fc-event-end):after{border-left:0}.fc-direction-ltr .fc-timeline-event:not(.fc-event-end):after,.fc-direction-rtl .fc-timeline-event:not(.fc-event-start):before{border-right:0}.fc-timeline-more-link{background:var(--fc-more-link-bg-color);color:var(--fc-more-link-text-color);cursor:pointer;font-size:var(--fc-small-font-size);padding:1px}.fc-timeline-more-link-inner{display:inline-block;left:0;padding:2px;right:0}.fc .fc-timeline-bg{bottom:0;left:0;position:absolute;right:0;top:0;width:0;z-index:2}.fc .fc-timeline-bg .fc-non-business{z-index:1}.fc .fc-timeline-bg .fc-bg-event{z-index:2}.fc .fc-timeline-bg .fc-highlight{z-index:3}.fc .fc-timeline-bg-harness{bottom:0;position:absolute;top:0}";
1352
+ var css_248z = ".fc-timeline-slots{z-index:1}.fc-timeline-slot-minor{border-style:dotted}.fc-timeline-now-indicator-container{bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0;z-index:4}.fc-timeline-now-indicator-arrow,.fc-timeline-now-indicator-line{border-color:var(--fc-now-indicator-color);border-style:solid;pointer-events:none;position:absolute;top:0}.fc-timeline-now-indicator-arrow{border-left-color:transparent;border-right-color:transparent;border-width:6px 5px 0;margin:0 -5px}.fc-timeline-now-indicator-line{border-width:0 0 0 1px;bottom:0}.fc-timeline-events{z-index:3}.fc-timeline-events-overlap-enabled{padding-bottom:10px}.fc-timeline-event{border-radius:0;font-size:var(--fc-small-font-size);margin-bottom:1px;z-index:1}.fc-timeline-event.fc-event-mirror{z-index:2}.fc-direction-ltr .fc-timeline-event.fc-event-end{margin-right:1px}.fc-direction-rtl .fc-timeline-event.fc-event-end{margin-left:1px}.fc-timeline-event-spacious{margin-bottom:0;padding-bottom:5px;padding-top:5px}.fc-timeline-event .fc-event-inner{align-items:center;display:flex;flex-direction:row;padding:2px 1px}.fc-timeline-event:not(.fc-event-end) .fc-event-inner:after,.fc-timeline-event:not(.fc-event-start) .fc-event-inner:before{border-color:transparent #000;border-style:solid;border-width:5px;content:\"\";flex-grow:0;flex-shrink:0;height:0;margin:0 1px;opacity:.5;width:0}.fc-direction-ltr .fc-timeline-event:not(.fc-event-start) .fc-event-inner:before,.fc-direction-rtl .fc-timeline-event:not(.fc-event-end) .fc-event-inner:after{border-left:0}.fc-direction-ltr .fc-timeline-event:not(.fc-event-end) .fc-event-inner:after,.fc-direction-rtl .fc-timeline-event:not(.fc-event-start) .fc-event-inner:before{border-right:0}.fc-timeline-event .fc-event-time{font-weight:700}.fc-timeline-event .fc-event-time,.fc-timeline-event .fc-event-title{padding:0 2px}.fc-timeline-more-link{align-items:flex-start;background:var(--fc-more-link-bg-color);color:var(--fc-more-link-text-color);cursor:pointer;display:flex;flex-direction:column;font-size:var(--fc-small-font-size);padding:1px}.fc-direction-ltr .fc-timeline-more-link{margin-right:1px}.fc-direction-rtl .fc-timeline-more-link{margin-left:1px}.fc-timeline-more-link-inner{padding:2px}.fc-timeline-bg{bottom:0;left:0;position:absolute;right:0;top:0;z-index:2}.fc-timeline-bg .fc-non-business{z-index:1}.fc-timeline-bg .fc-bg-event{z-index:2}.fc-timeline-bg .fc-highlight{z-index:3}.fc-timeline-bg-harness{bottom:0;position:absolute;top:0}";
1241
1353
  injectStyles(css_248z);
1242
1354
 
1243
- export { TimelineCoords, TimelineHeader, TimelineHeaderRows, TimelineLane, TimelineLaneBg, TimelineLaneSlicer, TimelineSlats, TimelineView, buildSlatCols, buildTimelineDateProfile, coordToCss, coordsToCss };
1355
+ export { TimelineHeaderRow, TimelineLane, TimelineLaneBg, TimelineLaneSlicer, TimelineNowIndicatorArrow, TimelineNowIndicatorLine, TimelineSlats, TimelineView, buildTimelineDateProfile, computeSlotWidth, coordToCss, coordsToCss, createHorizontalStyle, createVerticalStyle, timeToCoord };