@fullcalendar/scrollgrid 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,351 +1,73 @@
1
- import { computeEdges, removeElement, findElements, translateRect, computeInnerRect, applyStyle, BaseComponent, setRef, getIsRtlScrollbarOnLeft, Scroller, isPropsEqual, Emitter, DelayedRunner, config, memoizeArraylike, renderMicroColGroup, RefMap, getScrollGridClassNames, getCanVGrowWithinCell, getSectionClassNames, getAllowYScrolling, getSectionHasLiquidHeight, renderChunkContent, memoizeHashlike, computeShrinkWidth, getScrollbarWidths, collectFromHash, mapHash, isArraysEqual, sanitizeShrinkWidth, hasShrinkWidth, compareObjs, isColPropsEqual } from '@fullcalendar/core/internal.js';
2
- import { createRef, createElement, Fragment } from '@fullcalendar/core/preact.js';
1
+ import { Emitter, isArraysEqual } from '@fullcalendar/core/internal.js';
3
2
 
4
- // TODO: assume the el has no borders?
5
- function getScrollCanvasOrigin(scrollEl) {
6
- let rect = scrollEl.getBoundingClientRect();
7
- let edges = computeEdges(scrollEl); // TODO: pass in isRtl?
8
- return {
9
- left: rect.left + edges.borderLeft + edges.scrollbarLeft - getScrollFromLeftEdge(scrollEl),
10
- top: rect.top + edges.borderTop - scrollEl.scrollTop,
11
- };
12
- }
13
- function getScrollFromLeftEdge(el) {
14
- let scrollLeft = el.scrollLeft;
15
- let computedStyles = window.getComputedStyle(el); // TODO: pass in isRtl instead?
16
- if (computedStyles.direction === 'rtl') {
17
- switch (getRtlScrollSystem()) {
18
- case 'negative':
19
- scrollLeft *= -1; // convert to 'reverse'. fall through...
20
- case 'reverse': // scrollLeft is distance between scrollframe's right edge scrollcanvas's right edge
21
- scrollLeft = el.scrollWidth - scrollLeft - el.clientWidth;
22
- }
23
- }
24
- return scrollLeft;
25
- }
26
- function setScrollFromLeftEdge(el, scrollLeft) {
27
- let computedStyles = window.getComputedStyle(el); // TODO: pass in isRtl instead?
28
- if (computedStyles.direction === 'rtl') {
29
- switch (getRtlScrollSystem()) {
30
- case 'reverse':
31
- scrollLeft = el.scrollWidth - scrollLeft;
32
- break;
33
- case 'negative':
34
- scrollLeft = -(el.scrollWidth - scrollLeft);
35
- break;
36
- }
37
- }
38
- el.scrollLeft = scrollLeft;
39
- }
40
- // Horizontal Scroll System Detection
41
- // ----------------------------------------------------------------------------------------------
42
- let _rtlScrollSystem;
43
- function getRtlScrollSystem() {
44
- return _rtlScrollSystem || (_rtlScrollSystem = detectRtlScrollSystem());
45
- }
46
- function detectRtlScrollSystem() {
47
- let el = document.createElement('div');
48
- el.style.position = 'absolute';
49
- el.style.top = '-1000px';
50
- el.style.width = '100px'; // must be at least the side of scrollbars or you get inaccurate values (#7335)
51
- el.style.height = '100px'; // "
52
- el.style.overflow = 'scroll';
53
- el.style.direction = 'rtl';
54
- let innerEl = document.createElement('div');
55
- innerEl.style.width = '200px';
56
- innerEl.style.height = '200px';
57
- el.appendChild(innerEl);
58
- document.body.appendChild(el);
59
- let system;
60
- if (el.scrollLeft > 0) {
61
- system = 'positive'; // scroll is a positive number from the left edge
62
- }
63
- else {
64
- el.scrollLeft = 1;
65
- if (el.scrollLeft > 0) {
66
- system = 'reverse'; // scroll is a positive number from the right edge
67
- }
68
- else {
69
- system = 'negative'; // scroll is a negative number from the right edge
70
- }
71
- }
72
- removeElement(el);
73
- return system;
74
- }
75
-
76
- const STICKY_SELECTOR = '.fc-sticky';
77
3
  /*
78
- Goes beyond mere position:sticky, allows horizontal centering
79
-
80
- REQUIREMENT: fc-sticky elements, if the fc-sticky className is taken away, should NOT have relative or absolute positioning.
81
- 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.
82
-
83
- TODO: don't query text-align:center. isn't compatible with flexbox centering. instead, check natural X coord within parent container
4
+ Fires:
5
+ - scrollEnd: (x, y) => void
84
6
  */
