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.
- package/package.json +26 -0
- package/src/core/render.js +400 -0
- package/src/core/state.js +183 -0
- package/src/core-utils/defaults.js +4 -0
- package/src/core-utils/rect.js +49 -0
- package/src/core-utils/rect.test.js +111 -0
- package/src/core-utils/roundToPixels.js +3 -0
- package/src/core-utils/stringifyId.js +27 -0
- package/src/core-utils/stringifyId.test.js +27 -0
- package/src/index.js +595 -0
- package/src/state-utils/getActive.js +17 -0
- package/src/state-utils/getCellSection.js +22 -0
- package/src/state-utils/getCellType.js +7 -0
- package/src/state-utils/getClipboardData.js +53 -0
- package/src/state-utils/getColumnIndex.js +25 -0
- package/src/state-utils/getCombinedCells.js +6 -0
- package/src/state-utils/getDataFormatting.js +46 -0
- package/src/state-utils/getEditableCells.js +23 -0
- package/src/state-utils/getEditedCellsAndFilters.js +4 -0
- package/src/state-utils/getEdition.js +6 -0
- package/src/state-utils/getFilterFormatting.js +3 -0
- package/src/state-utils/getFiltered.js +61 -0
- package/src/state-utils/getFilteringRules.js +5 -0
- package/src/state-utils/getFixedSize.js +8 -0
- package/src/state-utils/getFormatResolver.js +5 -0
- package/src/state-utils/getFormattingRules.js +5 -0
- package/src/state-utils/getHighlightedCells.js +40 -0
- package/src/state-utils/getHoveredCell.js +43 -0
- package/src/state-utils/getInputFormatting.js +3 -0
- package/src/state-utils/getInputPlacement.js +51 -0
- package/src/state-utils/getInvoked.js +5 -0
- package/src/state-utils/getIsTextValid.js +3 -0
- package/src/state-utils/getKeys.js +3 -0
- package/src/state-utils/getLookup.js +3 -0
- package/src/state-utils/getMeasureFormatting.js +3 -0
- package/src/state-utils/getMeasured.js +113 -0
- package/src/state-utils/getMousePosition.js +8 -0
- package/src/state-utils/getNewSortBy.js +20 -0
- package/src/state-utils/getPinned.js +8 -0
- package/src/state-utils/getPlaced.js +45 -0
- package/src/state-utils/getReducedCells.js +6 -0
- package/src/state-utils/getRenderFormatting.js +122 -0
- package/src/state-utils/getResizable.js +49 -0
- package/src/state-utils/getResolved.js +42 -0
- package/src/state-utils/getResolvedFilters.js +7 -0
- package/src/state-utils/getResolvedSortBy.js +7 -0
- package/src/state-utils/getRowIndex.js +25 -0
- package/src/state-utils/getScrollRect.js +41 -0
- package/src/state-utils/getSections.js +101 -0
- package/src/state-utils/getSelection.js +5 -0
- package/src/state-utils/getSorted.js +130 -0
- package/src/state-utils/getSortingFormatting.js +3 -0
- package/src/state-utils/getSortingRules.js +5 -0
- package/src/state-utils/getTextResolver.js +5 -0
- package/src/state-utils/getToggledValue.js +11 -0
- package/src/state-utils/getTotalSize.js +6 -0
- package/src/state-utils/getUnfolded.js +86 -0
- package/src/types/Edition.js +37 -0
- package/src/types/FilteringRules.js +48 -0
- package/src/types/FormatResolver.js +19 -0
- package/src/types/FormattingRules.js +120 -0
- package/src/types/FormattingRules.test.js +90 -0
- package/src/types/RulesLookup.js +118 -0
- package/src/types/Selection.js +25 -0
- package/src/types/SortingRules.js +62 -0
- package/src/types/TextResolver.js +60 -0
- 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,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
|
+
}
|