UNPKG

@fullcalendar/scrollgrid

Version:

Tabular data chunked into scrollable panes

857 lines (845 loc) 39.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var internal_cjs = require('@fullcalendar/core/internal.cjs'); var preact_cjs = require('@fullcalendar/core/preact.cjs'); // TODO: assume the el has no borders? function getScrollCanvasOrigin(scrollEl) { let rect = scrollEl.getBoundingClientRect(); let edges = internal_cjs.computeEdges(scrollEl); // TODO: pass in isRtl? return { left: rect.left + edges.borderLeft + edges.scrollbarLeft - getScrollFromLeftEdge(scrollEl), top: rect.top + edges.borderTop - scrollEl.scrollTop, }; } function getScrollFromLeftEdge(el) { let scrollLeft = el.scrollLeft; let computedStyles = window.getComputedStyle(el); // TODO: pass in isRtl instead? if (computedStyles.direction === 'rtl') { switch (getRtlScrollSystem()) { case 'negative': scrollLeft *= -1; // convert to 'reverse'. fall through... case 'reverse': // scrollLeft is distance between scrollframe's right edge scrollcanvas's right edge scrollLeft = el.scrollWidth - scrollLeft - el.clientWidth; } } return scrollLeft; } function setScrollFromLeftEdge(el, scrollLeft) { let computedStyles = window.getComputedStyle(el); // TODO: pass in isRtl instead? if (computedStyles.direction === 'rtl') { switch (getRtlScrollSystem()) { case 'reverse': scrollLeft = el.scrollWidth - scrollLeft; break; case 'negative': scrollLeft = -(el.scrollWidth - scrollLeft); break; } } el.scrollLeft = scrollLeft; } // Horizontal Scroll System Detection // ---------------------------------------------------------------------------------------------- let _rtlScrollSystem; function getRtlScrollSystem() { return _rtlScrollSystem || (_rtlScrollSystem = detectRtlScrollSystem()); } function detectRtlScrollSystem() { let el = document.createElement('div'); el.style.position = 'absolute'; el.style.top = '-1000px'; el.style.width = '100px'; // must be at least the side of scrollbars or you get inaccurate values (#7335) el.style.height = '100px'; // " el.style.overflow = 'scroll'; el.style.direction = 'rtl'; let innerEl = document.createElement('div'); innerEl.style.width = '200px'; innerEl.style.height = '200px'; el.appendChild(innerEl); document.body.appendChild(el); let system; if (el.scrollLeft > 0) { system = 'positive'; // scroll is a positive number from the left edge } else { el.scrollLeft = 1; if (el.scrollLeft > 0) { system = 'reverse'; // scroll is a positive number from the right edge } else { system = 'negative'; // scroll is a negative number from the right edge } } internal_cjs.removeElement(el); return system; } const STICKY_SELECTOR = '.fc-sticky'; /* Goes beyond mere position:sticky, allows horizontal centering REQUIREMENT: fc-sticky elements, if the fc-sticky className is taken away, should NOT have relative or absolute positioning. This is because we attach the coords with JS, and the VDOM might take away the fc-sticky class but doesn't know kill the positioning. TODO: don't query text-align:center. isn't compatible with flexbox centering. instead, check natural X coord within parent container */ class StickyScrolling { constructor(scrollEl, isRtl) { this.scrollEl = scrollEl; this.isRtl = isRtl; this.updateSize = () => { let { scrollEl } = this; let els = internal_cjs.findElements(scrollEl, STICKY_SELECTOR); let elGeoms = this.queryElGeoms(els); let viewportWidth = scrollEl.clientWidth; assignStickyPositions(els, elGeoms, viewportWidth); }; } queryElGeoms(els) { let { scrollEl, isRtl } = this; let canvasOrigin = getScrollCanvasOrigin(scrollEl); let elGeoms = []; for (let el of els) { let parentBound = internal_cjs.translateRect(internal_cjs.computeInnerRect(el.parentNode, true, true), // weird way to call this!!! -canvasOrigin.left, -canvasOrigin.top); let elRect = el.getBoundingClientRect(); let computedStyles = window.getComputedStyle(el); let textAlign = window.getComputedStyle(el.parentNode).textAlign; // ask the parent let naturalBound = null; if (textAlign === 'start') { textAlign = isRtl ? 'right' : 'left'; } else if (textAlign === 'end') { textAlign = isRtl ? 'left' : 'right'; } if (computedStyles.position !== 'sticky') { naturalBound = internal_cjs.translateRect(elRect, -canvasOrigin.left - (parseFloat(computedStyles.left) || 0), // could be 'auto' -canvasOrigin.top - (parseFloat(computedStyles.top) || 0)); } elGeoms.push({ parentBound, naturalBound, elWidth: elRect.width, elHeight: elRect.height, textAlign, }); } return elGeoms; } } function assignStickyPositions(els, elGeoms, viewportWidth) { els.forEach((el, i) => { let { textAlign, elWidth, parentBound } = elGeoms[i]; let parentWidth = parentBound.right - parentBound.left; let left; if (textAlign === 'center' && parentWidth > viewportWidth) { left = (viewportWidth - elWidth) / 2; } else { // if parent container can be completely in view, we don't need stickiness left = ''; } internal_cjs.applyStyle(el, { left, right: left, top: 0, }); }); } class ClippedScroller extends internal_cjs.BaseComponent { constructor() { super(...arguments); this.elRef = preact_cjs.createRef(); this.state = { xScrollbarWidth: 0, yScrollbarWidth: 0, }; this.handleScroller = (scroller) => { this.scroller = scroller; internal_cjs.setRef(this.props.scrollerRef, scroller); }; this.handleSizing = () => { let { props } = this; if (props.overflowY === 'scroll-hidden') { this.setState({ yScrollbarWidth: this.scroller.getYScrollbarWidth() }); } if (props.overflowX === 'scroll-hidden') { this.setState({ xScrollbarWidth: this.scroller.getXScrollbarWidth() }); } }; } render() { let { props, state, context } = this; let isScrollbarOnLeft = context.isRtl && internal_cjs.getIsRtlScrollbarOnLeft(); let overcomeLeft = 0; let overcomeRight = 0; let overcomeBottom = 0; let { overflowX, overflowY } = props; if (props.forPrint) { overflowX = 'visible'; overflowY = 'visible'; } if (overflowX === 'scroll-hidden') { overcomeBottom = state.xScrollbarWidth; } if (overflowY === 'scroll-hidden') { if (state.yScrollbarWidth != null) { if (isScrollbarOnLeft) { overcomeLeft = state.yScrollbarWidth; } else { overcomeRight = state.yScrollbarWidth; } } } return (preact_cjs.createElement("div", { ref: this.elRef, className: 'fc-scroller-harness' + (props.liquid ? ' fc-scroller-harness-liquid' : '') }, preact_cjs.createElement(internal_cjs.Scroller, { ref: this.handleScroller, elRef: this.props.scrollerElRef, overflowX: overflowX === 'scroll-hidden' ? 'scroll' : overflowX, overflowY: overflowY === 'scroll-hidden' ? 'scroll' : overflowY, overcomeLeft: overcomeLeft, overcomeRight: overcomeRight, overcomeBottom: overcomeBottom, maxHeight: typeof props.maxHeight === 'number' ? (props.maxHeight + (overflowX === 'scroll-hidden' ? state.xScrollbarWidth : 0)) : '', liquid: props.liquid, liquidIsAbsolute: true }, props.children))); } componentDidMount() { this.handleSizing(); this.context.addResizeHandler(this.handleSizing); } getSnapshotBeforeUpdate(prevProps) { if (this.props.forPrint && !prevProps.forPrint) { return { simulateScrollLeft: this.scroller.el.scrollLeft }; } return {}; } componentDidUpdate(prevProps, prevState, snapshot) { const { props, scroller: { el: scrollerEl } } = this; if (!internal_cjs.isPropsEqual(prevProps, props)) { // an external change? this.handleSizing(); } if (snapshot.simulateScrollLeft !== undefined) { scrollerEl.style.left = -snapshot.simulateScrollLeft + 'px'; } else if (!props.forPrint && prevProps.forPrint) { const restoredScrollLeft = -parseInt(scrollerEl.style.left); scrollerEl.style.left = ''; scrollerEl.scrollLeft = restoredScrollLeft; } } componentWillUnmount() { this.context.removeResizeHandler(this.handleSizing); } needsXScrolling() { return this.scroller.needsXScrolling(); } needsYScrolling() { return this.scroller.needsYScrolling(); } } const WHEEL_EVENT_NAMES = 'wheel mousewheel DomMouseScroll MozMousePixelScroll'.split(' '); /* ALSO, with the ability to disable touch */ class ScrollListener { constructor(el) { this.el = el; this.emitter = new internal_cjs.Emitter(); this.isScrolling = false; this.isTouching = false; // user currently has finger down? this.isRecentlyWheeled = false; this.isRecentlyScrolled = false; this.wheelWaiter = new internal_cjs.DelayedRunner(this._handleWheelWaited.bind(this)); this.scrollWaiter = new internal_cjs.DelayedRunner(this._handleScrollWaited.bind(this)); // Handlers // ---------------------------------------------------------------------------------------------- this.handleScroll = () => { this.startScroll(); this.emitter.trigger('scroll', this.isRecentlyWheeled, this.isTouching); this.isRecentlyScrolled = true; this.scrollWaiter.request(500); }; // will fire *before* the scroll event is fired (might not cause a scroll) this.handleWheel = () => { this.isRecentlyWheeled = true; this.wheelWaiter.request(500); }; // will fire *before* the scroll event is fired (might not cause a scroll) this.handleTouchStart = () => { this.isTouching = true; }; this.handleTouchEnd = () => { this.isTouching = false; // if the user ended their touch, and the scroll area wasn't moving, // we consider this to be the end of the scroll. if (!this.isRecentlyScrolled) { this.endScroll(); // won't fire if already ended } }; el.addEventListener('scroll', this.handleScroll); el.addEventListener('touchstart', this.handleTouchStart, { passive: true }); el.addEventListener('touchend', this.handleTouchEnd); for (let eventName of WHEEL_EVENT_NAMES) { el.addEventListener(eventName, this.handleWheel); } } destroy() { let { el } = this; el.removeEventListener('scroll', this.handleScroll); el.removeEventListener('touchstart', this.handleTouchStart, { passive: true }); el.removeEventListener('touchend', this.handleTouchEnd); for (let eventName of WHEEL_EVENT_NAMES) { el.removeEventListener(eventName, this.handleWheel); } } // Start / Stop // ---------------------------------------------------------------------------------------------- startScroll() { if (!this.isScrolling) { this.isScrolling = true; this.emitter.trigger('scrollStart', this.isRecentlyWheeled, this.isTouching); } } endScroll() { if (this.isScrolling) { this.emitter.trigger('scrollEnd'); this.isScrolling = false; this.isRecentlyScrolled = true; this.isRecentlyWheeled = false; this.scrollWaiter.clear(); this.wheelWaiter.clear(); } } _handleScrollWaited() { this.isRecentlyScrolled = false; // only end the scroll if not currently touching. // if touching, the scrolling will end later, on touchend. if (!this.isTouching) { this.endScroll(); // won't fire if already ended } } _handleWheelWaited() { this.isRecentlyWheeled = false; } } class ScrollSyncer { constructor(isVertical, scrollEls) { this.isVertical = isVertical; this.scrollEls = scrollEls; this.isPaused = false; this.scrollListeners = scrollEls.map((el) => this.bindScroller(el)); } destroy() { for (let scrollListener of this.scrollListeners) { scrollListener.destroy(); } } bindScroller(el) { let { scrollEls, isVertical } = this; let scrollListener = new ScrollListener(el); const onScroll = (isWheel, isTouch) => { if (!this.isPaused) { if (!this.masterEl || (this.masterEl !== el && (isWheel || isTouch))) { this.assignMaster(el); } if (this.masterEl === el) { // dealing with current for (let otherEl of scrollEls) { if (otherEl !== el) { if (isVertical) { otherEl.scrollTop = el.scrollTop; } else { otherEl.scrollLeft = el.scrollLeft; } } } } } }; const onScrollEnd = () => { if (this.masterEl === el) { this.masterEl = null; } }; scrollListener.emitter.on('scroll', onScroll); scrollListener.emitter.on('scrollEnd', onScrollEnd); return scrollListener; } assignMaster(el) { this.masterEl = el; for (let scrollListener of this.scrollListeners) { if (scrollListener.el !== el) { scrollListener.endScroll(); // to prevent residual scrolls from reclaiming master } } } /* will normalize the scrollLeft value */ forceScrollLeft(scrollLeft) { this.isPaused = true; for (let listener of this.scrollListeners) { setScrollFromLeftEdge(listener.el, scrollLeft); } this.isPaused = false; } forceScrollTop(top) { this.isPaused = true; for (let listener of this.scrollListeners) { listener.el.scrollTop = top; } this.isPaused = false; } } internal_cjs.config.SCROLLGRID_RESIZE_INTERVAL = 500; /* TODO: make <ScrollGridSection> subcomponent NOTE: doesn't support collapsibleWidth (which is sortof a hack anyway) */ class ScrollGrid extends internal_cjs.BaseComponent { constructor() { super(...arguments); this.compileColGroupStats = internal_cjs.memoizeArraylike(compileColGroupStat, isColGroupStatsEqual); this.renderMicroColGroups = internal_cjs.memoizeArraylike(internal_cjs.renderMicroColGroup); // yucky to memoize VNodes, but much more efficient for consumers this.clippedScrollerRefs = new internal_cjs.RefMap(); // doesn't hold non-scrolling els used just for padding this.scrollerElRefs = new internal_cjs.RefMap(this._handleScrollerEl.bind(this)); this.chunkElRefs = new internal_cjs.RefMap(this._handleChunkEl.bind(this)); this.scrollSyncersBySection = {}; this.scrollSyncersByColumn = {}; // for row-height-syncing this.rowUnstableMap = new Map(); // no need to groom. always self-cancels this.rowInnerMaxHeightMap = new Map(); this.anyRowHeightsChanged = false; this.recentSizingCnt = 0; this.state = { shrinkWidths: [], forceYScrollbars: false, forceXScrollbars: false, scrollerClientWidths: {}, scrollerClientHeights: {}, sectionRowMaxHeights: [], }; this.handleSizing = (isForcedResize, sectionRowMaxHeightsChanged) => { if (!this.allowSizing()) { return; } if (!sectionRowMaxHeightsChanged) { // something else changed, probably external this.anyRowHeightsChanged = true; } let otherState = {}; // if reacting to self-change of sectionRowMaxHeightsChanged, or not stable, don't do anything if (isForcedResize || (!sectionRowMaxHeightsChanged && !this.rowUnstableMap.size)) { otherState.sectionRowMaxHeights = this.computeSectionRowMaxHeights(); } this.setState(Object.assign(Object.assign({ shrinkWidths: this.computeShrinkWidths() }, this.computeScrollerDims()), otherState), () => { if (!this.rowUnstableMap.size) { this.updateStickyScrolling(); // needs to happen AFTER final positioning committed to DOM } }); }; this.handleRowHeightChange = (rowEl, isStable) => { let { rowUnstableMap, rowInnerMaxHeightMap } = this; if (!isStable) { rowUnstableMap.set(rowEl, true); } else { rowUnstableMap.delete(rowEl); let innerMaxHeight = getRowInnerMaxHeight(rowEl); if (!rowInnerMaxHeightMap.has(rowEl) || rowInnerMaxHeightMap.get(rowEl) !== innerMaxHeight) { rowInnerMaxHeightMap.set(rowEl, innerMaxHeight); this.anyRowHeightsChanged = true; } if (!rowUnstableMap.size && this.anyRowHeightsChanged) { this.anyRowHeightsChanged = false; this.setState({ sectionRowMaxHeights: this.computeSectionRowMaxHeights(), }); } } }; } render() { let { props, state, context } = this; let { shrinkWidths } = state; let colGroupStats = this.compileColGroupStats(props.colGroups.map((colGroup) => [colGroup])); let microColGroupNodes = this.renderMicroColGroups(colGroupStats.map((stat, i) => [stat.cols, shrinkWidths[i]])); let classNames = internal_cjs.getScrollGridClassNames(props.liquid, context); this.getDims(); // TODO: make DRY let sectionConfigs = props.sections; let configCnt = sectionConfigs.length; let configI = 0; let currentConfig; let headSectionNodes = []; let bodySectionNodes = []; let footSectionNodes = []; while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'header') { headSectionNodes.push(this.renderSection(currentConfig, configI, colGroupStats, microColGroupNodes, state.sectionRowMaxHeights, true)); configI += 1; } while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'body') { bodySectionNodes.push(this.renderSection(currentConfig, configI, colGroupStats, microColGroupNodes, state.sectionRowMaxHeights, false)); configI += 1; } while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'footer') { footSectionNodes.push(this.renderSection(currentConfig, configI, colGroupStats, microColGroupNodes, state.sectionRowMaxHeights, true)); configI += 1; } const isBuggy = !internal_cjs.getCanVGrowWithinCell(); // see NOTE in SimpleScrollGrid const roleAttrs = { role: 'rowgroup' }; return preact_cjs.createElement('table', { ref: props.elRef, role: 'grid', className: classNames.join(' '), }, renderMacroColGroup(colGroupStats, shrinkWidths), Boolean(!isBuggy && headSectionNodes.length) && preact_cjs.createElement('thead', roleAttrs, ...headSectionNodes), Boolean(!isBuggy && bodySectionNodes.length) && preact_cjs.createElement('tbody', roleAttrs, ...bodySectionNodes), Boolean(!isBuggy && footSectionNodes.length) && preact_cjs.createElement('tfoot', roleAttrs, ...footSectionNodes), isBuggy && preact_cjs.createElement('tbody', roleAttrs, ...headSectionNodes, ...bodySectionNodes, ...footSectionNodes)); } renderSection(sectionConfig, sectionIndex, colGroupStats, microColGroupNodes, sectionRowMaxHeights, isHeader) { if ('outerContent' in sectionConfig) { return (preact_cjs.createElement(preact_cjs.Fragment, { key: sectionConfig.key }, sectionConfig.outerContent)); } return (preact_cjs.createElement("tr", { key: sectionConfig.key, role: "presentation", className: internal_cjs.getSectionClassNames(sectionConfig, this.props.liquid).join(' ') }, sectionConfig.chunks.map((chunkConfig, i) => this.renderChunk(sectionConfig, sectionIndex, colGroupStats[i], microColGroupNodes[i], chunkConfig, i, (sectionRowMaxHeights[sectionIndex] || [])[i] || [], isHeader)))); } renderChunk(sectionConfig, sectionIndex, colGroupStat, microColGroupNode, chunkConfig, chunkIndex, rowHeights, isHeader) { if ('outerContent' in chunkConfig) { return (preact_cjs.createElement(preact_cjs.Fragment, { key: chunkConfig.key }, chunkConfig.outerContent)); } let { state } = this; let { scrollerClientWidths, scrollerClientHeights } = state; let [sectionCnt, chunksPerSection] = this.getDims(); let index = sectionIndex * chunksPerSection + chunkIndex; let sideScrollIndex = (!this.context.isRtl || internal_cjs.getIsRtlScrollbarOnLeft()) ? chunksPerSection - 1 : 0; let isVScrollSide = chunkIndex === sideScrollIndex; let isLastSection = sectionIndex === sectionCnt - 1; let forceXScrollbars = isLastSection && state.forceXScrollbars; // NOOOO can result in `null` let forceYScrollbars = isVScrollSide && state.forceYScrollbars; // NOOOO can result in `null` let allowXScrolling = colGroupStat && colGroupStat.allowXScrolling; // rename? let allowYScrolling = internal_cjs.getAllowYScrolling(this.props, sectionConfig); // rename? do in section func? let chunkVGrow = internal_cjs.getSectionHasLiquidHeight(this.props, sectionConfig); // do in section func? let expandRows = sectionConfig.expandRows && chunkVGrow; let tableMinWidth = (colGroupStat && colGroupStat.totalColMinWidth) || ''; let content = internal_cjs.renderChunkContent(sectionConfig, chunkConfig, { tableColGroupNode: microColGroupNode, tableMinWidth, clientWidth: scrollerClientWidths[index] !== undefined ? scrollerClientWidths[index] : null, clientHeight: scrollerClientHeights[index] !== undefined ? scrollerClientHeights[index] : null, expandRows, syncRowHeights: Boolean(sectionConfig.syncRowHeights), rowSyncHeights: rowHeights, reportRowHeightChange: this.handleRowHeightChange, }, isHeader); let overflowX = forceXScrollbars ? (isLastSection ? 'scroll' : 'scroll-hidden') : !allowXScrolling ? 'hidden' : (isLastSection ? 'auto' : 'scroll-hidden'); let overflowY = forceYScrollbars ? (isVScrollSide ? 'scroll' : 'scroll-hidden') : !allowYScrolling ? 'hidden' : (isVScrollSide ? 'auto' : 'scroll-hidden'); // it *could* be possible to reduce DOM wrappers by only doing a ClippedScroller when allowXScrolling or allowYScrolling, // but if these values were to change, the inner components would be unmounted/remounted because of the parent change. content = (preact_cjs.createElement(ClippedScroller, { ref: this.clippedScrollerRefs.createRef(index), scrollerElRef: this.scrollerElRefs.createRef(index), overflowX: overflowX, overflowY: overflowY, forPrint: this.props.forPrint, liquid: chunkVGrow, maxHeight: sectionConfig.maxHeight }, content)); return preact_cjs.createElement(isHeader ? 'th' : 'td', { key: chunkConfig.key, ref: this.chunkElRefs.createRef(index), role: 'presentation', }, content); } componentDidMount() { this.getStickyScrolling = internal_cjs.memoizeArraylike(initStickyScrolling); this.getScrollSyncersBySection = internal_cjs.memoizeHashlike(initScrollSyncer.bind(this, true), null, destroyScrollSyncer); this.getScrollSyncersByColumn = internal_cjs.memoizeHashlike(initScrollSyncer.bind(this, false), null, destroyScrollSyncer); this.updateScrollSyncers(); this.handleSizing(false); this.context.addResizeHandler(this.handleSizing); } componentDidUpdate(prevProps, prevState) { this.updateScrollSyncers(); // TODO: need better solution when state contains non-sizing things this.handleSizing(false, prevState.sectionRowMaxHeights !== this.state.sectionRowMaxHeights); } componentWillUnmount() { this.context.removeResizeHandler(this.handleSizing); this.destroyScrollSyncers(); } allowSizing() { let now = new Date(); if (!this.lastSizingDate || now.valueOf() > this.lastSizingDate.valueOf() + internal_cjs.config.SCROLLGRID_RESIZE_INTERVAL) { this.lastSizingDate = now; this.recentSizingCnt = 0; return true; } return (this.recentSizingCnt += 1) <= 10; } computeShrinkWidths() { let colGroupStats = this.compileColGroupStats(this.props.colGroups.map((colGroup) => [colGroup])); let [sectionCnt, chunksPerSection] = this.getDims(); let cnt = sectionCnt * chunksPerSection; let shrinkWidths = []; colGroupStats.forEach((colGroupStat, i) => { if (colGroupStat.hasShrinkCol) { let chunkEls = this.chunkElRefs.collect(i, cnt, chunksPerSection); // in one col shrinkWidths[i] = internal_cjs.computeShrinkWidth(chunkEls); } }); return shrinkWidths; } // has the side effect of grooming rowInnerMaxHeightMap // TODO: somehow short-circuit if there are no new height changes computeSectionRowMaxHeights() { let newHeightMap = new Map(); let [sectionCnt, chunksPerSection] = this.getDims(); let sectionRowMaxHeights = []; for (let sectionI = 0; sectionI < sectionCnt; sectionI += 1) { let sectionConfig = this.props.sections[sectionI]; let assignableHeights = []; // chunk, row if (sectionConfig && sectionConfig.syncRowHeights) { let rowHeightsByChunk = []; for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { let index = sectionI * chunksPerSection + chunkI; let rowHeights = []; let chunkEl = this.chunkElRefs.currentMap[index]; if (chunkEl) { rowHeights = internal_cjs.findElements(chunkEl, '.fc-scrollgrid-sync-table tr').map((rowEl) => { let max = getRowInnerMaxHeight(rowEl); newHeightMap.set(rowEl, max); return max; }); } else { rowHeights = []; } rowHeightsByChunk.push(rowHeights); } let rowCnt = rowHeightsByChunk[0].length; let isEqualRowCnt = true; for (let chunkI = 1; chunkI < chunksPerSection; chunkI += 1) { let isOuterContent = sectionConfig.chunks[chunkI] && sectionConfig.chunks[chunkI].outerContent !== undefined; // can be null if (!isOuterContent && rowHeightsByChunk[chunkI].length !== rowCnt) { // skip outer content isEqualRowCnt = false; break; } } if (!isEqualRowCnt) { let chunkHeightSums = []; for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { chunkHeightSums.push(sumNumbers(rowHeightsByChunk[chunkI]) + rowHeightsByChunk[chunkI].length); } let maxTotalSum = Math.max(...chunkHeightSums); for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { let rowInChunkCnt = rowHeightsByChunk[chunkI].length; let rowInChunkTotalHeight = maxTotalSum - rowInChunkCnt; // subtract border // height of non-first row. we do this to avoid rounding, because it's unreliable within a table let rowInChunkHeightOthers = Math.floor(rowInChunkTotalHeight / rowInChunkCnt); // whatever is leftover goes to the first row let rowInChunkHeightFirst = rowInChunkTotalHeight - rowInChunkHeightOthers * (rowInChunkCnt - 1); let rowInChunkHeights = []; let row = 0; if (row < rowInChunkCnt) { rowInChunkHeights.push(rowInChunkHeightFirst); row += 1; } while (row < rowInChunkCnt) { rowInChunkHeights.push(rowInChunkHeightOthers); row += 1; } assignableHeights.push(rowInChunkHeights); } } else { for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { assignableHeights.push([]); } for (let row = 0; row < rowCnt; row += 1) { let rowHeightsAcrossChunks = []; for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { let h = rowHeightsByChunk[chunkI][row]; if (h != null) { // protect against outerContent rowHeightsAcrossChunks.push(h); } } let maxHeight = Math.max(...rowHeightsAcrossChunks); for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { assignableHeights[chunkI].push(maxHeight); } } } } sectionRowMaxHeights.push(assignableHeights); } this.rowInnerMaxHeightMap = newHeightMap; return sectionRowMaxHeights; } computeScrollerDims() { let scrollbarWidth = internal_cjs.getScrollbarWidths(); let [sectionCnt, chunksPerSection] = this.getDims(); let sideScrollI = (!this.context.isRtl || internal_cjs.getIsRtlScrollbarOnLeft()) ? chunksPerSection - 1 : 0; let lastSectionI = sectionCnt - 1; let currentScrollers = this.clippedScrollerRefs.currentMap; let scrollerEls = this.scrollerElRefs.currentMap; let forceYScrollbars = false; let forceXScrollbars = false; let scrollerClientWidths = {}; let scrollerClientHeights = {}; for (let sectionI = 0; sectionI < sectionCnt; sectionI += 1) { // along edge let index = sectionI * chunksPerSection + sideScrollI; let scroller = currentScrollers[index]; if (scroller && scroller.needsYScrolling()) { forceYScrollbars = true; break; } } for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { // along last row let index = lastSectionI * chunksPerSection + chunkI; let scroller = currentScrollers[index]; if (scroller && scroller.needsXScrolling()) { forceXScrollbars = true; break; } } for (let sectionI = 0; sectionI < sectionCnt; sectionI += 1) { for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { let index = sectionI * chunksPerSection + chunkI; let scrollerEl = scrollerEls[index]; if (scrollerEl) { // TODO: weird way to get this. need harness b/c doesn't include table borders let harnessEl = scrollerEl.parentNode; scrollerClientWidths[index] = Math.floor(harnessEl.getBoundingClientRect().width - ((chunkI === sideScrollI && forceYScrollbars) ? scrollbarWidth.y // use global because scroller might not have scrollbars yet but will need them in future : 0)); scrollerClientHeights[index] = Math.floor(harnessEl.getBoundingClientRect().height - ((sectionI === lastSectionI && forceXScrollbars) ? scrollbarWidth.x // use global because scroller might not have scrollbars yet but will need them in future : 0)); } } } return { forceYScrollbars, forceXScrollbars, scrollerClientWidths, scrollerClientHeights }; } updateStickyScrolling() { let { isRtl } = this.context; let argsByKey = this.scrollerElRefs.getAll().map((scrollEl) => [scrollEl, isRtl]); this.getStickyScrolling(argsByKey) .forEach((stickyScrolling) => stickyScrolling.updateSize()); } updateScrollSyncers() { let [sectionCnt, chunksPerSection] = this.getDims(); let cnt = sectionCnt * chunksPerSection; let scrollElsBySection = {}; let scrollElsByColumn = {}; let scrollElMap = this.scrollerElRefs.currentMap; for (let sectionI = 0; sectionI < sectionCnt; sectionI += 1) { let startIndex = sectionI * chunksPerSection; let endIndex = startIndex + chunksPerSection; scrollElsBySection[sectionI] = internal_cjs.collectFromHash(scrollElMap, startIndex, endIndex, 1); // use the filtered } for (let col = 0; col < chunksPerSection; col += 1) { scrollElsByColumn[col] = this.scrollerElRefs.collect(col, cnt, chunksPerSection); // DON'T use the filtered } this.scrollSyncersBySection = this.getScrollSyncersBySection(scrollElsBySection); this.scrollSyncersByColumn = this.getScrollSyncersByColumn(scrollElsByColumn); } destroyScrollSyncers() { internal_cjs.mapHash(this.scrollSyncersBySection, destroyScrollSyncer); internal_cjs.mapHash(this.scrollSyncersByColumn, destroyScrollSyncer); } getChunkConfigByIndex(index) { let chunksPerSection = this.getDims()[1]; let sectionI = Math.floor(index / chunksPerSection); let chunkI = index % chunksPerSection; let sectionConfig = this.props.sections[sectionI]; return sectionConfig && sectionConfig.chunks[chunkI]; } forceScrollLeft(col, scrollLeft) { let scrollSyncer = this.scrollSyncersByColumn[col]; if (scrollSyncer) { scrollSyncer.forceScrollLeft(scrollLeft); } } forceScrollTop(sectionI, scrollTop) { let scrollSyncer = this.scrollSyncersBySection[sectionI]; if (scrollSyncer) { scrollSyncer.forceScrollTop(scrollTop); } } _handleChunkEl(chunkEl, key) { let chunkConfig = this.getChunkConfigByIndex(parseInt(key, 10)); if (chunkConfig) { // null if section disappeared. bad, b/c won't null-set the elRef internal_cjs.setRef(chunkConfig.elRef, chunkEl); } } _handleScrollerEl(scrollerEl, key) { let chunkConfig = this.getChunkConfigByIndex(parseInt(key, 10)); if (chunkConfig) { // null if section disappeared. bad, b/c won't null-set the elRef internal_cjs.setRef(chunkConfig.scrollerElRef, scrollerEl); } } getDims() { let sectionCnt = this.props.sections.length; let chunksPerSection = sectionCnt ? this.props.sections[0].chunks.length : 0; return [sectionCnt, chunksPerSection]; } } ScrollGrid.addStateEquality({ shrinkWidths: internal_cjs.isArraysEqual, scrollerClientWidths: internal_cjs.isPropsEqual, scrollerClientHeights: internal_cjs.isPropsEqual, }); function sumNumbers(numbers) { let sum = 0; for (let n of numbers) { sum += n; } return sum; } function getRowInnerMaxHeight(rowEl) { let innerHeights = internal_cjs.findElements(rowEl, '.fc-scrollgrid-sync-inner').map(getElHeight); if (innerHeights.length) { return Math.max(...innerHeights); } return 0; } function getElHeight(el) { return el.offsetHeight; // better to deal with integers, for rounding, for PureComponent } function renderMacroColGroup(colGroupStats, shrinkWidths) { let children = colGroupStats.map((colGroupStat, i) => { let width = colGroupStat.width; if (width === 'shrink') { width = colGroupStat.totalColWidth + internal_cjs.sanitizeShrinkWidth(shrinkWidths[i]) + 1; // +1 for border :( } return ( // eslint-disable-next-line react/jsx-key preact_cjs.createElement("col", { style: { width } })); }); return preact_cjs.createElement('colgroup', {}, ...children); } function compileColGroupStat(colGroupConfig) { let totalColWidth = sumColProp(colGroupConfig.cols, 'width'); // excludes "shrink" let totalColMinWidth = sumColProp(colGroupConfig.cols, 'minWidth'); let hasShrinkCol = internal_cjs.hasShrinkWidth(colGroupConfig.cols); let allowXScrolling = colGroupConfig.width !== 'shrink' && Boolean(totalColWidth || totalColMinWidth || hasShrinkCol); return { hasShrinkCol, totalColWidth, totalColMinWidth, allowXScrolling, cols: colGroupConfig.cols, width: colGroupConfig.width, }; } function sumColProp(cols, propName) { let total = 0; for (let col of cols) { let val = col[propName]; if (typeof val === 'number') { total += val * (col.span || 1); } } return total; } const COL_GROUP_STAT_EQUALITY = { cols: internal_cjs.isColPropsEqual, }; function isColGroupStatsEqual(stat0, stat1) { return internal_cjs.compareObjs(stat0, stat1, COL_GROUP_STAT_EQUALITY); } // for memoizers... function initScrollSyncer(isVertical, ...scrollEls) { return new ScrollSyncer(isVertical, scrollEls); } function destroyScrollSyncer(scrollSyncer) { scrollSyncer.destroy(); } function initStickyScrolling(scrollEl, isRtl) { return new StickyScrolling(scrollEl, isRtl); } exports.ScrollGrid = ScrollGrid;