@full-ui/headless-grid 7.0.0-beta.5

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/LICENSE.md ADDED
@@ -0,0 +1,18 @@
1
+
2
+ For complete licensing information, visit:
3
+ https://fullcalendar.io/license
4
+
5
+ FullCalendar Premium is tri-licensed, meaning you must choose
6
+ one of three licenses to use. Here is a summary of those licenses:
7
+
8
+ - Commercial License
9
+ (a paid license, intended for commercial use)
10
+ https://fullcalendar.io/commercial-license
11
+
12
+ - Creative Commons Non-Commercial No-Derivatives
13
+ (intended for trial and non-commercial use)
14
+ https://creativecommons.org/licenses/by-nc-nd/4.0/
15
+
16
+ - AGPLv3 License
17
+ (intended for open-source projects)
18
+ https://www.gnu.org/licenses/agpl-3.0.en.html
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+
2
+ # Full UI :: Headless Grid
3
+
4
+ Headless datagrid library by the makers of FullCalendar
5
+
6
+ ## Description
7
+
8
+ This "headless" library provides a very minimal set of primitives to render a JavaScript data grid using DOM-related optimizations like absolute positioning and scroll-simulation, which are traditionally the thorniest aspects of making a performant grid. It intends to be utility-first, like Tanstack Table, though in some aspects it must perform framework-dependent optimizations to achieve 60 FPS.
9
+
10
+ > [!IMPORTANT]
11
+ > If you are a FullCalendar Premium subscriber, and wish to use this library, please reach out to the tech support email you received upon purchase.
12
+
13
+ In its current form, it's being used internally by the `@fullcalendar/resource-timeline` package, which renders a timeline view alongside a FullCalendar-built datagrid. Our goal is to make the *FullCalendar* datagrid the best in the world, though in the meantime, many users will want to use a datagrid of their choosing and integrate it with Resource-Timeline. This is the dual-purpose of this package: to allow shimming of third-party data grid libraries into FullCalendar's UI.
14
+
15
+ ## High-level API
16
+
17
+ - Column absolute positioning
18
+ - Row absolute positioning
19
+ - Scroll-view syncing across unrelated parents
20
+ - Virtual rendering (soon)
21
+ - A little bit of cross-framework magic
22
+
23
+ And more to come!... 👀
package/cjs/index.cjs ADDED
@@ -0,0 +1,371 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ function fracToCssDim(frac) {
6
+ return frac * 100 + '%';
7
+ }
8
+
9
+ function parseDimConfig(input, minDim = 0) {
10
+ if (input != null) {
11
+ if (typeof input === 'string') {
12
+ let m = input.match(/^(.*)(%|px)$/);
13
+ if (m) {
14
+ const num = parseFloat(m[1]);
15
+ if (!isNaN(num)) {
16
+ if (m[2] === '%') {
17
+ return { pixels: 0, frac: num / 100, min: minDim };
18
+ }
19
+ else {
20
+ return { pixels: num, frac: 0, min: minDim };
21
+ }
22
+ }
23
+ }
24
+ }
25
+ else if (typeof input === 'number' && !isNaN(input)) {
26
+ return { pixels: input, frac: 0, min: minDim };
27
+ }
28
+ }
29
+ }
30
+ function parseSiblingDimConfig(input, grow, // TODO: use (and sanitize)
31
+ minDim) {
32
+ const partialDimConfig = parseDimConfig(input, minDim);
33
+ return partialDimConfig
34
+ ? Object.assign(Object.assign({}, partialDimConfig), { grow: grow || 0 }) : { pixels: 0, frac: 0, grow: grow || 1, min: minDim };
35
+ }
36
+ /*
37
+ Ensure at least one column can grow
38
+ Mutates in-place
39
+ */
40
+ function ensureDimConfigsGrow(dimConfigs) {
41
+ for (const dimConfig of dimConfigs) {
42
+ if (dimConfig.grow) {
43
+ return;
44
+ }
45
+ }
46
+ // make all expand equally
47
+ for (const dimConfig of dimConfigs) {
48
+ dimConfig.grow = 1;
49
+ }
50
+ }
51
+ function pixelizeDimConfigs(dimConfigs, clientDim) {
52
+ const pixelDims = [];
53
+ let preGrowTotal = 0;
54
+ let growDenom = 0;
55
+ for (const dimConfig of dimConfigs) {
56
+ const constrainedPixels = Math.max(dimConfig.pixels + (dimConfig.frac * clientDim), dimConfig.min);
57
+ pixelDims.push(constrainedPixels);
58
+ preGrowTotal += constrainedPixels;
59
+ growDenom += dimConfig.grow;
60
+ }
61
+ if (preGrowTotal < clientDim) {
62
+ const remainder = clientDim - preGrowTotal;
63
+ for (let i = 0; i < dimConfigs.length; i++) {
64
+ pixelDims[i] += remainder * (dimConfigs[i].grow / growDenom);
65
+ }
66
+ }
67
+ return [pixelDims, preGrowTotal];
68
+ }
69
+ function flexifyDimConfigs(dimConfigs, pixelDims) {
70
+ const flexDims = [];
71
+ const flexGrows = [];
72
+ let pixelTotal = 0;
73
+ let fracTotal = 0;
74
+ for (let i = 0; i < dimConfigs.length; i++) {
75
+ const dimConfig = dimConfigs[i];
76
+ const constrainedPixels = Math.max(dimConfig.pixels, dimConfig.min);
77
+ flexDims.push(constrainedPixels);
78
+ flexGrows.push(pixelDims[i] - constrainedPixels); // a pixel value, but used as a proportion
79
+ pixelTotal += dimConfig.pixels + (dimConfig.grow ? dimConfig.min : 0);
80
+ fracTotal += dimConfig.frac; // not possible to enforce min for percentage here
81
+ }
82
+ const minCanvasDim = serializeDimConfig({
83
+ pixels: pixelTotal,
84
+ frac: fracTotal,
85
+ });
86
+ return [flexDims, flexGrows, minCanvasDim];
87
+ }
88
+ function serializeDimConfig({ pixels, frac }) {
89
+ if (!frac) {
90
+ return pixels;
91
+ }
92
+ if (!pixels) {
93
+ return fracToCssDim(frac);
94
+ }
95
+ return `calc(${fracToCssDim(frac)} + ${pixels}px)`;
96
+ }
97
+ function resizeSiblingDimConfig(dimConfigs, pixelDims, clientDim, resizeIndex, resizeDim) {
98
+ const newDimConfigs = [];
99
+ for (let i = 0; i < resizeIndex; i++) {
100
+ newDimConfigs.push(resizeDimConfig(dimConfigs[i], pixelDims[i], clientDim));
101
+ }
102
+ newDimConfigs.push(resizeDimConfig(dimConfigs[resizeIndex], resizeDim, clientDim));
103
+ const len = dimConfigs.length;
104
+ let anyGrow = false;
105
+ for (let i = resizeIndex + 1; i < len; i++) {
106
+ const dimConfig = dimConfigs[i];
107
+ newDimConfigs.push(dimConfig);
108
+ if (dimConfig.grow) {
109
+ anyGrow = true;
110
+ }
111
+ }
112
+ if (!anyGrow) {
113
+ for (let i = resizeIndex + 1; i < len; i++) {
114
+ newDimConfigs[i] = Object.assign({}, newDimConfigs[i], { grow: 1 });
115
+ }
116
+ }
117
+ return newDimConfigs;
118
+ }
119
+ function resizeDimConfig(dimConfig, newPixels, clientDim) {
120
+ const { min } = dimConfig;
121
+ newPixels = Math.max(min, newPixels);
122
+ if (dimConfig.pixels) {
123
+ return { pixels: newPixels, frac: 0, grow: 0, min };
124
+ }
125
+ return { pixels: 0, frac: newPixels / clientDim, grow: 0, min };
126
+ }
127
+
128
+ const ROW_BORDER_WIDTH = 1;
129
+ // Resource/Group Verticals
130
+ // -------------------------------------------------------------------------------------------------
131
+ function computeHeights(siblingNodes, getEntityKey, getEntityHeight, minHeight) {
132
+ const heightMap = new Map();
133
+ let [totalHeight, expandableCount] = computeTightHeights(siblingNodes, getEntityKey, getEntityHeight, heightMap);
134
+ if (minHeight != null && minHeight > totalHeight) {
135
+ expandHeights(siblingNodes, getEntityKey, heightMap, (minHeight - totalHeight) / expandableCount);
136
+ totalHeight = minHeight;
137
+ }
138
+ return [heightMap, totalHeight];
139
+ }
140
+ function computeTightHeights(siblingNodes, getEntityKey, getEntityHeight, heightMap) {
141
+ let totalHeight = 0;
142
+ let expandableCount = 0;
143
+ for (const siblingNode of siblingNodes) {
144
+ const entityKey = getEntityKey(siblingNode.entity);
145
+ let ownHeight = getEntityHeight(entityKey) || 0;
146
+ const [childrenHeight, childrenExpandableCount] = computeTightHeights(siblingNode.children, getEntityKey, getEntityHeight, heightMap);
147
+ if (siblingNode.pooledHeight) { // 'own' is side-by-side with children
148
+ if (ownHeight > childrenHeight) {
149
+ // children are smaller. grow them
150
+ expandHeights(siblingNode.children, getEntityKey, heightMap, (ownHeight - childrenHeight) / childrenExpandableCount);
151
+ }
152
+ else {
153
+ // own-element is smaller. grow to height of children
154
+ ownHeight = childrenHeight;
155
+ }
156
+ totalHeight += ownHeight;
157
+ }
158
+ else { // 'own' is above children
159
+ totalHeight += ownHeight + ROW_BORDER_WIDTH + childrenHeight;
160
+ }
161
+ heightMap.set(entityKey, ownHeight);
162
+ // expandable?
163
+ // vertically stacked, and not a group
164
+ if (siblingNode.pooledHeight === undefined) {
165
+ expandableCount++;
166
+ }
167
+ expandableCount += childrenExpandableCount;
168
+ }
169
+ return [
170
+ totalHeight + ROW_BORDER_WIDTH * (siblingNodes.length - 1),
171
+ expandableCount,
172
+ ];
173
+ }
174
+ function expandHeights(siblingNodes, getEntityKey, heightMap, expansion) {
175
+ for (const siblingNode of siblingNodes) {
176
+ // expandable?
177
+ // vertically stacked, and not a group
178
+ if (siblingNode.pooledHeight === undefined) {
179
+ const entityKey = getEntityKey(siblingNode.entity);
180
+ heightMap.set(entityKey, heightMap.get(entityKey) + expansion);
181
+ }
182
+ expandHeights(siblingNode.children, getEntityKey, heightMap, expansion);
183
+ }
184
+ }
185
+ function computeTopsFromHeights(siblingNodes, getEntityKey, heightMap) {
186
+ const topMap = new Map();
187
+ computeTopsStartingAt(siblingNodes, getEntityKey, heightMap, topMap, 0);
188
+ return topMap;
189
+ }
190
+ function computeTopsStartingAt(siblingNodes, getEntityKey, heightMap, topMap, top) {
191
+ for (let i = 0; i < siblingNodes.length; i++) {
192
+ const siblingNode = siblingNodes[i];
193
+ const entityKey = getEntityKey(siblingNode.entity);
194
+ topMap.set(entityKey, top);
195
+ if (!siblingNode.pooledHeight) {
196
+ top += heightMap.get(entityKey) + ROW_BORDER_WIDTH;
197
+ }
198
+ top = computeTopsStartingAt(siblingNode.children, getEntityKey, heightMap, topMap, top);
199
+ }
200
+ return top;
201
+ }
202
+ function findEntityByCoord(siblingNodes, entityTops, // keyed by createEntityId
203
+ entityHeights, // keyed by createEntityId
204
+ coord, // assumed >= 0
205
+ createEntityId) {
206
+ for (const siblingNode of siblingNodes) {
207
+ const entityKey = createEntityId(siblingNode.entity);
208
+ const siblingOwnTop = entityTops.get(entityKey);
209
+ const siblingOwnHeight = entityHeights.get(entityKey);
210
+ // intersection of the sibling's own element?
211
+ if (siblingOwnTop + siblingOwnHeight > coord) {
212
+ if (siblingNode.pooledHeight) {
213
+ // need more specific result
214
+ return findEntityByCoord(siblingNode.children, entityTops, entityHeights, coord, createEntityId);
215
+ }
216
+ else {
217
+ return [siblingNode.entity, siblingOwnTop, siblingOwnHeight];
218
+ }
219
+ }
220
+ else if (!siblingNode.pooledHeight) {
221
+ // search siblings below
222
+ const childrenRes = findEntityByCoord(siblingNode.children, entityTops, entityHeights, coord, createEntityId);
223
+ if (childrenRes) {
224
+ return childrenRes;
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ class ScrollerSyncer {
231
+ constructor(isHorizontal = false) {
232
+ this.isHorizontal = isHorizontal;
233
+ this.scrollers = [];
234
+ this.destroyFuncs = [];
235
+ this.isPaused = false;
236
+ }
237
+ handleChildren(scrollers) {
238
+ if (!isArraysEqual(this.scrollers, scrollers)) {
239
+ this.destroy();
240
+ for (const scroller of scrollers) {
241
+ if (scroller) { // could be null
242
+ this.destroyFuncs.push(this.bindScroller(scroller));
243
+ this.scrollers.push(scroller);
244
+ }
245
+ }
246
+ }
247
+ }
248
+ destroy() {
249
+ for (let destroyFunc of this.destroyFuncs) {
250
+ destroyFunc();
251
+ }
252
+ this.destroyFuncs = [];
253
+ this.scrollers = [];
254
+ }
255
+ get x() {
256
+ const { scrollers, masterScroller } = this;
257
+ return (masterScroller || scrollers[0]).x;
258
+ }
259
+ get y() {
260
+ const { scrollers, masterScroller } = this;
261
+ return (masterScroller || scrollers[0]).y;
262
+ }
263
+ scrollTo(scrollArg) {
264
+ this.isPaused = true;
265
+ const { scrollers } = this;
266
+ for (let scroller of scrollers) {
267
+ scroller.scrollTo(scrollArg);
268
+ }
269
+ this.isPaused = false;
270
+ }
271
+ addScrollEndListener(handler) {
272
+ var _a;
273
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.on('scrollEnd', handler);
274
+ }
275
+ removeScrollEndListener(handler) {
276
+ var _a;
277
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.off('scrollEnd', handler);
278
+ }
279
+ bindScroller(scroller) {
280
+ var _a, _b;
281
+ let { isHorizontal } = this;
282
+ const onScroll = (isUser) => {
283
+ if (!this.isPaused) {
284
+ if (!this.masterScroller || (this.masterScroller !== scroller && isUser)) {
285
+ this.assignMaster(scroller);
286
+ }
287
+ if (this.masterScroller === scroller) { // dealing with current
288
+ for (let otherScroller of this.scrollers) {
289
+ if (otherScroller !== scroller) {
290
+ if (isHorizontal) {
291
+ // TODO: user raw scrollLeft for better performance. No normalization necessary
292
+ otherScroller.scrollTo({ x: scroller.x });
293
+ }
294
+ else {
295
+ otherScroller.scrollTo({ y: scroller.y });
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
301
+ };
302
+ const onScrollEnd = (isUser) => {
303
+ var _a;
304
+ if (this.masterScroller === scroller) {
305
+ this.masterScroller = null;
306
+ const { x, y } = this; // new values
307
+ let isMoved = false;
308
+ if (this.isHorizontal) {
309
+ if (x !== this.prevX) {
310
+ this.prevX = x;
311
+ isMoved = true;
312
+ }
313
+ }
314
+ else {
315
+ if (y !== this.prevY) {
316
+ this.prevY = y;
317
+ isMoved = true;
318
+ }
319
+ }
320
+ if (isMoved) {
321
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.trigger('scrollEnd', isUser);
322
+ }
323
+ }
324
+ };
325
+ (_a = scroller.listener.emitter) === null || _a === void 0 ? void 0 : _a.on('scroll', onScroll);
326
+ (_b = scroller.listener.emitter) === null || _b === void 0 ? void 0 : _b.on('scrollEnd', onScrollEnd);
327
+ return () => {
328
+ var _a, _b;
329
+ (_a = scroller.listener.emitter) === null || _a === void 0 ? void 0 : _a.off('scroll', onScroll);
330
+ (_b = scroller.listener.emitter) === null || _b === void 0 ? void 0 : _b.off('scrollEnd', onScrollEnd);
331
+ };
332
+ }
333
+ assignMaster(masterScroller) {
334
+ this.masterScroller = masterScroller;
335
+ for (let scroller of this.scrollers) {
336
+ if (scroller !== masterScroller) {
337
+ scroller.endScroll(); // to prevent residual scrolls from reclaiming master
338
+ }
339
+ }
340
+ }
341
+ }
342
+ function isArraysEqual(array0, array1) {
343
+ if (array0 === array1) {
344
+ return true;
345
+ }
346
+ let len = array0.length;
347
+ let i;
348
+ if (len !== array1.length) { // not array? or not same length?
349
+ return false;
350
+ }
351
+ for (i = 0; i < len; i += 1) {
352
+ if (array0[i] === array1[i]) {
353
+ return false;
354
+ }
355
+ }
356
+ return true;
357
+ }
358
+
359
+ exports.ROW_BORDER_WIDTH = ROW_BORDER_WIDTH;
360
+ exports.ScrollerSyncer = ScrollerSyncer;
361
+ exports.computeHeights = computeHeights;
362
+ exports.computeTopsFromHeights = computeTopsFromHeights;
363
+ exports.ensureDimConfigsGrow = ensureDimConfigsGrow;
364
+ exports.findEntityByCoord = findEntityByCoord;
365
+ exports.flexifyDimConfigs = flexifyDimConfigs;
366
+ exports.parseDimConfig = parseDimConfig;
367
+ exports.parseSiblingDimConfig = parseSiblingDimConfig;
368
+ exports.pixelizeDimConfigs = pixelizeDimConfigs;
369
+ exports.resizeDimConfig = resizeDimConfig;
370
+ exports.resizeSiblingDimConfig = resizeSiblingDimConfig;
371
+ exports.serializeDimConfig = serializeDimConfig;
package/esm/index.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ type CssDimValue = string | number;
2
+
3
+ interface DimConfig {
4
+ pixels: number;
5
+ frac: number;
6
+ min: number;
7
+ }
8
+ interface SiblingDimConfig extends DimConfig {
9
+ grow: number;
10
+ }
11
+ declare function parseDimConfig(input: CssDimValue | undefined, minDim?: number): DimConfig | undefined;
12
+ declare function parseSiblingDimConfig(input: CssDimValue | undefined, grow: number | undefined, // TODO: use (and sanitize)
13
+ minDim: number | undefined): SiblingDimConfig;
14
+ declare function ensureDimConfigsGrow(dimConfigs: SiblingDimConfig[]): void;
15
+ declare function pixelizeDimConfigs(dimConfigs: SiblingDimConfig[], clientDim: number): [
16
+ pixelDims: number[],
17
+ minCanvasDim: number
18
+ ];
19
+ declare function flexifyDimConfigs(dimConfigs: SiblingDimConfig[], pixelDims: number[]): [
20
+ flexDims: number[],
21
+ flexGrows: number[],
22
+ minCanvasDim: CssDimValue
23
+ ];
24
+ declare function serializeDimConfig({ pixels, frac }: {
25
+ pixels: number;
26
+ frac: number;
27
+ }): CssDimValue;
28
+ declare function resizeSiblingDimConfig(dimConfigs: SiblingDimConfig[], pixelDims: number[], clientDim: number, resizeIndex: number, resizeDim: number): SiblingDimConfig[];
29
+ declare function resizeDimConfig(dimConfig: DimConfig, newPixels: number, clientDim: number): SiblingDimConfig;
30
+
31
+ interface GenericLayout<Entity> {
32
+ rowIndex: number;
33
+ visibleIndex: number;
34
+ entity: Entity;
35
+ pooledHeight?: boolean;
36
+ children: GenericLayout<Entity>[];
37
+ }
38
+ declare const ROW_BORDER_WIDTH = 1;
39
+ declare function computeHeights<Entity, Key>(siblingNodes: GenericLayout<Entity>[], getEntityKey: (entity: Entity) => Key, getEntityHeight: (entityKey: Key) => number, minHeight?: number): [
40
+ heightMap: Map<Key, number>,
41
+ totalHeight: number
42
+ ];
43
+ declare function computeTopsFromHeights<Entity, Key>(siblingNodes: GenericLayout<Entity>[], getEntityKey: (entity: Entity) => Key, heightMap: Map<Key, number>): Map<Key, number>;
44
+ declare function findEntityByCoord<Entity>(siblingNodes: GenericLayout<Entity>[], entityTops: Map<string, number>, // keyed by createEntityId
45
+ entityHeights: Map<string, number>, // keyed by createEntityId
46
+ coord: number, // assumed >= 0
47
+ createEntityId: (entity: Entity) => string): [
48
+ entity: Entity,
49
+ top: number,
50
+ height: number
51
+ ] | undefined;
52
+
53
+ interface Scroller {
54
+ scrollTo({ x, y }: {
55
+ x?: number;
56
+ y?: number;
57
+ }): void;
58
+ x: number;
59
+ y: number;
60
+ listener: any;
61
+ endScroll(): void;
62
+ }
63
+ declare class ScrollerSyncer {
64
+ private isHorizontal;
65
+ private emitter;
66
+ private scrollers;
67
+ private destroyFuncs;
68
+ private masterScroller;
69
+ private isPaused;
70
+ private prevX;
71
+ private prevY;
72
+ constructor(isHorizontal?: boolean);
73
+ handleChildren(scrollers: Scroller[]): void;
74
+ destroy(): void;
75
+ get x(): number;
76
+ get y(): number;
77
+ scrollTo(scrollArg: {
78
+ x?: number;
79
+ y?: number;
80
+ }): void;
81
+ addScrollEndListener(handler: (isUser: boolean) => void): void;
82
+ removeScrollEndListener(handler: (isUser: boolean) => void): void;
83
+ bindScroller(scroller: Scroller): () => void;
84
+ assignMaster(masterScroller: Scroller): void;
85
+ }
86
+
87
+ export { DimConfig, GenericLayout, ROW_BORDER_WIDTH, Scroller, ScrollerSyncer, SiblingDimConfig, computeHeights, computeTopsFromHeights, ensureDimConfigsGrow, findEntityByCoord, flexifyDimConfigs, parseDimConfig, parseSiblingDimConfig, pixelizeDimConfigs, resizeDimConfig, resizeSiblingDimConfig, serializeDimConfig };
package/esm/index.js ADDED
@@ -0,0 +1,355 @@
1
+ function fracToCssDim(frac) {
2
+ return frac * 100 + '%';
3
+ }
4
+
5
+ function parseDimConfig(input, minDim = 0) {
6
+ if (input != null) {
7
+ if (typeof input === 'string') {
8
+ let m = input.match(/^(.*)(%|px)$/);
9
+ if (m) {
10
+ const num = parseFloat(m[1]);
11
+ if (!isNaN(num)) {
12
+ if (m[2] === '%') {
13
+ return { pixels: 0, frac: num / 100, min: minDim };
14
+ }
15
+ else {
16
+ return { pixels: num, frac: 0, min: minDim };
17
+ }
18
+ }
19
+ }
20
+ }
21
+ else if (typeof input === 'number' && !isNaN(input)) {
22
+ return { pixels: input, frac: 0, min: minDim };
23
+ }
24
+ }
25
+ }
26
+ function parseSiblingDimConfig(input, grow, // TODO: use (and sanitize)
27
+ minDim) {
28
+ const partialDimConfig = parseDimConfig(input, minDim);
29
+ return partialDimConfig
30
+ ? Object.assign(Object.assign({}, partialDimConfig), { grow: grow || 0 }) : { pixels: 0, frac: 0, grow: grow || 1, min: minDim };
31
+ }
32
+ /*
33
+ Ensure at least one column can grow
34
+ Mutates in-place
35
+ */
36
+ function ensureDimConfigsGrow(dimConfigs) {
37
+ for (const dimConfig of dimConfigs) {
38
+ if (dimConfig.grow) {
39
+ return;
40
+ }
41
+ }
42
+ // make all expand equally
43
+ for (const dimConfig of dimConfigs) {
44
+ dimConfig.grow = 1;
45
+ }
46
+ }
47
+ function pixelizeDimConfigs(dimConfigs, clientDim) {
48
+ const pixelDims = [];
49
+ let preGrowTotal = 0;
50
+ let growDenom = 0;
51
+ for (const dimConfig of dimConfigs) {
52
+ const constrainedPixels = Math.max(dimConfig.pixels + (dimConfig.frac * clientDim), dimConfig.min);
53
+ pixelDims.push(constrainedPixels);
54
+ preGrowTotal += constrainedPixels;
55
+ growDenom += dimConfig.grow;
56
+ }
57
+ if (preGrowTotal < clientDim) {
58
+ const remainder = clientDim - preGrowTotal;
59
+ for (let i = 0; i < dimConfigs.length; i++) {
60
+ pixelDims[i] += remainder * (dimConfigs[i].grow / growDenom);
61
+ }
62
+ }
63
+ return [pixelDims, preGrowTotal];
64
+ }
65
+ function flexifyDimConfigs(dimConfigs, pixelDims) {
66
+ const flexDims = [];
67
+ const flexGrows = [];
68
+ let pixelTotal = 0;
69
+ let fracTotal = 0;
70
+ for (let i = 0; i < dimConfigs.length; i++) {
71
+ const dimConfig = dimConfigs[i];
72
+ const constrainedPixels = Math.max(dimConfig.pixels, dimConfig.min);
73
+ flexDims.push(constrainedPixels);
74
+ flexGrows.push(pixelDims[i] - constrainedPixels); // a pixel value, but used as a proportion
75
+ pixelTotal += dimConfig.pixels + (dimConfig.grow ? dimConfig.min : 0);
76
+ fracTotal += dimConfig.frac; // not possible to enforce min for percentage here
77
+ }
78
+ const minCanvasDim = serializeDimConfig({
79
+ pixels: pixelTotal,
80
+ frac: fracTotal,
81
+ });
82
+ return [flexDims, flexGrows, minCanvasDim];
83
+ }
84
+ function serializeDimConfig({ pixels, frac }) {
85
+ if (!frac) {
86
+ return pixels;
87
+ }
88
+ if (!pixels) {
89
+ return fracToCssDim(frac);
90
+ }
91
+ return `calc(${fracToCssDim(frac)} + ${pixels}px)`;
92
+ }
93
+ function resizeSiblingDimConfig(dimConfigs, pixelDims, clientDim, resizeIndex, resizeDim) {
94
+ const newDimConfigs = [];
95
+ for (let i = 0; i < resizeIndex; i++) {
96
+ newDimConfigs.push(resizeDimConfig(dimConfigs[i], pixelDims[i], clientDim));
97
+ }
98
+ newDimConfigs.push(resizeDimConfig(dimConfigs[resizeIndex], resizeDim, clientDim));
99
+ const len = dimConfigs.length;
100
+ let anyGrow = false;
101
+ for (let i = resizeIndex + 1; i < len; i++) {
102
+ const dimConfig = dimConfigs[i];
103
+ newDimConfigs.push(dimConfig);
104
+ if (dimConfig.grow) {
105
+ anyGrow = true;
106
+ }
107
+ }
108
+ if (!anyGrow) {
109
+ for (let i = resizeIndex + 1; i < len; i++) {
110
+ newDimConfigs[i] = Object.assign({}, newDimConfigs[i], { grow: 1 });
111
+ }
112
+ }
113
+ return newDimConfigs;
114
+ }
115
+ function resizeDimConfig(dimConfig, newPixels, clientDim) {
116
+ const { min } = dimConfig;
117
+ newPixels = Math.max(min, newPixels);
118
+ if (dimConfig.pixels) {
119
+ return { pixels: newPixels, frac: 0, grow: 0, min };
120
+ }
121
+ return { pixels: 0, frac: newPixels / clientDim, grow: 0, min };
122
+ }
123
+
124
+ const ROW_BORDER_WIDTH = 1;
125
+ // Resource/Group Verticals
126
+ // -------------------------------------------------------------------------------------------------
127
+ function computeHeights(siblingNodes, getEntityKey, getEntityHeight, minHeight) {
128
+ const heightMap = new Map();
129
+ let [totalHeight, expandableCount] = computeTightHeights(siblingNodes, getEntityKey, getEntityHeight, heightMap);
130
+ if (minHeight != null && minHeight > totalHeight) {
131
+ expandHeights(siblingNodes, getEntityKey, heightMap, (minHeight - totalHeight) / expandableCount);
132
+ totalHeight = minHeight;
133
+ }
134
+ return [heightMap, totalHeight];
135
+ }
136
+ function computeTightHeights(siblingNodes, getEntityKey, getEntityHeight, heightMap) {
137
+ let totalHeight = 0;
138
+ let expandableCount = 0;
139
+ for (const siblingNode of siblingNodes) {
140
+ const entityKey = getEntityKey(siblingNode.entity);
141
+ let ownHeight = getEntityHeight(entityKey) || 0;
142
+ const [childrenHeight, childrenExpandableCount] = computeTightHeights(siblingNode.children, getEntityKey, getEntityHeight, heightMap);
143
+ if (siblingNode.pooledHeight) { // 'own' is side-by-side with children
144
+ if (ownHeight > childrenHeight) {
145
+ // children are smaller. grow them
146
+ expandHeights(siblingNode.children, getEntityKey, heightMap, (ownHeight - childrenHeight) / childrenExpandableCount);
147
+ }
148
+ else {
149
+ // own-element is smaller. grow to height of children
150
+ ownHeight = childrenHeight;
151
+ }
152
+ totalHeight += ownHeight;
153
+ }
154
+ else { // 'own' is above children
155
+ totalHeight += ownHeight + ROW_BORDER_WIDTH + childrenHeight;
156
+ }
157
+ heightMap.set(entityKey, ownHeight);
158
+ // expandable?
159
+ // vertically stacked, and not a group
160
+ if (siblingNode.pooledHeight === undefined) {
161
+ expandableCount++;
162
+ }
163
+ expandableCount += childrenExpandableCount;
164
+ }
165
+ return [
166
+ totalHeight + ROW_BORDER_WIDTH * (siblingNodes.length - 1),
167
+ expandableCount,
168
+ ];
169
+ }
170
+ function expandHeights(siblingNodes, getEntityKey, heightMap, expansion) {
171
+ for (const siblingNode of siblingNodes) {
172
+ // expandable?
173
+ // vertically stacked, and not a group
174
+ if (siblingNode.pooledHeight === undefined) {
175
+ const entityKey = getEntityKey(siblingNode.entity);
176
+ heightMap.set(entityKey, heightMap.get(entityKey) + expansion);
177
+ }
178
+ expandHeights(siblingNode.children, getEntityKey, heightMap, expansion);
179
+ }
180
+ }
181
+ function computeTopsFromHeights(siblingNodes, getEntityKey, heightMap) {
182
+ const topMap = new Map();
183
+ computeTopsStartingAt(siblingNodes, getEntityKey, heightMap, topMap, 0);
184
+ return topMap;
185
+ }
186
+ function computeTopsStartingAt(siblingNodes, getEntityKey, heightMap, topMap, top) {
187
+ for (let i = 0; i < siblingNodes.length; i++) {
188
+ const siblingNode = siblingNodes[i];
189
+ const entityKey = getEntityKey(siblingNode.entity);
190
+ topMap.set(entityKey, top);
191
+ if (!siblingNode.pooledHeight) {
192
+ top += heightMap.get(entityKey) + ROW_BORDER_WIDTH;
193
+ }
194
+ top = computeTopsStartingAt(siblingNode.children, getEntityKey, heightMap, topMap, top);
195
+ }
196
+ return top;
197
+ }
198
+ function findEntityByCoord(siblingNodes, entityTops, // keyed by createEntityId
199
+ entityHeights, // keyed by createEntityId
200
+ coord, // assumed >= 0
201
+ createEntityId) {
202
+ for (const siblingNode of siblingNodes) {
203
+ const entityKey = createEntityId(siblingNode.entity);
204
+ const siblingOwnTop = entityTops.get(entityKey);
205
+ const siblingOwnHeight = entityHeights.get(entityKey);
206
+ // intersection of the sibling's own element?
207
+ if (siblingOwnTop + siblingOwnHeight > coord) {
208
+ if (siblingNode.pooledHeight) {
209
+ // need more specific result
210
+ return findEntityByCoord(siblingNode.children, entityTops, entityHeights, coord, createEntityId);
211
+ }
212
+ else {
213
+ return [siblingNode.entity, siblingOwnTop, siblingOwnHeight];
214
+ }
215
+ }
216
+ else if (!siblingNode.pooledHeight) {
217
+ // search siblings below
218
+ const childrenRes = findEntityByCoord(siblingNode.children, entityTops, entityHeights, coord, createEntityId);
219
+ if (childrenRes) {
220
+ return childrenRes;
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ class ScrollerSyncer {
227
+ constructor(isHorizontal = false) {
228
+ this.isHorizontal = isHorizontal;
229
+ this.scrollers = [];
230
+ this.destroyFuncs = [];
231
+ this.isPaused = false;
232
+ }
233
+ handleChildren(scrollers) {
234
+ if (!isArraysEqual(this.scrollers, scrollers)) {
235
+ this.destroy();
236
+ for (const scroller of scrollers) {
237
+ if (scroller) { // could be null
238
+ this.destroyFuncs.push(this.bindScroller(scroller));
239
+ this.scrollers.push(scroller);
240
+ }
241
+ }
242
+ }
243
+ }
244
+ destroy() {
245
+ for (let destroyFunc of this.destroyFuncs) {
246
+ destroyFunc();
247
+ }
248
+ this.destroyFuncs = [];
249
+ this.scrollers = [];
250
+ }
251
+ get x() {
252
+ const { scrollers, masterScroller } = this;
253
+ return (masterScroller || scrollers[0]).x;
254
+ }
255
+ get y() {
256
+ const { scrollers, masterScroller } = this;
257
+ return (masterScroller || scrollers[0]).y;
258
+ }
259
+ scrollTo(scrollArg) {
260
+ this.isPaused = true;
261
+ const { scrollers } = this;
262
+ for (let scroller of scrollers) {
263
+ scroller.scrollTo(scrollArg);
264
+ }
265
+ this.isPaused = false;
266
+ }
267
+ addScrollEndListener(handler) {
268
+ var _a;
269
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.on('scrollEnd', handler);
270
+ }
271
+ removeScrollEndListener(handler) {
272
+ var _a;
273
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.off('scrollEnd', handler);
274
+ }
275
+ bindScroller(scroller) {
276
+ var _a, _b;
277
+ let { isHorizontal } = this;
278
+ const onScroll = (isUser) => {
279
+ if (!this.isPaused) {
280
+ if (!this.masterScroller || (this.masterScroller !== scroller && isUser)) {
281
+ this.assignMaster(scroller);
282
+ }
283
+ if (this.masterScroller === scroller) { // dealing with current
284
+ for (let otherScroller of this.scrollers) {
285
+ if (otherScroller !== scroller) {
286
+ if (isHorizontal) {
287
+ // TODO: user raw scrollLeft for better performance. No normalization necessary
288
+ otherScroller.scrollTo({ x: scroller.x });
289
+ }
290
+ else {
291
+ otherScroller.scrollTo({ y: scroller.y });
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+ };
298
+ const onScrollEnd = (isUser) => {
299
+ var _a;
300
+ if (this.masterScroller === scroller) {
301
+ this.masterScroller = null;
302
+ const { x, y } = this; // new values
303
+ let isMoved = false;
304
+ if (this.isHorizontal) {
305
+ if (x !== this.prevX) {
306
+ this.prevX = x;
307
+ isMoved = true;
308
+ }
309
+ }
310
+ else {
311
+ if (y !== this.prevY) {
312
+ this.prevY = y;
313
+ isMoved = true;
314
+ }
315
+ }
316
+ if (isMoved) {
317
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.trigger('scrollEnd', isUser);
318
+ }
319
+ }
320
+ };
321
+ (_a = scroller.listener.emitter) === null || _a === void 0 ? void 0 : _a.on('scroll', onScroll);
322
+ (_b = scroller.listener.emitter) === null || _b === void 0 ? void 0 : _b.on('scrollEnd', onScrollEnd);
323
+ return () => {
324
+ var _a, _b;
325
+ (_a = scroller.listener.emitter) === null || _a === void 0 ? void 0 : _a.off('scroll', onScroll);
326
+ (_b = scroller.listener.emitter) === null || _b === void 0 ? void 0 : _b.off('scrollEnd', onScrollEnd);
327
+ };
328
+ }
329
+ assignMaster(masterScroller) {
330
+ this.masterScroller = masterScroller;
331
+ for (let scroller of this.scrollers) {
332
+ if (scroller !== masterScroller) {
333
+ scroller.endScroll(); // to prevent residual scrolls from reclaiming master
334
+ }
335
+ }
336
+ }
337
+ }
338
+ function isArraysEqual(array0, array1) {
339
+ if (array0 === array1) {
340
+ return true;
341
+ }
342
+ let len = array0.length;
343
+ let i;
344
+ if (len !== array1.length) { // not array? or not same length?
345
+ return false;
346
+ }
347
+ for (i = 0; i < len; i += 1) {
348
+ if (array0[i] === array1[i]) {
349
+ return false;
350
+ }
351
+ }
352
+ return true;
353
+ }
354
+
355
+ export { ROW_BORDER_WIDTH, ScrollerSyncer, computeHeights, computeTopsFromHeights, ensureDimConfigsGrow, findEntityByCoord, flexifyDimConfigs, parseDimConfig, parseSiblingDimConfig, pixelizeDimConfigs, resizeDimConfig, resizeSiblingDimConfig, serializeDimConfig };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@full-ui/headless-grid",
3
+ "version": "7.0.0-beta.5",
4
+ "keywords": [
5
+ "headless",
6
+ "grid",
7
+ "datagrid"
8
+ ],
9
+ "title": "Headless DataGrid Library",
10
+ "description": "Headless datagrid library by the makers of FullCalendar",
11
+ "type": "module",
12
+ "homepage": "https://fullcalendar.io/docs/premium",
13
+ "bugs": "https://fullcalendar.io/reporting-bugs",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/fullcalendar/fullcalendar-workspace.git",
17
+ "directory": "premium/packages/headless-grid"
18
+ },
19
+ "license": "SEE LICENSE IN LICENSE.md",
20
+ "author": {
21
+ "name": "Adam Shaw",
22
+ "email": "arshaw@arshaw.com",
23
+ "url": "http://arshaw.com/"
24
+ },
25
+ "copyright": "2025 Adam Shaw",
26
+ "types": "./esm/index.d.ts",
27
+ "module": "./esm/index.js",
28
+ "main": "./cjs/index.cjs",
29
+ "exports": {
30
+ "./package.json": "./package.json",
31
+ ".": {
32
+ "import": {
33
+ "types": "./esm/index.d.ts",
34
+ "default": "./esm/index.js"
35
+ },
36
+ "require": "./cjs/index.cjs"
37
+ }
38
+ },
39
+ "sideEffects": false
40
+ }