js-spread-grid 0.0.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.
Files changed (67) hide show
  1. package/package.json +26 -0
  2. package/src/core/render.js +400 -0
  3. package/src/core/state.js +183 -0
  4. package/src/core-utils/defaults.js +4 -0
  5. package/src/core-utils/rect.js +49 -0
  6. package/src/core-utils/rect.test.js +111 -0
  7. package/src/core-utils/roundToPixels.js +3 -0
  8. package/src/core-utils/stringifyId.js +27 -0
  9. package/src/core-utils/stringifyId.test.js +27 -0
  10. package/src/index.js +595 -0
  11. package/src/state-utils/getActive.js +17 -0
  12. package/src/state-utils/getCellSection.js +22 -0
  13. package/src/state-utils/getCellType.js +7 -0
  14. package/src/state-utils/getClipboardData.js +53 -0
  15. package/src/state-utils/getColumnIndex.js +25 -0
  16. package/src/state-utils/getCombinedCells.js +6 -0
  17. package/src/state-utils/getDataFormatting.js +46 -0
  18. package/src/state-utils/getEditableCells.js +23 -0
  19. package/src/state-utils/getEditedCellsAndFilters.js +4 -0
  20. package/src/state-utils/getEdition.js +6 -0
  21. package/src/state-utils/getFilterFormatting.js +3 -0
  22. package/src/state-utils/getFiltered.js +61 -0
  23. package/src/state-utils/getFilteringRules.js +5 -0
  24. package/src/state-utils/getFixedSize.js +8 -0
  25. package/src/state-utils/getFormatResolver.js +5 -0
  26. package/src/state-utils/getFormattingRules.js +5 -0
  27. package/src/state-utils/getHighlightedCells.js +40 -0
  28. package/src/state-utils/getHoveredCell.js +43 -0
  29. package/src/state-utils/getInputFormatting.js +3 -0
  30. package/src/state-utils/getInputPlacement.js +51 -0
  31. package/src/state-utils/getInvoked.js +5 -0
  32. package/src/state-utils/getIsTextValid.js +3 -0
  33. package/src/state-utils/getKeys.js +3 -0
  34. package/src/state-utils/getLookup.js +3 -0
  35. package/src/state-utils/getMeasureFormatting.js +3 -0
  36. package/src/state-utils/getMeasured.js +113 -0
  37. package/src/state-utils/getMousePosition.js +8 -0
  38. package/src/state-utils/getNewSortBy.js +20 -0
  39. package/src/state-utils/getPinned.js +8 -0
  40. package/src/state-utils/getPlaced.js +45 -0
  41. package/src/state-utils/getReducedCells.js +6 -0
  42. package/src/state-utils/getRenderFormatting.js +122 -0
  43. package/src/state-utils/getResizable.js +49 -0
  44. package/src/state-utils/getResolved.js +42 -0
  45. package/src/state-utils/getResolvedFilters.js +7 -0
  46. package/src/state-utils/getResolvedSortBy.js +7 -0
  47. package/src/state-utils/getRowIndex.js +25 -0
  48. package/src/state-utils/getScrollRect.js +41 -0
  49. package/src/state-utils/getSections.js +101 -0
  50. package/src/state-utils/getSelection.js +5 -0
  51. package/src/state-utils/getSorted.js +130 -0
  52. package/src/state-utils/getSortingFormatting.js +3 -0
  53. package/src/state-utils/getSortingRules.js +5 -0
  54. package/src/state-utils/getTextResolver.js +5 -0
  55. package/src/state-utils/getToggledValue.js +11 -0
  56. package/src/state-utils/getTotalSize.js +6 -0
  57. package/src/state-utils/getUnfolded.js +86 -0
  58. package/src/types/Edition.js +37 -0
  59. package/src/types/FilteringRules.js +48 -0
  60. package/src/types/FormatResolver.js +19 -0
  61. package/src/types/FormattingRules.js +120 -0
  62. package/src/types/FormattingRules.test.js +90 -0
  63. package/src/types/RulesLookup.js +118 -0
  64. package/src/types/Selection.js +25 -0
  65. package/src/types/SortingRules.js +62 -0
  66. package/src/types/TextResolver.js +60 -0
  67. package/src/types/VisibilityResolver.js +61 -0
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "js-spread-grid",
3
+ "version": "0.0.1",
4
+ "description": "Fast grid for js applications",
5
+ "author": "Tomasz Rewak <tomasz.rewak@gmail.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/TomaszRewak/js-spread-grid.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/TomaszRewak/js-spread-grid/issues"
13
+ },
14
+ "homepage": "https://github.com/TomaszRewak/js-spread-grid/issues",
15
+ "main": "src/index.js",
16
+ "type": "module",
17
+ "scripts": {
18
+ "build-bundle": "webpack --mode=production --output-library-type=umd --output-filename ./index.js",
19
+ "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js"
20
+ },
21
+ "devDependencies": {
22
+ "jest": "^29.7.0",
23
+ "webpack": "^5.90.3",
24
+ "webpack-cli": "^5.1.4"
25
+ }
26
+ }
@@ -0,0 +1,400 @@
1
+ import { defaultFont } from "../core-utils/defaults.js";
2
+ import roundToPixels from "../core-utils/roundToPixels.js";
3
+
4
+ function renderSection(context, vertical, horizontal) {
5
+ const state = context.state;
6
+ const canvas = context.canvases[`${vertical}-${horizontal}`];
7
+ const verticalSection = state.sections[vertical];
8
+ const horizontalSection = state.sections[horizontal];
9
+ const columns = horizontalSection.columns;
10
+ const rows = verticalSection.rows;
11
+
12
+ if (rows.length === 0 || columns.length === 0) {
13
+ if (canvas.parentElement)
14
+ canvas.parentElement.removeChild(canvas);
15
+ return;
16
+ }
17
+
18
+ if (!canvas.parentElement)
19
+ context.element.appendChild(canvas);
20
+
21
+ // Checking how often this is called
22
+ console.log('draw');
23
+
24
+ // TODO: Borders are still blurry after scrolling at high zoom-out levels
25
+
26
+ const ctx = canvas.getContext("2d", { alpha: false });
27
+ // TODO: Make that "1" configurable as cell spacing
28
+ const scrollRect = state.scrollRect;
29
+ const textResolver = state.textResolver;
30
+ // TODO: Make sure those formatters are split based on the rule areas
31
+ const formatResolver = state.renderFormatResolver;
32
+ const borderWidth = state.borderWidth;
33
+ const sectionBorders = {
34
+ top: verticalSection.showTopBorder,
35
+ bottom: verticalSection.showBottomBorder,
36
+ left: horizontalSection.showLeftBorder,
37
+ right: horizontalSection.showRightBorder
38
+ };
39
+ const borderOffset = borderWidth / 2;
40
+ const rowCount = rows.length;
41
+ const columnCount = columns.length;
42
+ const horizontalBorderCount = rowCount - 1 + (sectionBorders.top ? 1 : 0) + (sectionBorders.bottom ? 1 : 0);
43
+ const verticalBorderCount = columnCount - 1 + (sectionBorders.left ? 1 : 0) + (sectionBorders.right ? 1 : 0);
44
+ const rowHeights = rows.map(row => row.height);
45
+ const columnWidths = columns.map(column => column.width);
46
+ const totalWidth = columnWidths.reduce((a, b) => a + b, 0) + verticalBorderCount * borderWidth;
47
+ const totalHeight = rowHeights.reduce((a, b) => a + b, 0) + horizontalBorderCount * borderWidth;
48
+
49
+ const left = horizontal === 'center'
50
+ ? scrollRect.left
51
+ : 0;
52
+ const top = vertical === 'middle'
53
+ ? scrollRect.top
54
+ : 0;
55
+ const width = horizontal === 'center'
56
+ ? scrollRect.width
57
+ : horizontalSection.width;
58
+ const height = vertical === 'middle'
59
+ ? scrollRect.height
60
+ : verticalSection.height;
61
+
62
+ // TODO: Move somewhere else
63
+ const horizontalOffsets = columnWidths.reduce((acc, width, index) => {
64
+ const prevOffset = acc[index];
65
+ const offset = prevOffset + width + borderWidth;
66
+ acc.push(offset);
67
+ return acc;
68
+ }, [sectionBorders.left ? borderWidth : 0]);
69
+ const verticalOffsets = rowHeights.reduce((acc, height, index) => {
70
+ const prevOffset = acc[index];
71
+ const offset = prevOffset + height + borderWidth;
72
+ acc.push(offset);
73
+ return acc;
74
+ }, [sectionBorders.top ? borderWidth : 0]);
75
+
76
+ const columnOffsets = horizontalOffsets.slice(0, -1);
77
+ const rowOffsets = verticalOffsets.slice(0, -1);
78
+
79
+ const minVisibleColumnIndex = Math.max(columnOffsets.findLastIndex(offset => offset <= left), 0);
80
+ const maxVisibleColumnIndex = columnOffsets.findLastIndex(offset => offset <= left + width);
81
+ const minVisibleRowIndex = Math.max(rowOffsets.findLastIndex(offset => offset <= top), 0);
82
+ const maxVisibleRowIndex = rowOffsets.findLastIndex(offset => offset <= top + height);
83
+
84
+ const minVisibleVerticalBorderIndex = Math.max(minVisibleColumnIndex, sectionBorders.left ? 0 : 1);
85
+ const maxVisibleVerticalBorderIndex = maxVisibleColumnIndex + (sectionBorders.right ? 1 : 0);
86
+ const minVisibleHorizontalBorderIndex = Math.max(minVisibleRowIndex, sectionBorders.top ? 0 : 1);
87
+ const maxVisibleHorizontalBorderIndex = maxVisibleRowIndex + (sectionBorders.bottom ? 1 : 0);
88
+
89
+ const cells = Array.from({ length: maxVisibleRowIndex - minVisibleRowIndex + 1 }, (_, rowIndex) => {
90
+ const row = rows[rowIndex + minVisibleRowIndex];
91
+ return Array.from({ length: maxVisibleColumnIndex - minVisibleColumnIndex + 1 }, (_, columnIndex) => {
92
+ const column = columns[columnIndex + minVisibleColumnIndex];
93
+ return formatResolver.resolve(row, column);
94
+ });
95
+ });
96
+ const getCell = (rowIndex, columnIndex) => cells[rowIndex - minVisibleRowIndex][columnIndex - minVisibleColumnIndex];
97
+
98
+ canvas.width = Math.round(width * devicePixelRatio);
99
+ canvas.height = Math.round(height * devicePixelRatio);
100
+ canvas.style.width = `${width}px`;
101
+ canvas.style.height = `${height}px`;
102
+ canvas.style.marginLeft = `${left}px`;
103
+ canvas.style.marginTop = `${top}px`;
104
+ canvas.style.marginRight = `${totalWidth - width - left}px`;
105
+ canvas.style.marginBottom = `${totalHeight - height - top}px`;;
106
+
107
+ ctx.fillStyle = "#E9E9E9";
108
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
109
+
110
+ const setTransform = (x, y) => {
111
+ ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, (x - left) * devicePixelRatio, (y - top) * devicePixelRatio);
112
+ };
113
+ const setClip = (x, y, width, height) => {
114
+ ctx.beginPath();
115
+ ctx.rect(x, y, width, height);
116
+ ctx.clip();
117
+ };
118
+
119
+ // Draw cells
120
+ for (let columnIndex = minVisibleColumnIndex; columnIndex <= maxVisibleColumnIndex; columnIndex++) {
121
+ ctx.save();
122
+ setTransform(horizontalOffsets[columnIndex], 0);
123
+ setClip(0, 0, columnWidths[columnIndex], totalHeight);
124
+
125
+ for (let rowIndex = minVisibleRowIndex; rowIndex <= maxVisibleRowIndex; rowIndex++) {
126
+ const cell = getCell(rowIndex, columnIndex);
127
+ const style = cell.style;
128
+ const cellTop = verticalOffsets[rowIndex];
129
+ const cellLeft = horizontalOffsets[columnIndex];
130
+ const cellWidth = columnWidths[columnIndex];
131
+ const cellHeight = rowHeights[rowIndex];
132
+ const text = cell.text;
133
+ const textAlign = style.textAlign || 'left';
134
+ const textBaseline = style.textBaseline || 'middle';
135
+ const padding = cell.padding;
136
+
137
+ setTransform(cellLeft, cellTop);
138
+
139
+ ctx.fillStyle = style.background || 'white';
140
+ ctx.fillRect(0, 0, cellWidth, cellHeight);
141
+
142
+ if ('draw' in cell)
143
+ cell.draw(ctx);
144
+
145
+ if (style.highlight) {
146
+ ctx.fillStyle = style.highlight;
147
+ ctx.fillRect(0, 0, cellWidth, cellHeight);
148
+ }
149
+
150
+ if (style.corner) {
151
+ ctx.fillStyle = style.corner;
152
+ ctx.beginPath();
153
+ ctx.moveTo(cellWidth - 7, cellHeight);
154
+ ctx.lineTo(cellWidth, cellHeight);
155
+ ctx.lineTo(cellWidth, cellHeight - 7);
156
+ ctx.fill();
157
+ }
158
+
159
+ ctx.fillStyle = style.foreground || 'black';
160
+ ctx.font = style.font || defaultFont;
161
+ ctx.textAlign = textAlign;
162
+
163
+ const fontMetrics = textResolver.getFontMetrics(style.font);
164
+
165
+ // TODO: Make sure that values are rounded using devicePixelRatio
166
+ const textX = roundToPixels(
167
+ textAlign === 'left' ? padding.left :
168
+ textAlign === 'center' ? cellWidth / 2 :
169
+ textAlign === 'right' ? cellWidth - padding.right :
170
+ 0,
171
+ devicePixelRatio
172
+ );
173
+
174
+ const textY = roundToPixels(
175
+ textBaseline === 'top' ? fontMetrics.middle + fontMetrics.topOffset + padding.top :
176
+ textBaseline === 'middle' ? cellHeight / 2 + fontMetrics.middle :
177
+ textBaseline === 'bottom' ? cellHeight + fontMetrics.middle - fontMetrics.bottomOffset - padding.bottom :
178
+ 0,
179
+ devicePixelRatio
180
+ );
181
+
182
+ const fitsVertically = textY - fontMetrics.middle - fontMetrics.topOffset >= 0 && textY - fontMetrics.middle + fontMetrics.bottomOffset <= cellHeight;
183
+
184
+ if (fitsVertically) {
185
+ ctx.fillText(text, textX, textY);
186
+ }
187
+ else {
188
+ // TODO: Clip if the text is too high (and draw some indicator if you do)
189
+ ctx.strokeStyle = '#E9E9E9';
190
+ ctx.lineWidth = borderWidth;
191
+
192
+ ctx.beginPath();
193
+ ctx.moveTo(0, borderWidth + borderOffset);
194
+ ctx.lineTo(cellWidth, borderWidth + borderOffset);
195
+ ctx.moveTo(0, cellHeight - borderWidth - borderOffset);
196
+ ctx.lineTo(cellWidth, cellHeight - borderWidth - borderOffset);
197
+ ctx.stroke();
198
+
199
+ ctx.save();
200
+ setClip(0, 2 * borderWidth, cellWidth, cellHeight - 4 * borderWidth);
201
+
202
+ ctx.fillText(text, textX, textY);
203
+
204
+ ctx.restore();
205
+ }
206
+ }
207
+
208
+ ctx.restore();
209
+ }
210
+
211
+ setTransform(0, 0);
212
+
213
+ // Draw borders
214
+
215
+ // TODO: clip drawing area to the middle of neighboring columns/rows (might be useful for redrawing only the changed cells)
216
+ // TODO: move somewhere (?)
217
+ const drawBorder = (x1, y1, x2, y2, style) => {
218
+ if (!style)
219
+ return;
220
+
221
+ const width = style.width * borderWidth;
222
+
223
+ const isHorizontal = y1 === y2;
224
+
225
+ // TODO: Don't add offset if the border is not continued
226
+ const xa = x1 - (isHorizontal ? width / 2 : 0);
227
+ const ya = y1 - (!isHorizontal ? width / 2 : 0);
228
+ const xb = x2 + (isHorizontal ? width / 2 : 0);
229
+ const yb = y2 + (!isHorizontal ? width / 2 : 0);
230
+
231
+ ctx.strokeStyle = style.color || 'black'; // TODO: resolve this color earlier
232
+ ctx.lineWidth = width;
233
+
234
+ if (style.dash) {
235
+ ctx.setLineDash(style.dash.map(value => value / devicePixelRatio));
236
+ ctx.lineDashOffset = (isHorizontal ? xa : ya);
237
+ }
238
+ else {
239
+ ctx.setLineDash([]);
240
+ }
241
+
242
+ ctx.beginPath();
243
+ ctx.moveTo(xa, ya);
244
+ ctx.lineTo(xb, yb);
245
+ ctx.stroke();
246
+ }
247
+
248
+ const selectBorder = (borderStyleA, borderStyleB) => {
249
+ if (!borderStyleA)
250
+ return borderStyleB;
251
+
252
+ if (!borderStyleB)
253
+ return borderStyleA;
254
+
255
+ if (borderStyleA.index > borderStyleB.index)
256
+ return borderStyleA;
257
+
258
+ return borderStyleB;
259
+ }
260
+
261
+ // TODO: Move somewhere else (?)
262
+ for (let horizontalBorderIndex = minVisibleHorizontalBorderIndex; horizontalBorderIndex <= maxVisibleHorizontalBorderIndex; horizontalBorderIndex++) {
263
+ const topRowIndex = horizontalBorderIndex - 1;
264
+ const bottomRowIndex = horizontalBorderIndex;
265
+
266
+ for (let columnIndex = minVisibleColumnIndex; columnIndex <= maxVisibleColumnIndex; columnIndex++) {
267
+ const topBorderStyle = topRowIndex >= minVisibleRowIndex ? getCell(topRowIndex, columnIndex).style.borderBottom : null;
268
+ const bottomBorderStyle = bottomRowIndex <= maxVisibleRowIndex ? getCell(bottomRowIndex, columnIndex).style.borderTop : null;
269
+
270
+ const borderStyle = selectBorder(topBorderStyle, bottomBorderStyle);
271
+
272
+ drawBorder(
273
+ horizontalOffsets[columnIndex] - borderOffset,
274
+ verticalOffsets[bottomRowIndex] - borderOffset,
275
+ horizontalOffsets[columnIndex + 1] - borderOffset,
276
+ verticalOffsets[bottomRowIndex] - borderOffset,
277
+ borderStyle);
278
+ }
279
+ }
280
+
281
+ for (let verticalBorderIndex = minVisibleVerticalBorderIndex; verticalBorderIndex <= maxVisibleVerticalBorderIndex; verticalBorderIndex++) {
282
+ const leftColumnIndex = verticalBorderIndex - 1;
283
+ const rightColumnIndex = verticalBorderIndex;
284
+
285
+ for (let rowIndex = minVisibleRowIndex; rowIndex <= maxVisibleRowIndex; rowIndex++) {
286
+ const leftBorderStyle = leftColumnIndex >= minVisibleColumnIndex ? getCell(rowIndex, leftColumnIndex).style.borderRight : null;
287
+ const rightBorderStyle = rightColumnIndex <= maxVisibleColumnIndex ? getCell(rowIndex, rightColumnIndex).style.borderLeft : null;
288
+
289
+ const borderStyle = selectBorder(leftBorderStyle, rightBorderStyle);
290
+
291
+ drawBorder(
292
+ horizontalOffsets[rightColumnIndex] - borderOffset,
293
+ verticalOffsets[rowIndex] - borderOffset,
294
+ horizontalOffsets[rightColumnIndex] - borderOffset,
295
+ verticalOffsets[rowIndex + 1] - borderOffset,
296
+ borderStyle);
297
+ }
298
+ }
299
+ }
300
+
301
+ function renderInput(context) {
302
+ const element = context.element;
303
+ const input = context.input;
304
+ const state = context.state;
305
+ const inputPlacement = state.inputPlacement;
306
+
307
+ if (!inputPlacement) {
308
+ if (input.parentElement) {
309
+ const hasFocus = document.activeElement === input;
310
+ input.parentElement.removeChild(input);
311
+ if (hasFocus)
312
+ element.focus({ preventScroll: true });
313
+ }
314
+ return;
315
+ }
316
+
317
+ const canvas = context.canvases[inputPlacement.section];
318
+
319
+ input.style.left = 'left' in inputPlacement ? `${inputPlacement.left}px` : '0';
320
+ input.style.top = 'top' in inputPlacement ? `${inputPlacement.top}px` : '0';
321
+ input.style.right = 'right' in inputPlacement ? `${inputPlacement.right}px` : '0';
322
+ input.style.bottom = 'bottom' in inputPlacement ? `${inputPlacement.bottom}px` : '0';
323
+ input.style.marginLeft = 'marginLeft' in inputPlacement ? `${inputPlacement.marginLeft}px` : '0';
324
+ input.style.marginTop = 'marginTop' in inputPlacement ? `${inputPlacement.marginTop}px` : '0';
325
+ input.style.width = `${inputPlacement.width}px`;
326
+ input.style.height = `${inputPlacement.height}px`;
327
+ input.style.gridArea = canvas.style.gridArea;
328
+ input.style.zIndex = canvas.style.zIndex;
329
+ input.style.backgroundColor = state.isTextValid ? 'white' : '#eb3434';
330
+
331
+ if (!input.parentElement) {
332
+ const hasFocus = document.activeElement === element;
333
+ element.appendChild(input);
334
+ if (hasFocus)
335
+ input.focus({ preventScroll: true });
336
+ }
337
+ }
338
+
339
+ function renderCursor(context) {
340
+ const element = context.element;
341
+ const state = context.state;
342
+
343
+ if (state.resizableColumn && state.resizableRow)
344
+ element.style.cursor = 'nwse-resize';
345
+ else if (state.resizableColumn)
346
+ element.style.cursor = 'col-resize';
347
+ else if (state.resizableRow)
348
+ element.style.cursor = 'row-resize';
349
+ else
350
+ element.style.cursor = 'default';
351
+ }
352
+
353
+ function renderInternal(context) {
354
+ renderSection(context, 'top', 'left');
355
+ renderSection(context, 'top', 'center');
356
+ renderSection(context, 'top', 'right');
357
+ renderSection(context, 'middle', 'left');
358
+ renderSection(context, 'middle', 'center');
359
+ renderSection(context, 'middle', 'right');
360
+ renderSection(context, 'bottom', 'left');
361
+ renderSection(context, 'bottom', 'center');
362
+ renderSection(context, 'bottom', 'right');
363
+
364
+ renderInput(context);
365
+ renderCursor(context);
366
+ }
367
+
368
+ function renderError(context) {
369
+ const element = context.element;
370
+ const error = context.error;
371
+
372
+ element.style.backgroundColor = '#9f0000';
373
+ element.style.color = 'white';
374
+ element.style.padding = '20px';
375
+ element.style.display = 'flex';
376
+ element.style.flexDirection = 'column';
377
+ element.innerHTML = `
378
+ <div style="font-size: 16px;">
379
+ An error occurred while rendering the grid, please contact the support.
380
+ </div>
381
+ <div style="font-size: 20px; font-weight: bold; padding: 20px 0;">
382
+ ${error.message}
383
+ </div>
384
+ <div style="font-size: 16px; white-space: pre-wrap;">${error.stack}</div>
385
+ `;
386
+ }
387
+
388
+ export default function render(context) {
389
+ if (!context.error) {
390
+ try {
391
+ renderInternal(context);
392
+ }
393
+ catch (error) {
394
+ context.error = error;
395
+ }
396
+ }
397
+
398
+ if (context.error)
399
+ renderError(context);
400
+ }
@@ -0,0 +1,183 @@
1
+ import getEditableCells from "../state-utils/getEditableCells.js";
2
+ import getDataFormatting from "../state-utils/getDataFormatting.js";
3
+ import getInputFormatting from "../state-utils/getInputFormatting.js";
4
+ import getRenderFormatting from "../state-utils/getRenderFormatting.js";
5
+ import getSections from "../state-utils/getSections.js";
6
+ import getEditedCellsAndFilters from "../state-utils/getEditedCellsAndFilters.js";
7
+ import getEdition from "../state-utils/getEdition.js";
8
+ import getSelection from "../state-utils/getSelection.js";
9
+ import getInvoked from "../state-utils/getInvoked.js";
10
+ import { getResolvedColumns, getResolvedRows } from "../state-utils/getResolved.js";
11
+ import { getPlacedColumns, getPlacedRows } from "../state-utils/getPlaced.js";
12
+ import getFormattingRules from "../state-utils/getFormattingRules.js";
13
+ import getFormatResolver from "../state-utils/getFormatResolver.js";
14
+ import { getFilteredColumns as getFilteredColumns, getFilteredRows as getFilteredRows } from "../state-utils/getFiltered.js";
15
+ import getFixedSize from "../state-utils/getFixedSize.js";
16
+ import getTotalSize from "../state-utils/getTotalSize.js";
17
+ import getTextResolver from "../state-utils/getTextResolver.js";
18
+ import getScrollRect from "../state-utils/getScrollRect.js";
19
+ import getHoveredCell from "../state-utils/getHoveredCell.js";
20
+ import getLookup from "../state-utils/getLookup.js";
21
+ import getHighlightedCells from "../state-utils/getHighlightedCells.js";
22
+ import getInputPlacement from "../state-utils/getInputPlacement.js";
23
+ import getIsTextValid from "../state-utils/getIsTextValid.js";
24
+ import { getUnfoldedColumns, getUnfoldedRows } from "../state-utils/getUnfolded.js";
25
+ import getFilteringRules from "../state-utils/getFilteringRules.js";
26
+ import getFilterFormatting from "../state-utils/getFilterFormatting.js";
27
+ import getMeasureFormatting from "../state-utils/getMeasureFormatting.js";
28
+ import { getMeasuredColumns, getMeasuredRows } from "../state-utils/getMeasured.js";
29
+ import getKeys from "../state-utils/getKeys.js";
30
+ import getSortingFormatting from "../state-utils/getSortingFormatting.js";
31
+ import getSortingRules from "../state-utils/getSortingRules.js";
32
+ import { getSortedColumns, getSortedRows } from "../state-utils/getSorted.js";
33
+ import { getResizableColumn, getResizableRow } from "../state-utils/getResizable.js";
34
+ import getResolvedSortBy from "../state-utils/getResolvedSortBy.js";
35
+ import getResolvedFilters from "../state-utils/getResolvedFilters.js";
36
+ import { getActiveColumns } from "../state-utils/getActive.js";
37
+
38
+ // TODO: write some test to check if the cache is working properly
39
+ function updateStateInternal(context) {
40
+ console.count('updateState');
41
+
42
+ const options = { ...context.localOptions, ...context.externalOptions };
43
+ const memory = context.memory;
44
+ const previousState = context.state;
45
+
46
+ // TODO: Move to utils
47
+ function cache(key, func, dependencies) {
48
+ const previousDependencies = memory[key] && memory[key].dependencies;
49
+ if (!previousDependencies || dependencies.some((dependency, index) => dependency !== previousDependencies[index]))
50
+ memory[key] = { value: func(...dependencies), dependencies };
51
+ return memory[key].value;
52
+ }
53
+
54
+ const devicePixelRatio = window.devicePixelRatio; // TODO: Trigger update on devicePixelRatio change
55
+ const borderWidth = options.borderWidth / devicePixelRatio;
56
+ const data = options.data;
57
+ const text = context.input.value;
58
+ const sortBy = cache('sortBy', getResolvedSortBy, [options.sortBy]);
59
+ const filters = cache('filters', getResolvedFilters, [options.filters]);
60
+ const textResolver = cache('textResolver', getTextResolver, []);
61
+ const dataFormatting = cache('dataFormatting', getDataFormatting, [options.formatting, options.dataSelector, sortBy]);
62
+ const editedCellsAndFilters = cache('editedCellsAndFilters', getEditedCellsAndFilters, [options.editedCells, filters]);
63
+ const edition = cache('edition', getEdition, [editedCellsAndFilters]);
64
+ const invokedColumns = cache('invokedColumns', getInvoked, [options.columns, data]);
65
+ const invokedRows = cache('invokedRows', getInvoked, [options.rows, data]);
66
+ // TODO: throw on duplicate ids
67
+ // TODO: throw on duplicate row/column filter ids
68
+ const unfoldedColumns = cache('unfoldedColumns', getUnfoldedColumns, [invokedColumns, data]);
69
+ const unfoldedRows = cache('unfoldedRows', getUnfoldedRows, [invokedRows, data]);
70
+ const unfilteredColumns = cache('unfilteredColumns', getResolvedColumns, [unfoldedColumns, options.pinnedLeft, options.pinnedRight, options.columnWidths]);
71
+ const unfilteredRows = cache('unfilteredRows', getResolvedRows, [unfoldedRows, options.pinnedTop, options.pinnedBottom, options.rowHeights]);
72
+ const unfilteredColumnKeys = cache('unfilteredColumnKeys', getKeys, [unfilteredColumns]);
73
+ const unfilteredRowKeys = cache('unfilteredRowKeys', getKeys, [unfilteredRows]);
74
+
75
+ // Filtering
76
+ const filterFormatting = cache('filterFormatting', getFilterFormatting, [dataFormatting]);
77
+ const filterFormattingRules = cache('filterFormattingRules', getFormattingRules, [filterFormatting]);
78
+ const filteringRules = cache('filteringRules', getFilteringRules, [options.filtering]);
79
+ const filteredColumns = cache('filteredColumns', getFilteredColumns, [filters, filteringRules, filterFormattingRules, data, unfilteredRows, unfilteredColumns, edition]);
80
+ const filteredRows = cache('filteredRows', getFilteredRows, [filters, filteringRules, filterFormattingRules, data, unfilteredRows, unfilteredColumns, edition]);
81
+
82
+ // Sorting
83
+ const sortingFormatting = cache('sortingFormatting', getSortingFormatting, [dataFormatting]);
84
+ const sortingFormattingRules = cache('sortingFormattingRules', getFormattingRules, [sortingFormatting]);
85
+ const sortingRules = cache('sortingRules', getSortingRules, [options.sorting]);
86
+ const sortedColumns = cache('sortedColumns', getSortedColumns, [sortBy, sortingRules, sortingFormattingRules, data, filteredRows, filteredColumns, edition]);
87
+ const sortedRows = cache('sortedRows', getSortedRows, [sortBy, sortingRules, sortingFormattingRules, data, filteredRows, filteredColumns, edition]);
88
+
89
+ // Shaping
90
+ const shapedColumns = sortedColumns;
91
+ const shapedRows = sortedRows;
92
+
93
+ // Measuring
94
+ const measureFormatting = cache('measureFormatting', getMeasureFormatting, [dataFormatting]);
95
+ const measureFormattingRules = cache('measureFormattingRules', getFormattingRules, [measureFormatting]);
96
+ const measureFormatResolver = cache('measureFormatResolver', getFormatResolver, [measureFormattingRules, data, shapedRows, shapedColumns, edition]);
97
+ const columnWidthCache = context.columnWidthCache;
98
+ const rowHeightCache = context.rowHeightCache;
99
+ const measuredColumns = cache('measuredColumns', getMeasuredColumns, [shapedColumns, shapedRows, textResolver, measureFormatResolver, columnWidthCache, unfilteredColumnKeys]);
100
+ const measuredRows = cache('measuredRows', getMeasuredRows, [shapedColumns, shapedRows, textResolver, measureFormatResolver, rowHeightCache, unfilteredRowKeys]);
101
+
102
+ // Placement
103
+ const columns = cache('columns', getPlacedColumns, [measuredColumns, devicePixelRatio, borderWidth]);
104
+ const rows = cache('rows', getPlacedRows, [measuredRows, devicePixelRatio, borderWidth]);
105
+ const columnLookup = cache('columnLookup', getLookup, [columns]);
106
+ const rowLookup = cache('rowLookup', getLookup, [rows]);
107
+ const focusedCell = options.focusedCell;
108
+ const sections = cache('sections', getSections, [columns, rows]);
109
+ const selectedCells = options.selectedCells;
110
+ const fixedSize = cache('fixedSize', getFixedSize, [sections.top.height, sections.bottom.height, sections.left.width, sections.right.width]);
111
+ const totalSize = cache('totalSize', getTotalSize, [columns, rows]);
112
+ // TODO: do some proper caching, so that if value is not changed, the old value is returned (currently not working because of scrolling)
113
+ const hoveredCell = getHoveredCell(context.element, context.mousePosition, rows, columns, fixedSize, totalSize);
114
+ const resizableColumn = context.resizingColumn || cache('resizableColumn', getResizableColumn, [columns, columnLookup, rowLookup, hoveredCell, context.mousePosition]);
115
+ const resizableRow = context.resizingRow || cache('resizableRow', getResizableRow, [rows, columnLookup, rowLookup, hoveredCell, context.mousePosition]);
116
+ const isMouseDown = context.isMouseDown;
117
+ const isResizing = !!resizableColumn || !!resizableRow;
118
+ const highlightedCells = cache('highlightedCells', getHighlightedCells, [isMouseDown, isResizing, focusedCell, hoveredCell, columns, rows, columnLookup, rowLookup]);
119
+ const selection = cache('selection', getSelection, [selectedCells]);
120
+ const highlight = cache('highlight', getSelection, [highlightedCells]);
121
+ // TODO: addDataFormattingRules and addRenderFormattingRules should remove unnecessary rules
122
+ const renderFormatting = cache('renderFormatting', getRenderFormatting, [dataFormatting, hoveredCell, focusedCell, selection, highlight, edition, resizableColumn, resizableRow]);
123
+ const renderFormattingRules = cache('renderFormattingRules', getFormattingRules, [renderFormatting]);
124
+ const renderFormatResolver = cache('renderFormatResolver', getFormatResolver, [renderFormattingRules, data, rows, columns, edition]);
125
+ const inputFormatting = cache('inputFormatting', getInputFormatting, [dataFormatting]);
126
+ const inputFormattingRules = cache('inputFormattingRules', getFormattingRules, [inputFormatting]);
127
+ const inputFormatResolver = cache('inputFormatResolver', getFormatResolver, [inputFormattingRules, data, rows, columns, edition]);
128
+ const editableCells = cache('editableCells', getEditableCells, [selectedCells, inputFormatResolver, columnLookup, rowLookup]);
129
+ const inputPlacement = cache('inputPlacement', getInputPlacement, [editableCells, focusedCell, columnLookup, rowLookup, sections]);
130
+ const isTextValid = cache('isTextValid', getIsTextValid, [text, editableCells]);
131
+
132
+ // cache result, but not call
133
+ const scrollRect = getScrollRect(previousState?.scrollRect, totalSize, fixedSize, context.element);
134
+
135
+ // callbacks
136
+ cache('activeColumns', getActiveColumns, [columns, options.onActiveColumnsChange]);
137
+ cache('activeRows', getActiveColumns, [rows, options.onActiveRowsChange]);
138
+
139
+ context.state = {
140
+ options,
141
+ devicePixelRatio,
142
+ borderWidth,
143
+ data,
144
+ dataFormatting,
145
+ edition,
146
+ filteredColumns,
147
+ filteredRows,
148
+ columns,
149
+ rows,
150
+ sections,
151
+ selectedCells,
152
+ selection,
153
+ highlight,
154
+ hoveredCell,
155
+ focusedCell,
156
+ renderFormatting,
157
+ renderFormatResolver,
158
+ inputFormatting,
159
+ inputFormatResolver,
160
+ fixedSize,
161
+ totalSize,
162
+ textResolver,
163
+ scrollRect,
164
+ highlightedCells,
165
+ inputPlacement,
166
+ columnLookup,
167
+ rowLookup,
168
+ text,
169
+ isTextValid,
170
+ resizableColumn,
171
+ resizableRow,
172
+ };
173
+ }
174
+
175
+ export default function updateState(context) {
176
+ if (!context.error) {
177
+ try {
178
+ updateStateInternal(context);
179
+ } catch (error) {
180
+ context.error = error;
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,4 @@
1
+ const defaultFont = '12px Calibri';
2
+ const defaultPadding = { top: 2, right: 5, bottom: 2, left: 5 };
3
+
4
+ export { defaultFont, defaultPadding };
@@ -0,0 +1,49 @@
1
+ export function contains(bounds, rect) {
2
+ return (
3
+ rect.top >= bounds.top &&
4
+ rect.left >= bounds.left &&
5
+ rect.top + rect.height <= bounds.top + bounds.height &&
6
+ rect.left + rect.width <= bounds.left + bounds.width
7
+ );
8
+ }
9
+
10
+ export function clip(bounds, rect) {
11
+ const newRect = {
12
+ top: Math.max(bounds.top, rect.top),
13
+ left: Math.max(bounds.left, rect.left),
14
+ width: Math.min(bounds.left + bounds.width, rect.left + rect.width) - Math.max(bounds.left, rect.left),
15
+ height: Math.min(bounds.top + bounds.height, rect.top + rect.height) - Math.max(bounds.top, rect.top)
16
+ };
17
+
18
+ if (newRect.width >= 0 && newRect.height >= 0)
19
+ return newRect;
20
+
21
+ return {
22
+ top: bounds.top,
23
+ left: bounds.left,
24
+ width: 0,
25
+ height: 0
26
+ }
27
+ }
28
+
29
+ export function expand(rect, margin) {
30
+ return {
31
+ top: rect.top - margin,
32
+ left: rect.left - margin,
33
+ width: rect.width + margin * 2,
34
+ height: rect.height + margin * 2
35
+ };
36
+ }
37
+
38
+ export function area(rect) {
39
+ return rect.width * rect.height;
40
+ }
41
+
42
+ export function subtract(rect, margin) {
43
+ return {
44
+ top: rect.top,
45
+ left: rect.left,
46
+ width: Math.max(0, rect.width - margin.left - margin.right),
47
+ height: Math.max(0, rect.height - margin.top - margin.bottom)
48
+ };
49
+ }