85
- class StickyScrolling {
86
- constructor(scrollEl, isRtl) {
87
- this.scrollEl = scrollEl;
88
- this.isRtl = isRtl;
89
- this.updateSize = () => {
90
- let { scrollEl } = this;
91
- let els = findElements(scrollEl, STICKY_SELECTOR);
92
- let elGeoms = this.queryElGeoms(els);
93
- let viewportWidth = scrollEl.clientWidth;
94
- assignStickyPositions(els, elGeoms, viewportWidth);
95
- };
96
- }
97
- queryElGeoms(els) {
98
- let { scrollEl, isRtl } = this;
99
- let canvasOrigin = getScrollCanvasOrigin(scrollEl);
100
- let elGeoms = [];
101
- for (let el of els) {
102
- let parentBound = translateRect(computeInnerRect(el.parentNode, true, true), // weird way to call this!!!
103
- -canvasOrigin.left, -canvasOrigin.top);
104
- let elRect = el.getBoundingClientRect();
105
- let computedStyles = window.getComputedStyle(el);
106
- let textAlign = window.getComputedStyle(el.parentNode).textAlign; // ask the parent
107
- let naturalBound = null;
108
- if (textAlign === 'start') {
109
- textAlign = isRtl ? 'right' : 'left';
110
- }
111
- else if (textAlign === 'end') {
112
- textAlign = isRtl ? 'left' : 'right';
113
- }
114
- if (computedStyles.position !== 'sticky') {
115
- naturalBound = translateRect(elRect, -canvasOrigin.left - (parseFloat(computedStyles.left) || 0), // could be 'auto'
116
- -canvasOrigin.top - (parseFloat(computedStyles.top) || 0));
117
- }
118
- elGeoms.push({
119
- parentBound,
120
- naturalBound,
121
- elWidth: elRect.width,
122
- elHeight: elRect.height,
123
- textAlign,
124
- });
125
- }
126
- return elGeoms;
127
- }
128
- }
129
- function assignStickyPositions(els, elGeoms, viewportWidth) {
130
- els.forEach((el, i) => {
131
- let { textAlign, elWidth, parentBound } = elGeoms[i];
132
- let parentWidth = parentBound.right - parentBound.left;
133
- let left;
134
- if (textAlign === 'center' &&
135
- parentWidth > viewportWidth) {
136
- left = (viewportWidth - elWidth) / 2;
137
- }
138
- else { // if parent container can be completely in view, we don't need stickiness
139
- left = '';
140
- }
141
- applyStyle(el, {
142
- left,
143
- right: left,
144
- top: 0,
145
- });
146
- });
147
- }
148
-
149
- class ClippedScroller extends BaseComponent {
150
- constructor() {
151
- super(...arguments);
152
- this.elRef = createRef();
153
- this.state = {
154
- xScrollbarWidth: 0,
155
- yScrollbarWidth: 0,
156
- };
157
- this.handleScroller = (scroller) => {
158
- this.scroller = scroller;
159
- setRef(this.props.scrollerRef, scroller);
160
- };
161
- this.handleSizing = () => {
162
- let { props } = this;
163
- if (props.overflowY === 'scroll-hidden') {
164
- this.setState({ yScrollbarWidth: this.scroller.getYScrollbarWidth() });
165
- }
166
- if (props.overflowX === 'scroll-hidden') {
167
- this.setState({ xScrollbarWidth: this.scroller.getXScrollbarWidth() });
168
- }
169
- };
7
+ class ScrollerSyncer {
8
+ constructor(isHorizontal = false) {
9
+ this.isHorizontal = isHorizontal;
10
+ this.emitter = new Emitter();
11
+ this.scrollers = [];
12
+ this.destroyFuncs = [];
13
+ this.isPaused = false;
170
14
  }
171
- render() {
172
- let { props, state, context } = this;
173
- let isScrollbarOnLeft = context.isRtl && getIsRtlScrollbarOnLeft();
174
- let overcomeLeft = 0;
175
- let overcomeRight = 0;
176
- let overcomeBottom = 0;
177
- let { overflowX, overflowY } = props;
178
- if (props.forPrint) {
179
- overflowX = 'visible';
180
- overflowY = 'visible';
181
- }
182
- if (overflowX === 'scroll-hidden') {
183
- overcomeBottom = state.xScrollbarWidth;
184
- }
185
- if (overflowY === 'scroll-hidden') {
186
- if (state.yScrollbarWidth != null) {
187
- if (isScrollbarOnLeft) {
188
- overcomeLeft = state.yScrollbarWidth;
189
- }
190
- else {
191
- overcomeRight = state.yScrollbarWidth;
15
+ handleChildren(scrollers) {
16
+ if (!isArraysEqual(this.scrollers, scrollers)) {
17
+ this.destroy();
18
+ for (const scroller of scrollers) {
19
+ if (scroller) { // could be null
20
+ this.destroyFuncs.push(this.bindScroller(scroller));
21
+ this.scrollers.push(scroller);
192
22
  }
193
23
  }
194
24
  }
195
- return (createElement("div", { ref: this.elRef, className: 'fc-scroller-harness' + (props.liquid ? ' fc-scroller-harness-liquid' : '') },
196
- createElement(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'
197
- ? (props.maxHeight + (overflowX === 'scroll-hidden' ? state.xScrollbarWidth : 0))
198
- : '', liquid: props.liquid, liquidIsAbsolute: true }, props.children)));
199
- }
200
- componentDidMount() {
201
- this.handleSizing();
202
- this.context.addResizeHandler(this.handleSizing);
203
- }
204
- getSnapshotBeforeUpdate(prevProps) {
205
- if (this.props.forPrint && !prevProps.forPrint) {
206
- return { simulateScrollLeft: this.scroller.el.scrollLeft };
207
- }
208
- return {};
209
- }
210
- componentDidUpdate(prevProps, prevState, snapshot) {
211
- const { props, scroller: { el: scrollerEl } } = this;
212
- if (!isPropsEqual(prevProps, props)) { // an external change?
213
- this.handleSizing();
214
- }
215
- if (snapshot.simulateScrollLeft !== undefined) {
216
- scrollerEl.style.left = -snapshot.simulateScrollLeft + 'px';
217
- }
218
- else if (!props.forPrint && prevProps.forPrint) {
219
- const restoredScrollLeft = -parseInt(scrollerEl.style.left);
220
- scrollerEl.style.left = '';
221
- scrollerEl.scrollLeft = restoredScrollLeft;
222
- }
223
- }
224
- componentWillUnmount() {
225
- this.context.removeResizeHandler(this.handleSizing);
226
- }
227
- needsXScrolling() {
228
- return this.scroller.needsXScrolling();
229
- }
230
- needsYScrolling() {
231
- return this.scroller.needsYScrolling();
232
- }
233
- }
234
-
235
- const WHEEL_EVENT_NAMES = 'wheel mousewheel DomMouseScroll MozMousePixelScroll'.split(' ');
236
- /*
237
- ALSO, with the ability to disable touch
238
- */
239
- class ScrollListener {
240
- constructor(el) {
241
- this.el = el;
242
- this.emitter = new Emitter();
243
- this.isScrolling = false;
244
- this.isTouching = false; // user currently has finger down?
245
- this.isRecentlyWheeled = false;
246
- this.isRecentlyScrolled = false;
247
- this.wheelWaiter = new DelayedRunner(this._handleWheelWaited.bind(this));
248
- this.scrollWaiter = new DelayedRunner(this._handleScrollWaited.bind(this));
249
- // Handlers
250
- // ----------------------------------------------------------------------------------------------
251
- this.handleScroll = () => {
252
- this.startScroll();
253
- this.emitter.trigger('scroll', this.isRecentlyWheeled, this.isTouching);
254
- this.isRecentlyScrolled = true;
255
- this.scrollWaiter.request(500);
256
- };
257
- // will fire *before* the scroll event is fired (might not cause a scroll)
258
- this.handleWheel = () => {
259
- this.isRecentlyWheeled = true;
260
- this.wheelWaiter.request(500);
261
- };
262
- // will fire *before* the scroll event is fired (might not cause a scroll)
263
- this.handleTouchStart = () => {
264
- this.isTouching = true;
265
- };
266
- this.handleTouchEnd = () => {
267
- this.isTouching = false;
268
- // if the user ended their touch, and the scroll area wasn't moving,
269
- // we consider this to be the end of the scroll.
270
- if (!this.isRecentlyScrolled) {
271
- this.endScroll(); // won't fire if already ended
272
- }
273
- };
274
- el.addEventListener('scroll', this.handleScroll);
275
- el.addEventListener('touchstart', this.handleTouchStart, { passive: true });
276
- el.addEventListener('touchend', this.handleTouchEnd);
277
- for (let eventName of WHEEL_EVENT_NAMES) {
278
- el.addEventListener(eventName, this.handleWheel);
279
- }
280
25
  }
281
26
  destroy() {
282
- let { el } = this;
283
- el.removeEventListener('scroll', this.handleScroll);
284
- el.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
285
- el.removeEventListener('touchend', this.handleTouchEnd);
286
- for (let eventName of WHEEL_EVENT_NAMES) {
287
- el.removeEventListener(eventName, this.handleWheel);
27
+ for (let destroyFunc of this.destroyFuncs) {
28
+ destroyFunc();
288
29
  }
30
+ this.destroyFuncs = [];
31
+ this.scrollers = [];
289
32
  }
290
- // Start / Stop
291
- // ----------------------------------------------------------------------------------------------
292
- startScroll() {
293
- if (!this.isScrolling) {
294
- this.isScrolling = true;
295
- this.emitter.trigger('scrollStart', this.isRecentlyWheeled, this.isTouching);
296
- }
33
+ get x() {
34
+ const { scrollers, masterScroller } = this;
35
+ return (masterScroller || scrollers[0]).x;
297
36
  }
298
- endScroll() {
299
- if (this.isScrolling) {
300
- this.emitter.trigger('scrollEnd');
301
- this.isScrolling = false;
302
- this.isRecentlyScrolled = true;
303
- this.isRecentlyWheeled = false;
304
- this.scrollWaiter.clear();
305
- this.wheelWaiter.clear();
306
- }
37
+ get y() {
38
+ const { scrollers, masterScroller } = this;
39
+ return (masterScroller || scrollers[0]).y;
307
40
  }
308
- _handleScrollWaited() {
309
- this.isRecentlyScrolled = false;
310
- // only end the scroll if not currently touching.
311
- // if touching, the scrolling will end later, on touchend.
312
- if (!this.isTouching) {
313
- this.endScroll(); // won't fire if already ended
41
+ scrollTo(scrollArg) {
42
+ this.isPaused = true;
43
+ const { scrollers } = this;
44
+ for (let scroller of scrollers) {
45
+ scroller.scrollTo(scrollArg);
314
46
  }
315
- }
316
- _handleWheelWaited() {
317
- this.isRecentlyWheeled = false;
318
- }
319
- }
320
-
321
- class ScrollSyncer {
322
- constructor(isVertical, scrollEls) {
323
- this.isVertical = isVertical;
324
- this.scrollEls = scrollEls;
325
47
  this.isPaused = false;
326
- this.scrollListeners = scrollEls.map((el) => this.bindScroller(el));
327
48
  }
328
- destroy() {
329
- for (let scrollListener of this.scrollListeners) {
330
- scrollListener.destroy();
331
- }
49
+ addScrollEndListener(handler) {
50
+ this.emitter.on('scrollEnd', handler);
51
+ }
52
+ removeScrollEndListener(handler) {
53
+ this.emitter.off('scrollEnd', handler);
332
54
  }
333
- bindScroller(el) {
334
- let { scrollEls, isVertical } = this;
335
- let scrollListener = new ScrollListener(el);
55
+ bindScroller(scroller) {
56
+ let { isHorizontal } = this;
336
57
  const onScroll = (isWheel, isTouch) => {
337
58
  if (!this.isPaused) {
338
- if (!this.masterEl || (this.masterEl !== el && (isWheel || isTouch))) {
339
- this.assignMaster(el);
59
+ if (!this.masterScroller || (this.masterScroller !== scroller && (isWheel || isTouch))) {
60
+ this.assignMaster(scroller);
340
61
  }
341
- if (this.masterEl === el) { // dealing with current
342
- for (let otherEl of scrollEls) {
343
- if (otherEl !== el) {
344
- if (isVertical) {
345
- otherEl.scrollTop = el.scrollTop;
62
+ if (this.masterScroller === scroller) { // dealing with current
63
+ for (let otherScroller of this.scrollers) {
64
+ if (otherScroller !== scroller) {
65
+ if (isHorizontal) {
66
+ // TODO: user raw scrollLeft for better performance. No normalization necessary
67
+ otherScroller.scrollTo({ x: scroller.x });
346
68
  }
347
69
  else {
348
- otherEl.scrollLeft = el.scrollLeft;
70
+ otherScroller.scrollTo({ y: scroller.y });
349
71
  }
350
72
  }
351
73
  }
@@ -353,500 +75,26 @@ class ScrollSyncer {
353
75
  }
354
76
  };
355
77
  const onScrollEnd = () => {
356
- if (this.masterEl === el) {
357
- this.masterEl = null;
78
+ if (this.masterScroller === scroller) {
79
+ this.masterScroller = null;
80
+ this.emitter.trigger('scrollEnd', this.x, this.y);
358
81
  }
359
82
  };
360
- scrollListener.emitter.on('scroll', onScroll);
361
- scrollListener.emitter.on('scrollEnd', onScrollEnd);
362
- return scrollListener;
363
- }
364
- assignMaster(el) {
365
- this.masterEl = el;
366
- for (let scrollListener of this.scrollListeners) {
367
- if (scrollListener.el !== el) {
368
- scrollListener.endScroll(); // to prevent residual scrolls from reclaiming master
369
- }
370
- }
371
- }
372
- /*
373
- will normalize the scrollLeft value
374
- */
375
- forceScrollLeft(scrollLeft) {
376
- this.isPaused = true;
377
- for (let listener of this.scrollListeners) {
378
- setScrollFromLeftEdge(listener.el, scrollLeft);
379
- }
380
- this.isPaused = false;
381
- }
382
- forceScrollTop(top) {
383
- this.isPaused = true;
384
- for (let listener of this.scrollListeners) {
385
- listener.el.scrollTop = top;
386
- }
387
- this.isPaused = false;
388
- }
389
- }
390
-
391
- config.SCROLLGRID_RESIZE_INTERVAL = 500;
392
- /*
393
- TODO: make <ScrollGridSection> subcomponent
394
- NOTE: doesn't support collapsibleWidth (which is sortof a hack anyway)
395
- */
396
- class ScrollGrid extends BaseComponent {
397
- constructor() {
398
- super(...arguments);
399
- this.compileColGroupStats = memoizeArraylike(compileColGroupStat, isColGroupStatsEqual);
400
- this.renderMicroColGroups = memoizeArraylike(renderMicroColGroup); // yucky to memoize VNodes, but much more efficient for consumers
401
- this.clippedScrollerRefs = new RefMap();
402
- // doesn't hold non-scrolling els used just for padding
403
- this.scrollerElRefs = new RefMap(this._handleScrollerEl.bind(this));
404
- this.chunkElRefs = new RefMap(this._handleChunkEl.bind(this));
405
- this.scrollSyncersBySection = {};
406
- this.scrollSyncersByColumn = {};
407
- // for row-height-syncing
408
- this.rowUnstableMap = new Map(); // no need to groom. always self-cancels
409
- this.rowInnerMaxHeightMap = new Map();
410
- this.anyRowHeightsChanged = false;
411
- this.recentSizingCnt = 0;
412
- this.state = {
413
- shrinkWidths: [],
414
- forceYScrollbars: false,
415
- forceXScrollbars: false,
416
- scrollerClientWidths: {},
417
- scrollerClientHeights: {},
418
- sectionRowMaxHeights: [],
83
+ scroller.listener.emitter.on('scroll', onScroll);
84
+ scroller.listener.emitter.on('scrollEnd', onScrollEnd);
85
+ return () => {
86
+ scroller.listener.emitter.off('scroll', onScroll);
87
+ scroller.listener.emitter.off('scrollEnd', onScrollEnd);
419
88
  };
420
- this.handleSizing = (isForcedResize, sectionRowMaxHeightsChanged) => {
421
- if (!this.allowSizing()) {
422
- return;
423
- }
424
- if (!sectionRowMaxHeightsChanged) { // something else changed, probably external
425
- this.anyRowHeightsChanged = true;
426
- }
427
- let otherState = {};
428
- // if reacting to self-change of sectionRowMaxHeightsChanged, or not stable, don't do anything
429
- if (isForcedResize || (!sectionRowMaxHeightsChanged && !this.rowUnstableMap.size)) {
430
- otherState.sectionRowMaxHeights = this.computeSectionRowMaxHeights();
431
- }
432
- this.setState(Object.assign(Object.assign({ shrinkWidths: this.computeShrinkWidths() }, this.computeScrollerDims()), otherState), () => {
433
- if (!this.rowUnstableMap.size) {
434
- this.updateStickyScrolling(); // needs to happen AFTER final positioning committed to DOM
435
- }
436
- });
437
- };
438
- this.handleRowHeightChange = (rowEl, isStable) => {
439
- let { rowUnstableMap, rowInnerMaxHeightMap } = this;
440
- if (!isStable) {
441
- rowUnstableMap.set(rowEl, true);
442
- }
443
- else {
444
- rowUnstableMap.delete(rowEl);
445
- let innerMaxHeight = getRowInnerMaxHeight(rowEl);
446
- if (!rowInnerMaxHeightMap.has(rowEl) || rowInnerMaxHeightMap.get(rowEl) !== innerMaxHeight) {
447
- rowInnerMaxHeightMap.set(rowEl, innerMaxHeight);
448
- this.anyRowHeightsChanged = true;
449
- }
450
- if (!rowUnstableMap.size && this.anyRowHeightsChanged) {
451
- this.anyRowHeightsChanged = false;
452
- this.setState({
453
- sectionRowMaxHeights: this.computeSectionRowMaxHeights(),
454
- });
455
- }
456
- }
457
- };
458
- }
459
- render() {
460
- let { props, state, context } = this;
461
- let { shrinkWidths } = state;
462
- let colGroupStats = this.compileColGroupStats(props.colGroups.map((colGroup) => [colGroup]));
463
- let microColGroupNodes = this.renderMicroColGroups(colGroupStats.map((stat, i) => [stat.cols, shrinkWidths[i]]));
464
- let classNames = getScrollGridClassNames(props.liquid, context);
465
- this.getDims();
466
- // TODO: make DRY
467
- let sectionConfigs = props.sections;
468
- let configCnt = sectionConfigs.length;
469
- let configI = 0;
470
- let currentConfig;
471
- let headSectionNodes = [];
472
- let bodySectionNodes = [];
473
- let footSectionNodes = [];
474
- while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'header') {
475
- headSectionNodes.push(this.renderSection(currentConfig, configI, colGroupStats, microColGroupNodes, state.sectionRowMaxHeights, true));
476
- configI += 1;
477
- }
478
- while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'body') {
479
- bodySectionNodes.push(this.renderSection(currentConfig, configI, colGroupStats, microColGroupNodes, state.sectionRowMaxHeights, false));
480
- configI += 1;
481
- }
482
- while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'footer') {
483
- footSectionNodes.push(this.renderSection(currentConfig, configI, colGroupStats, microColGroupNodes, state.sectionRowMaxHeights, true));
484
- configI += 1;
485
- }
486
- const isBuggy = !getCanVGrowWithinCell(); // see NOTE in SimpleScrollGrid
487
- const roleAttrs = { role: 'rowgroup' };
488
- return createElement('table', {
489
- ref: props.elRef,
490
- role: 'grid',
491
- className: classNames.join(' '),
492
- }, renderMacroColGroup(colGroupStats, shrinkWidths), Boolean(!isBuggy && headSectionNodes.length) && createElement('thead', roleAttrs, ...headSectionNodes), Boolean(!isBuggy && bodySectionNodes.length) && createElement('tbody', roleAttrs, ...bodySectionNodes), Boolean(!isBuggy && footSectionNodes.length) && createElement('tfoot', roleAttrs, ...footSectionNodes), isBuggy && createElement('tbody', roleAttrs, ...headSectionNodes, ...bodySectionNodes, ...footSectionNodes));
493
- }
494
- renderSection(sectionConfig, sectionIndex, colGroupStats, microColGroupNodes, sectionRowMaxHeights, isHeader) {
495
- if ('outerContent' in sectionConfig) {
496
- return (createElement(Fragment, { key: sectionConfig.key }, sectionConfig.outerContent));
497
- }
498
- return (createElement("tr", { key: sectionConfig.key, role: "presentation", className: 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))));
499
- }
500
- renderChunk(sectionConfig, sectionIndex, colGroupStat, microColGroupNode, chunkConfig, chunkIndex, rowHeights, isHeader) {
501
- if ('outerContent' in chunkConfig) {
502
- return (createElement(Fragment, { key: chunkConfig.key }, chunkConfig.outerContent));
503
- }
504
- let { state } = this;
505
- let { scrollerClientWidths, scrollerClientHeights } = state;
506
- let [sectionCnt, chunksPerSection] = this.getDims();
507
- let index = sectionIndex * chunksPerSection + chunkIndex;
508
- let sideScrollIndex = (!this.context.isRtl || getIsRtlScrollbarOnLeft()) ? chunksPerSection - 1 : 0;
509
- let isVScrollSide = chunkIndex === sideScrollIndex;
510
- let isLastSection = sectionIndex === sectionCnt - 1;
511
- let forceXScrollbars = isLastSection && state.forceXScrollbars; // NOOOO can result in `null`
512
- let forceYScrollbars = isVScrollSide && state.forceYScrollbars; // NOOOO can result in `null`
513
- let allowXScrolling = colGroupStat && colGroupStat.allowXScrolling; // rename?
514
- let allowYScrolling = getAllowYScrolling(this.props, sectionConfig); // rename? do in section func?
515
- let chunkVGrow = getSectionHasLiquidHeight(this.props, sectionConfig); // do in section func?
516
- let expandRows = sectionConfig.expandRows && chunkVGrow;
517
- let tableMinWidth = (colGroupStat && colGroupStat.totalColMinWidth) || '';
518
- let content = renderChunkContent(sectionConfig, chunkConfig, {
519
- tableColGroupNode: microColGroupNode,
520
- tableMinWidth,
521
- clientWidth: scrollerClientWidths[index] !== undefined ? scrollerClientWidths[index] : null,
522
- clientHeight: scrollerClientHeights[index] !== undefined ? scrollerClientHeights[index] : null,
523
- expandRows,
524
- syncRowHeights: Boolean(sectionConfig.syncRowHeights),
525
- rowSyncHeights: rowHeights,
526
- reportRowHeightChange: this.handleRowHeightChange,
527
- }, isHeader);
528
- let overflowX = forceXScrollbars ? (isLastSection ? 'scroll' : 'scroll-hidden') :
529
- !allowXScrolling ? 'hidden' :
530
- (isLastSection ? 'auto' : 'scroll-hidden');
531
- let overflowY = forceYScrollbars ? (isVScrollSide ? 'scroll' : 'scroll-hidden') :
532
- !allowYScrolling ? 'hidden' :
533
- (isVScrollSide ? 'auto' : 'scroll-hidden');
534
- // it *could* be possible to reduce DOM wrappers by only doing a ClippedScroller when allowXScrolling or allowYScrolling,
535
- // but if these values were to change, the inner components would be unmounted/remounted because of the parent change.
536
- content = (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));
537
- return createElement(isHeader ? 'th' : 'td', {
538
- key: chunkConfig.key,
539
- ref: this.chunkElRefs.createRef(index),
540
- role: 'presentation',
541
- }, content);
542
- }
543
- componentDidMount() {
544
- this.getStickyScrolling = memoizeArraylike(initStickyScrolling);
545
- this.getScrollSyncersBySection = memoizeHashlike(initScrollSyncer.bind(this, true), null, destroyScrollSyncer);
546
- this.getScrollSyncersByColumn = memoizeHashlike(initScrollSyncer.bind(this, false), null, destroyScrollSyncer);
547
- this.updateScrollSyncers();
548
- this.handleSizing(false);
549
- this.context.addResizeHandler(this.handleSizing);
550
- }
551
- componentDidUpdate(prevProps, prevState) {
552
- this.updateScrollSyncers();
553
- // TODO: need better solution when state contains non-sizing things
554
- this.handleSizing(false, prevState.sectionRowMaxHeights !== this.state.sectionRowMaxHeights);
555
- }
556
- componentWillUnmount() {
557
- this.context.removeResizeHandler(this.handleSizing);
558
- this.destroyScrollSyncers();
559
- }
560
- allowSizing() {
561
- let now = new Date();
562
- if (!this.lastSizingDate ||
563
- now.valueOf() > this.lastSizingDate.valueOf() + config.SCROLLGRID_RESIZE_INTERVAL) {
564
- this.lastSizingDate = now;
565
- this.recentSizingCnt = 0;
566
- return true;
567
- }
568
- return (this.recentSizingCnt += 1) <= 10;
569
- }
570
- computeShrinkWidths() {
571
- let colGroupStats = this.compileColGroupStats(this.props.colGroups.map((colGroup) => [colGroup]));
572
- let [sectionCnt, chunksPerSection] = this.getDims();
573
- let cnt = sectionCnt * chunksPerSection;
574
- let shrinkWidths = [];
575
- colGroupStats.forEach((colGroupStat, i) => {
576
- if (colGroupStat.hasShrinkCol) {
577
- let chunkEls = this.chunkElRefs.collect(i, cnt, chunksPerSection); // in one col
578
- shrinkWidths[i] = computeShrinkWidth(chunkEls);
579
- }
580
- });
581
- return shrinkWidths;
582
- }
583
- // has the side effect of grooming rowInnerMaxHeightMap
584
- // TODO: somehow short-circuit if there are no new height changes
585
- computeSectionRowMaxHeights() {
586
- let newHeightMap = new Map();
587
- let [sectionCnt, chunksPerSection] = this.getDims();
588
- let sectionRowMaxHeights = [];
589
- for (let sectionI = 0; sectionI < sectionCnt; sectionI += 1) {
590
- let sectionConfig = this.props.sections[sectionI];
591
- let assignableHeights = []; // chunk, row
592
- if (sectionConfig && sectionConfig.syncRowHeights) {
593
- let rowHeightsByChunk = [];
594
- for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) {
595
- let index = sectionI * chunksPerSection + chunkI;
596
- let rowHeights = [];
597
- let chunkEl = this.chunkElRefs.currentMap[index];
598
- if (chunkEl) {
599
- rowHeights = findElements(chunkEl, '.fc-scrollgrid-sync-table tr').map((rowEl) => {
600
- let max = getRowInnerMaxHeight(rowEl);
601
- newHeightMap.set(rowEl, max);
602
- return max;
603
- });
604
- }
605
- else {
606
- rowHeights = [];
607
- }
608
- rowHeightsByChunk.push(rowHeights);
609
- }
610
- let rowCnt = rowHeightsByChunk[0].length;
611
- let isEqualRowCnt = true;
612
- for (let chunkI = 1; chunkI < chunksPerSection; chunkI += 1) {
613
- let isOuterContent = sectionConfig.chunks[chunkI] && sectionConfig.chunks[chunkI].outerContent !== undefined; // can be null
614
- if (!isOuterContent && rowHeightsByChunk[chunkI].length !== rowCnt) { // skip outer content
615
- isEqualRowCnt = false;
616
- break;
617
- }
618
- }
619
- if (!isEqualRowCnt) {
620
- let chunkHeightSums = [];
621
- for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) {
622
- chunkHeightSums.push(sumNumbers(rowHeightsByChunk[chunkI]) + rowHeightsByChunk[chunkI].length);
623
- }
624
- let maxTotalSum = Math.max(...chunkHeightSums);
625
- for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) {
626
- let rowInChunkCnt = rowHeightsByChunk[chunkI].length;
627
- let rowInChunkTotalHeight = maxTotalSum - rowInChunkCnt; // subtract border
628
- // height of non-first row. we do this to avoid rounding, because it's unreliable within a table
629
- let rowInChunkHeightOthers = Math.floor(rowInChunkTotalHeight / rowInChunkCnt);
630
- // whatever is leftover goes to the first row
631
- let rowInChunkHeightFirst = rowInChunkTotalHeight - rowInChunkHeightOthers * (rowInChunkCnt - 1);
632
- let rowInChunkHeights = [];
633
- let row = 0;
634
- if (row < rowInChunkCnt) {
635
- rowInChunkHeights.push(rowInChunkHeightFirst);
636
- row += 1;
637
- }
638
- while (row < rowInChunkCnt) {
639
- rowInChunkHeights.push(rowInChunkHeightOthers);
640
- row += 1;
641
- }
642
- assignableHeights.push(rowInChunkHeights);
643
- }
644
- }
645
- else {
646
- for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) {
647
- assignableHeights.push([]);
648
- }
649
- for (let row = 0; row < rowCnt; row += 1) {
650
- let rowHeightsAcrossChunks = [];
651
- for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) {
652
- let h = rowHeightsByChunk[chunkI][row];
653
- if (h != null) { // protect against outerContent
654
- rowHeightsAcrossChunks.push(h);
655
- }
656
- }
657
- let maxHeight = Math.max(...rowHeightsAcrossChunks);
658
- for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) {
659
- assignableHeights[chunkI].push(maxHeight);
660
- }
661
- }
662
- }
663
- }
664
- sectionRowMaxHeights.push(assignableHeights);
665
- }
666
- this.rowInnerMaxHeightMap = newHeightMap;
667
- return sectionRowMaxHeights;
668
89
  }
669
- computeScrollerDims() {
670
- let scrollbarWidth = getScrollbarWidths();
671
- let [sectionCnt, chunksPerSection] = this.getDims();
672
- let sideScrollI = (!this.context.isRtl || getIsRtlScrollbarOnLeft()) ? chunksPerSection - 1 : 0;
673
- let lastSectionI = sectionCnt - 1;
674
- let currentScrollers = this.clippedScrollerRefs.currentMap;
675
- let scrollerEls = this.scrollerElRefs.currentMap;
676
- let forceYScrollbars = false;
677
- let forceXScrollbars = false;
678
- let scrollerClientWidths = {};
679
- let scrollerClientHeights = {};
680
- for (let sectionI = 0; sectionI < sectionCnt; sectionI += 1) { // along edge
681
- let index = sectionI * chunksPerSection + sideScrollI;
682
- let scroller = currentScrollers[index];
683
- if (scroller && scroller.needsYScrolling()) {
684
- forceYScrollbars = true;
685
- break;
686
- }
687
- }
688
- for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) { // along last row
689
- let index = lastSectionI * chunksPerSection + chunkI;
690
- let scroller = currentScrollers[index];
691
- if (scroller && scroller.needsXScrolling()) {
692
- forceXScrollbars = true;
693
- break;
694
- }
695
- }
696
- for (let sectionI = 0; sectionI < sectionCnt; sectionI += 1) {
697
- for (let chunkI = 0; chunkI < chunksPerSection; chunkI += 1) {
698
- let index = sectionI * chunksPerSection + chunkI;
699
- let scrollerEl = scrollerEls[index];
700
- if (scrollerEl) {
701
- // TODO: weird way to get this. need harness b/c doesn't include table borders
702
- let harnessEl = scrollerEl.parentNode;
703
- scrollerClientWidths[index] = Math.floor(harnessEl.getBoundingClientRect().width - ((chunkI === sideScrollI && forceYScrollbars)
704
- ? scrollbarWidth.y // use global because scroller might not have scrollbars yet but will need them in future
705
- : 0));
706
- scrollerClientHeights[index] = Math.floor(harnessEl.getBoundingClientRect().height - ((sectionI === lastSectionI && forceXScrollbars)
707
- ? scrollbarWidth.x // use global because scroller might not have scrollbars yet but will need them in future
708
- : 0));
709
- }
90
+ assignMaster(masterScroller) {
91
+ this.masterScroller = masterScroller;
92
+ for (let scroller of this.scrollers) {
93
+ if (scroller !== masterScroller) {
94
+ scroller.endScroll(); // to prevent residual scrolls from reclaiming master
710
95
  }
711
96
  }
712
- return { forceYScrollbars, forceXScrollbars, scrollerClientWidths, scrollerClientHeights };
713
- }
714
- updateStickyScrolling() {
715
- let { isRtl } = this.context;
716
- let argsByKey = this.scrollerElRefs.getAll().map((scrollEl) => [scrollEl, isRtl]);
717
- this.getStickyScrolling(argsByKey)
718
- .forEach((stickyScrolling) => stickyScrolling.updateSize());
719
- }
720
- updateScrollSyncers() {
721
- let [sectionCnt, chunksPerSection] = this.getDims();
722
- let cnt = sectionCnt * chunksPerSection;
723
- let scrollElsBySection = {};
724
- let scrollElsByColumn = {};
725
- let scrollElMap = this.scrollerElRefs.currentMap;
726
- for (let sectionI = 0; sectionI < sectionCnt; sectionI += 1) {
727
- let startIndex = sectionI * chunksPerSection;
728
- let endIndex = startIndex + chunksPerSection;
729
- scrollElsBySection[sectionI] = collectFromHash(scrollElMap, startIndex, endIndex, 1); // use the filtered
730
- }
731
- for (let col = 0; col < chunksPerSection; col += 1) {
732
- scrollElsByColumn[col] = this.scrollerElRefs.collect(col, cnt, chunksPerSection); // DON'T use the filtered
733
- }
734
- this.scrollSyncersBySection = this.getScrollSyncersBySection(scrollElsBySection);
735
- this.scrollSyncersByColumn = this.getScrollSyncersByColumn(scrollElsByColumn);
736
- }
737
- destroyScrollSyncers() {
738
- mapHash(this.scrollSyncersBySection, destroyScrollSyncer);
739
- mapHash(this.scrollSyncersByColumn, destroyScrollSyncer);
740
- }
741
- getChunkConfigByIndex(index) {
742
- let chunksPerSection = this.getDims()[1];
743
- let sectionI = Math.floor(index / chunksPerSection);
744
- let chunkI = index % chunksPerSection;
745
- let sectionConfig = this.props.sections[sectionI];
746
- return sectionConfig && sectionConfig.chunks[chunkI];
747
- }
748
- forceScrollLeft(col, scrollLeft) {
749
- let scrollSyncer = this.scrollSyncersByColumn[col];
750
- if (scrollSyncer) {
751
- scrollSyncer.forceScrollLeft(scrollLeft);
752
- }
753
- }
754
- forceScrollTop(sectionI, scrollTop) {
755
- let scrollSyncer = this.scrollSyncersBySection[sectionI];
756
- if (scrollSyncer) {
757
- scrollSyncer.forceScrollTop(scrollTop);
758
- }
759
97
  }
760
- _handleChunkEl(chunkEl, key) {
761
- let chunkConfig = this.getChunkConfigByIndex(parseInt(key, 10));
762
- if (chunkConfig) { // null if section disappeared. bad, b/c won't null-set the elRef
763
- setRef(chunkConfig.elRef, chunkEl);
764
- }
765
- }
766
- _handleScrollerEl(scrollerEl, key) {
767
- let chunkConfig = this.getChunkConfigByIndex(parseInt(key, 10));
768
- if (chunkConfig) { // null if section disappeared. bad, b/c won't null-set the elRef
769
- setRef(chunkConfig.scrollerElRef, scrollerEl);
770
- }
771
- }
772
- getDims() {
773
- let sectionCnt = this.props.sections.length;
774
- let chunksPerSection = sectionCnt ? this.props.sections[0].chunks.length : 0;
775
- return [sectionCnt, chunksPerSection];
776
- }
777
- }
778
- ScrollGrid.addStateEquality({
779
- shrinkWidths: isArraysEqual,
780
- scrollerClientWidths: isPropsEqual,
781
- scrollerClientHeights: isPropsEqual,
782
- });
783
- function sumNumbers(numbers) {
784
- let sum = 0;
785
- for (let n of numbers) {
786
- sum += n;
787
- }
788
- return sum;
789
- }
790
- function getRowInnerMaxHeight(rowEl) {
791
- let innerHeights = findElements(rowEl, '.fc-scrollgrid-sync-inner').map(getElHeight);
792
- if (innerHeights.length) {
793
- return Math.max(...innerHeights);
794
- }
795
- return 0;
796
- }
797
- function getElHeight(el) {
798
- return el.offsetHeight; // better to deal with integers, for rounding, for PureComponent
799
- }
800
- function renderMacroColGroup(colGroupStats, shrinkWidths) {
801
- let children = colGroupStats.map((colGroupStat, i) => {
802
- let width = colGroupStat.width;
803
- if (width === 'shrink') {
804
- width = colGroupStat.totalColWidth + sanitizeShrinkWidth(shrinkWidths[i]) + 1; // +1 for border :(
805
- }
806
- return ( // eslint-disable-next-line react/jsx-key
807
- createElement("col", { style: { width } }));
808
- });
809
- return createElement('colgroup', {}, ...children);
810
- }
811
- function compileColGroupStat(colGroupConfig) {
812
- let totalColWidth = sumColProp(colGroupConfig.cols, 'width'); // excludes "shrink"
813
- let totalColMinWidth = sumColProp(colGroupConfig.cols, 'minWidth');
814
- let hasShrinkCol = hasShrinkWidth(colGroupConfig.cols);
815
- let allowXScrolling = colGroupConfig.width !== 'shrink' && Boolean(totalColWidth || totalColMinWidth || hasShrinkCol);
816
- return {
817
- hasShrinkCol,
818
- totalColWidth,
819
- totalColMinWidth,
820
- allowXScrolling,
821
- cols: colGroupConfig.cols,
822
- width: colGroupConfig.width,
823
- };
824
- }
825
- function sumColProp(cols, propName) {
826
- let total = 0;
827
- for (let col of cols) {
828
- let val = col[propName];
829
- if (typeof val === 'number') {
830
- total += val * (col.span || 1);
831
- }
832
- }
833
- return total;
834
- }
835
- const COL_GROUP_STAT_EQUALITY = {
836
- cols: isColPropsEqual,
837
- };
838
- function isColGroupStatsEqual(stat0, stat1) {
839
- return compareObjs(stat0, stat1, COL_GROUP_STAT_EQUALITY);
840
- }
841
- // for memoizers...
842
- function initScrollSyncer(isVertical, ...scrollEls) {
843
- return new ScrollSyncer(isVertical, scrollEls);
844
- }
845
- function destroyScrollSyncer(scrollSyncer) {
846
- scrollSyncer.destroy();
847
- }
848
- function initStickyScrolling(scrollEl, isRtl) {
849
- return new StickyScrolling(scrollEl, isRtl);
850
98
  }
851
99
 
852
- export { ScrollGrid };
100
+ export { ScrollerSyncer };