PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.5
1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 trunk 0.0.2 0.2.1 0.3.2 0.3.3 0.4.1 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 All 42 releases
tableberg / src / table-structure.ts

table-structure.ts in Tableberg – Simple Gutenberg Table Block 1.1.5, at src/table-structure.ts

541 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import {
2 Cell,
3 CellElement,
4 CellKey,
5 ColumnConfigs,
6 getCellKey,
7 parseCellKey,
8 RowConfigs,
9 Span,
10 TableConfig,
11 } from "./attributes";
12
13 type Coord = [number, number];
14
15 interface TableUpdateResult {
16 table: TableConfig;
17 rows: RowConfigs;
18 columns: ColumnConfigs;
19 cells: Record<CellKey, Cell>;
20 }
21
22 function cloneValue<T>(value: T): T {
23 return JSON.parse(JSON.stringify(value)) as T;
24 }
25
26 function getSpan(cell?: Cell): Span {
27 return cell?.span || { rowSpan: 1, colSpan: 1 };
28 }
29
30 function getSpanMap(cells: Record<CellKey, Cell>): Map<string, Span> {
31 const map = new Map<string, Span>();
32
33 Object.entries(cells).forEach(([key, cell]) => {
34 map.set(key, getSpan(cell));
35 });
36
37 return map;
38 }
39
40 function shiftRecordForInsert<T>(
41 record: Array<T | null | undefined>,
42 fromIndex: number
43 ): Array<T | null | undefined> {
44 const next: Array<T | null | undefined> = [];
45
46 Object.entries(record).forEach(([key, value]) => {
47 if (value === null || value === undefined) {
48 return;
49 }
50
51 const index = Number(key);
52 const nextIndex = index >= fromIndex ? index + 1 : index;
53 next[nextIndex] = value;
54 });
55
56 return next;
57 }
58
59 function shiftRecordForDelete<T>(
60 record: Array<T | null | undefined>,
61 removedIndex: number
62 ): Array<T | null | undefined> {
63 const next: Array<T | null | undefined> = [];
64
65 Object.entries(record).forEach(([key, value]) => {
66 if (value === null || value === undefined) {
67 return;
68 }
69
70 const index = Number(key);
71
72 if (index === removedIndex) {
73 return;
74 }
75
76 const nextIndex = index > removedIndex ? index - 1 : index;
77 next[nextIndex] = value;
78 });
79
80 return next;
81 }
82
83 function remapRecord<T>(
84 record: Array<T | null | undefined>,
85 mapIndex: (index: number) => number
86 ): Array<T | null | undefined> {
87 const next: Array<T | null | undefined> = [];
88
89 Object.entries(record).forEach(([key, value]) => {
90 if (value === null || value === undefined) {
91 return;
92 }
93
94 const index = Number(key);
95 next[mapIndex(index)] = value;
96 });
97
98 return next;
99 }
100
101 function normalizeInsertRowIndex(table: TableConfig, rowIndex: number): number {
102 if (rowIndex <= 0) {
103 return 0;
104 }
105
106 if (rowIndex >= table.rows) {
107 return table.rows;
108 }
109
110 return rowIndex;
111 }
112
113 function normalizeInsertColumnIndex(
114 table: TableConfig,
115 colIndex: number
116 ): number {
117 if (colIndex <= 0) {
118 return 0;
119 }
120
121 if (colIndex >= table.cols) {
122 return table.cols;
123 }
124
125 return colIndex;
126 }
127
128 function normalizeDeleteRowIndex(table: TableConfig, rowIndex: number): number {
129 if (rowIndex <= 0) {
130 return 0;
131 }
132
133 if (rowIndex >= table.rows - 1) {
134 return table.rows - 1;
135 }
136
137 return rowIndex;
138 }
139
140 function normalizeDeleteColumnIndex(
141 table: TableConfig,
142 colIndex: number
143 ): number {
144 if (colIndex <= 0) {
145 return 0;
146 }
147
148 if (colIndex >= table.cols - 1) {
149 return table.cols - 1;
150 }
151
152 return colIndex;
153 }
154
155 export function hasMergedCells(cells: Record<CellKey, Cell>): boolean {
156 return Object.values(cells).some(cell => {
157 const span = getSpan(cell);
158 return span.rowSpan > 1 || span.colSpan > 1;
159 });
160 }
161
162 export function insertRowAt(
163 table: TableConfig,
164 rows: RowConfigs,
165 columns: ColumnConfigs,
166 cells: Record<CellKey, Cell>,
167 rowIndex: number,
168 defaultElement?: CellElement
169 ): TableUpdateResult {
170 const insertAt = normalizeInsertRowIndex(table, rowIndex);
171 const skippedColumns = new Set<number>();
172 const nextCells: Record<CellKey, Cell> = {};
173
174 Object.entries(cells).forEach(([key, cell]) => {
175 const [row, col] = parseCellKey(key);
176 const span = getSpan(cell);
177 const nextCell = cloneValue(cell);
178
179 if (row < insertAt && row + span.rowSpan > insertAt) {
180 nextCell.span = {
181 rowSpan: span.rowSpan + 1,
182 colSpan: span.colSpan,
183 };
184
185 for (let c = col; c < col + span.colSpan; c++) {
186 skippedColumns.add(c);
187 }
188
189 nextCells[getCellKey(row, col)] = nextCell;
190 return;
191 }
192
193 if (row >= insertAt) {
194 nextCells[getCellKey(row + 1, col)] = nextCell;
195 return;
196 }
197
198 nextCells[getCellKey(row, col)] = nextCell;
199 });
200
201 for (let col = 0; col < table.cols; col++) {
202 if (!skippedColumns.has(col)) {
203 const key = getCellKey(insertAt, col);
204 nextCells[key] = defaultElement
205 ? { elements: [cloneValue(defaultElement)] }
206 : {};
207 }
208 }
209
210 return {
211 table: {
212 ...table,
213 rows: table.rows + 1,
214 },
215 rows: shiftRecordForInsert(rows, insertAt),
216 columns: columns.slice(),
217 cells: nextCells,
218 };
219 }
220
221 export function insertColumnAt(
222 table: TableConfig,
223 rows: RowConfigs,
224 columns: ColumnConfigs,
225 cells: Record<CellKey, Cell>,
226 colIndex: number,
227 defaultElement?: CellElement
228 ): TableUpdateResult {
229 const insertAt = normalizeInsertColumnIndex(table, colIndex);
230 const skippedRows = new Set<number>();
231 const nextCells: Record<CellKey, Cell> = {};
232
233 Object.entries(cells).forEach(([key, cell]) => {
234 const [row, col] = parseCellKey(key);
235 const span = getSpan(cell);
236 const nextCell = cloneValue(cell);
237
238 if (col < insertAt && col + span.colSpan > insertAt) {
239 nextCell.span = {
240 rowSpan: span.rowSpan,
241 colSpan: span.colSpan + 1,
242 };
243
244 for (let r = row; r < row + span.rowSpan; r++) {
245 skippedRows.add(r);
246 }
247
248 nextCells[getCellKey(row, col)] = nextCell;
249 return;
250 }
251
252 if (col >= insertAt) {
253 nextCells[getCellKey(row, col + 1)] = nextCell;
254 return;
255 }
256
257 nextCells[getCellKey(row, col)] = nextCell;
258 });
259
260 for (let row = 0; row < table.rows; row++) {
261 if (!skippedRows.has(row)) {
262 const key = getCellKey(row, insertAt);
263 nextCells[key] = defaultElement
264 ? { elements: [cloneValue(defaultElement)] }
265 : {};
266 }
267 }
268
269 return {
270 table: {
271 ...table,
272 cols: table.cols + 1,
273 },
274 rows: rows.slice(),
275 columns: shiftRecordForInsert(columns, insertAt),
276 cells: nextCells,
277 };
278 }
279
280 export function deleteRowAt(
281 table: TableConfig,
282 rows: RowConfigs,
283 columns: ColumnConfigs,
284 cells: Record<CellKey, Cell>,
285 rowIndex: number
286 ): TableUpdateResult {
287 const deleteAt = normalizeDeleteRowIndex(table, rowIndex);
288 const spanMap = getSpanMap(cells);
289 const nextCells: Record<CellKey, Cell> = {};
290
291 Object.entries(cells).forEach(([key, cell]) => {
292 const [row, col] = parseCellKey(key);
293 const span = spanMap.get(key) || { rowSpan: 1, colSpan: 1 };
294 const nextCell = cloneValue(cell);
295
296 if (row === deleteAt) {
297 if (span.rowSpan > 1) {
298 nextCell.span = {
299 rowSpan: span.rowSpan - 1,
300 colSpan: span.colSpan,
301 };
302 nextCells[getCellKey(row, col)] = nextCell;
303 }
304 return;
305 }
306
307 if (row > deleteAt) {
308 nextCells[getCellKey(row - 1, col)] = nextCell;
309 return;
310 }
311
312 if (row < deleteAt && row + span.rowSpan > deleteAt) {
313 nextCell.span = {
314 rowSpan: span.rowSpan - 1,
315 colSpan: span.colSpan,
316 };
317 nextCells[getCellKey(row, col)] = nextCell;
318 return;
319 }
320
321 nextCells[getCellKey(row, col)] = nextCell;
322 });
323
324 return {
325 table: {
326 ...table,
327 rows: table.rows - 1,
328 },
329 rows: shiftRecordForDelete(rows, deleteAt),
330 columns: columns.slice(),
331 cells: nextCells,
332 };
333 }
334
335 export function deleteColumnAt(
336 table: TableConfig,
337 rows: RowConfigs,
338 columns: ColumnConfigs,
339 cells: Record<CellKey, Cell>,
340 colIndex: number
341 ): TableUpdateResult {
342 const deleteAt = normalizeDeleteColumnIndex(table, colIndex);
343 const spanMap = getSpanMap(cells);
344 const nextCells: Record<CellKey, Cell> = {};
345
346 Object.entries(cells).forEach(([key, cell]) => {
347 const [row, col] = parseCellKey(key);
348 const span = spanMap.get(key) || { rowSpan: 1, colSpan: 1 };
349 const nextCell = cloneValue(cell);
350
351 if (col === deleteAt) {
352 if (span.colSpan > 1) {
353 nextCell.span = {
354 rowSpan: span.rowSpan,
355 colSpan: span.colSpan - 1,
356 };
357 nextCells[getCellKey(row, col)] = nextCell;
358 }
359 return;
360 }
361
362 if (col > deleteAt) {
363 nextCells[getCellKey(row, col - 1)] = nextCell;
364 return;
365 }
366
367 if (col < deleteAt && col + span.colSpan > deleteAt) {
368 nextCell.span = {
369 rowSpan: span.rowSpan,
370 colSpan: span.colSpan - 1,
371 };
372 nextCells[getCellKey(row, col)] = nextCell;
373 return;
374 }
375
376 nextCells[getCellKey(row, col)] = nextCell;
377 });
378
379 return {
380 table: {
381 ...table,
382 cols: table.cols - 1,
383 },
384 rows: rows.slice(),
385 columns: shiftRecordForDelete(columns, deleteAt),
386 cells: nextCells,
387 };
388 }
389
390 export function duplicateRowAt(
391 table: TableConfig,
392 rows: RowConfigs,
393 columns: ColumnConfigs,
394 cells: Record<CellKey, Cell>,
395 rowIndex: number
396 ): TableUpdateResult {
397 const sourceRow = normalizeDeleteRowIndex(table, rowIndex);
398 const inserted = insertRowAt(table, rows, columns, cells, sourceRow + 1);
399 const targetRow = sourceRow + 1;
400 const nextCells = { ...inserted.cells };
401
402 for (let col = 0; col < inserted.table.cols; col++) {
403 const sourceCell = cells[getCellKey(sourceRow, col)];
404 const targetKey = getCellKey(targetRow, col);
405
406 if (!sourceCell) {
407 nextCells[targetKey] = {
408 ...nextCells[targetKey],
409 elements: [],
410 };
411 continue;
412 }
413
414 nextCells[targetKey] = cloneValue(sourceCell);
415 }
416
417 const nextRows = inserted.rows.slice();
418 if (rows[sourceRow]) {
419 nextRows[targetRow] = cloneValue(rows[sourceRow]);
420 }
421
422 return {
423 ...inserted,
424 rows: nextRows,
425 cells: nextCells,
426 };
427 }
428
429 export function duplicateColumnAt(
430 table: TableConfig,
431 rows: RowConfigs,
432 columns: ColumnConfigs,
433 cells: Record<CellKey, Cell>,
434 colIndex: number
435 ): TableUpdateResult {
436 const sourceCol = normalizeDeleteColumnIndex(table, colIndex);
437 const inserted = insertColumnAt(table, rows, columns, cells, sourceCol + 1);
438 const targetCol = sourceCol + 1;
439 const nextCells = { ...inserted.cells };
440
441 for (let row = 0; row < inserted.table.rows; row++) {
442 const sourceCell = cells[getCellKey(row, sourceCol)];
443 const targetKey = getCellKey(row, targetCol);
444
445 if (!sourceCell) {
446 nextCells[targetKey] = {
447 ...nextCells[targetKey],
448 elements: [],
449 };
450 continue;
451 }
452
453 nextCells[targetKey] = cloneValue(sourceCell);
454 }
455
456 const nextColumns = inserted.columns.slice();
457 if (columns[sourceCol]) {
458 nextColumns[targetCol] = cloneValue(columns[sourceCol]);
459 }
460
461 return {
462 ...inserted,
463 columns: nextColumns,
464 cells: nextCells,
465 };
466 }
467
468 export function moveRowTo(
469 table: TableConfig,
470 rows: RowConfigs,
471 columns: ColumnConfigs,
472 cells: Record<CellKey, Cell>,
473 subjectRow: number,
474 targetRow: number
475 ): TableUpdateResult {
476 const from = normalizeDeleteRowIndex(table, subjectRow);
477 const to = normalizeDeleteRowIndex(table, targetRow);
478
479 if (from === to) {
480 return { table, rows, columns, cells };
481 }
482
483 const mapRow = (row: number): number => {
484 if (row === from) return to;
485 if (from < to && row > from && row <= to) return row - 1;
486 if (from > to && row >= to && row < from) return row + 1;
487 return row;
488 };
489
490 const nextCells: Record<CellKey, Cell> = {};
491
492 Object.entries(cells).forEach(([key, cell]) => {
493 const [row, col] = parseCellKey(key);
494 nextCells[getCellKey(mapRow(row), col)] = cloneValue(cell);
495 });
496
497 return {
498 table: { ...table },
499 rows: remapRecord(rows, mapRow),
500 columns: columns.slice(),
501 cells: nextCells,
502 };
503 }
504
505 export function moveColumnTo(
506 table: TableConfig,
507 rows: RowConfigs,
508 columns: ColumnConfigs,
509 cells: Record<CellKey, Cell>,
510 subjectCol: number,
511 targetCol: number
512 ): TableUpdateResult {
513 const from = normalizeDeleteColumnIndex(table, subjectCol);
514 const to = normalizeDeleteColumnIndex(table, targetCol);
515
516 if (from === to) {
517 return { table, rows, columns, cells };
518 }
519
520 const mapCol = (col: number): number => {
521 if (col === from) return to;
522 if (from < to && col > from && col <= to) return col - 1;
523 if (from > to && col >= to && col < from) return col + 1;
524 return col;
525 };
526
527 const nextCells: Record<CellKey, Cell> = {};
528
529 Object.entries(cells).forEach(([key, cell]) => {
530 const [row, col] = parseCellKey(key);
531 nextCells[getCellKey(row, mapCol(col))] = cloneValue(cell);
532 });
533
534 return {
535 table: { ...table },
536 rows: rows.slice(),
537 columns: remapRecord(columns, mapCol),
538 cells: nextCells,
539 };
540 }
541